-
Notifications
You must be signed in to change notification settings - Fork 55
CLI-1631: validating backup link before importing database #1958
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
faaad8a
37e199c
0a58a1d
0f22eba
343becc
3bd4c75
4275303
39a8333
c767a92
ccb527a
4433969
c293862
db07310
c6ab7ff
394f17e
95d55fe
e9198b3
79f56b7
eee2337
32e4306
549cebf
262088b
3b22ba8
4dffde7
5c42e98
051c5c6
494e4cc
77a4588
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,7 @@ | |
| use Exception; | ||
| use GuzzleHttp\Exception\RequestException; | ||
| use GuzzleHttp\TransferStats; | ||
| use Psr\Http\Message\ResponseInterface; | ||
| use Psr\Http\Message\UriInterface; | ||
| use Psr\Log\LoggerInterface; | ||
| use React\EventLoop\Loop; | ||
|
|
@@ -68,6 +69,26 @@ public function __construct( | |
| parent::__construct($this->localMachineHelper, $this->datastoreCloud, $this->datastoreAcli, $this->cloudCredentials, $this->telemetryHelper, $this->projectDir, $this->cloudApiClientService, $this->sshHelper, $this->sshDir, $logger, $this->selfUpdateManager); | ||
| } | ||
|
|
||
| /** | ||
| * Get the backup download URL. | ||
| */ | ||
| protected function getBackupDownloadUrl(): ?UriInterface | ||
| { | ||
| return $this->backupDownloadUrl ?? null; | ||
| } | ||
|
|
||
| /** | ||
| * Set the backup download URL. | ||
| */ | ||
| protected function setBackupDownloadUrl(string|UriInterface $url): void | ||
| { | ||
| if (is_string($url)) { | ||
| $this->backupDownloadUrl = new \GuzzleHttp\Psr7\Uri($url); | ||
| } else { | ||
| $this->backupDownloadUrl = $url; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @see https://github.com/drush-ops/drush/blob/c21a5a24a295cc0513bfdecead6f87f1a2cf91a2/src/Sql/SqlMysql.php#L168 | ||
| * @return string[] | ||
|
|
@@ -94,22 +115,27 @@ private function listTablesQuoted(string $out): array | |
| return $tables; | ||
| } | ||
|
|
||
| public static function getBackupPath(object $environment, DatabaseResponse $database, object $backupResponse): string | ||
| protected static function getBackupPath(object $environment, DatabaseResponse $database, object $backupResponse): string | ||
| { | ||
| // Databases have a machine name not exposed via the API; we can only | ||
| // approximately reconstruct it and match the filename you'd get downloading | ||
| // a backup from Cloud UI. | ||
| if ($database->flags->default) { | ||
| $dbMachineName = $database->name . $environment->name; | ||
| } else { | ||
| $dbMachineName = 'db' . $database->id; | ||
| } | ||
| $filename = implode('-', [ | ||
| $environment->name, | ||
| $database->name, | ||
| $dbMachineName, | ||
| $backupResponse->completedAt, | ||
| ]) . '.sql.gz'; | ||
| if (PHP_OS_FAMILY === 'Windows') { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why change this? I don't see anything in the parent ticket mentioning Windows compatibility
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @danepowell it was due to failing testcases specifically for windows There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think that was meant for me, but I'm glad you found the problem! |
||
| // Use short filename to comply with 8.3 format and avoid long path issues. | ||
| // The hash-based filename still preserves the '.sql.gz' extension. | ||
| $hash = substr(md5($environment->name . $database->name . $dbMachineName . $backupResponse->completedAt), 0, 8); | ||
| $filename = $hash . '.sql.gz'; | ||
| } else { | ||
| $completedAtFormatted = $backupResponse->completedAt; | ||
| $filename = implode('-', [ | ||
| $environment->name, | ||
| $database->name, | ||
| $dbMachineName, | ||
| $completedAtFormatted, | ||
| ]) . '.sql.gz'; | ||
| } | ||
| return Path::join(sys_get_temp_dir(), $filename); | ||
| } | ||
|
|
||
|
|
@@ -261,19 +287,21 @@ static function (mixed $totalBytes, mixed $downloadedBytes) use (&$progress, $ou | |
| if ($codebaseUuid) { | ||
| // Download the backup file directly from the provided URL. | ||
| $downloadUrl = $backupResponse->links->download->href; | ||
| $this->httpClient->request('GET', $downloadUrl, [ | ||
| $response = $this->httpClient->request('GET', $downloadUrl, [ | ||
| 'progress' => static function (mixed $totalBytes, mixed $downloadedBytes) use (&$progress, $output): void { | ||
| self::displayDownloadProgress($totalBytes, $downloadedBytes, $progress, $output); | ||
| }, | ||
| 'sink' => $localFilepath, | ||
| ]); | ||
| $this->validateDownloadResponse($response, $localFilepath); | ||
| return $localFilepath; | ||
| } | ||
| $acquiaCloudClient->stream( | ||
| "get", | ||
| "/environments/$environment->uuid/databases/$database->name/backups/$backupResponse->id/actions/download", | ||
| $acquiaCloudClient->getOptions() | ||
| ); | ||
| $this->validateDownloadedFile($localFilepath); | ||
| return $localFilepath; | ||
| } catch (RequestException $exception) { | ||
| // Deal with broken SSL certificates. | ||
|
|
@@ -308,17 +336,51 @@ static function (mixed $totalBytes, mixed $downloadedBytes) use (&$progress, $ou | |
| throw new AcquiaCliException('Could not download backup'); | ||
| } | ||
|
|
||
| public function setBackupDownloadUrl(UriInterface $url): void | ||
| /** | ||
| * Validates the HTTP response from a database backup download request. | ||
| * | ||
| * @param string $localFilepath The local file path where the backup was downloaded | ||
| * @throws \Acquia\Cli\Exception\AcquiaCliException If the response is invalid | ||
| */ | ||
| private function validateDownloadResponse(ResponseInterface $response, string $localFilepath): void | ||
| { | ||
| $this->backupDownloadUrl = $url; | ||
| $statusCode = $response->getStatusCode(); | ||
|
|
||
| // Check for successful HTTP response (any 2xx status). | ||
| if ($statusCode < 200 || $statusCode >= 300) { | ||
| // Clean up the potentially corrupted file. | ||
| if (file_exists($localFilepath)) { | ||
| $this->localMachineHelper->getFilesystem()->remove($localFilepath); | ||
| } | ||
| throw new AcquiaCliException( | ||
| 'Database backup download failed with HTTP status {status}', | ||
| ['status' => $statusCode] | ||
| ); | ||
| } | ||
|
|
||
| // Validate the downloaded file. | ||
| $this->validateDownloadedFile($localFilepath); | ||
| } | ||
|
|
||
| private function getBackupDownloadUrl(): ?UriInterface | ||
| /** | ||
| * Validates that the downloaded backup file exists. | ||
| * | ||
| * @param string $localFilepath The local file path to validate | ||
| * @throws \Acquia\Cli\Exception\AcquiaCliException If the file is invalid | ||
| */ | ||
| private function validateDownloadedFile(string $localFilepath): void | ||
| { | ||
| return $this->backupDownloadUrl ?? null; | ||
| // Check if file exists. | ||
| if (!file_exists($localFilepath)) { | ||
| throw new AcquiaCliException( | ||
| 'Database backup download failed: file was not created' | ||
| ); | ||
| } | ||
|
|
||
| // File exists - assume it's valid since it comes from trusted Acquia API. | ||
| } | ||
|
|
||
| public static function displayDownloadProgress(mixed $totalBytes, mixed $downloadedBytes, mixed &$progress, OutputInterface $output): void | ||
| protected static function displayDownloadProgress(mixed $totalBytes, mixed $downloadedBytes, mixed &$progress, OutputInterface $output): void | ||
| { | ||
| if ($totalBytes > 0 && is_null($progress)) { | ||
| $progress = new ProgressBar($output, $totalBytes); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.