Break out the backup functions of the daemon in prep for S3 support

This commit is contained in:
Dane Everitt 2020-04-13 22:01:07 -07:00
parent fd9487ea4d
commit 11035b561a
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
7 changed files with 260 additions and 227 deletions

35
api/backup_endpoints.go Normal file
View File

@ -0,0 +1,35 @@
package api
import (
"encoding/json"
"fmt"
"github.com/pkg/errors"
)
type BackupRequest struct {
Checksum string `json:"checksum"`
Size int64 `json:"size"`
Successful bool `json:"successful"`
}
// Notifies the panel that a specific backup has been completed and is now
// available for a user to view and download.
func (r *PanelRequest) SendBackupStatus(backup string, data BackupRequest) (*RequestError, error) {
b, err := json.Marshal(data)
if err != nil {
return nil, errors.WithStack(err)
}
resp, err := r.Post(fmt.Sprintf("/backups/%s", backup), b)
if err != nil {
return nil, errors.WithStack(err)
}
defer resp.Body.Close()
r.Response = resp
if r.HasError() {
return r.Error(), nil
}
return nil, nil
}

View File

@ -203,29 +203,3 @@ func (r *PanelRequest) SendTransferSuccess(uuid string) (*RequestError, error) {
return nil, nil return nil, nil
} }
type BackupRequest struct {
Successful bool `json:"successful"`
Sha256Hash string `json:"sha256_hash"`
FileSize int64 `json:"file_size"`
}
func (r *PanelRequest) SendBackupStatus(uuid string, backup string, data BackupRequest) (*RequestError, error) {
b, err := json.Marshal(data)
if err != nil {
return nil, errors.WithStack(err)
}
resp, err := r.Post(fmt.Sprintf("/servers/%s/backup/%s", uuid, backup), b)
if err != nil {
return nil, errors.WithStack(err)
}
defer resp.Body.Close()
r.Response = resp
if r.HasError() {
return r.Error(), nil
}
return nil, nil
}

View File

@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pterodactyl/wings/router/tokens" "github.com/pterodactyl/wings/router/tokens"
"github.com/pterodactyl/wings/server/backup"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
@ -25,13 +26,20 @@ func getDownloadBackup(c *gin.Context) {
return return
} }
p, st, err := s.LocateBackup(token.BackupUuid) b, st, err := backup.LocateLocal(token.BackupUuid)
if err != nil { if err != nil {
if os.IsNotExist(err) {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "The requested backup was not found on this server.",
})
return
}
TrackedServerError(err, s).AbortWithServerError(c) TrackedServerError(err, s).AbortWithServerError(c)
return return
} }
f, err := os.Open(p) f, err := os.Open(b.Path())
if err != nil { if err != nil {
TrackedServerError(err, s).AbortWithServerError(c) TrackedServerError(err, s).AbortWithServerError(c)
return return

View File

@ -3,26 +3,23 @@ package router
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pterodactyl/wings/server" "github.com/pterodactyl/wings/server"
"github.com/pterodactyl/wings/server/backup"
"go.uber.org/zap" "go.uber.org/zap"
"net/http" "net/http"
"os"
) )
// Backs up a server. // Backs up a server.
func postServerBackup(c *gin.Context) { func postServerBackup(c *gin.Context) {
s := GetServer(c.Param("server")) s := GetServer(c.Param("server"))
var data struct{ data := &backup.Backup{}
Uuid string `json:"uuid"`
IgnoredFiles []string `json:"ignored_files"`
}
c.BindJSON(&data) c.BindJSON(&data)
go func(backup *server.Backup) { go func(b *backup.Backup, serv *server.Server) {
if err := backup.BackupAndNotify(); err != nil { if err := serv.BackupRoot(b); err != nil {
zap.S().Errorw("failed to generate backup for server", zap.Error(err)) zap.S().Errorw("failed to generate backup for server", zap.Error(err))
} }
}(s.NewBackup(data.Uuid, data.IgnoredFiles)) }(data, s)
c.Status(http.StatusAccepted) c.Status(http.StatusAccepted)
} }
@ -31,13 +28,13 @@ func postServerBackup(c *gin.Context) {
func deleteServerBackup(c *gin.Context) { func deleteServerBackup(c *gin.Context) {
s := GetServer(c.Param("server")) s := GetServer(c.Param("server"))
p, _, err := s.LocateBackup(c.Param("backup")) b, _, err := backup.LocateLocal(c.Param("backup"))
if err != nil { if err != nil {
TrackedServerError(err, s).AbortWithServerError(c) TrackedServerError(err, s).AbortWithServerError(c)
return return
} }
if err := os.Remove(p); err != nil { if err := b.Remove(); err != nil {
TrackedServerError(err, s).AbortWithServerError(c) TrackedServerError(err, s).AbortWithServerError(c)
return return
} }

View File

@ -1,210 +1,39 @@
package server package server
import ( import (
"crypto/sha256"
"encoding/hex"
"github.com/mholt/archiver/v3"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/pterodactyl/wings/api" "github.com/pterodactyl/wings/server/backup"
"github.com/pterodactyl/wings/config"
"go.uber.org/zap" "go.uber.org/zap"
"io"
"os"
"path"
"strings"
"sync"
) )
type Backup struct { // Performs a server backup and then emits the event over the server websocket. We
Uuid string `json:"uuid"` // let the actual backup system handle notifying the panel of the status, but that
IgnoredFiles []string `json:"ignored_files"` // won't emit a websocket event.
server *Server func (s *Server) BackupRoot(b *backup.Backup) error {
localDirectory string r, err := b.LocalBackup(s.Filesystem.Path())
}
// Create a new Backup struct from data passed through in a request.
func (s *Server) NewBackup(uuid string, ignore []string) *Backup {
return &Backup{
Uuid: uuid,
IgnoredFiles: ignore,
server: s,
localDirectory: path.Join(config.Get().System.BackupDirectory, s.Uuid),
}
}
// Locates the backup for a server and returns the local path. This will obviously only
// work if the backup was created as a local backup.
func (s *Server) LocateBackup(uuid string) (string, os.FileInfo, error) {
p := path.Join(config.Get().System.BackupDirectory, s.Uuid, uuid+".tar.gz")
st, err := os.Stat(p)
if err != nil { if err != nil {
return "", nil, err if notifyError := b.NotifyPanel(r, false); notifyError != nil {
} zap.S().Warnw("failed to notify panel of failed backup state", zap.String("backup", b.Uuid), zap.Error(err))
if st.IsDir() {
return "", nil, errors.New("invalid archive found; is directory")
}
return p, st, nil
}
// Ensures that the local backup destination for files exists.
func (b *Backup) ensureLocalBackupLocation() error {
if _, err := os.Stat(b.localDirectory); err != nil {
if !os.IsNotExist(err) {
return errors.WithStack(err)
} }
return os.MkdirAll(b.localDirectory, 0700)
}
return nil
}
// Returns the path for this specific backup.
func (b *Backup) GetPath() string {
return path.Join(b.localDirectory, b.Uuid+".tar.gz")
}
func (b *Backup) GetChecksum() ([]byte, error) {
h := sha256.New()
f, err := os.Open(b.GetPath())
if err != nil {
return []byte{}, errors.WithStack(err)
}
defer f.Close()
if _, err := io.Copy(h, f); err != nil {
return []byte{}, errors.WithStack(err)
}
return h.Sum(nil), nil
}
// Generates a backup of the selected files and pushes it to the defined location
// for this instance.
func (b *Backup) Backup() (*api.BackupRequest, error) {
rootPath := b.server.Filesystem.Path()
if err := b.ensureLocalBackupLocation(); err != nil {
return nil, errors.WithStack(err)
}
zap.S().Debugw("starting archive of server files for backup", zap.String("server", b.server.Uuid), zap.String("backup", b.Uuid))
if err := archiver.Archive([]string{rootPath}, b.GetPath()); err != nil {
if strings.HasPrefix(err.Error(), "file already exists") {
zap.S().Debugw("backup already exists on system, removing and re-attempting", zap.String("backup", b.Uuid))
if rerr := os.Remove(b.GetPath()); rerr != nil {
return nil, errors.WithStack(rerr)
}
// Re-attempt this backup.
return b.Backup()
}
// If there was some error with the archive, just go ahead and ensure the backup
// is completely destroyed at this point. Ignore any errors from this function.
os.Remove(b.GetPath())
return nil, err
}
wg := sync.WaitGroup{}
wg.Add(2)
var checksum string
// Calculate the checksum for the file.
go func() {
defer wg.Done()
resp, err := b.GetChecksum()
if err != nil {
zap.S().Errorw("failed to calculate checksum for backup", zap.String("backup", b.Uuid), zap.Error(err))
}
checksum = hex.EncodeToString(resp)
}()
var s int64
go func() {
defer wg.Done()
st, err := os.Stat(b.GetPath())
if err != nil {
return
}
s = st.Size()
}()
wg.Wait()
return &api.BackupRequest{
Successful: true,
Sha256Hash: checksum,
FileSize: s,
}, nil
}
// Performs a server backup and then notifies the Panel of the completed status
// so that the backup shows up for the user correctly.
func (b *Backup) BackupAndNotify() error {
resp, err := b.Backup()
if err != nil {
b.notifyPanel(resp)
return errors.WithStack(err) return errors.WithStack(err)
} }
if err := b.notifyPanel(resp); err != nil { // Try to notify the panel about the status of this backup. If for some reason this request
// These errors indicate that the Panel will not know about the status of this // fails, delete the archive from the daemon and return that error up the chain to the caller.
// backup, so let's just go ahead and delete it, and let the Panel handle the if notifyError := b.NotifyPanel(r, true); notifyError != nil {
// cleanup process for the backups. b.Remove()
//
// @todo perhaps in the future we can sync the backups from the servers on boot?
os.Remove(b.GetPath())
return err return notifyError
} }
// Emit an event over the socket so we can update the backup in realtime on // Emit an event over the socket so we can update the backup in realtime on
// the frontend for the server. // the frontend for the server.
b.server.Events().PublishJson(BackupCompletedEvent+":"+b.Uuid, map[string]interface{}{ s.Events().PublishJson(BackupCompletedEvent+":"+b.Uuid, map[string]interface{}{
"uuid": b.Uuid, "uuid": b.Uuid,
"sha256_hash": resp.Sha256Hash, "sha256_hash": r.Checksum,
"file_size": resp.FileSize, "file_size": r.Size,
}) })
return nil return nil
} }
func (b *Backup) notifyPanel(request *api.BackupRequest) error {
r := api.NewRequester()
rerr, err := r.SendBackupStatus(b.server.Uuid, b.Uuid, *request)
if rerr != nil || err != nil {
if err != nil {
zap.S().Errorw(
"failed to notify panel of backup status due to internal code error",
zap.String("server", b.server.Uuid),
zap.String("backup", b.Uuid),
zap.Error(err),
)
return err
}
zap.S().Warnw(
rerr.String(),
zap.String("server", b.server.Uuid),
zap.String("backup", b.Uuid),
)
return errors.New(rerr.String())
}
return nil
}

103
server/backup/backup.go Normal file
View File

@ -0,0 +1,103 @@
package backup
import (
"crypto/sha256"
"github.com/pkg/errors"
"github.com/pterodactyl/wings/api"
"github.com/pterodactyl/wings/config"
"go.uber.org/zap"
"io"
"os"
"path"
)
type Backup struct {
// The UUID of this backup object. This must line up with a backup from
// the panel instance.
Uuid string `json:"uuid"`
// An array of files to ignore when generating this backup. This should be
// compatible with a standard .gitignore structure.
IgnoredFiles []string `json:"ignored_files"`
}
type ArchiveDetails struct {
Checksum string `json:"checksum"`
Size int64 `json:"size"`
}
// Returns a request object.
func (ad *ArchiveDetails) ToRequest(successful bool) api.BackupRequest {
return api.BackupRequest{
Checksum: ad.Checksum,
Size: ad.Size,
Successful: successful,
}
}
// Returns the path for this specific backup.
func (b *Backup) Path() string {
return path.Join(config.Get().System.BackupDirectory, b.Uuid+".tar.gz")
}
// Returns the SHA256 checksum of a backup.
func (b *Backup) Checksum() ([]byte, error) {
h := sha256.New()
f, err := os.Open(b.Path())
if err != nil {
return []byte{}, errors.WithStack(err)
}
defer f.Close()
if _, err := io.Copy(h, f); err != nil {
return []byte{}, errors.WithStack(err)
}
return h.Sum(nil), nil
}
// Removes a backup from the system.
func (b *Backup) Remove() error {
return os.Remove(b.Path())
}
// Notifies the panel of a backup's state and returns an error if one is encountered
// while performing this action.
func (b *Backup) NotifyPanel(ad *ArchiveDetails, successful bool) error {
r := api.NewRequester()
rerr, err := r.SendBackupStatus(b.Uuid, ad.ToRequest(successful))
if rerr != nil || err != nil {
if err != nil {
zap.S().Errorw(
"failed to notify panel of backup status due to internal code error",
zap.String("backup", b.Uuid),
zap.Error(err),
)
return err
}
zap.S().Warnw(rerr.String(), zap.String("backup", b.Uuid))
return errors.New(rerr.String())
}
return nil
}
// Ensures that the local backup destination for files exists.
func (b *Backup) ensureLocalBackupLocation() error {
d := config.Get().System.BackupDirectory
if _, err := os.Stat(d); err != nil {
if !os.IsNotExist(err) {
return errors.WithStack(err)
}
return os.MkdirAll(d, 0700)
}
return nil
}

View File

@ -0,0 +1,87 @@
package backup
import (
"encoding/hex"
"github.com/mholt/archiver/v3"
"github.com/pkg/errors"
"go.uber.org/zap"
"os"
"strings"
"sync"
)
// Locates the backup for a server and returns the local path. This will obviously only
// work if the backup was created as a local backup.
func LocateLocal(uuid string) (*Backup, os.FileInfo, error) {
b := &Backup{
Uuid: uuid,
IgnoredFiles: nil,
}
st, err := os.Stat(b.Path())
if err != nil {
return nil, nil, err
}
if st.IsDir() {
return nil, nil, errors.New("invalid archive found; is directory")
}
return b, st, nil
}
// Generates a backup of the selected files and pushes it to the defined location
// for this instance.
func (b *Backup) LocalBackup(dir string) (*ArchiveDetails, error) {
if err := archiver.Archive([]string{dir}, b.Path()); err != nil {
if strings.HasPrefix(err.Error(), "file already exists") {
if rerr := os.Remove(b.Path()); rerr != nil {
return nil, errors.WithStack(rerr)
}
// Re-attempt this backup by calling it with the same information.
return b.LocalBackup(dir)
}
// If there was some error with the archive, just go ahead and ensure the backup
// is completely destroyed at this point. Ignore any errors from this function.
os.Remove(b.Path())
return nil, err
}
wg := sync.WaitGroup{}
wg.Add(2)
var checksum string
// Calculate the checksum for the file.
go func() {
defer wg.Done()
resp, err := b.Checksum()
if err != nil {
zap.S().Errorw("failed to calculate checksum for backup", zap.String("backup", b.Uuid), zap.Error(err))
}
checksum = hex.EncodeToString(resp)
}()
var sz int64
go func() {
defer wg.Done()
st, err := os.Stat(b.Path())
if err != nil {
return
}
sz = st.Size()
}()
wg.Wait()
return &ArchiveDetails{
Checksum: checksum,
Size: sz,
}, nil
}