Path: blob/1.0-develop/app/Http/Controllers/Api/Remote/Backups/BackupRemoteUploadController.php
10284 views
<?php12namespace Pterodactyl\Http\Controllers\Api\Remote\Backups;34use Carbon\CarbonImmutable;5use Illuminate\Http\Request;6use Pterodactyl\Models\Backup;7use Illuminate\Http\JsonResponse;8use Pterodactyl\Http\Controllers\Controller;9use Pterodactyl\Extensions\Backups\BackupManager;10use Pterodactyl\Extensions\Filesystem\S3Filesystem;11use Pterodactyl\Exceptions\Http\HttpForbiddenException;12use Symfony\Component\HttpKernel\Exception\ConflictHttpException;13use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;1415class BackupRemoteUploadController extends Controller16{17public const DEFAULT_MAX_PART_SIZE = 5 * 1024 * 1024 * 1024;1819/**20* BackupRemoteUploadController constructor.21*/22public function __construct(private BackupManager $backupManager)23{24}2526/**27* Returns the required presigned urls to upload a backup to S3 cloud storage.28*29* @throws \Exception30* @throws \Throwable31* @throws \Illuminate\Database\Eloquent\ModelNotFoundException32*/33public function __invoke(Request $request, string $backup): JsonResponse34{35// Get the node associated with the request.36/** @var \Pterodactyl\Models\Node $node */37$node = $request->attributes->get('node');3839// Get the size query parameter.40$size = (int) $request->query('size');41if (empty($size)) {42throw new BadRequestHttpException('A non-empty "size" query parameter must be provided.');43}4445$model = Backup::query()->where('uuid', $backup)->firstOrFail();4647// Check that the backup is "owned" by the node making the request. This avoids other nodes48// from messing with backups that they don't own.49$server = $model->server;50if ($server->node_id !== $node->id) {51throw new HttpForbiddenException('You do not have permission to access that backup.');52}5354// Prevent backups that have already been completed from trying to55// be uploaded again.56if (!is_null($model->completed_at)) {57throw new ConflictHttpException('This backup is already in a completed state.');58}5960// Ensure we are using the S3 adapter.61$adapter = $this->backupManager->adapter();62if (!$adapter instanceof S3Filesystem) {63throw new BadRequestHttpException('The configured backup adapter is not an S3 compatible adapter.');64}6566// The path where backup will be uploaded to67$path = sprintf('%s/%s.tar.gz', $model->server->uuid, $model->uuid);6869// Get the S3 client70$client = $adapter->getClient();71$expires = CarbonImmutable::now()->addMinutes(config('backups.presigned_url_lifespan', 60));7273// Params for generating the presigned urls74$params = [75'Bucket' => $adapter->getBucket(),76'Key' => $path,77'ContentType' => 'application/x-gzip',78];7980$storageClass = config('backups.disks.s3.storage_class');81if (!is_null($storageClass)) {82$params['StorageClass'] = $storageClass;83}8485// Execute the CreateMultipartUpload request86$result = $client->execute($client->getCommand('CreateMultipartUpload', $params));8788// Get the UploadId from the CreateMultipartUpload request, this is needed to create89// the other presigned urls.90$params['UploadId'] = $result->get('UploadId');9192// Retrieve configured part size93$maxPartSize = $this->getConfiguredMaxPartSize();9495// Create as many UploadPart presigned urls as needed96$parts = [];97for ($i = 0; $i < ($size / $maxPartSize); ++$i) {98$parts[] = $client->createPresignedRequest(99$client->getCommand('UploadPart', array_merge($params, ['PartNumber' => $i + 1])),100$expires101)->getUri()->__toString();102}103104// Set the upload_id on the backup in the database.105$model->update(['upload_id' => $params['UploadId']]);106107return new JsonResponse([108'parts' => $parts,109'part_size' => $maxPartSize,110]);111}112113/**114* Get the configured maximum size of a single part in the multipart upload.115*116* The function tries to retrieve a configured value from the configuration.117* If no value is specified, a fallback value will be used.118*119* Note if the received config cannot be converted to int (0), is zero or is negative,120* the fallback value will be used too.121*122* The fallback value is {@see BackupRemoteUploadController::DEFAULT_MAX_PART_SIZE}.123*/124private function getConfiguredMaxPartSize(): int125{126$maxPartSize = (int) config('backups.max_part_size', self::DEFAULT_MAX_PART_SIZE);127if ($maxPartSize <= 0) {128$maxPartSize = self::DEFAULT_MAX_PART_SIZE;129}130131return $maxPartSize;132}133}134135136