package router import ( "net/http" "os" "strings" "emperror.dev/errors" "github.com/apex/log" "github.com/gin-gonic/gin" "github.com/pterodactyl/wings/router/middleware" "github.com/pterodactyl/wings/server" "github.com/pterodactyl/wings/server/backup" ) // postServerBackup performs a backup against a given server instance using the // provided backup adapter. func postServerBackup(c *gin.Context) { s := middleware.ExtractServer(c) logger := middleware.ExtractLogger(c) var data struct { Adapter backup.AdapterType `json:"adapter"` Uuid string `json:"uuid"` Ignore string `json:"ignore"` } if err := c.BindJSON(&data); err != nil { return } var adapter backup.BackupInterface switch data.Adapter { case backup.LocalBackupAdapter: adapter = backup.NewLocal(data.Uuid, data.Ignore) case backup.S3BackupAdapter: adapter = backup.NewS3(data.Uuid, data.Ignore) default: middleware.CaptureAndAbort(c, errors.New("router/backups: provided adapter is not valid: "+string(data.Adapter))) return } // Attach the server ID and the request ID to the adapter log context for easier // parsing in the logs. adapter.WithLogContext(map[string]interface{}{ "server": s.Id(), "request_id": c.GetString("request_id"), }) go func(b backup.BackupInterface, s *server.Server, logger *log.Entry) { if err := s.Backup(b); err != nil { logger.WithField("error", errors.WithStackIf(err)).Error("router: failed to generate server backup") } }(adapter, s, logger) c.Status(http.StatusAccepted) } // postServerRestoreBackup handles restoring a backup for a server by downloading // or finding the given backup on the system and then unpacking the archive into // the server's data directory. If the TruncateDirectory field is provided and // is true all of the files will be deleted for the server. // // This endpoint will block until the backup is fully restored allowing for a // spinner to be displayed in the Panel UI effectively. // // TODO: stop the server if it is running; internally mark it as suspended func postServerRestoreBackup(c *gin.Context) { s := middleware.ExtractServer(c) logger := middleware.ExtractLogger(c) var data struct { Adapter backup.AdapterType `binding:"required,oneof=wings s3" json:"adapter"` TruncateDirectory bool `json:"truncate_directory"` // A UUID is always required for this endpoint, however the download URL // is only present when the given adapter type is s3. DownloadUrl string `json:"download_url"` } if err := c.BindJSON(&data); err != nil { return } if data.Adapter == backup.S3BackupAdapter && data.DownloadUrl == "" { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "The download_url field is required when the backup adapter is set to S3."}) return } logger.Info("processing server backup restore request") if data.TruncateDirectory { logger.Info(`recieved "truncate_directory" flag in request: deleting server files`) if err := s.Filesystem().TruncateRootDirectory(); err != nil { middleware.CaptureAndAbort(c, err) return } } // Now that we've cleaned up the data directory if necessary, grab the backup file // and attempt to restore it into the server directory. if data.Adapter == backup.LocalBackupAdapter { b, _, err := backup.LocateLocal(c.Param("backup")) if err != nil { middleware.CaptureAndAbort(c, err) return } go func(logger *log.Entry) { logger.Info("restoring server from local backup...") if err := s.RestoreBackup(b, nil); err != nil { logger.WithField("error", err).Error("failed to restore local backup to server") } }(logger) c.Status(http.StatusAccepted) return } // Since this is not a local backup we need to stream the archive and then // parse over the contents as we go in order to restore it to the server. client := http.Client{} logger.Info("downloading backup from remote location...") req, err := http.NewRequestWithContext(c.Request.Context(), http.MethodGet, data.DownloadUrl, nil) if err != nil { middleware.CaptureAndAbort(c, err) return } res, err := client.Do(req) if err != nil { middleware.CaptureAndAbort(c, err) return } defer res.Body.Close() // Don't allow content types that we know are going to give us problems. if res.Header.Get("Content-Type") == "" || !strings.Contains("application/x-gzip application/gzip", res.Header.Get("Content-Type")) { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ "error": "The provided backup link is not a supported content type. \"" + res.Header.Get("Content-Type") + "\" is not application/x-gzip.", }) return } go func(uuid string, logger *log.Entry) { logger.Info("restoring server from remote S3 backup...") if err := s.RestoreBackup(backup.NewS3(uuid, ""), nil); err != nil { logger.WithField("error", err).Error("failed to restore remote S3 backup to server") } }(c.Param("backup"), logger) c.Status(http.StatusAccepted) } // deleteServerBackup deletes a local backup of a server. If the backup is not // found on the machine just return a 404 error. The service calling this // endpoint can make its own decisions as to how it wants to handle that // response. func deleteServerBackup(c *gin.Context) { b, _, err := backup.LocateLocal(c.Param("backup")) if err != nil { // Just return from the function at this point if the backup was not located. if errors.Is(err, os.ErrNotExist) { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ "error": "The requested backup was not found on this server.", }) return } middleware.CaptureAndAbort(c, err) return } // I'm not entirely sure how likely this is to happen, however if we did manage to // locate the backup previously and it is now missing when we go to delete, just // treat it as having been successful, rather than returning a 404. if err := b.Remove(); err != nil && !errors.Is(err, os.ErrNotExist) { middleware.CaptureAndAbort(c, err) return } c.Status(http.StatusNoContent) }