-
-
Notifications
You must be signed in to change notification settings - Fork 286
/
router_server_backup.go
199 lines (181 loc) · 7.12 KB
/
router_server_backup.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
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)
client := middleware.ExtractApiClient(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(client, data.Uuid, data.Ignore)
case backup.S3BackupAdapter:
adapter = backup.NewS3(client, 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
func postServerRestoreBackup(c *gin.Context) {
s := middleware.ExtractServer(c)
client := middleware.ExtractApiClient(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
}
s.SetRestoring(true)
hasError := true
defer func() {
if !hasError {
return
}
s.SetRestoring(false)
}()
logger.Info("processing server backup restore request")
if data.TruncateDirectory {
logger.Info("received \"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(client, c.Param("backup"))
if err != nil {
middleware.CaptureAndAbort(c, err)
return
}
go func(s *server.Server, b backup.BackupInterface, logger *log.Entry) {
logger.Info("starting restoration process for server backup using local driver")
if err := s.RestoreBackup(b, nil); err != nil {
logger.WithField("error", err).Error("failed to restore local backup to server")
}
s.Events().Publish(server.DaemonMessageEvent, "Completed server restoration from local backup.")
s.Events().Publish(server.BackupRestoreCompletedEvent, "")
logger.Info("completed server restoration from local backup")
s.SetRestoring(false)
}(s, b, logger)
hasError = false
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.
httpClient := http.Client{}
logger.Info("downloading backup from remote location...")
// TODO: this will hang if there is an issue. We can't use c.Request.Context() (or really any)
// since it will be canceled when the request is closed which happens quickly since we push
// this into the background.
//
// For now I'm just using the server context so at least the request is canceled if
// the server gets deleted.
req, err := http.NewRequestWithContext(s.Context(), http.MethodGet, data.DownloadUrl, nil)
if err != nil {
middleware.CaptureAndAbort(c, err)
return
}
res, err := httpClient.Do(req)
if err != nil {
middleware.CaptureAndAbort(c, err)
return
}
// 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")) {
_ = res.Body.Close()
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(s *server.Server, uuid string, logger *log.Entry) {
logger.Info("starting restoration process for server backup using S3 driver")
if err := s.RestoreBackup(backup.NewS3(client, uuid, ""), res.Body); err != nil {
logger.WithField("error", errors.WithStack(err)).Error("failed to restore remote S3 backup to server")
}
s.Events().Publish(server.DaemonMessageEvent, "Completed server restoration from S3 backup.")
s.Events().Publish(server.BackupRestoreCompletedEvent, "")
logger.Info("completed server restoration from S3 backup")
s.SetRestoring(false)
}(s, c.Param("backup"), logger)
hasError = false
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(middleware.ExtractApiClient(c), 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)
}