2024-01-06 05:24:57 +00:00
< ? php
namespace App\Livewire\Project\Database ;
2025-11-02 14:19:13 +00:00
use App\Models\S3Storage ;
2024-01-06 05:24:57 +00:00
use App\Models\Server ;
2026-01-02 15:29:48 +00:00
use App\Models\Service ;
2025-08-23 16:50:35 +00:00
use Illuminate\Foundation\Auth\Access\AuthorizesRequests ;
2024-11-04 13:18:16 +00:00
use Illuminate\Support\Facades\Auth ;
2024-01-10 14:42:54 +00:00
use Illuminate\Support\Facades\Storage ;
2026-01-02 15:29:48 +00:00
use Livewire\Attributes\Computed ;
2024-06-10 20:43:34 +00:00
use Livewire\Component ;
2024-01-06 05:24:57 +00:00
class Import extends Component
{
2025-08-23 16:50:35 +00:00
use AuthorizesRequests ;
2025-11-25 15:40:35 +00:00
/**
* Validate that a string is safe for use as an S3 bucket name .
* Allows alphanumerics , dots , dashes , and underscores .
*/
private function validateBucketName ( string $bucket ) : bool
{
return preg_match ( '/^[a-zA-Z0-9.\-_]+$/' , $bucket ) === 1 ;
}
/**
* Validate that a string is safe for use as an S3 path .
* Allows alphanumerics , dots , dashes , underscores , slashes , and common file characters .
*/
private function validateS3Path ( string $path ) : bool
{
// Must not be empty
if ( empty ( $path )) {
return false ;
}
// Must not contain dangerous shell metacharacters or command injection patterns
$dangerousPatterns = [
'..' , // Directory traversal
'$(' , // Command substitution
'`' , // Backtick command substitution
'|' , // Pipe
';' , // Command separator
'&' , // Background/AND
'>' , // Redirect
'<' , // Redirect
" \n " , // Newline
" \r " , // Carriage return
" \0 " , // Null byte
" ' " , // Single quote
'"' , // Double quote
'\\' , // Backslash
];
foreach ( $dangerousPatterns as $pattern ) {
if ( str_contains ( $path , $pattern )) {
return false ;
}
}
// Allow alphanumerics, dots, dashes, underscores, slashes, spaces, plus, equals, at
return preg_match ( '/^[a-zA-Z0-9.\-_\/\s+@=]+$/' , $path ) === 1 ;
}
/**
* Validate that a string is safe for use as a file path on the server .
*/
private function validateServerPath ( string $path ) : bool
{
// Must be an absolute path
if ( ! str_starts_with ( $path , '/' )) {
return false ;
}
// Must not contain dangerous shell metacharacters or command injection patterns
$dangerousPatterns = [
'..' , // Directory traversal
'$(' , // Command substitution
'`' , // Backtick command substitution
'|' , // Pipe
';' , // Command separator
'&' , // Background/AND
'>' , // Redirect
'<' , // Redirect
" \n " , // Newline
" \r " , // Carriage return
" \0 " , // Null byte
" ' " , // Single quote
'"' , // Double quote
'\\' , // Backslash
];
foreach ( $dangerousPatterns as $pattern ) {
if ( str_contains ( $path , $pattern )) {
return false ;
}
}
// Allow alphanumerics, dots, dashes, underscores, slashes, and spaces
return preg_match ( '/^[a-zA-Z0-9.\-_\/\s]+$/' , $path ) === 1 ;
}
2024-04-11 11:20:46 +00:00
public bool $unsupported = false ;
2024-06-10 20:43:34 +00:00
2026-01-02 15:29:48 +00:00
// Store IDs instead of models for proper Livewire serialization
public ? int $resourceId = null ;
2024-06-10 20:43:34 +00:00
2026-01-02 15:29:48 +00:00
public ? string $resourceType = null ;
2024-06-10 20:43:34 +00:00
2026-01-02 15:29:48 +00:00
public ? int $serverId = null ;
// View-friendly properties to avoid computed property access in Blade
public string $resourceUuid = '' ;
public string $resourceStatus = '' ;
public string $resourceDbType = '' ;
public array $parameters = [];
public array $containers = [];
2024-06-10 20:43:34 +00:00
2024-01-06 05:24:57 +00:00
public bool $scpInProgress = false ;
2024-06-10 20:43:34 +00:00
2024-01-06 05:24:57 +00:00
public bool $importRunning = false ;
2024-04-11 10:13:11 +00:00
public ? string $filename = null ;
2024-06-10 20:43:34 +00:00
2024-04-11 10:13:11 +00:00
public ? string $filesize = null ;
2024-06-10 20:43:34 +00:00
2024-04-11 10:13:11 +00:00
public bool $isUploading = false ;
2024-06-10 20:43:34 +00:00
2024-04-11 10:13:11 +00:00
public int $progress = 0 ;
2024-06-10 20:43:34 +00:00
2024-04-11 10:13:11 +00:00
public bool $error = false ;
2024-01-06 05:24:57 +00:00
public string $container ;
2024-06-10 20:43:34 +00:00
2024-01-06 05:24:57 +00:00
public array $importCommands = [];
2024-06-10 20:43:34 +00:00
2025-01-07 12:00:41 +00:00
public bool $dumpAll = false ;
public string $restoreCommandText = '' ;
2025-01-07 13:02:19 +00:00
public string $customLocation = '' ;
2025-11-26 09:43:07 +00:00
public ? int $activityId = null ;
2026-01-19 18:08:32 +00:00
public string $postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d ${POSTGRES_DB:-${POSTGRES_USER:-postgres}}' ;
2024-06-10 20:43:34 +00:00
2024-02-24 20:12:34 +00:00
public string $mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE' ;
2024-06-10 20:43:34 +00:00
2024-02-24 20:12:34 +00:00
public string $mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE' ;
2024-06-10 20:43:34 +00:00
2024-04-25 21:44:55 +00:00
public string $mongodbRestoreCommand = 'mongorestore --authenticationDatabase=admin --username $MONGO_INITDB_ROOT_USERNAME --password $MONGO_INITDB_ROOT_PASSWORD --uri mongodb://localhost:27017 --gzip --archive=' ;
2024-01-06 05:24:57 +00:00
2025-11-02 14:19:13 +00:00
// S3 Restore properties
2026-01-02 15:29:48 +00:00
public array $availableS3Storages = [];
2025-11-02 14:19:13 +00:00
public ? int $s3StorageId = null ;
public string $s3Path = '' ;
public ? int $s3FileSize = null ;
2026-01-02 15:29:48 +00:00
#[Computed]
public function resource ()
{
if ( $this -> resourceId === null || $this -> resourceType === null ) {
return null ;
}
return $this -> resourceType :: find ( $this -> resourceId );
}
#[Computed]
public function server ()
{
if ( $this -> serverId === null ) {
return null ;
}
return Server :: find ( $this -> serverId );
}
2024-01-10 14:42:54 +00:00
public function getListeners ()
{
2024-11-04 13:18:16 +00:00
$userId = Auth :: id ();
2024-06-10 20:43:34 +00:00
2024-01-10 14:42:54 +00:00
return [
" echo-private:user. { $userId } ,DatabaseStatusChanged " => '$refresh' ,
2025-11-26 09:43:07 +00:00
'slideOverClosed' => 'resetActivityId' ,
2024-01-10 14:42:54 +00:00
];
}
2024-06-10 20:43:34 +00:00
2025-11-26 09:43:07 +00:00
public function resetActivityId ()
{
$this -> activityId = null ;
}
2024-01-06 05:24:57 +00:00
public function mount ()
{
$this -> parameters = get_route_parameters ();
$this -> getContainers ();
2025-11-02 14:19:13 +00:00
$this -> loadAvailableS3Storages ();
2024-01-06 05:24:57 +00:00
}
2025-01-07 12:00:41 +00:00
public function updatedDumpAll ( $value )
{
2025-12-09 07:40:19 +00:00
$morphClass = $this -> resource -> getMorphClass ();
2026-01-02 15:29:48 +00:00
2025-12-09 07:40:19 +00:00
// Handle ServiceDatabase by checking the database type
if ( $morphClass === \App\Models\ServiceDatabase :: class ) {
$dbType = $this -> resource -> databaseType ();
if ( str_contains ( $dbType , 'mysql' )) {
$morphClass = 'mysql' ;
} elseif ( str_contains ( $dbType , 'mariadb' )) {
$morphClass = 'mariadb' ;
} elseif ( str_contains ( $dbType , 'postgres' )) {
$morphClass = 'postgresql' ;
}
}
2026-01-02 15:29:48 +00:00
2025-12-09 07:40:19 +00:00
switch ( $morphClass ) {
2025-01-07 14:31:43 +00:00
case \App\Models\StandaloneMariadb :: class :
2025-12-09 07:40:19 +00:00
case 'mariadb' :
2025-01-07 12:00:41 +00:00
if ( $value === true ) {
$this -> mariadbRestoreCommand = <<< 'EOD'
for pid in $ ( mariadb - u root - p $MARIADB_ROOT_PASSWORD - N - e " SELECT id FROM information_schema.processlist WHERE user != 'root'; " ); do
mariadb - u root - p $MARIADB_ROOT_PASSWORD - e " KILL $pid " 2 >/ dev / null || true
done && \
mariadb - u root - p $MARIADB_ROOT_PASSWORD - N - e " SELECT CONCAT('DROP DATABASE IF EXISTS \ `',schema_name,' \ `;') FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema','mysql','performance_schema','sys'); " | mariadb - u root - p $MARIADB_ROOT_PASSWORD && \
2026-01-08 15:29:08 +00:00
mariadb - u root - p $MARIADB_ROOT_PASSWORD - e " CREATE DATABASE IF NOT EXISTS \ ` ${ MARIADB_DATABASE:-default } \ `; " && \
( gunzip - cf $tmpPath 2 >/ dev / null || cat $tmpPath ) | sed - e '/^CREATE DATABASE/d' - e '/^USE \`mysql\`/d' | mariadb - u root - p $MARIADB_ROOT_PASSWORD $ { MARIADB_DATABASE :- default }
2025-01-07 12:00:41 +00:00
EOD ;
2026-01-08 15:29:08 +00:00
$this -> restoreCommandText = $this -> mariadbRestoreCommand . ' && (gunzip -cf <temp_backup_file> 2>/dev/null || cat <temp_backup_file>) | mariadb -u root -p$MARIADB_ROOT_PASSWORD ${MARIADB_DATABASE:-default}' ;
2025-01-07 12:00:41 +00:00
} else {
$this -> mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE' ;
}
break ;
2025-01-07 14:31:43 +00:00
case \App\Models\StandaloneMysql :: class :
2025-12-09 07:40:19 +00:00
case 'mysql' :
2025-01-07 12:00:41 +00:00
if ( $value === true ) {
$this -> mysqlRestoreCommand = <<< 'EOD'
for pid in $ ( mysql - u root - p $MYSQL_ROOT_PASSWORD - N - e " SELECT id FROM information_schema.processlist WHERE user != 'root'; " ); do
mysql - u root - p $MYSQL_ROOT_PASSWORD - e " KILL $pid " 2 >/ dev / null || true
done && \
mysql - u root - p $MYSQL_ROOT_PASSWORD - N - e " SELECT CONCAT('DROP DATABASE IF EXISTS \ `',schema_name,' \ `;') FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema','mysql','performance_schema','sys'); " | mysql - u root - p $MYSQL_ROOT_PASSWORD && \
2026-01-08 15:29:08 +00:00
mysql - u root - p $MYSQL_ROOT_PASSWORD - e " CREATE DATABASE IF NOT EXISTS \ ` ${ MYSQL_DATABASE:-default } \ `; " && \
( gunzip - cf $tmpPath 2 >/ dev / null || cat $tmpPath ) | sed - e '/^CREATE DATABASE/d' - e '/^USE \`mysql\`/d' | mysql - u root - p $MYSQL_ROOT_PASSWORD $ { MYSQL_DATABASE :- default }
2025-01-07 12:00:41 +00:00
EOD ;
2026-01-08 15:29:08 +00:00
$this -> restoreCommandText = $this -> mysqlRestoreCommand . ' && (gunzip -cf <temp_backup_file> 2>/dev/null || cat <temp_backup_file>) | mysql -u root -p$MYSQL_ROOT_PASSWORD ${MYSQL_DATABASE:-default}' ;
2025-01-07 12:00:41 +00:00
} else {
$this -> mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE' ;
}
break ;
2025-01-07 14:31:43 +00:00
case \App\Models\StandalonePostgresql :: class :
2025-12-09 07:40:19 +00:00
case 'postgresql' :
2025-01-07 12:00:41 +00:00
if ( $value === true ) {
$this -> postgresqlRestoreCommand = <<< 'EOD'
2026-01-08 15:29:08 +00:00
psql - U $ { POSTGRES_USER } - c " SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname IS NOT NULL AND pid <> pg_backend_pid() " && \
psql - U $ { POSTGRES_USER } - t - c " SELECT datname FROM pg_database WHERE NOT datistemplate " | xargs - I {} dropdb - U $ { POSTGRES_USER } -- if - exists {} && \
2026-01-19 18:08:32 +00:00
createdb - U $ { POSTGRES_USER } $ { POSTGRES_DB :- $ { POSTGRES_USER :- postgres }}
2025-01-07 12:00:41 +00:00
EOD ;
2026-01-19 18:08:32 +00:00
$this -> restoreCommandText = $this -> postgresqlRestoreCommand . ' && (gunzip -cf <temp_backup_file> 2>/dev/null || cat <temp_backup_file>) | psql -U ${POSTGRES_USER} -d ${POSTGRES_DB:-${POSTGRES_USER:-postgres}}' ;
2025-01-07 12:00:41 +00:00
} else {
2026-01-19 18:08:32 +00:00
$this -> postgresqlRestoreCommand = 'pg_restore -U ${POSTGRES_USER} -d ${POSTGRES_DB:-${POSTGRES_USER:-postgres}}' ;
2025-01-07 12:00:41 +00:00
}
break ;
}
}
2024-01-06 05:24:57 +00:00
public function getContainers ()
{
2026-01-02 15:29:48 +00:00
$this -> containers = [];
$teamId = data_get ( auth () -> user () -> currentTeam (), 'id' );
// Try to find resource by route parameter
$databaseUuid = data_get ( $this -> parameters , 'database_uuid' );
$stackServiceUuid = data_get ( $this -> parameters , 'stack_service_uuid' );
$resource = null ;
if ( $databaseUuid ) {
// Standalone database route
$resource = getResourceByUuid ( $databaseUuid , $teamId );
if ( is_null ( $resource )) {
abort ( 404 );
}
} elseif ( $stackServiceUuid ) {
// ServiceDatabase route - look up the service database
$serviceUuid = data_get ( $this -> parameters , 'service_uuid' );
$service = Service :: whereUuid ( $serviceUuid ) -> first ();
if ( ! $service ) {
abort ( 404 );
}
$resource = $service -> databases () -> whereUuid ( $stackServiceUuid ) -> first ();
if ( is_null ( $resource )) {
abort ( 404 );
}
} else {
2024-04-10 13:00:46 +00:00
abort ( 404 );
2024-01-06 05:24:57 +00:00
}
2026-01-02 15:29:48 +00:00
2025-10-14 15:33:42 +00:00
$this -> authorize ( 'view' , $resource );
2026-01-02 15:29:48 +00:00
// Store IDs for Livewire serialization
$this -> resourceId = $resource -> id ;
$this -> resourceType = get_class ( $resource );
// Store view-friendly properties
$this -> resourceStatus = $resource -> status ? ? '' ;
// Handle ServiceDatabase server access differently
if ( $resource -> getMorphClass () === \App\Models\ServiceDatabase :: class ) {
$server = $resource -> service ? -> server ;
if ( ! $server ) {
abort ( 404 , 'Server not found for this service database.' );
}
$this -> serverId = $server -> id ;
$this -> container = $resource -> name . '-' . $resource -> service -> uuid ;
$this -> resourceUuid = $resource -> uuid ; // Use ServiceDatabase's own UUID
// Determine database type for ServiceDatabase
$dbType = $resource -> databaseType ();
if ( str_contains ( $dbType , 'postgres' )) {
$this -> resourceDbType = 'standalone-postgresql' ;
} elseif ( str_contains ( $dbType , 'mysql' )) {
$this -> resourceDbType = 'standalone-mysql' ;
} elseif ( str_contains ( $dbType , 'mariadb' )) {
$this -> resourceDbType = 'standalone-mariadb' ;
} elseif ( str_contains ( $dbType , 'mongo' )) {
$this -> resourceDbType = 'standalone-mongodb' ;
} else {
$this -> resourceDbType = $dbType ;
}
2025-12-09 07:40:19 +00:00
} else {
2026-01-02 15:29:48 +00:00
$server = $resource -> destination ? -> server ;
if ( ! $server ) {
abort ( 404 , 'Server not found for this database.' );
}
$this -> serverId = $server -> id ;
$this -> container = $resource -> uuid ;
$this -> resourceUuid = $resource -> uuid ;
$this -> resourceDbType = $resource -> type ();
2025-12-09 07:40:19 +00:00
}
2026-01-02 15:29:48 +00:00
if ( str ( $resource -> status ) -> startsWith ( 'running' )) {
$this -> containers [] = $this -> container ;
2024-01-06 05:24:57 +00:00
}
2024-01-10 14:42:54 +00:00
if (
2026-01-02 15:29:48 +00:00
$resource -> getMorphClass () === \App\Models\StandaloneRedis :: class ||
$resource -> getMorphClass () === \App\Models\StandaloneKeydb :: class ||
$resource -> getMorphClass () === \App\Models\StandaloneDragonfly :: class ||
$resource -> getMorphClass () === \App\Models\StandaloneClickhouse :: class
2024-01-10 14:42:54 +00:00
) {
2024-04-11 11:20:46 +00:00
$this -> unsupported = true ;
2024-01-06 05:24:57 +00:00
}
2026-01-02 15:29:48 +00:00
2025-12-09 07:40:19 +00:00
// Mark unsupported ServiceDatabase types (Redis, KeyDB, etc.)
2026-01-02 15:29:48 +00:00
if ( $resource -> getMorphClass () === \App\Models\ServiceDatabase :: class ) {
$dbType = $resource -> databaseType ();
if ( str_contains ( $dbType , 'redis' ) || str_contains ( $dbType , 'keydb' ) ||
2025-12-09 07:40:19 +00:00
str_contains ( $dbType , 'dragonfly' ) || str_contains ( $dbType , 'clickhouse' )) {
$this -> unsupported = true ;
}
}
2024-01-06 05:24:57 +00:00
}
2024-01-10 14:42:54 +00:00
2025-01-07 13:02:19 +00:00
public function checkFile ()
{
if ( filled ( $this -> customLocation )) {
2025-11-25 15:40:35 +00:00
// Validate the custom location to prevent command injection
if ( ! $this -> validateServerPath ( $this -> customLocation )) {
$this -> dispatch ( 'error' , 'Invalid file path. Path must be absolute and contain only safe characters (alphanumerics, dots, dashes, underscores, slashes).' );
return ;
}
2026-01-02 15:29:48 +00:00
if ( ! $this -> server ) {
$this -> dispatch ( 'error' , 'Server not found. Please refresh the page.' );
return ;
}
2025-01-07 13:02:19 +00:00
try {
2025-11-25 15:40:35 +00:00
$escapedPath = escapeshellarg ( $this -> customLocation );
$result = instant_remote_process ([ " ls -l { $escapedPath } " ], $this -> server , throwError : false );
2025-01-07 13:02:19 +00:00
if ( blank ( $result )) {
$this -> dispatch ( 'error' , 'The file does not exist or has been deleted.' );
2025-01-07 14:31:43 +00:00
return ;
2025-01-07 13:02:19 +00:00
}
$this -> filename = $this -> customLocation ;
$this -> dispatch ( 'success' , 'The file exists.' );
2025-01-07 14:31:43 +00:00
} catch ( \Throwable $e ) {
2025-01-07 13:02:19 +00:00
return handleError ( $e , $this );
}
}
}
2026-03-01 11:45:55 +00:00
public function runImport ( string $password = '' ) : bool | string
2024-01-06 05:24:57 +00:00
{
2026-03-01 11:45:55 +00:00
if ( ! verifyPasswordConfirmation ( $password , $this )) {
return 'The provided password is incorrect.' ;
}
2025-08-23 16:50:35 +00:00
$this -> authorize ( 'update' , $this -> resource );
2024-10-31 14:23:19 +00:00
if ( $this -> filename === '' ) {
2024-04-11 10:13:11 +00:00
$this -> dispatch ( 'error' , 'Please select a file to import.' );
2024-06-10 20:43:34 +00:00
2026-03-01 11:45:55 +00:00
return true ;
2024-04-11 10:13:11 +00:00
}
2026-01-02 15:29:48 +00:00
if ( ! $this -> server ) {
$this -> dispatch ( 'error' , 'Server not found. Please refresh the page.' );
2026-03-01 11:45:55 +00:00
return true ;
2026-01-02 15:29:48 +00:00
}
2024-01-06 05:24:57 +00:00
try {
2025-08-26 08:27:31 +00:00
$this -> importRunning = true ;
2025-01-07 13:02:19 +00:00
$this -> importCommands = [];
2026-01-02 15:29:48 +00:00
$backupFileName = " upload/ { $this -> resourceUuid } /restore " ;
2025-01-07 13:02:19 +00:00
2025-11-17 13:13:10 +00:00
// Check if an uploaded file exists first (takes priority over custom location)
if ( Storage :: exists ( $backupFileName )) {
$path = Storage :: path ( $backupFileName );
2026-01-02 15:29:48 +00:00
$tmpPath = '/tmp/' . basename ( $backupFileName ) . '_' . $this -> resourceUuid ;
2025-01-07 13:02:19 +00:00
instant_scp ( $path , $tmpPath , $this -> server );
Storage :: delete ( $backupFileName );
$this -> importCommands [] = " docker cp { $tmpPath } { $this -> container } : { $tmpPath } " ;
2025-11-17 13:13:10 +00:00
} elseif ( filled ( $this -> customLocation )) {
2025-11-25 15:40:35 +00:00
// Validate the custom location to prevent command injection
if ( ! $this -> validateServerPath ( $this -> customLocation )) {
$this -> dispatch ( 'error' , 'Invalid file path. Path must be absolute and contain only safe characters.' );
2026-03-01 11:45:55 +00:00
return true ;
2025-11-25 15:40:35 +00:00
}
2026-01-02 15:29:48 +00:00
$tmpPath = '/tmp/restore_' . $this -> resourceUuid ;
2025-11-25 15:40:35 +00:00
$escapedCustomLocation = escapeshellarg ( $this -> customLocation );
$this -> importCommands [] = " docker cp { $escapedCustomLocation } { $this -> container } : { $tmpPath } " ;
2025-11-17 13:13:10 +00:00
} else {
$this -> dispatch ( 'error' , 'The file does not exist or has been deleted.' );
2026-03-01 11:45:55 +00:00
return true ;
2024-04-11 10:13:11 +00:00
}
2024-01-06 05:24:57 +00:00
2025-01-07 12:00:41 +00:00
// Copy the restore command to a script file
2026-01-02 15:29:48 +00:00
$scriptPath = " /tmp/restore_ { $this -> resourceUuid } .sh " ;
2025-01-07 12:00:41 +00:00
2025-11-25 15:40:35 +00:00
$restoreCommand = $this -> buildRestoreCommand ( $tmpPath );
2024-01-06 05:24:57 +00:00
2025-09-15 15:56:48 +00:00
$restoreCommandBase64 = base64_encode ( $restoreCommand );
$this -> importCommands [] = " echo \" { $restoreCommandBase64 } \" | base64 -d > { $scriptPath } " ;
2025-01-07 12:00:41 +00:00
$this -> importCommands [] = " chmod +x { $scriptPath } " ;
$this -> importCommands [] = " docker cp { $scriptPath } { $this -> container } : { $scriptPath } " ;
$this -> importCommands [] = " docker exec { $this -> container } sh -c ' { $scriptPath } ' " ;
2024-01-06 05:24:57 +00:00
$this -> importCommands [] = " docker exec { $this -> container } sh -c 'echo \" Import finished with exit code $ ? \" ' " ;
2025-01-07 14:31:43 +00:00
if ( ! empty ( $this -> importCommands )) {
2025-01-07 13:02:19 +00:00
$activity = remote_process ( $this -> importCommands , $this -> server , ignore_errors : true , callEventOnFinish : 'RestoreJobFinished' , callEventData : [
'scriptPath' => $scriptPath ,
'tmpPath' => $tmpPath ,
'container' => $this -> container ,
'serverId' => $this -> server -> id ,
]);
2025-11-17 09:05:18 +00:00
2025-11-26 09:43:07 +00:00
// Track the activity ID
$this -> activityId = $activity -> id ;
2025-11-17 09:05:18 +00:00
// Dispatch activity to the monitor and open slide-over
2025-11-02 16:10:34 +00:00
$this -> dispatch ( 'activityMonitor' , $activity -> id );
2025-11-17 09:05:18 +00:00
$this -> dispatch ( 'databaserestore' );
2024-01-06 05:24:57 +00:00
}
2025-01-07 14:31:43 +00:00
} catch ( \Throwable $e ) {
2026-03-01 11:45:55 +00:00
handleError ( $e , $this );
return true ;
2025-01-07 12:00:41 +00:00
} finally {
$this -> filename = null ;
2025-01-07 13:02:19 +00:00
$this -> importCommands = [];
2024-01-06 05:24:57 +00:00
}
2026-03-01 11:45:55 +00:00
return true ;
2024-01-06 05:24:57 +00:00
}
2025-11-02 14:19:13 +00:00
public function loadAvailableS3Storages ()
{
try {
$this -> availableS3Storages = S3Storage :: ownedByCurrentTeam ([ 'id' , 'name' , 'description' ])
-> where ( 'is_usable' , true )
2026-01-02 15:29:48 +00:00
-> get ()
-> map ( fn ( $s ) => [ 'id' => $s -> id , 'name' => $s -> name , 'description' => $s -> description ])
-> toArray ();
2025-11-02 14:19:13 +00:00
} catch ( \Throwable $e ) {
2026-01-02 15:29:48 +00:00
$this -> availableS3Storages = [];
2025-11-02 14:19:13 +00:00
}
}
2025-11-25 09:18:30 +00:00
public function updatedS3Path ( $value )
{
// Reset validation state when path changes
$this -> s3FileSize = null ;
// Ensure path starts with a slash
if ( $value !== null && $value !== '' ) {
$this -> s3Path = str ( $value ) -> trim () -> start ( '/' ) -> value ();
}
}
public function updatedS3StorageId ()
{
// Reset validation state when storage changes
$this -> s3FileSize = null ;
}
2025-11-02 14:19:13 +00:00
public function checkS3File ()
{
if ( ! $this -> s3StorageId ) {
$this -> dispatch ( 'error' , 'Please select an S3 storage.' );
return ;
}
if ( blank ( $this -> s3Path )) {
$this -> dispatch ( 'error' , 'Please provide an S3 path.' );
return ;
}
2025-11-25 15:40:35 +00:00
// Clean the path (remove leading slash if present)
$cleanPath = ltrim ( $this -> s3Path , '/' );
// Validate the S3 path early to prevent command injection in subsequent operations
if ( ! $this -> validateS3Path ( $cleanPath )) {
$this -> dispatch ( 'error' , 'Invalid S3 path. Path must contain only safe characters (alphanumerics, dots, dashes, underscores, slashes).' );
return ;
}
2025-11-02 14:19:13 +00:00
try {
2025-11-02 15:33:34 +00:00
$s3Storage = S3Storage :: ownedByCurrentTeam () -> findOrFail ( $this -> s3StorageId );
2025-11-02 14:19:13 +00:00
2025-11-25 15:40:35 +00:00
// Validate bucket name early
if ( ! $this -> validateBucketName ( $s3Storage -> bucket )) {
$this -> dispatch ( 'error' , 'Invalid S3 bucket name. Bucket name must contain only alphanumerics, dots, dashes, and underscores.' );
return ;
}
2025-11-02 14:19:13 +00:00
// Test connection
$s3Storage -> testConnection ();
// Build S3 disk configuration
$disk = Storage :: build ([
'driver' => 's3' ,
'region' => $s3Storage -> region ,
'key' => $s3Storage -> key ,
'secret' => $s3Storage -> secret ,
'bucket' => $s3Storage -> bucket ,
'endpoint' => $s3Storage -> endpoint ,
'use_path_style_endpoint' => true ,
]);
// Check if file exists
if ( ! $disk -> exists ( $cleanPath )) {
$this -> dispatch ( 'error' , 'File not found in S3. Please check the path.' );
return ;
}
// Get file size
$this -> s3FileSize = $disk -> size ( $cleanPath );
$this -> dispatch ( 'success' , 'File found in S3. Size: ' . formatBytes ( $this -> s3FileSize ));
} catch ( \Throwable $e ) {
$this -> s3FileSize = null ;
return handleError ( $e , $this );
}
}
2026-03-01 11:45:55 +00:00
public function restoreFromS3 ( string $password = '' ) : bool | string
2025-11-02 14:19:13 +00:00
{
2026-03-01 11:45:55 +00:00
if ( ! verifyPasswordConfirmation ( $password , $this )) {
return 'The provided password is incorrect.' ;
}
2025-11-02 14:19:13 +00:00
$this -> authorize ( 'update' , $this -> resource );
if ( ! $this -> s3StorageId || blank ( $this -> s3Path )) {
$this -> dispatch ( 'error' , 'Please select S3 storage and provide a path first.' );
2026-03-01 11:45:55 +00:00
return true ;
2025-11-02 14:19:13 +00:00
}
if ( is_null ( $this -> s3FileSize )) {
$this -> dispatch ( 'error' , 'Please check the file first by clicking "Check File".' );
2026-03-01 11:45:55 +00:00
return true ;
2025-11-02 14:19:13 +00:00
}
2026-01-02 15:29:48 +00:00
if ( ! $this -> server ) {
$this -> dispatch ( 'error' , 'Server not found. Please refresh the page.' );
2026-03-01 11:45:55 +00:00
return true ;
2026-01-02 15:29:48 +00:00
}
2025-11-02 14:19:13 +00:00
try {
2025-11-17 09:05:18 +00:00
$this -> importRunning = true ;
2025-11-02 14:19:13 +00:00
2025-11-02 15:33:34 +00:00
$s3Storage = S3Storage :: ownedByCurrentTeam () -> findOrFail ( $this -> s3StorageId );
2025-11-02 14:19:13 +00:00
$key = $s3Storage -> key ;
$secret = $s3Storage -> secret ;
$bucket = $s3Storage -> bucket ;
$endpoint = $s3Storage -> endpoint ;
2025-11-25 15:40:35 +00:00
// Validate bucket name to prevent command injection
if ( ! $this -> validateBucketName ( $bucket )) {
$this -> dispatch ( 'error' , 'Invalid S3 bucket name. Bucket name must contain only alphanumerics, dots, dashes, and underscores.' );
2026-03-01 11:45:55 +00:00
return true ;
2025-11-25 15:40:35 +00:00
}
2025-11-17 09:05:18 +00:00
// Clean the S3 path
2025-11-02 14:19:13 +00:00
$cleanPath = ltrim ( $this -> s3Path , '/' );
2025-11-25 15:40:35 +00:00
// Validate the S3 path to prevent command injection
if ( ! $this -> validateS3Path ( $cleanPath )) {
$this -> dispatch ( 'error' , 'Invalid S3 path. Path must contain only safe characters (alphanumerics, dots, dashes, underscores, slashes).' );
2026-03-01 11:45:55 +00:00
return true ;
2025-11-25 15:40:35 +00:00
}
2025-11-02 14:19:13 +00:00
// Get helper image
$helperImage = config ( 'constants.coolify.helper_image' );
2025-11-17 09:05:18 +00:00
$latestVersion = getHelperVersion ();
2025-11-02 14:19:13 +00:00
$fullImageName = " { $helperImage } : { $latestVersion } " ;
2025-11-17 09:05:18 +00:00
// Get the database destination network
2026-01-02 15:29:48 +00:00
if ( $this -> resource -> getMorphClass () === \App\Models\ServiceDatabase :: class ) {
$destinationNetwork = $this -> resource -> service -> destination -> network ? ? 'coolify' ;
} else {
$destinationNetwork = $this -> resource -> destination -> network ? ? 'coolify' ;
}
2025-11-02 14:19:13 +00:00
2025-11-17 09:05:18 +00:00
// Generate unique names for this operation
2026-01-02 15:29:48 +00:00
$containerName = " s3-restore- { $this -> resourceUuid } " ;
2025-11-17 09:05:18 +00:00
$helperTmpPath = '/tmp/' . basename ( $cleanPath );
2026-01-02 15:29:48 +00:00
$serverTmpPath = " /tmp/s3-restore- { $this -> resourceUuid } - " . basename ( $cleanPath );
$containerTmpPath = " /tmp/restore_ { $this -> resourceUuid } - " . basename ( $cleanPath );
$scriptPath = " /tmp/restore_ { $this -> resourceUuid } .sh " ;
2025-11-02 14:19:13 +00:00
2025-11-17 09:05:18 +00:00
// Prepare all commands in sequence
$commands = [];
2025-11-02 14:19:13 +00:00
2025-11-17 13:23:50 +00:00
// 1. Clean up any existing helper container and temp files from previous runs
2025-11-17 09:05:18 +00:00
$commands [] = " docker rm -f { $containerName } 2>/dev/null || true " ;
2025-11-17 13:23:50 +00:00
$commands [] = " rm -f { $serverTmpPath } 2>/dev/null || true " ;
$commands [] = " docker exec { $this -> container } rm -f { $containerTmpPath } { $scriptPath } 2>/dev/null || true " ;
2025-11-02 14:19:13 +00:00
2025-11-17 09:05:18 +00:00
// 2. Start helper container on the database network
2025-11-17 13:13:10 +00:00
$commands [] = " docker run -d --network { $destinationNetwork } --name { $containerName } { $fullImageName } sleep 3600 " ;
2025-11-02 14:19:13 +00:00
2025-11-17 09:05:18 +00:00
// 3. Configure S3 access in helper container
$escapedEndpoint = escapeshellarg ( $endpoint );
$escapedKey = escapeshellarg ( $key );
$escapedSecret = escapeshellarg ( $secret );
$commands [] = " docker exec { $containerName } mc alias set s3temp { $escapedEndpoint } { $escapedKey } { $escapedSecret } " ;
2025-11-02 14:19:13 +00:00
2025-11-25 15:40:35 +00:00
// 4. Check file exists in S3 (bucket and path already validated above)
$escapedBucket = escapeshellarg ( $bucket );
$escapedCleanPath = escapeshellarg ( $cleanPath );
$escapedS3Source = escapeshellarg ( " s3temp/ { $bucket } / { $cleanPath } " );
$commands [] = " docker exec { $containerName } mc stat { $escapedS3Source } " ;
2025-11-02 14:19:13 +00:00
2025-11-17 13:23:50 +00:00
// 5. Download from S3 to helper container (progress shown by default)
2025-11-25 15:40:35 +00:00
$escapedHelperTmpPath = escapeshellarg ( $helperTmpPath );
$commands [] = " docker exec { $containerName } mc cp { $escapedS3Source } { $escapedHelperTmpPath } " ;
2025-11-02 14:19:13 +00:00
2025-11-17 13:23:50 +00:00
// 6. Copy from helper to server, then immediately to database container
2025-11-17 09:05:18 +00:00
$commands [] = " docker cp { $containerName } : { $helperTmpPath } { $serverTmpPath } " ;
$commands [] = " docker cp { $serverTmpPath } { $this -> container } : { $containerTmpPath } " ;
2025-11-02 14:19:13 +00:00
2025-11-17 13:23:50 +00:00
// 7. Cleanup helper container and server temp file immediately (no longer needed)
$commands [] = " docker rm -f { $containerName } 2>/dev/null || true " ;
$commands [] = " rm -f { $serverTmpPath } 2>/dev/null || true " ;
2025-11-17 09:05:18 +00:00
// 8. Build and execute restore command inside database container
$restoreCommand = $this -> buildRestoreCommand ( $containerTmpPath );
2025-11-02 14:19:13 +00:00
$restoreCommandBase64 = base64_encode ( $restoreCommand );
2025-11-17 09:05:18 +00:00
$commands [] = " echo \" { $restoreCommandBase64 } \" | base64 -d > { $scriptPath } " ;
$commands [] = " chmod +x { $scriptPath } " ;
$commands [] = " docker cp { $scriptPath } { $this -> container } : { $scriptPath } " ;
2025-11-17 13:23:50 +00:00
// 9. Execute restore and cleanup temp files immediately after completion
$commands [] = " docker exec { $this -> container } sh -c ' { $scriptPath } && rm -f { $containerTmpPath } { $scriptPath } ' " ;
2025-11-17 09:05:18 +00:00
$commands [] = " docker exec { $this -> container } sh -c 'echo \" Import finished with exit code $ ? \" ' " ;
2025-11-17 13:23:50 +00:00
// Execute all commands with cleanup event (as safety net for edge cases)
2025-11-17 09:05:18 +00:00
$activity = remote_process ( $commands , $this -> server , ignore_errors : true , callEventOnFinish : 'S3RestoreJobFinished' , callEventData : [
'containerName' => $containerName ,
'serverTmpPath' => $serverTmpPath ,
'scriptPath' => $scriptPath ,
'containerTmpPath' => $containerTmpPath ,
'container' => $this -> container ,
'serverId' => $this -> server -> id ,
]);
2025-11-02 14:19:13 +00:00
2025-11-26 09:43:07 +00:00
// Track the activity ID
$this -> activityId = $activity -> id ;
2025-11-17 09:05:18 +00:00
// Dispatch activity to the monitor and open slide-over
$this -> dispatch ( 'activityMonitor' , $activity -> id );
$this -> dispatch ( 'databaserestore' );
2025-11-17 13:23:50 +00:00
$this -> dispatch ( 'info' , 'Restoring database from S3. Progress will be shown in the activity monitor...' );
2025-11-02 14:19:13 +00:00
} catch ( \Throwable $e ) {
2025-11-17 09:05:18 +00:00
$this -> importRunning = false ;
2026-03-01 11:45:55 +00:00
handleError ( $e , $this );
2025-11-17 09:05:18 +00:00
2026-03-01 11:45:55 +00:00
return true ;
2025-11-02 14:19:13 +00:00
}
2026-03-01 11:45:55 +00:00
return true ;
2025-11-02 14:19:13 +00:00
}
2025-11-17 09:05:18 +00:00
public function buildRestoreCommand ( string $tmpPath ) : string
2025-11-02 14:19:13 +00:00
{
2025-12-09 07:40:19 +00:00
$morphClass = $this -> resource -> getMorphClass ();
2026-01-02 15:29:48 +00:00
2025-12-09 07:40:19 +00:00
// Handle ServiceDatabase by checking the database type
if ( $morphClass === \App\Models\ServiceDatabase :: class ) {
$dbType = $this -> resource -> databaseType ();
if ( str_contains ( $dbType , 'mysql' )) {
$morphClass = 'mysql' ;
} elseif ( str_contains ( $dbType , 'mariadb' )) {
$morphClass = 'mariadb' ;
} elseif ( str_contains ( $dbType , 'postgres' )) {
$morphClass = 'postgresql' ;
} elseif ( str_contains ( $dbType , 'mongo' )) {
$morphClass = 'mongodb' ;
}
}
2026-01-02 15:29:48 +00:00
2025-12-09 07:40:19 +00:00
switch ( $morphClass ) {
2025-11-17 09:05:18 +00:00
case \App\Models\StandaloneMariadb :: class :
2025-12-09 07:40:19 +00:00
case 'mariadb' :
2025-11-17 09:05:18 +00:00
$restoreCommand = $this -> mariadbRestoreCommand ;
if ( $this -> dumpAll ) {
2026-01-08 15:29:08 +00:00
$restoreCommand .= " && (gunzip -cf { $tmpPath } 2>/dev/null || cat { $tmpPath } ) | mariadb -u root -p \$ MARIADB_ROOT_PASSWORD \$ { MARIADB_DATABASE:-default} " ;
2025-11-17 09:05:18 +00:00
} else {
$restoreCommand .= " < { $tmpPath } " ;
}
break ;
case \App\Models\StandaloneMysql :: class :
2025-12-09 07:40:19 +00:00
case 'mysql' :
2025-11-17 09:05:18 +00:00
$restoreCommand = $this -> mysqlRestoreCommand ;
if ( $this -> dumpAll ) {
2026-01-08 15:29:08 +00:00
$restoreCommand .= " && (gunzip -cf { $tmpPath } 2>/dev/null || cat { $tmpPath } ) | mysql -u root -p \$ MYSQL_ROOT_PASSWORD \$ { MYSQL_DATABASE:-default} " ;
2025-11-17 09:05:18 +00:00
} else {
$restoreCommand .= " < { $tmpPath } " ;
}
break ;
case \App\Models\StandalonePostgresql :: class :
2025-12-09 07:40:19 +00:00
case 'postgresql' :
2025-11-17 09:05:18 +00:00
$restoreCommand = $this -> postgresqlRestoreCommand ;
if ( $this -> dumpAll ) {
2026-01-19 18:08:32 +00:00
$restoreCommand .= " && (gunzip -cf { $tmpPath } 2>/dev/null || cat { $tmpPath } ) | psql -U \$ { POSTGRES_USER} -d \$ { POSTGRES_DB:- \$ { POSTGRES_USER:-postgres}} " ;
2025-11-17 09:05:18 +00:00
} else {
$restoreCommand .= " { $tmpPath } " ;
}
break ;
case \App\Models\StandaloneMongodb :: class :
2025-12-09 07:40:19 +00:00
case 'mongodb' :
2025-11-17 09:05:18 +00:00
$restoreCommand = $this -> mongodbRestoreCommand ;
if ( $this -> dumpAll === false ) {
$restoreCommand .= " { $tmpPath } " ;
}
break ;
default :
$restoreCommand = '' ;
2025-11-02 14:19:13 +00:00
}
2025-11-17 09:05:18 +00:00
return $restoreCommand ;
2025-11-02 14:19:13 +00:00
}
2024-01-06 05:24:57 +00:00
}