diff --git a/app/Jobs/RunFileBackupTaskJob.php b/app/Jobs/RunFileBackupTaskJob.php index c2456769..a1afe8da 100644 --- a/app/Jobs/RunFileBackupTaskJob.php +++ b/app/Jobs/RunFileBackupTaskJob.php @@ -4,7 +4,7 @@ namespace App\Jobs; -use App\Services\Backup\Tasks\FileBackup; +use App\Services\Backup\Tasks\FileBackupTask; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -24,7 +24,7 @@ public function __construct(public int $backupTaskId) public function handle(): void { - $action = new FileBackup; + $action = new FileBackupTask; $action->handle($this->backupTaskId); } } diff --git a/app/Services/Backup/SFTPAdapter.php b/app/Services/Backup/Adapters/SFTPAdapter.php similarity index 94% rename from app/Services/Backup/SFTPAdapter.php rename to app/Services/Backup/Adapters/SFTPAdapter.php index 8c50566c..5449df3a 100644 --- a/app/Services/Backup/SFTPAdapter.php +++ b/app/Services/Backup/Adapters/SFTPAdapter.php @@ -1,7 +1,8 @@ scriptRunTime = microtime(true); + $this->backupTask = $this->obtainBackupTask($backupTaskId); + } + + /** + * @return void + */ + abstract protected function performBackup(): void; + + /** + * @return void + * @throws Exception + */ + public function handle(): void + { + Log::info("Starting backup task: {$this->backupTask->id}"); + + $this->initializeBackup(); + + try { + $this->performBackup(); + $this->finalizeSuccessfulBackup(); + } catch (Exception $exception) { + $this->handleBackupFailure($exception); + } finally { + $this->cleanupBackup(); + } + } + + /** + * @return void + * @throws Exception + */ + protected function initializeBackup(): void + { + $this->backupTask->setScriptUpdateTime(); + $this->backupTaskLog = $this->recordBackupTaskLog($this->backupTask->id, $this->logOutput); + $this->updateBackupTaskStatus($this->backupTask, BackupTaskModel::STATUS_RUNNING); + $this->logMessage('Backup task started.'); + $this->updateBackupTaskLogOutput($this->backupTaskLog, $this->logOutput); + } + + /** + * @return void + * @throws Exception + */ + protected function finalizeSuccessfulBackup(): void + { + $this->logMessage('Backup task has finished successfully!'); + $this->backupTaskLog->setSuccessfulTime(); + $this->updateBackupTaskLogOutput($this->backupTaskLog, $this->logOutput); + } + + /** + * @param Exception $exception + * @return void + */ + protected function handleBackupFailure(Exception $exception): void + { + $this->logOutput .= 'Error in backup process: ' . $exception->getMessage() . "\n"; + Log::error("Error in backup process for task {$this->backupTask->id}: " . $exception->getMessage(), ['exception' => $exception]); + } + + /** + * @return void + */ + protected function cleanupBackup(): void + { + $this->updateBackupTaskLogOutput($this->backupTaskLog, $this->logOutput); + $this->backupTaskLog->setFinishedTime(); + $this->updateBackupTaskStatus($this->backupTask, BackupTaskModel::STATUS_READY); + $this->backupTask->sendNotifications(); + $this->backupTask->updateLastRanAt(); + $this->backupTask->resetScriptUpdateTime(); + + $elapsedTime = microtime(true) - $this->scriptRunTime; + $this->backupTask->data()->create([ + 'duration' => $elapsedTime, + 'size' => $this->backupSize, + ]); + + Log::info("Completed backup task: {$this->backupTask->label} ({$this->backupTask->id})."); + } + + /** + * @param string $message + * @param string $timezone + * @return string + * @throws Exception + */ + protected function logWithTimestamp(string $message, string $timezone): string + { + $timestampedMessage = parent::logWithTimestamp($message, $timezone); + $this->logOutput .= $timestampedMessage; + return $timestampedMessage; + } + + /** + * @param string $message + * @return void + * @throws Exception + */ + protected function logMessage(string $message): void + { + $this->logWithTimestamp($message, $this->backupTask->user->timezone); + } + + /** + * @param string $extension + * @return string + */ + protected function generateBackupFileName(string $extension): string + { + $prefix = $this->backupTask->hasFileNameAppended() ? $this->backupTask->appended_file_name . '_' : ''; + return "{$prefix}backup_{$this->backupTask->id}_" . date('YmdHis') . ".{$extension}"; + } +} diff --git a/app/Services/Backup/Tasks/DatabaseBackup.php b/app/Services/Backup/Tasks/DatabaseBackup.php deleted file mode 100644 index faea8028..00000000 --- a/app/Services/Backup/Tasks/DatabaseBackup.php +++ /dev/null @@ -1,122 +0,0 @@ - $backupTaskId]); - - $scriptRunTime = microtime(true); - - $backupTask = $this->obtainBackupTask($backupTaskId); - $backupTask->setScriptUpdateTime(); - $remoteServer = $backupTask->remoteServer; - $backupDestinationModel = $backupTask->backupDestination; - $databaseName = $backupTask->database_name; - $databasePassword = $remoteServer->getDecryptedDatabasePassword(); - $userTimezone = $backupTask->user->timezone; - $storagePath = $backupTask->getAttributeValue('store_path'); - - $logOutput = ''; - $backupTaskLog = $this->recordBackupTaskLog($backupTaskId, $logOutput); - $this->updateBackupTaskLogOutput($backupTaskLog, $logOutput); - - $logOutput .= $this->logWithTimestamp('Backup task started.', $userTimezone); - $this->updateBackupTaskLogOutput($backupTaskLog, $logOutput); - - if (! $remoteServer->hasDatabasePassword()) { - $logOutput .= $this->logWithTimestamp('Backup task failed.', $userTimezone); - $errorMessage = $this->logWithTimestamp('Please provide a database password for the remote server.', $userTimezone); - $logOutput .= $errorMessage; - Log::error('Database password not provided for remote server.', ['backup_task_id' => $backupTaskId]); - $this->handleFailure($backupTask, $logOutput, $errorMessage); - - return; - } - - $this->updateBackupTaskStatus($backupTask, BackupTask::STATUS_RUNNING); - $backupTask->setScriptUpdateTime(); - - try { - Log::info('Establishing SFTP connection.', ['remote_server' => $remoteServer->ip_address]); - $sftp = $this->establishSFTPConnection($remoteServer, $backupTask); - $logOutput .= $this->logWithTimestamp('SSH Connection established to the server.', $userTimezone); - $this->updateBackupTaskLogOutput($backupTaskLog, $logOutput); - - Log::info('Determining database type.', ['backup_task_id' => $backupTaskId]); - $databaseType = $this->getDatabaseType($sftp); - $logOutput .= $this->logWithTimestamp("Database type detected: {$databaseType}.", $userTimezone); - $this->updateBackupTaskLogOutput($backupTaskLog, $logOutput); - - $backupTask->setScriptUpdateTime(); - - if ($backupTask->hasFileNameAppended()) { - $dumpFileName = $backupTask->appended_file_name . '_backup_' . $backupTaskId . '_' . date('YmdHis') . '.sql'; - } else { - $dumpFileName = 'backup_' . $backupTaskId . '_' . date('YmdHis') . '.sql'; - } - $remoteDumpPath = "/tmp/{$dumpFileName}"; - Log::info('Dumping remote database.', ['backup_task_id' => $backupTaskId, 'dump_file_name' => $dumpFileName]); - $this->dumpRemoteDatabase($sftp, $databaseType, $remoteDumpPath, $databasePassword, $databaseName, $backupTask->excluded_database_tables); - Log::info('Database dump completed.', ['backup_task_id' => $backupTaskId, 'remote_dump_path' => $remoteDumpPath]); - - Log::info('Streaming database dump to destination.', ['backup_task_id' => $backupTaskId, 'remote_dump_path' => $remoteDumpPath, 'file_name' => $dumpFileName]); - if (! $this->backupDestinationDriver($backupDestinationModel->type, $sftp, $remoteDumpPath, $backupDestinationModel, $dumpFileName, $storagePath)) { - $errorMessage = $this->logWithTimestamp('Failed to upload the dump file to destination.', $userTimezone); - Log::error('Failed to upload the dump file to destination.', ['backup_task_id' => $backupTaskId, 'remote_dump_path' => $remoteDumpPath]); - $this->handleFailure($backupTask, $logOutput, $errorMessage); - - return; - } - Log::info('Database dump successfully uploaded to destination.', ['backup_task_id' => $backupTaskId, 'file_name' => $dumpFileName]); - - $backupTask->setScriptUpdateTime(); - - if ($backupTask->isRotatingBackups()) { - Log::info('Rotating old backups.', ['backup_task_id' => $backupTaskId]); - $backupDestination = $this->createBackupDestinationInstance($backupDestinationModel); - $this->rotateOldBackups($backupDestination, $backupTaskId, $backupTask->maximum_backups_to_keep, '.sql', 'backup_'); - } - - $logOutput .= $this->logWithTimestamp("Database backup has been uploaded to {$backupDestinationModel->label} - {$backupDestinationModel->type()}: {$dumpFileName}", $userTimezone); - $this->updateBackupTaskLogOutput($backupTaskLog, $logOutput); - - Log::info('Cleaning up remote dump file.', ['remote_dump_path' => $remoteDumpPath]); - $sftp->delete($remoteDumpPath); - Log::info('Remote dump file deleted.', ['remote_dump_path' => $remoteDumpPath]); - - $logOutput .= $this->logWithTimestamp('Cleaned up the temporary file on the server.', $userTimezone); - $logOutput .= $this->logWithTimestamp('Backup task completed successfully!', $userTimezone); - $backupTaskLog->setSuccessfulTime(); - $this->updateBackupTaskLogOutput($backupTaskLog, $logOutput); - - } catch (Exception $exception) { - $logOutput .= 'Error in backup process: ' . $exception->getMessage() . "\n"; - Log::error('Error in backup process.', ['backup_task_id' => $backupTaskId, 'error' => $exception->getMessage(), 'exception' => $exception]); - $this->updateBackupTaskLogOutput($backupTaskLog, $logOutput); - } finally { - $this->updateBackupTaskLogOutput($backupTaskLog, $logOutput); - $backupTaskLog->setFinishedTime(); - $this->updateBackupTaskStatus($backupTask, BackupTask::STATUS_READY); - $backupTask->sendNotifications(); - $backupTask->updateLastRanAt(); - $backupTask->resetScriptUpdateTime(); - Log::info('Backup task completed.', ['backup_task_id' => $backupTaskId]); - - $elapsedTime = microtime(true) - $scriptRunTime; - $backupTask->data()->create([ - 'duration' => $elapsedTime, - ]); - Log::info('Script execution time.', ['backup_task_id' => $backupTaskId, 'elapsed_time' => $elapsedTime]); - } - } -} diff --git a/app/Services/Backup/Tasks/DatabaseBackupTask.php b/app/Services/Backup/Tasks/DatabaseBackupTask.php new file mode 100644 index 00000000..ca18f7a3 --- /dev/null +++ b/app/Services/Backup/Tasks/DatabaseBackupTask.php @@ -0,0 +1,57 @@ +backupTask->remoteServer; + $backupDestinationModel = $this->backupTask->backupDestination; + $databaseName = $this->backupTask->database_name; + $databasePassword = $remoteServer->getDecryptedDatabasePassword(); + $storagePath = $this->backupTask->getAttributeValue('store_path'); + + if (!$remoteServer->hasDatabasePassword()) { + throw new RuntimeException('Please provide a database password for the remote server.'); + } + + $sftp = $this->establishSFTPConnection($remoteServer, $this->backupTask); + $this->logMessage('SSH Connection established to the server.'); + + $databaseType = $this->getDatabaseType($sftp); + $this->logMessage("Database type detected: {$databaseType}."); + + $this->backupTask->setScriptUpdateTime(); + + $dumpFileName = $this->generateBackupFileName('sql'); + $remoteDumpPath = "/tmp/{$dumpFileName}"; + + $this->dumpRemoteDatabase($sftp, $databaseType, $remoteDumpPath, $databasePassword, $databaseName, $this->backupTask->excluded_database_tables); + + if (!$this->backupDestinationDriver($backupDestinationModel->type, $sftp, $remoteDumpPath, $backupDestinationModel, $dumpFileName, $storagePath)) { + throw new RuntimeException('Failed to upload the dump file to destination.'); + } + + $this->backupTask->setScriptUpdateTime(); + + if ($this->backupTask->isRotatingBackups()) { + $backupDestination = $this->createBackupDestinationInstance($backupDestinationModel); + $this->rotateOldBackups($backupDestination, $this->backupTask->id, $this->backupTask->maximum_backups_to_keep, '.sql', 'backup_'); + } + + $this->logMessage("Database backup has been uploaded to {$backupDestinationModel->label} - {$backupDestinationModel->type()}: {$dumpFileName}"); + + $sftp->delete($remoteDumpPath); + $this->logMessage('Cleaned up the temporary file on the server.'); + } +} diff --git a/app/Services/Backup/Tasks/FileBackup.php b/app/Services/Backup/Tasks/FileBackup.php deleted file mode 100644 index 4b9450e9..00000000 --- a/app/Services/Backup/Tasks/FileBackup.php +++ /dev/null @@ -1,133 +0,0 @@ -obtainBackupTask($backupTaskId); - $backupTask->setScriptUpdateTime(); - $remoteServer = $backupTask->remoteServer; - $backupDestinationModel = $backupTask->backupDestination; - $sourcePath = $backupTask->getAttributeValue('source_path'); - $userTimezone = $backupTask->user->timezone; - $storagePath = $backupTask->getAttributeValue('store_path'); - - $logOutput = ''; - $backupTaskLog = $this->recordBackupTaskLog($backupTaskId, $logOutput); - $this->updateBackupTaskLogOutput($backupTaskLog, $logOutput); - - $this->updateBackupTaskStatus($backupTask, BackupTask::STATUS_RUNNING); - - $logOutput .= $this->logWithTimestamp('Backup task started.', $userTimezone); - $this->updateBackupTaskLogOutput($backupTaskLog, $logOutput); - - try { - Log::info("Establishing SFTP connection for backup task: {$backupTaskId}"); - $sftp = $this->establishSFTPConnection($remoteServer, $backupTask); - $logOutput .= $this->logWithTimestamp('SSH Connection established to the server.', $userTimezone); - $this->updateBackupTaskLogOutput($backupTaskLog, $logOutput); - - Log::info("Checking if source path exists: {$sourcePath} for backup task: {$backupTaskId}"); - if (! $this->checkPathExists($sftp, $sourcePath)) { - $errorMessage = $this->logWithTimestamp('The path specified does not exist.', $userTimezone); - Log::error("Source path does not exist: {$sourcePath} for backup task: {$backupTaskId}"); - $this->handleFailure($backupTask, $logOutput, $errorMessage); - - return; - } - - $backupTask->setScriptUpdateTime(); - - Log::info("Checking directory size for path: {$sourcePath} for backup task: {$backupTaskId}"); - $dirSize = $this->getRemoteDirectorySize($sftp, $sourcePath); - $dirSizeInMB = number_format($dirSize / 1024 / 1024, 1); - $logOutput .= $this->logWithTimestamp("Directory size of {$sourcePath}: {$dirSizeInMB} MB.", $userTimezone); - $this->updateBackupTaskLogOutput($backupTaskLog, $logOutput); - - if ($dirSize > BackupConstants::FILE_SIZE_LIMIT) { - Log::error("Directory size exceeds limit: {$dirSize} bytes for path: {$sourcePath} for backup task: {$backupTaskId}"); - $this->handleFailure($backupTask, $logOutput, $this->logWithTimestamp('Directory size exceeds the limit.', $userTimezone)); - - return; - } - - $excludeDirs = []; - if ($this->isLaravelDirectory($sftp, $sourcePath)) { - Log::info("Laravel directory detected, excluding node_modules and vendor folders for path: {$sourcePath}"); - $excludeDirs = ['node_modules', 'vendor']; - } - - $zipFileName = $backupTask->hasFileNameAppended() - ? $backupTask->appended_file_name . '_backup_' . $backupTaskId . '_' . date('YmdHis') . '.zip' - : 'backup_' . $backupTaskId . '_' . date('YmdHis') . '.zip'; - - $remoteZipPath = "/tmp/{$zipFileName}"; - Log::info("Zipping directory: {$sourcePath} to {$remoteZipPath} for backup task: {$backupTaskId}"); - $this->zipRemoteDirectory($sftp, $sourcePath, $remoteZipPath, $excludeDirs); - $logOutput .= $this->logWithTimestamp("Directory has been zipped: {$remoteZipPath}", $userTimezone); - $this->updateBackupTaskLogOutput($backupTaskLog, $logOutput); - - $backupTask->setScriptUpdateTime(); - - Log::info("Starting to stream zip file to backup destination for backup task: {$backupTaskId}"); - if (! $this->backupDestinationDriver($backupDestinationModel->type, $sftp, $remoteZipPath, $backupDestinationModel, $zipFileName, $storagePath)) { - $errorMessage = $this->logWithTimestamp('Failed to upload the zip file to destination.', $userTimezone); - Log::error("Failed to upload the zip file to destination for backup task: {$backupTaskId}. Remote zip path: {$remoteZipPath}, Backup destination: {$backupDestinationModel->label}, Filename: {$zipFileName}"); - $this->handleFailure($backupTask, $logOutput, $errorMessage); - - return; - } - - if ($backupTask->isRotatingBackups()) { - Log::info("Rotating old backups for backup task: {$backupTaskId}"); - $backupDestination = $this->createBackupDestinationInstance($backupDestinationModel); - $this->rotateOldBackups($backupDestination, $backupTaskId, $backupTask->maximum_backups_to_keep, '.zip', 'backup_'); - } - - $logOutput .= $this->logWithTimestamp("Backup has been uploaded to {$backupDestinationModel->label} - {$backupDestinationModel->type()}: {$zipFileName}", $userTimezone); - $this->updateBackupTaskLogOutput($backupTaskLog, $logOutput); - - $sftp->delete($remoteZipPath); - Log::info("Remote zip file deleted for backup task: {$backupTaskId}"); - - $backupTask->setScriptUpdateTime(); - - $logOutput .= $this->logWithTimestamp('Cleaned up the temporary zip file on server.', $userTimezone); - $logOutput .= $this->logWithTimestamp('Backup task has finished successfully!', $userTimezone); - $backupTaskLog->setSuccessfulTime(); - $this->updateBackupTaskLogOutput($backupTaskLog, $logOutput); - - } catch (Exception $exception) { - $logOutput .= 'Error in backup process: ' . $exception->getMessage() . "\n"; - Log::error("Error in backup process for task {$backupTaskId}: " . $exception->getMessage(), ['exception' => $exception]); - } finally { - $this->updateBackupTaskLogOutput($backupTaskLog, $logOutput); - $backupTaskLog->setFinishedTime(); - $this->updateBackupTaskStatus($backupTask, BackupTask::STATUS_READY); - $backupTask->sendNotifications(); - $backupTask->updateLastRanAt(); - $backupTask->resetScriptUpdateTime(); - Log::info('[BACKUP TASK] Completed backup task: ' . $backupTask->label . ' (' . $backupTask->id . ').'); - - $elapsedTime = microtime(true) - $scriptRunTime; - $backupTask->data()->create([ - 'duration' => $elapsedTime, - 'size' => $dirSize ?? null, - ]); - } - } -} diff --git a/app/Services/Backup/Tasks/FileBackupTask.php b/app/Services/Backup/Tasks/FileBackupTask.php new file mode 100644 index 00000000..355dbf60 --- /dev/null +++ b/app/Services/Backup/Tasks/FileBackupTask.php @@ -0,0 +1,66 @@ +backupTask->remoteServer; + $backupDestinationModel = $this->backupTask->backupDestination; + $sourcePath = $this->backupTask->getAttributeValue('source_path'); + $storagePath = $this->backupTask->getAttributeValue('store_path'); + + $sftp = $this->establishSFTPConnection($remoteServer, $this->backupTask); + $this->logMessage('SSH Connection established to the server.'); + + if (!$this->checkPathExists($sftp, $sourcePath)) { + throw new RuntimeException('The path specified does not exist.'); + } + + $this->backupTask->setScriptUpdateTime(); + + $dirSize = $this->getRemoteDirectorySize($sftp, $sourcePath); + $this->backupSize = $dirSize; + $dirSizeInMB = number_format($dirSize / 1024 / 1024, 1); + $this->logMessage("Directory size of {$sourcePath}: {$dirSizeInMB} MB."); + + if ($dirSize > BackupConstants::FILE_SIZE_LIMIT) { + throw new RuntimeException('Directory size exceeds the limit.'); + } + + $excludeDirs = $this->isLaravelDirectory($sftp, $sourcePath) ? ['node_modules', 'vendor'] : []; + + $zipFileName = $this->generateBackupFileName('zip'); + $remoteZipPath = "/tmp/{$zipFileName}"; + + $this->zipRemoteDirectory($sftp, $sourcePath, $remoteZipPath, $excludeDirs); + $this->logMessage("Directory has been zipped: {$remoteZipPath}"); + + $this->backupTask->setScriptUpdateTime(); + + if (!$this->backupDestinationDriver($backupDestinationModel->type, $sftp, $remoteZipPath, $backupDestinationModel, $zipFileName, $storagePath)) { + throw new RuntimeException('Failed to upload the zip file to destination.'); + } + + if ($this->backupTask->isRotatingBackups()) { + $backupDestination = $this->createBackupDestinationInstance($backupDestinationModel); + $this->rotateOldBackups($backupDestination, $this->backupTask->id, $this->backupTask->maximum_backups_to_keep, '.zip', 'backup_'); + } + + $this->logMessage("Backup has been uploaded to {$backupDestinationModel->label} - {$backupDestinationModel->type()}: {$zipFileName}"); + + $sftp->delete($remoteZipPath); + $this->logMessage('Cleaned up the temporary zip file on server.'); + } +} diff --git a/app/Services/Backup/Traits/BackupHelpers.php b/app/Services/Backup/Traits/BackupHelpers.php index c35cae58..409d37e6 100644 --- a/app/Services/Backup/Traits/BackupHelpers.php +++ b/app/Services/Backup/Traits/BackupHelpers.php @@ -4,7 +4,7 @@ namespace App\Services\Backup\Traits; -use App\Services\Backup\SFTPInterface; +use app\Services\Backup\Contracts\SFTPInterface; use Exception; use Illuminate\Support\Facades\Log;