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/BackupRemoteUploadController.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\Http\Controllers\Controller;
10
use Pterodactyl\Extensions\Backups\BackupManager;
11
use Pterodactyl\Extensions\Filesystem\S3Filesystem;
12
use Pterodactyl\Exceptions\Http\HttpForbiddenException;
13
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
14
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
15
16
class BackupRemoteUploadController extends Controller
17
{
18
public const DEFAULT_MAX_PART_SIZE = 5 * 1024 * 1024 * 1024;
19
20
/**
21
* BackupRemoteUploadController constructor.
22
*/
23
public function __construct(private BackupManager $backupManager)
24
{
25
}
26
27
/**
28
* Returns the required presigned urls to upload a backup to S3 cloud storage.
29
*
30
* @throws \Exception
31
* @throws \Throwable
32
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
33
*/
34
public function __invoke(Request $request, string $backup): JsonResponse
35
{
36
// Get the node associated with the request.
37
/** @var \Pterodactyl\Models\Node $node */
38
$node = $request->attributes->get('node');
39
40
// Get the size query parameter.
41
$size = (int) $request->query('size');
42
if (empty($size)) {
43
throw new BadRequestHttpException('A non-empty "size" query parameter must be provided.');
44
}
45
46
$model = Backup::query()->where('uuid', $backup)->firstOrFail();
47
48
// Check that the backup is "owned" by the node making the request. This avoids other nodes
49
// from messing with backups that they don't own.
50
$server = $model->server;
51
if ($server->node_id !== $node->id) {
52
throw new HttpForbiddenException('You do not have permission to access that backup.');
53
}
54
55
// Prevent backups that have already been completed from trying to
56
// be uploaded again.
57
if (!is_null($model->completed_at)) {
58
throw new ConflictHttpException('This backup is already in a completed state.');
59
}
60
61
// Ensure we are using the S3 adapter.
62
$adapter = $this->backupManager->adapter();
63
if (!$adapter instanceof S3Filesystem) {
64
throw new BadRequestHttpException('The configured backup adapter is not an S3 compatible adapter.');
65
}
66
67
// The path where backup will be uploaded to
68
$path = sprintf('%s/%s.tar.gz', $model->server->uuid, $model->uuid);
69
70
// Get the S3 client
71
$client = $adapter->getClient();
72
$expires = CarbonImmutable::now()->addMinutes(config('backups.presigned_url_lifespan', 60));
73
74
// Params for generating the presigned urls
75
$params = [
76
'Bucket' => $adapter->getBucket(),
77
'Key' => $path,
78
'ContentType' => 'application/x-gzip',
79
];
80
81
$storageClass = config('backups.disks.s3.storage_class');
82
if (!is_null($storageClass)) {
83
$params['StorageClass'] = $storageClass;
84
}
85
86
// Execute the CreateMultipartUpload request
87
$result = $client->execute($client->getCommand('CreateMultipartUpload', $params));
88
89
// Get the UploadId from the CreateMultipartUpload request, this is needed to create
90
// the other presigned urls.
91
$params['UploadId'] = $result->get('UploadId');
92
93
// Retrieve configured part size
94
$maxPartSize = $this->getConfiguredMaxPartSize();
95
96
// Create as many UploadPart presigned urls as needed
97
$parts = [];
98
for ($i = 0; $i < ($size / $maxPartSize); ++$i) {
99
$parts[] = $client->createPresignedRequest(
100
$client->getCommand('UploadPart', array_merge($params, ['PartNumber' => $i + 1])),
101
$expires
102
)->getUri()->__toString();
103
}
104
105
// Set the upload_id on the backup in the database.
106
$model->update(['upload_id' => $params['UploadId']]);
107
108
return new JsonResponse([
109
'parts' => $parts,
110
'part_size' => $maxPartSize,
111
]);
112
}
113
114
/**
115
* Get the configured maximum size of a single part in the multipart upload.
116
*
117
* The function tries to retrieve a configured value from the configuration.
118
* If no value is specified, a fallback value will be used.
119
*
120
* Note if the received config cannot be converted to int (0), is zero or is negative,
121
* the fallback value will be used too.
122
*
123
* The fallback value is {@see BackupRemoteUploadController::DEFAULT_MAX_PART_SIZE}.
124
*/
125
private function getConfiguredMaxPartSize(): int
126
{
127
$maxPartSize = (int) config('backups.max_part_size', self::DEFAULT_MAX_PART_SIZE);
128
if ($maxPartSize <= 0) {
129
$maxPartSize = self::DEFAULT_MAX_PART_SIZE;
130
}
131
132
return $maxPartSize;
133
}
134
}
135
136