Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
pterodactyl
GitHub Repository: pterodactyl/panel
Path: blob/1.0-develop/app/Http/Controllers/Api/Remote/Backups/BackupStatusController.php
10284 views
1
<?php
2
3
namespace Pterodactyl\Http\Controllers\Api\Remote\Backups;
4
5
use Carbon\CarbonImmutable;
6
use Illuminate\Http\Request;
7
use Pterodactyl\Models\Backup;
8
use Illuminate\Http\JsonResponse;
9
use Pterodactyl\Facades\Activity;
10
use Pterodactyl\Exceptions\DisplayException;
11
use Pterodactyl\Http\Controllers\Controller;
12
use Pterodactyl\Extensions\Backups\BackupManager;
13
use Pterodactyl\Extensions\Filesystem\S3Filesystem;
14
use Pterodactyl\Exceptions\Http\HttpForbiddenException;
15
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
16
use Pterodactyl\Http\Requests\Api\Remote\ReportBackupCompleteRequest;
17
18
class BackupStatusController extends Controller
19
{
20
/**
21
* BackupStatusController constructor.
22
*/
23
public function __construct(private BackupManager $backupManager)
24
{
25
}
26
27
/**
28
* Handles updating the state of a backup.
29
*
30
* @throws \Throwable
31
*/
32
public function index(ReportBackupCompleteRequest $request, string $backup): JsonResponse
33
{
34
// Get the node associated with the request.
35
/** @var \Pterodactyl\Models\Node $node */
36
$node = $request->attributes->get('node');
37
38
/** @var Backup $model */
39
$model = Backup::query()
40
->where('uuid', $backup)
41
->firstOrFail();
42
43
// Check that the backup is "owned" by the node making the request. This avoids other nodes
44
// from messing with backups that they don't own.
45
/** @var \Pterodactyl\Models\Server $server */
46
$server = $model->server;
47
if ($server->node_id !== $node->id) {
48
throw new HttpForbiddenException('You do not have permission to access that backup.');
49
}
50
51
if ($model->is_successful) {
52
throw new BadRequestHttpException('Cannot update the status of a backup that is already marked as completed.');
53
}
54
55
$action = $request->boolean('successful') ? 'server:backup.complete' : 'server:backup.fail';
56
$log = Activity::event($action)->subject($model, $model->server)->property('name', $model->name);
57
58
$log->transaction(function () use ($model, $request) {
59
$successful = $request->boolean('successful');
60
61
$model->fill([
62
'is_successful' => $successful,
63
// Change the lock state to unlocked if this was a failed backup so that it can be
64
// deleted easily. Also does not make sense to have a locked backup on the system
65
// that is failed.
66
'is_locked' => $successful ? $model->is_locked : false,
67
'checksum' => $successful ? ($request->input('checksum_type') . ':' . $request->input('checksum')) : null,
68
'bytes' => $successful ? $request->input('size') : 0,
69
'completed_at' => CarbonImmutable::now(),
70
])->save();
71
72
// Check if we are using the s3 backup adapter. If so, make sure we mark the backup as
73
// being completed in S3 correctly.
74
$adapter = $this->backupManager->adapter();
75
if ($adapter instanceof S3Filesystem) {
76
$this->completeMultipartUpload($model, $adapter, $successful, $request->input('parts'));
77
}
78
});
79
80
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
81
}
82
83
/**
84
* Handles toggling the restoration status of a server. The server status field should be
85
* set back to null, even if the restoration failed. This is not an unsolvable state for
86
* the server, and the user can keep trying to restore, or just use the reinstall button.
87
*
88
* The only thing the successful field does is update the entry value for the audit logs
89
* table tracking for this restoration.
90
*
91
* @throws \Throwable
92
*/
93
public function restore(Request $request, string $backup): JsonResponse
94
{
95
/** @var Backup $model */
96
$model = Backup::query()->where('uuid', $backup)->firstOrFail();
97
98
$model->server->update(['status' => null]);
99
100
Activity::event($request->boolean('successful') ? 'server:backup.restore-complete' : 'server.backup.restore-failed')
101
->subject($model, $model->server)
102
->property('name', $model->name)
103
->log();
104
105
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
106
}
107
108
/**
109
* Marks a multipart upload in a given S3-compatible instance as failed or successful for
110
* the given backup.
111
*
112
* @throws \Exception
113
* @throws DisplayException
114
*/
115
protected function completeMultipartUpload(Backup $backup, S3Filesystem $adapter, bool $successful, ?array $parts): void
116
{
117
// This should never really happen, but if it does don't let us fall victim to Amazon's
118
// wildly fun error messaging. Just stop the process right here.
119
if (empty($backup->upload_id)) {
120
// A failed backup doesn't need to error here, this can happen if the backup encounters
121
// an error before we even start the upload. AWS gives you tooling to clear these failed
122
// multipart uploads as needed too.
123
if (!$successful) {
124
return;
125
}
126
127
throw new DisplayException('Cannot complete backup request: no upload_id present on model.');
128
}
129
130
$params = [
131
'Bucket' => $adapter->getBucket(),
132
'Key' => sprintf('%s/%s.tar.gz', $backup->server->uuid, $backup->uuid),
133
'UploadId' => $backup->upload_id,
134
];
135
136
$client = $adapter->getClient();
137
if (!$successful) {
138
$client->execute($client->getCommand('AbortMultipartUpload', $params));
139
140
return;
141
}
142
143
// Otherwise send a CompleteMultipartUpload request.
144
$params['MultipartUpload'] = [
145
'Parts' => [],
146
];
147
148
if (is_null($parts)) {
149
$params['MultipartUpload']['Parts'] = $client->execute($client->getCommand('ListParts', $params))['Parts'];
150
} else {
151
foreach ($parts as $part) {
152
$params['MultipartUpload']['Parts'][] = [
153
'ETag' => $part['etag'],
154
'PartNumber' => $part['part_number'],
155
];
156
}
157
}
158
159
$client->execute($client->getCommand('CompleteMultipartUpload', $params));
160
}
161
}
162
163