Skip to content

Commit

Permalink
added backup apis and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ganigeorgiev committed May 13, 2023
1 parent 3b0f60f commit e8b4a7e
Show file tree
Hide file tree
Showing 104 changed files with 3,190 additions and 1,015 deletions.
67 changes: 35 additions & 32 deletions apis/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import (
"context"
"log"
"net/http"
"net/url"
"path/filepath"
"time"

"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/forms"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/tools/types"
"github.com/spf13/cast"
)
Expand All @@ -23,28 +25,22 @@ func bindBackupApi(app core.App, rg *echo.Group) {
subGroup := rg.Group("/backups", ActivityLogger(app))
subGroup.GET("", api.list, RequireAdminAuth())
subGroup.POST("", api.create, RequireAdminAuth())
subGroup.GET("/:name", api.download)
subGroup.DELETE("/:name", api.delete, RequireAdminAuth())
subGroup.POST("/:name/restore", api.restore, RequireAdminAuth())
subGroup.GET("/:key", api.download)
subGroup.DELETE("/:key", api.delete, RequireAdminAuth())
subGroup.POST("/:key/restore", api.restore, RequireAdminAuth())
}

type backupApi struct {
app core.App
}

type backupItem struct {
Name string `json:"name"`
Size int64 `json:"size"`
Modified types.DateTime `json:"modified"`
}

func (api *backupApi) list(c echo.Context) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

fsys, err := api.app.NewBackupsFilesystem()
if err != nil {
return NewBadRequestError("Failed to load backups filesystem", err)
return NewBadRequestError("Failed to load backups filesystem.", err)
}
defer fsys.Close()

Expand All @@ -55,13 +51,13 @@ func (api *backupApi) list(c echo.Context) error {
return NewBadRequestError("Failed to retrieve backup items. Raw error: \n"+err.Error(), nil)
}

result := make([]backupItem, len(backups))
result := make([]models.BackupFileInfo, len(backups))

for i, obj := range backups {
modified, _ := types.ParseDateTime(obj.ModTime)

result[i] = backupItem{
Name: obj.Key,
result[i] = models.BackupFileInfo{
Key: obj.Key,
Size: obj.Size,
Modified: modified,
}
Expand All @@ -71,7 +67,7 @@ func (api *backupApi) list(c echo.Context) error {
}

func (api *backupApi) create(c echo.Context) error {
if cast.ToString(api.app.Cache().Get(core.CacheActiveBackupsKey)) != "" {
if api.app.Cache().Has(core.CacheKeyActiveBackup) {
return NewBadRequestError("Try again later - another backup/restore process has already been started", nil)
}

Expand All @@ -83,9 +79,11 @@ func (api *backupApi) create(c echo.Context) error {
return form.Submit(func(next forms.InterceptorNextFunc[string]) forms.InterceptorNextFunc[string] {
return func(name string) error {
if err := next(name); err != nil {
return NewBadRequestError("Failed to create backup", err)
return NewBadRequestError("Failed to create backup.", err)
}

// we don't retrieve the generated backup file because it may not be
// available yet due to the eventually consistent nature of some S3 providers
return c.NoContent(http.StatusNoContent)
}
})
Expand All @@ -107,15 +105,15 @@ func (api *backupApi) download(c echo.Context) error {

fsys, err := api.app.NewBackupsFilesystem()
if err != nil {
return NewBadRequestError("Failed to load backups filesystem", err)
return NewBadRequestError("Failed to load backups filesystem.", err)
}
defer fsys.Close()

fsys.SetContext(ctx)

name := c.PathParam("name")
key := c.PathParam("key")

br, err := fsys.GetFile(name)
br, err := fsys.GetFile(key)
if err != nil {
return NewBadRequestError("Failed to retrieve backup item. Raw error: \n"+err.Error(), nil)
}
Expand All @@ -124,42 +122,43 @@ func (api *backupApi) download(c echo.Context) error {
return fsys.Serve(
c.Response(),
c.Request(),
name,
filepath.Base(name), // without the path prefix (if any)
key,
filepath.Base(key), // without the path prefix (if any)
)
}

func (api *backupApi) restore(c echo.Context) error {
if cast.ToString(api.app.Cache().Get(core.CacheActiveBackupsKey)) != "" {
return NewBadRequestError("Try again later - another backup/restore process has already been started", nil)
if api.app.Cache().Has(core.CacheKeyActiveBackup) {
return NewBadRequestError("Try again later - another backup/restore process has already been started.", nil)
}

name := c.PathParam("name")
// @todo remove the extra unescape after https://github.com/labstack/echo/issues/2447
key, _ := url.PathUnescape(c.PathParam("key"))

existsCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

fsys, err := api.app.NewBackupsFilesystem()
if err != nil {
return NewBadRequestError("Failed to load backups filesystem", err)
return NewBadRequestError("Failed to load backups filesystem.", err)
}
defer fsys.Close()

fsys.SetContext(existsCtx)

if exists, err := fsys.Exists(name); !exists {
return NewNotFoundError("Missing or invalid backup file", err)
if exists, err := fsys.Exists(key); !exists {
return NewBadRequestError("Missing or invalid backup file.", err)
}

go func() {
// wait max 10 minutes
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
// wait max 15 minutes to fetch the backup
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()

// give some optimistic time to write the response
time.Sleep(1 * time.Second)

if err := api.app.RestoreBackup(ctx, name); err != nil && api.app.IsDebug() {
if err := api.app.RestoreBackup(ctx, key); err != nil && api.app.IsDebug() {
log.Println(err)
}
}()
Expand All @@ -173,15 +172,19 @@ func (api *backupApi) delete(c echo.Context) error {

fsys, err := api.app.NewBackupsFilesystem()
if err != nil {
return NewBadRequestError("Failed to load backups filesystem", err)
return NewBadRequestError("Failed to load backups filesystem.", err)
}
defer fsys.Close()

fsys.SetContext(ctx)

name := c.PathParam("name")
key := c.PathParam("key")

if key != "" && cast.ToString(api.app.Cache().Get(core.CacheKeyActiveBackup)) == key {
return NewBadRequestError("The backup is currently being used and cannot be deleted.", nil)
}

if err := fsys.Delete(name); err != nil {
if err := fsys.Delete(key); err != nil {
return NewBadRequestError("Invalid or already deleted backup file. Raw error: \n"+err.Error(), nil)
}

Expand Down
Loading

0 comments on commit e8b4a7e

Please sign in to comment.