diff --git a/docker-compose.yml b/docker-compose.yml index b0292045ca..91f39ff944 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -76,6 +76,7 @@ services: - appwrite-config:/storage/config:rw - appwrite-certificates:/storage/certificates:rw - appwrite-functions:/storage/functions:rw + - appwrite-builds:/storage/builds:rw - ./phpunit.xml:/usr/src/code/phpunit.xml - ./tests:/usr/src/code/tests - ./app:/usr/src/code/app diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Deployments/DownloadBuild.php b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/DownloadBuild.php new file mode 100644 index 0000000000..caa6d2389e --- /dev/null +++ b/src/Appwrite/Platform/Modules/Sites/Http/Deployments/DownloadBuild.php @@ -0,0 +1,119 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/build/download') + ->desc('Download build') + ->groups(['api', 'sites']) + ->label('scope', 'functions.read') //TODO: Update the scope to sites later + ->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) + ->label('sdk.namespace', 'sites') + ->label('sdk.method', 'getBuildDownload') + ->label('sdk.description', '/docs/references/sites/get-build-download.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', '*/*') + ->label('sdk.methodType', 'location') + ->param('siteId', '', new UID(), 'Site ID.') + ->param('deploymentId', '', new UID(), 'Deployment ID.') + ->inject('response') + ->inject('request') + ->inject('dbForProject') + ->inject('deviceForBuilds') + ->callback([$this, 'action']); + } + + public function action(string $siteId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceForBuilds) + { + $site = $dbForProject->getDocument('sites', $siteId); + if ($site->isEmpty()) { + throw new Exception(Exception::SITE_NOT_FOUND); + } + + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + if ($deployment->isEmpty()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + if ($deployment->getAttribute('resourceId') !== $site->getId()) { + throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); + } + + $build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId')); + if ($build->isEmpty()) { + throw new Exception(Exception::BUILD_NOT_FOUND); + } + + $path = $build->getAttribute('path', ''); + if (!$deviceForBuilds->exists($path)) { + throw new Exception(Exception::BUILD_NOT_FOUND); + } + + $response + ->setContentType('application/gzip') + ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache + ->addHeader('X-Peak', \memory_get_peak_usage()) + ->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"'); + + $size = $deviceForBuilds->getFileSize($path); + $rangeHeader = $request->getHeader('range'); + + if (!empty($rangeHeader)) { + $start = $request->getRangeStart(); + $end = $request->getRangeEnd(); + $unit = $request->getRangeUnit(); + + if ($end === null) { + $end = min(($start + MAX_OUTPUT_CHUNK_SIZE - 1), ($size - 1)); + } + + if ($unit !== 'bytes' || $start >= $end || $end >= $size) { + throw new Exception(Exception::STORAGE_INVALID_RANGE); + } + + $response + ->addHeader('Accept-Ranges', 'bytes') + ->addHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $size) + ->addHeader('Content-Length', $end - $start + 1) + ->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT); + + $response->send($deviceForBuilds->read($path, $start, ($end - $start + 1))); + } + + if ($size > APP_STORAGE_READ_BUFFER) { + for ($i = 0; $i < ceil($size / MAX_OUTPUT_CHUNK_SIZE); $i++) { + $response->chunk( + $deviceForBuilds->read( + $path, + ($i * MAX_OUTPUT_CHUNK_SIZE), + min(MAX_OUTPUT_CHUNK_SIZE, $size - ($i * MAX_OUTPUT_CHUNK_SIZE)) + ), + (($i + 1) * MAX_OUTPUT_CHUNK_SIZE) >= $size + ); + } + } else { + $response->send($deviceForBuilds->read($path)); + } + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Services/Http.php b/src/Appwrite/Platform/Modules/Sites/Services/Http.php index 4739c5aebd..7552f16c4d 100644 --- a/src/Appwrite/Platform/Modules/Sites/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Sites/Services/Http.php @@ -5,6 +5,7 @@ namespace Appwrite\Platform\Modules\Sites\Services; use Appwrite\Platform\Modules\Sites\Http\Deployments\CancelDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\CreateDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\DeleteDeployment; +use Appwrite\Platform\Modules\Sites\Http\Deployments\DownloadBuild; use Appwrite\Platform\Modules\Sites\Http\Deployments\DownloadDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\GetDeployment; use Appwrite\Platform\Modules\Sites\Http\Deployments\ListDeployments; @@ -43,6 +44,7 @@ class Http extends Service $this->addAction(UpdateDeployment::getName(), new UpdateDeployment()); $this->addAction(DeleteDeployment::getName(), new DeleteDeployment()); $this->addAction(DownloadDeployment::getName(), new DownloadDeployment()); + $this->addAction(DownloadBuild::getName(), new DownloadBuild()); $this->addAction(RebuildDeployment::getName(), new RebuildDeployment()); $this->addAction(CancelDeployment::getName(), new CancelDeployment()); $this->addAction(CreateVariable::getName(), new CreateVariable());