2020-04-04 10:59:25 -07:00
< ? php
2024-03-12 22:39:16 -04:00
namespace App\Http\Controllers\Api\Client\Servers ;
2020-04-04 10:59:25 -07:00
2024-04-18 03:50:20 -04:00
use App\Enums\ServerState ;
2025-12-11 14:34:27 +01:00
use App\Enums\SubuserPermission ;
2026-07-08 13:06:59 +02:00
use App\Extensions\BackupAdapter\BackupAdapterService ;
2024-03-12 22:39:16 -04:00
use App\Facades\Activity ;
2025-09-24 13:34:19 +02:00
use App\Http\Controllers\Api\Client\ClientApiController ;
use App\Http\Requests\Api\Client\Servers\Backups\RenameBackupRequest ;
use App\Http\Requests\Api\Client\Servers\Backups\RestoreBackupRequest ;
use App\Http\Requests\Api\Client\Servers\Backups\StoreBackupRequest ;
use App\Models\Backup ;
use App\Models\Server ;
use App\Repositories\Daemon\DaemonBackupRepository ;
2024-03-12 22:39:16 -04:00
use App\Services\Backups\DeleteBackupService ;
use App\Services\Backups\DownloadLinkService ;
use App\Services\Backups\InitiateBackupService ;
use App\Transformers\Api\Client\BackupTransformer ;
2025-02-26 16:12:19 +01:00
use Dedoc\Scramble\Attributes\Group ;
2025-09-24 13:34:19 +02:00
use Illuminate\Auth\Access\AuthorizationException ;
use Illuminate\Http\JsonResponse ;
use Illuminate\Http\Request ;
use Spatie\Fractalistic\Exceptions\InvalidTransformation ;
use Spatie\Fractalistic\Exceptions\NoTransformerSpecified ;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException ;
use Throwable ;
2020-04-04 10:59:25 -07:00
2025-02-26 16:12:19 +01:00
#[Group('Server - Backup')]
2020-04-04 10:59:25 -07:00
class BackupController extends ClientApiController
{
2020-04-09 22:35:38 -07:00
public function __construct (
2025-01-16 14:53:50 -05:00
private readonly DaemonBackupRepository $daemonRepository ,
private readonly DeleteBackupService $deleteBackupService ,
private readonly InitiateBackupService $initiateBackupService ,
private readonly DownloadLinkService $downloadLinkService ,
2026-07-08 13:06:59 +02:00
private readonly BackupAdapterService $backupService
2020-04-09 22:35:38 -07:00
) {
2020-04-04 10:59:25 -07:00
parent :: __construct ();
}
/**
2025-02-26 16:12:19 +01:00
* List backups
*
2025-03-03 14:41:19 -05:00
* Returns all the backups for a given server instance in a paginated result set .
*
* @ return array < array - key , mixed >
2020-04-04 10:59:25 -07:00
*
2025-01-16 14:53:50 -05:00
* @ throws AuthorizationException
2020-04-04 10:59:25 -07:00
*/
2021-01-25 19:25:15 -08:00
public function index ( Request $request , Server $server ) : array
2020-04-04 10:59:25 -07:00
{
2026-07-09 09:55:11 -04:00
throw_unless ( $request -> user () -> can ( SubuserPermission :: BackupRead , $server ), new AuthorizationException ());
2021-01-17 17:51:09 -08:00
2020-12-06 12:01:42 -08:00
$limit = min ( $request -> query ( 'per_page' ) ? ? 20 , 50 );
2021-01-17 11:46:08 -08:00
2020-12-06 12:01:42 -08:00
return $this -> fractal -> collection ( $server -> backups () -> paginate ( $limit ))
2020-04-04 10:59:25 -07:00
-> transformWith ( $this -> getTransformer ( BackupTransformer :: class ))
2021-08-04 21:34:00 -06:00
-> addMeta ([
2024-03-17 13:46:01 -04:00
'backup_count' => $server -> backups () -> nonFailed () -> count (),
2021-08-04 21:34:00 -06:00
])
2020-04-04 10:59:25 -07:00
-> toArray ();
}
/**
2025-02-26 16:12:19 +01:00
* Create backup
*
2020-04-04 10:59:25 -07:00
* Starts the backup process for a server .
*
2025-03-03 14:41:19 -05:00
* @ return array < array - key , mixed >
*
2025-09-08 13:12:33 -04:00
* @ throws InvalidTransformation
* @ throws NoTransformerSpecified
* @ throws Throwable
2020-04-04 10:59:25 -07:00
*/
2021-01-25 19:25:15 -08:00
public function store ( StoreBackupRequest $request , Server $server ) : array
2020-04-04 10:59:25 -07:00
{
2022-05-29 16:19:04 -04:00
$action = $this -> initiateBackupService
-> setIgnoredFiles ( explode ( PHP_EOL , $request -> input ( 'ignored' ) ? ? '' ));
// Only set the lock status if the user even has permission to delete backups,
// otherwise ignore this status. This gets a little funky since it isn't clear
// how best to allow a user to create a backup that is locked without also preventing
// them from just filling up a server with backups that can never be deleted?
2025-12-11 14:34:27 +01:00
if ( $request -> user () -> can ( SubuserPermission :: BackupDelete , $server )) {
2026-01-13 08:39:50 +01:00
$action -> setIsLocked ( $request -> boolean ( 'is_locked' ));
2022-05-29 16:19:04 -04:00
}
2021-01-17 11:46:08 -08:00
2026-01-13 08:39:50 +01:00
$backup = Activity :: event ( 'server:backup.start' ) -> transaction ( function ( $log ) use ( $action , $server , $request ) {
2026-04-20 17:25:54 +02:00
$server -> backups () -> lockForUpdate () -> count ();
2021-01-17 11:46:08 -08:00
2026-01-13 08:39:50 +01:00
$backup = $action -> handle ( $server , $request -> input ( 'name' ));
$log -> subject ( $backup ) -> property ([
'name' => $backup -> name ,
'locked' => $request -> boolean ( 'is_locked' ),
]);
return $backup ;
});
2020-04-04 12:26:39 -07:00
return $this -> fractal -> item ( $backup )
-> transformWith ( $this -> getTransformer ( BackupTransformer :: class ))
-> toArray ();
2020-04-04 10:59:25 -07:00
}
2021-05-03 21:26:09 -07:00
/**
2025-02-26 16:12:19 +01:00
* Toggle lock
*
2021-05-03 21:26:09 -07:00
* Toggles the lock status of a given backup for a server .
*
2025-03-03 14:41:19 -05:00
* @ return array < array - key , mixed >
*
2025-09-08 13:12:33 -04:00
* @ throws Throwable
* @ throws AuthorizationException
2021-05-03 21:26:09 -07:00
*/
public function toggleLock ( Request $request , Server $server , Backup $backup ) : array
{
2026-07-09 09:55:11 -04:00
throw_unless ( $request -> user () -> can ( SubuserPermission :: BackupDelete , $server ), new AuthorizationException ());
2021-05-03 21:26:09 -07:00
2022-05-29 16:19:04 -04:00
$action = $backup -> is_locked ? 'server:backup.unlock' : 'server:backup.lock' ;
2021-05-03 21:26:09 -07:00
2022-05-29 16:19:04 -04:00
$backup -> update ([ 'is_locked' => ! $backup -> is_locked ]);
2021-05-03 21:26:09 -07:00
2022-05-29 16:19:04 -04:00
Activity :: event ( $action ) -> subject ( $backup ) -> property ( 'name' , $backup -> name ) -> log ();
2021-05-03 21:26:09 -07:00
return $this -> fractal -> item ( $backup )
-> transformWith ( $this -> getTransformer ( BackupTransformer :: class ))
-> toArray ();
}
2020-04-09 22:08:09 -07:00
/**
2025-02-26 16:12:19 +01:00
* View backup
*
2020-04-09 22:08:09 -07:00
* Returns information about a single backup .
*
2025-03-03 14:41:19 -05:00
* @ return array < array - key , mixed >
*
2025-09-08 13:12:33 -04:00
* @ throws AuthorizationException
2020-04-09 22:08:09 -07:00
*/
2021-01-25 19:25:15 -08:00
public function view ( Request $request , Server $server , Backup $backup ) : array
2020-04-04 10:59:25 -07:00
{
2026-07-09 09:55:11 -04:00
throw_unless ( $request -> user () -> can ( SubuserPermission :: BackupRead , $server ), new AuthorizationException ());
2021-01-17 17:51:09 -08:00
2020-04-09 22:08:09 -07:00
return $this -> fractal -> item ( $backup )
-> transformWith ( $this -> getTransformer ( BackupTransformer :: class ))
-> toArray ();
2020-04-04 10:59:25 -07:00
}
2020-04-09 22:08:09 -07:00
/**
2025-02-26 16:12:19 +01:00
* Delete backup
*
2020-04-09 22:08:09 -07:00
* Deletes a backup from the panel as well as the remote source where it is currently
* being stored .
*
2025-09-08 13:12:33 -04:00
* @ throws Throwable
2020-04-09 22:08:09 -07:00
*/
2021-01-25 19:25:15 -08:00
public function delete ( Request $request , Server $server , Backup $backup ) : JsonResponse
2020-04-04 10:59:25 -07:00
{
2026-07-09 09:55:11 -04:00
throw_unless ( $request -> user () -> can ( SubuserPermission :: BackupDelete , $server ), new AuthorizationException ());
2021-01-17 17:51:09 -08:00
2022-05-29 16:19:04 -04:00
$this -> deleteBackupService -> handle ( $backup );
2021-01-17 15:25:49 -08:00
2022-05-29 16:19:04 -04:00
Activity :: event ( 'server:backup.delete' )
-> subject ( $backup )
-> property ([ 'name' => $backup -> name , 'failed' => ! $backup -> is_successful ])
-> log ();
2020-04-04 10:59:25 -07:00
2021-01-17 11:46:08 -08:00
return new JsonResponse ([], JsonResponse :: HTTP_NO_CONTENT );
2020-04-04 10:59:25 -07:00
}
2021-01-17 17:51:09 -08:00
/**
2025-02-26 16:12:19 +01:00
* Download backup
*
2021-01-17 17:51:09 -08:00
* Download the backup for a given server instance . For daemon local files , the file
* will be streamed back through the Panel . For AWS S3 files , a signed URL will be generated
* which the user is redirected to .
2021-01-30 19:12:22 -08:00
*
2025-09-08 13:12:33 -04:00
* @ throws Throwable
* @ throws AuthorizationException
2021-01-17 17:51:09 -08:00
*/
2021-01-25 19:25:15 -08:00
public function download ( Request $request , Server $server , Backup $backup ) : JsonResponse
2021-01-17 17:51:09 -08:00
{
2026-07-09 09:55:11 -04:00
throw_unless ( $request -> user () -> can ( SubuserPermission :: BackupDownload , $server ), new AuthorizationException ());
2021-01-17 17:51:09 -08:00
2026-07-08 13:06:59 +02:00
$schema = $this -> backupService -> get ( $backup -> backupHost -> schema );
if ( ! $schema ) {
2021-03-07 09:45:27 -08:00
throw new BadRequestHttpException ( 'The backup requested references an unknown disk driver type and cannot be downloaded.' );
2021-01-17 17:51:09 -08:00
}
2021-03-07 09:45:27 -08:00
$url = $this -> downloadLinkService -> handle ( $backup , $request -> user ());
2022-05-29 16:19:04 -04:00
Activity :: event ( 'server:backup.download' ) -> subject ( $backup ) -> property ( 'name' , $backup -> name ) -> log ();
2021-03-07 09:45:27 -08:00
return new JsonResponse ([
'object' => 'signed_url' ,
'attributes' => [ 'url' => $url ],
]);
2021-01-17 17:51:09 -08:00
}
2025-07-31 23:47:15 +02:00
/**
* Rename backup
*
* Updates the name of a backup for a server instance .
*
* @ return array < array - key , mixed >
*
2025-09-08 13:12:33 -04:00
* @ throws Throwable
* @ throws AuthorizationException
2025-07-31 23:47:15 +02:00
*/
public function rename ( RenameBackupRequest $request , Server $server , Backup $backup ) : array
{
$oldName = $backup -> name ;
$newName = $request -> input ( 'name' );
$backup -> update ([ 'name' => $newName ]);
if ( $oldName !== $newName ) {
Activity :: event ( 'server:backup.rename' )
-> subject ( $backup )
-> property ([ 'old_name' => $oldName , 'new_name' => $newName ])
-> log ();
}
return $this -> fractal -> item ( $backup )
-> transformWith ( $this -> getTransformer ( BackupTransformer :: class ))
-> toArray ();
}
2021-01-17 17:51:09 -08:00
/**
2025-02-26 16:12:19 +01:00
* Restore backup
*
2024-03-12 22:39:16 -04:00
* Handles restoring a backup by making a request to the daemon instance telling it
2021-01-17 17:51:09 -08:00
* to begin the process of finding ( or downloading ) the backup and unpacking it
* over the server files .
*
2022-10-14 10:59:20 -06:00
* If the " truncate " flag is passed through in this request then all the
2021-01-17 17:51:09 -08:00
* files that currently exist on the server will be deleted before restoring .
2022-10-14 10:59:20 -06:00
* Otherwise , the archive will simply be unpacked over the existing files .
2021-01-17 17:51:09 -08:00
*
2025-09-08 13:12:33 -04:00
* @ throws Throwable
2021-01-17 17:51:09 -08:00
*/
2023-02-23 19:23:12 +00:00
public function restore ( RestoreBackupRequest $request , Server $server , Backup $backup ) : JsonResponse
2021-01-17 17:51:09 -08:00
{
// Cannot restore a backup unless a server is fully installed and not currently
// processing a different backup restoration request.
2026-07-09 09:55:11 -04:00
throw_unless ( is_null ( $server -> status ), new BadRequestHttpException ( 'This server is not currently in a state that allows for a backup to be restored.' ));
2021-01-17 17:51:09 -08:00
2026-07-09 09:55:11 -04:00
throw_if ( ! $backup -> is_successful && is_null ( $backup -> completed_at ), new BadRequestHttpException ( 'This backup cannot be restored at this time: not completed or failed.' ));
2021-01-18 20:11:49 -08:00
2022-05-29 16:19:04 -04:00
$log = Activity :: event ( 'server:backup.restore' )
-> subject ( $backup )
-> property ([ 'name' => $backup -> name , 'truncate' => $request -> input ( 'truncate' )]);
2021-01-17 17:51:09 -08:00
2022-05-29 16:19:04 -04:00
$log -> transaction ( function () use ( $backup , $server , $request ) {
2026-07-08 13:06:59 +02:00
$url = $this -> downloadLinkService -> handle ( $backup , $request -> user ());
2021-01-17 17:51:09 -08:00
// Update the status right away for the server so that we know not to allow certain
// actions against it via the Panel API.
2024-04-18 03:50:20 -04:00
$server -> update ([ 'status' => ServerState :: RestoringBackup ]);
2021-01-17 17:51:09 -08:00
2026-07-08 13:06:59 +02:00
$this -> daemonRepository -> setServer ( $server ) -> restore ( $backup , $url , $request -> input ( 'truncate' ));
2021-01-17 17:51:09 -08:00
});
return new JsonResponse ([], JsonResponse :: HTTP_NO_CONTENT );
}
2020-04-04 10:59:25 -07:00
}