| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | package core | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"context" | 
					
						
							|  |  |  | 	"errors" | 
					
						
							|  |  |  | 	"fmt" | 
					
						
							|  |  |  | 	"io" | 
					
						
							| 
									
										
										
										
											2023-11-26 19:33:17 +08:00
										 |  |  | 	"log/slog" | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 	"os" | 
					
						
							|  |  |  | 	"path/filepath" | 
					
						
							|  |  |  | 	"runtime" | 
					
						
							| 
									
										
										
										
											2023-05-14 03:10:14 +08:00
										 |  |  | 	"sort" | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 	"time" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	"github.com/pocketbase/pocketbase/tools/archive" | 
					
						
							|  |  |  | 	"github.com/pocketbase/pocketbase/tools/filesystem" | 
					
						
							| 
									
										
										
										
											2023-08-29 01:34:44 +08:00
										 |  |  | 	"github.com/pocketbase/pocketbase/tools/inflector" | 
					
						
							| 
									
										
										
										
											2023-05-22 01:46:47 +08:00
										 |  |  | 	"github.com/pocketbase/pocketbase/tools/osutils" | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 	"github.com/pocketbase/pocketbase/tools/security" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | const ( | 
					
						
							|  |  |  | 	StoreKeyActiveBackup = "@activeBackup" | 
					
						
							|  |  |  | ) | 
					
						
							| 
									
										
										
										
											2023-11-26 19:33:17 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | // 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.
 | 
					
						
							|  |  |  | //
 | 
					
						
							| 
									
										
										
										
											2023-06-16 17:24:28 +08:00
										 |  |  | // To safely perform the backup, it is recommended to have free disk space
 | 
					
						
							|  |  |  | // for at least 2x the size of the pb_data directory.
 | 
					
						
							|  |  |  | //
 | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | // 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 { | 
					
						
							| 
									
										
										
										
											2023-11-26 19:33:17 +08:00
										 |  |  | 	if app.Store().Has(StoreKeyActiveBackup) { | 
					
						
							| 
									
										
										
										
											2023-05-14 03:10:14 +08:00
										 |  |  | 		return errors.New("try again later - another backup/restore operation has already been started") | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-26 19:33:17 +08:00
										 |  |  | 	app.Store().Set(StoreKeyActiveBackup, name) | 
					
						
							|  |  |  | 	defer app.Store().Remove(StoreKeyActiveBackup) | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	event := new(BackupEvent) | 
					
						
							|  |  |  | 	event.App = app | 
					
						
							|  |  |  | 	event.Context = ctx | 
					
						
							|  |  |  | 	event.Name = name | 
					
						
							|  |  |  | 	// default root dir entries to exclude from the backup generation
 | 
					
						
							|  |  |  | 	event.Exclude = []string{LocalBackupsDirName, LocalTempDirName, LocalAutocertCacheDirName} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return app.OnBackupCreate().Trigger(event, func(e *BackupEvent) error { | 
					
						
							|  |  |  | 		// generate a default name if missing
 | 
					
						
							|  |  |  | 		if e.Name == "" { | 
					
						
							|  |  |  | 			e.Name = generateBackupName(e.App, "pb_backup_") | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-09-06 12:03:07 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		// 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) | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-08-08 19:15:29 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		// archive pb_data in a temp directory, exluding the "backups" and the temp dirs
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// Run in transaction to temporary block other writes (transactions uses the NonconcurrentDB connection).
 | 
					
						
							|  |  |  | 		// ---
 | 
					
						
							|  |  |  | 		tempPath := filepath.Join(localTempDir, "pb_backup_"+security.PseudorandomString(6)) | 
					
						
							|  |  |  | 		createErr := e.App.RunInTransaction(func(txApp App) error { | 
					
						
							|  |  |  | 			return txApp.AuxRunInTransaction(func(txApp App) error { | 
					
						
							| 
									
										
										
										
											2024-11-17 18:33:23 +08:00
										 |  |  | 				// run manual checkpoint and truncate the WAL files
 | 
					
						
							|  |  |  | 				// (errors are ignored because it is not that important and the PRAGMA may not be supported by the used driver)
 | 
					
						
							|  |  |  | 				txApp.DB().NewQuery("PRAGMA wal_checkpoint(TRUNCATE)").Execute() | 
					
						
							|  |  |  | 				txApp.AuxDB().NewQuery("PRAGMA wal_checkpoint(TRUNCATE)").Execute() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 				return archive.Create(txApp.DataDir(), tempPath, e.Exclude...) | 
					
						
							|  |  |  | 			}) | 
					
						
							| 
									
										
										
										
											2024-09-18 11:24:22 +08:00
										 |  |  | 		}) | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		if createErr != nil { | 
					
						
							|  |  |  | 			return createErr | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		defer os.Remove(tempPath) | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		// persist the backup in the backups filesystem
 | 
					
						
							|  |  |  | 		// ---
 | 
					
						
							|  |  |  | 		fsys, err := e.App.NewBackupsFilesystem() | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		defer fsys.Close() | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		fsys.SetContext(e.Context) | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		file, err := filesystem.NewFileFromPath(tempPath) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		file.OriginalName = e.Name | 
					
						
							|  |  |  | 		file.Name = file.OriginalName | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		if err := fsys.UploadFile(file, file.Name); err != nil { | 
					
						
							|  |  |  | 			return err | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		return nil | 
					
						
							|  |  |  | 	}) | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // 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)
 | 
					
						
							|  |  |  | //
 | 
					
						
							| 
									
										
										
										
											2023-05-22 12:58:44 +08:00
										 |  |  | //  2. Extract the backup in a temp directory inside the app "pb_data"
 | 
					
						
							|  |  |  | //     (eg. "pb_data/.pb_temp_to_delete/pb_restore").
 | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | //
 | 
					
						
							| 
									
										
										
										
											2023-05-22 12:58:44 +08:00
										 |  |  | //  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
 | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | //     to delete the currently open "pb_data" files.
 | 
					
						
							|  |  |  | //
 | 
					
						
							| 
									
										
										
										
											2023-05-22 12:58:44 +08:00
										 |  |  | //  4. Move the extracted dir content to the app "pb_data".
 | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | //
 | 
					
						
							| 
									
										
										
										
											2023-07-30 18:40:22 +08:00
										 |  |  | //  5. Restart the app (on successful app bootstap it will also remove the old pb_data).
 | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | //
 | 
					
						
							|  |  |  | // If a failure occure during the restore process the dir changes are reverted.
 | 
					
						
							| 
									
										
										
										
											2023-05-14 03:10:14 +08:00
										 |  |  | // If for whatever reason the revert is not possible, it panics.
 | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | func (app *BaseApp) RestoreBackup(ctx context.Context, name string) error { | 
					
						
							| 
									
										
										
										
											2023-11-26 19:33:17 +08:00
										 |  |  | 	if app.Store().Has(StoreKeyActiveBackup) { | 
					
						
							| 
									
										
										
										
											2023-05-14 03:10:14 +08:00
										 |  |  | 		return errors.New("try again later - another backup/restore operation has already been started") | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-26 19:33:17 +08:00
										 |  |  | 	app.Store().Set(StoreKeyActiveBackup, name) | 
					
						
							|  |  |  | 	defer app.Store().Remove(StoreKeyActiveBackup) | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	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} | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	return app.OnBackupRestore().Trigger(event, func(e *BackupEvent) error { | 
					
						
							|  |  |  | 		if runtime.GOOS == "windows" { | 
					
						
							|  |  |  | 			return errors.New("restore is not supported on Windows") | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		fsys, err := e.App.NewBackupsFilesystem() | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		defer fsys.Close() | 
					
						
							| 
									
										
										
										
											2023-08-08 19:15:29 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		fsys.SetContext(e.Context) | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		// fetch the backup file in a temp location
 | 
					
						
							|  |  |  | 		br, err := fsys.GetFile(name) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		defer br.Close() | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		// 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) | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		// 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()) | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		if _, err := io.Copy(tempZip, br); err != nil { | 
					
						
							|  |  |  | 			return err | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		extractedDataDir := filepath.Join(localTempDir, "pb_restore_"+security.PseudorandomString(4)) | 
					
						
							|  |  |  | 		defer os.RemoveAll(extractedDataDir) | 
					
						
							|  |  |  | 		if err := archive.Extract(tempZip.Name(), extractedDataDir); err != nil { | 
					
						
							|  |  |  | 			return err | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		// 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) | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		// 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 { | 
					
						
							|  |  |  | 			e.App.Logger().Debug( | 
					
						
							|  |  |  | 				"[RestoreBackup] Failed to remove the temp zip backup file", | 
					
						
							|  |  |  | 				slog.String("file", tempZip.Name()), | 
					
						
							|  |  |  | 				slog.String("error", err.Error()), | 
					
						
							|  |  |  | 			) | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		// 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)
 | 
					
						
							|  |  |  | 		oldTempDataDir := filepath.Join(localTempDir, "old_pb_data_"+security.PseudorandomString(4)) | 
					
						
							|  |  |  | 		if err := osutils.MoveDirContent(e.App.DataDir(), oldTempDataDir, e.Exclude...); err != nil { | 
					
						
							|  |  |  | 			return fmt.Errorf("failed to move the current pb_data content to a temp location: %w", err) | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		// move the extracted archive content to the app's pb_data
 | 
					
						
							|  |  |  | 		if err := osutils.MoveDirContent(extractedDataDir, e.App.DataDir(), e.Exclude...); err != nil { | 
					
						
							|  |  |  | 			return fmt.Errorf("failed to move the extracted archive content to pb_data: %w", err) | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		revertDataDirChanges := func() error { | 
					
						
							|  |  |  | 			if err := osutils.MoveDirContent(e.App.DataDir(), extractedDataDir, e.Exclude...); err != nil { | 
					
						
							|  |  |  | 				return fmt.Errorf("failed to revert the extracted dir change: %w", err) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if err := osutils.MoveDirContent(oldTempDataDir, e.App.DataDir(), e.Exclude...); err != nil { | 
					
						
							|  |  |  | 				return fmt.Errorf("failed to revert old pb_data dir change: %w", err) | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 			return nil | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		// restart the app
 | 
					
						
							|  |  |  | 		if err := e.App.Restart(); err != nil { | 
					
						
							|  |  |  | 			if revertErr := revertDataDirChanges(); revertErr != nil { | 
					
						
							|  |  |  | 				panic(revertErr) | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 			return fmt.Errorf("failed to restart the app process: %w", err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return nil | 
					
						
							|  |  |  | 	}) | 
					
						
							| 
									
										
										
										
											2023-05-09 02:52:40 +08:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2023-05-14 03:10:14 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | // registerAutobackupHooks registers the autobackup app serve hooks.
 | 
					
						
							|  |  |  | func (app *BaseApp) registerAutobackupHooks() { | 
					
						
							|  |  |  | 	const jobId = "__auto_pb_backup__" | 
					
						
							| 
									
										
										
										
											2023-05-14 03:10:14 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	loadJob := func() { | 
					
						
							|  |  |  | 		rawSchedule := app.Settings().Backups.Cron | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		if rawSchedule == "" { | 
					
						
							|  |  |  | 			app.Cron().Remove(jobId) | 
					
						
							| 
									
										
										
										
											2023-05-14 03:10:14 +08:00
										 |  |  | 			return | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 		app.Cron().Add(jobId, rawSchedule, func() { | 
					
						
							| 
									
										
										
										
											2023-08-29 01:34:44 +08:00
										 |  |  | 			const autoPrefix = "@auto_pb_backup_" | 
					
						
							| 
									
										
										
										
											2023-05-14 03:10:14 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 			name := generateBackupName(app, autoPrefix) | 
					
						
							| 
									
										
										
										
											2023-05-14 03:10:14 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-26 19:33:17 +08:00
										 |  |  | 			if err := app.CreateBackup(context.Background(), name); err != nil { | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 				app.Logger().Error( | 
					
						
							| 
									
										
										
										
											2023-11-26 19:33:17 +08:00
										 |  |  | 					"[Backup cron] Failed to create backup", | 
					
						
							|  |  |  | 					slog.String("name", name), | 
					
						
							|  |  |  | 					slog.String("error", err.Error()), | 
					
						
							|  |  |  | 				) | 
					
						
							| 
									
										
										
										
											2023-05-14 03:10:14 +08:00
										 |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			maxKeep := app.Settings().Backups.CronMaxKeep | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if maxKeep == 0 { | 
					
						
							|  |  |  | 				return // no explicit limit
 | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			fsys, err := app.NewBackupsFilesystem() | 
					
						
							| 
									
										
										
										
											2023-11-26 19:33:17 +08:00
										 |  |  | 			if err != nil { | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 				app.Logger().Error( | 
					
						
							| 
									
										
										
										
											2023-11-26 19:33:17 +08:00
										 |  |  | 					"[Backup cron] Failed to initialize the backup filesystem", | 
					
						
							|  |  |  | 					slog.String("error", err.Error()), | 
					
						
							|  |  |  | 				) | 
					
						
							| 
									
										
										
										
											2023-05-14 03:10:14 +08:00
										 |  |  | 				return | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			defer fsys.Close() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			files, err := fsys.List(autoPrefix) | 
					
						
							| 
									
										
										
										
											2023-11-26 19:33:17 +08:00
										 |  |  | 			if err != nil { | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 				app.Logger().Error( | 
					
						
							| 
									
										
										
										
											2023-11-26 19:33:17 +08:00
										 |  |  | 					"[Backup cron] Failed to list autogenerated backups", | 
					
						
							|  |  |  | 					slog.String("error", err.Error()), | 
					
						
							|  |  |  | 				) | 
					
						
							| 
									
										
										
										
											2023-05-14 03:10:14 +08:00
										 |  |  | 				return | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if maxKeep >= len(files) { | 
					
						
							|  |  |  | 				return // nothing to remove
 | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// sort desc
 | 
					
						
							|  |  |  | 			sort.Slice(files, func(i, j int) bool { | 
					
						
							|  |  |  | 				return files[i].ModTime.After(files[j].ModTime) | 
					
						
							|  |  |  | 			}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// keep only the most recent n auto backup files
 | 
					
						
							|  |  |  | 			toRemove := files[maxKeep:] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			for _, f := range toRemove { | 
					
						
							| 
									
										
										
										
											2023-11-26 19:33:17 +08:00
										 |  |  | 				if err := fsys.Delete(f.Key); err != nil { | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 					app.Logger().Error( | 
					
						
							| 
									
										
										
										
											2023-11-26 19:33:17 +08:00
										 |  |  | 						"[Backup cron] Failed to remove old autogenerated backup", | 
					
						
							|  |  |  | 						slog.String("key", f.Key), | 
					
						
							|  |  |  | 						slog.String("error", err.Error()), | 
					
						
							|  |  |  | 					) | 
					
						
							| 
									
										
										
										
											2023-05-14 03:10:14 +08:00
										 |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		}) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	app.OnBootstrap().BindFunc(func(e *BootstrapEvent) error { | 
					
						
							|  |  |  | 		if err := e.Next(); err != nil { | 
					
						
							|  |  |  | 			return err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-14 03:10:14 +08:00
										 |  |  | 		loadJob() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return nil | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | 	app.OnSettingsReload().BindFunc(func(e *SettingsReloadEvent) error { | 
					
						
							|  |  |  | 		if err := e.Next(); err != nil { | 
					
						
							|  |  |  | 			return err | 
					
						
							| 
									
										
										
										
											2023-05-14 03:10:14 +08:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		loadJob() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return nil | 
					
						
							|  |  |  | 	}) | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2023-08-29 01:34:44 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-09-30 00:23:19 +08:00
										 |  |  | func generateBackupName(app App, prefix string) string { | 
					
						
							| 
									
										
										
										
											2023-08-29 01:34:44 +08:00
										 |  |  | 	appName := inflector.Snakecase(app.Settings().Meta.AppName) | 
					
						
							|  |  |  | 	if len(appName) > 50 { | 
					
						
							|  |  |  | 		appName = appName[:50] | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return fmt.Sprintf( | 
					
						
							|  |  |  | 		"%s%s_%s.zip", | 
					
						
							|  |  |  | 		prefix, | 
					
						
							|  |  |  | 		appName, | 
					
						
							|  |  |  | 		time.Now().UTC().Format("20060102150405"), | 
					
						
							|  |  |  | 	) | 
					
						
							|  |  |  | } |