diff --git a/app/Http/Controllers/Api/Remote/Backups/BackupSizeController.php b/app/Http/Controllers/Api/Remote/Backups/BackupSizeController.php new file mode 100644 index 000000000..ad5fc0958 --- /dev/null +++ b/app/Http/Controllers/Api/Remote/Backups/BackupSizeController.php @@ -0,0 +1,145 @@ +attributes->get('node'); + + // Find the server + /** @var Server $server */ + $server = Server::query() + ->where('uuid', $uuid) + ->firstOrFail(); + + // Check that the server belongs to the node making the request + if ($server->node_id !== $node->id) { + throw new HttpForbiddenException('You do not have permission to access that server.'); + } + + // Validate the request data + $validatedData = $request->validate([ + 'server_uuid' => ['required', 'string', Rule::in([$uuid])], + 'backups' => ['required', 'array', 'min:1'], + 'backups.*.backup_uuid' => ['required', 'string', 'uuid'], + 'backups.*.new_size' => ['required', 'integer', 'min:0'], + ]); + + $updatedCount = 0; + $errors = []; + $backupsToUpdate = []; + + // First pass: validate all backups and prepare updates + foreach ($validatedData['backups'] as $backupData) { + /** @var Backup $backup */ + $backup = Backup::query() + ->where('uuid', $backupData['backup_uuid']) + ->where('server_id', $server->id) + ->first(); + + if (!$backup) { + $errors[] = [ + 'backup_uuid' => $backupData['backup_uuid'], + 'error' => 'Backup not found or does not belong to this server' + ]; + continue; + } + + // Only update successful backups + if (!$backup->is_successful) { + $errors[] = [ + 'backup_uuid' => $backupData['backup_uuid'], + 'error' => 'Cannot update size of unsuccessful backup' + ]; + continue; + } + + $backupsToUpdate[] = [ + 'backup' => $backup, + 'old_size' => $backup->bytes, + 'new_size' => $backupData['new_size'], + ]; + } + + // If we have validation errors but some backups can be updated, proceed with partial update + // If ALL backups failed validation, return early without making any changes + if (empty($backupsToUpdate)) { + return new JsonResponse([ + 'updated_count' => 0, + 'total_requested' => count($validatedData['backups']), + 'errors' => $errors, + ], JsonResponse::HTTP_BAD_REQUEST); + } + + // Second pass: perform all updates in a transaction for atomicity + try { + \DB::transaction(function () use ($backupsToUpdate, &$updatedCount, $server) { + foreach ($backupsToUpdate as $updateData) { + $backup = $updateData['backup']; + $oldSize = $updateData['old_size']; + $newSize = $updateData['new_size']; + + $backup->update(['bytes' => $newSize]); + $updatedCount++; + + \Log::info('Updated backup size after deduplication recalculation', [ + 'backup_uuid' => $backup->uuid, + 'server_uuid' => $server->uuid, + 'old_size' => $oldSize, + 'new_size' => $newSize, + 'size_difference' => $newSize - $oldSize, + ]); + } + }); + } catch (\Exception $e) { + \Log::error('Failed to update backup sizes in transaction', [ + 'server_uuid' => $server->uuid, + 'error' => $e->getMessage(), + ]); + + return new JsonResponse([ + 'updated_count' => 0, + 'total_requested' => count($validatedData['backups']), + 'errors' => [['error' => 'Transaction failed: ' . $e->getMessage()]], + ], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); + } + + \Log::info('Backup size recalculation completed', [ + 'server_uuid' => $server->uuid, + 'total_backups' => count($validatedData['backups']), + 'updated_count' => $updatedCount, + 'error_count' => count($errors), + ]); + + $responseData = [ + 'updated_count' => $updatedCount, + 'total_requested' => count($validatedData['backups']), + ]; + + if (!empty($errors)) { + $responseData['errors'] = $errors; + } + + $statusCode = $updatedCount > 0 ? JsonResponse::HTTP_OK : JsonResponse::HTTP_BAD_REQUEST; + return new JsonResponse($responseData, $statusCode); + } +} \ No newline at end of file diff --git a/routes/api-remote.php b/routes/api-remote.php index 29c8452fa..4f9b4f14b 100644 --- a/routes/api-remote.php +++ b/routes/api-remote.php @@ -6,6 +6,7 @@ use Pterodactyl\Http\Controllers\Api\Remote\RusticConfigController; use Pterodactyl\Http\Controllers\Api\Remote\SftpAuthenticationController; use Pterodactyl\Http\Controllers\Api\Remote\Backups\BackupDeleteController; use Pterodactyl\Http\Controllers\Api\Remote\Backups\BackupRemoteUploadController; +use Pterodactyl\Http\Controllers\Api\Remote\Backups\BackupSizeController; use Pterodactyl\Http\Controllers\Api\Remote\Backups\BackupStatusController; use Pterodactyl\Http\Controllers\Api\Remote\Servers\ServerDetailsController; use Pterodactyl\Http\Controllers\Api\Remote\Servers\ServerInstallController; @@ -24,6 +25,7 @@ Route::group(['prefix' => '/servers/{uuid}'], function () { Route::post('/install', [ServerInstallController::class, 'store']); Route::get('/rustic-config', [RusticConfigController::class, 'show']); + Route::post('/backup-sizes', [BackupSizeController::class, 'update']); Route::get('/transfer/failure', [ServerTransferController::class, 'failure']); Route::get('/transfer/success', [ServerTransferController::class, 'success']);