forked from pterodactyl/panel
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathBackupStatusController.php
More file actions
161 lines (137 loc) · 6.21 KB
/
BackupStatusController.php
File metadata and controls
161 lines (137 loc) · 6.21 KB
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
<?php
namespace Pterodactyl\Http\Controllers\Api\Remote\Backups;
use Carbon\CarbonImmutable;
use Illuminate\Http\Request;
use Pterodactyl\Models\Backup;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Facades\Activity;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Extensions\Backups\BackupManager;
use Pterodactyl\Extensions\Filesystem\S3Filesystem;
use Pterodactyl\Exceptions\Http\HttpForbiddenException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Pterodactyl\Http\Requests\Api\Remote\ReportBackupCompleteRequest;
class BackupStatusController extends Controller
{
/**
* BackupStatusController constructor.
*/
public function __construct(private BackupManager $backupManager)
{
}
/**
* Handles updating the state of a backup.
*
* @throws \Throwable
*/
public function index(ReportBackupCompleteRequest $request, string $backup): JsonResponse
{
// Get the node associated with the request.
/** @var \Pterodactyl\Models\Node $node */
$node = $request->attributes->get('node');
/** @var Backup $model */
$model = Backup::query()
->where('uuid', $backup)
->firstOrFail();
// Check that the backup is "owned" by the node making the request. This avoids other nodes
// from messing with backups that they don't own.
/** @var \Pterodactyl\Models\Server $server */
$server = $model->server;
if ($server->node_id !== $node->id) {
throw new HttpForbiddenException('You do not have permission to access that backup.');
}
if ($model->is_successful) {
throw new BadRequestHttpException('Cannot update the status of a backup that is already marked as completed.');
}
$action = $request->boolean('successful') ? 'server:backup.complete' : 'server:backup.fail';
$log = Activity::event($action)->subject($model, $model->server)->property('name', $model->name);
$log->transaction(function () use ($model, $request) {
$successful = $request->boolean('successful');
$model->fill([
'is_successful' => $successful,
// Change the lock state to unlocked if this was a failed backup so that it can be
// deleted easily. Also does not make sense to have a locked backup on the system
// that is failed.
'is_locked' => $successful ? $model->is_locked : false,
'checksum' => $successful ? ($request->input('checksum_type') . ':' . $request->input('checksum')) : null,
'bytes' => $successful ? $request->input('size') : 0,
'completed_at' => CarbonImmutable::now(),
])->save();
// Check if we are using the s3 backup adapter. If so, make sure we mark the backup as
// being completed in S3 correctly.
$adapter = $this->backupManager->adapter();
if ($adapter instanceof S3Filesystem) {
$this->completeMultipartUpload($model, $adapter, $successful, $request->input('parts'));
}
});
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
/**
* Handles toggling the restoration status of a server. The server status field should be
* set back to null, even if the restoration failed. This is not an unsolvable state for
* the server, and the user can keep trying to restore, or just use the reinstall button.
*
* The only thing the successful field does is update the entry value for the audit logs
* table tracking for this restoration.
*
* @throws \Throwable
*/
public function restore(Request $request, string $backup): JsonResponse
{
/** @var Backup $model */
$model = Backup::query()->where('uuid', $backup)->firstOrFail();
$model->server->update(['status' => null]);
Activity::event($request->boolean('successful') ? 'server:backup.restore-complete' : 'server.backup.restore-failed')
->subject($model, $model->server)
->property('name', $model->name)
->log();
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}
/**
* Marks a multipart upload in a given S3-compatible instance as failed or successful for
* the given backup.
*
* @throws \Exception
* @throws DisplayException
*/
protected function completeMultipartUpload(Backup $backup, S3Filesystem $adapter, bool $successful, ?array $parts): void
{
// This should never really happen, but if it does don't let us fall victim to Amazon's
// wildly fun error messaging. Just stop the process right here.
if (empty($backup->upload_id)) {
// A failed backup doesn't need to error here, this can happen if the backup encounters
// an error before we even start the upload. AWS gives you tooling to clear these failed
// multipart uploads as needed too.
if (!$successful) {
return;
}
throw new DisplayException('Cannot complete backup request: no upload_id present on model.');
}
$params = [
'Bucket' => $adapter->getBucket(),
'Key' => sprintf('%s/%s.tar.gz', $backup->server->uuid, $backup->uuid),
'UploadId' => $backup->upload_id,
];
$client = $adapter->getClient();
if (!$successful) {
$client->execute($client->getCommand('AbortMultipartUpload', $params));
return;
}
// Otherwise send a CompleteMultipartUpload request.
$params['MultipartUpload'] = [
'Parts' => [],
];
if (is_null($parts)) {
$params['MultipartUpload']['Parts'] = $client->execute($client->getCommand('ListParts', $params))['Parts'];
} else {
foreach ($parts as $part) {
$params['MultipartUpload']['Parts'][] = [
'ETag' => $part['etag'],
'PartNumber' => $part['part_number'],
];
}
}
$client->execute($client->getCommand('CompleteMultipartUpload', $params));
}
}