(untested!) added temp backup api scaffoldings before introducing autobackups and rotations

This commit is contained in:
Gani Georgiev
2023-05-08 21:52:40 +03:00
parent 60eee96034
commit d3314e1e23
17 changed files with 914 additions and 40 deletions
+189
View File
@@ -0,0 +1,189 @@
package apis
import (
"context"
"log"
"net/http"
"path/filepath"
"time"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/forms"
"github.com/pocketbase/pocketbase/tools/types"
"github.com/spf13/cast"
)
// bindBackupApi registers the file api endpoints and the corresponding handlers.
//
// @todo add hooks once the app hooks api restructuring is finalized
func bindBackupApi(app core.App, rg *echo.Group) {
api := backupApi{app: app}
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())
}
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)
}
defer fsys.Close()
fsys.SetContext(ctx)
backups, err := fsys.List("")
if err != nil {
return NewBadRequestError("Failed to retrieve backup items. Raw error: \n"+err.Error(), nil)
}
result := make([]backupItem, len(backups))
for i, obj := range backups {
modified, _ := types.ParseDateTime(obj.ModTime)
result[i] = backupItem{
Name: obj.Key,
Size: obj.Size,
Modified: modified,
}
}
return c.JSON(http.StatusOK, result)
}
func (api *backupApi) create(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)
}
form := forms.NewBackupCreate(api.app)
if err := c.Bind(form); err != nil {
return NewBadRequestError("An error occurred while loading the submitted data.", err)
}
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 c.NoContent(http.StatusNoContent)
}
})
}
func (api *backupApi) download(c echo.Context) error {
fileToken := c.QueryParam("token")
_, err := api.app.Dao().FindAdminByToken(
fileToken,
api.app.Settings().AdminFileToken.Secret,
)
if err != nil {
return NewForbiddenError("Insufficient permissions to access the resource.", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
fsys, err := api.app.NewBackupsFilesystem()
if err != nil {
return NewBadRequestError("Failed to load backups filesystem", err)
}
defer fsys.Close()
fsys.SetContext(ctx)
name := c.PathParam("name")
br, err := fsys.GetFile(name)
if err != nil {
return NewBadRequestError("Failed to retrieve backup item. Raw error: \n"+err.Error(), nil)
}
defer br.Close()
return fsys.Serve(
c.Response(),
c.Request(),
name,
filepath.Base(name), // 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)
}
name := c.PathParam("name")
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)
}
defer fsys.Close()
fsys.SetContext(existsCtx)
if exists, err := fsys.Exists(name); !exists {
return NewNotFoundError("Missing or invalid backup file", err)
}
go func() {
// wait max 10 minutes
ctx, cancel := context.WithTimeout(context.Background(), 10*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() {
log.Println(err)
}
}()
return c.NoContent(http.StatusNoContent)
}
func (api *backupApi) delete(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)
}
defer fsys.Close()
fsys.SetContext(ctx)
name := c.PathParam("name")
if err := fsys.Delete(name); err != nil {
return NewBadRequestError("Invalid or already deleted backup file. Raw error: \n"+err.Error(), nil)
}
return c.NoContent(http.StatusNoContent)
}
+2 -1
View File
@@ -113,6 +113,7 @@ func InitApi(app core.App) (*echo.Echo, error) {
bindFileApi(app, api)
bindRealtimeApi(app, api)
bindLogsApi(app, api)
bindBackupApi(app, api)
bindHealthApi(app, api)
// trigger the custom BeforeServe hook for the created api router
@@ -191,7 +192,7 @@ func bindStaticAdminUI(app core.App, e *echo.Echo) error {
return nil
}
const totalAdminsCacheKey = "totalAdmins"
const totalAdminsCacheKey = "@totalAdmins"
func updateTotalAdminsCache(app core.App) error {
total, err := app.Dao().TotalAdmins()
+10 -1
View File
@@ -1,6 +1,7 @@
package apis
import (
"context"
"crypto/tls"
"log"
"net"
@@ -85,7 +86,7 @@ func Serve(app core.App, options *ServeOptions) error {
GetCertificate: certManager.GetCertificate,
NextProtos: []string{acme.ALPNProto},
},
ReadTimeout: 5 * time.Minute,
ReadTimeout: 10 * time.Minute,
ReadHeaderTimeout: 30 * time.Second,
// WriteTimeout: 60 * time.Second, // breaks sse!
Handler: router,
@@ -119,6 +120,14 @@ func Serve(app core.App, options *ServeOptions) error {
regular.Printf(" ➜ Admin UI: %s\n", color.CyanString("%s://%s/_/", schema, serverConfig.Addr))
}
// try to gracefully shutdown the server on app termination
app.OnTerminate().Add(func(e *core.TerminateEvent) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
serverConfig.Shutdown(ctx)
return nil
})
// start HTTPS server
if options.HttpsAddr != "" {
// if httpAddr is set, start an HTTP server to redirect the traffic to the HTTPS version