Merge pull request #9434 from appwrite/chore-combine-download-endpoints

Chore: Combine download endpoints
This commit is contained in:
Matej Bačo 2025-03-04 15:10:24 +01:00 committed by GitHub
commit 03c9e57773
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 82 additions and 314 deletions

View file

@ -1,133 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Deployments\Builds\Download;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
use Utopia\Swoole\Request;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getDeploymentBuildDownload';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId/build/download')
->desc('Download build')
->groups(['api', 'functions'])
->label('scope', 'functions.read')
->label('sdk', new Method(
namespace: 'functions',
name: 'getDeploymentBuildDownload',
description: <<<EOT
Get a function build content by its unique ID. The endpoint response return with a 'Content-Disposition: attachment' header that tells the browser to start downloading the file to user downloads directory.
EOT,
auth: [AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_NONE
)
],
type: MethodType::LOCATION,
contentType: ContentType::ANY,
))
->param('functionId', '', new UID(), 'Function ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->inject('response')
->inject('request')
->inject('dbForProject')
->inject('deviceForBuilds')
->callback([$this, 'action']);
}
public function action(string $functionId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceForBuilds)
{
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::FUNCTION_NOT_FOUND);
}
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
if ($deployment->getAttribute('resourceId') !== $function->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));
}
}
}

View file

@ -15,6 +15,7 @@ use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
use Utopia\Swoole\Request;
use Utopia\Validator\WhiteList;
class Get extends Action
{
@ -30,6 +31,7 @@ class Get extends Action
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId/download')
->httpAlias('/v1/functions/:functionId/deployments/:deploymentId/build/download', [ 'type' => 'output' ])
->groups(['api', 'functions'])
->desc('Download deployment')
->label('scope', 'functions.read')
@ -52,14 +54,16 @@ class Get extends Action
))
->param('functionId', '', new UID(), 'Function ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->param('type', 'source', new WhiteList(['source', 'output']), 'Deployment file to download. Can be: "source", "output".', true)
->inject('response')
->inject('request')
->inject('dbForProject')
->inject('deviceForFunctions')
->inject('deviceForBuilds')
->callback([$this, 'action']);
}
public function action(string $functionId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceForFunctions)
public function action(string $functionId, string $deploymentId, string $type, Response $response, Request $request, Database $dbForProject, Device $deviceForFunctions, Device $deviceForBuilds)
{
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
@ -75,8 +79,23 @@ class Get extends Action
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$path = $deployment->getAttribute('path', '');
if (!$deviceForFunctions->exists($path)) {
switch ($type) {
case 'output':
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId'));
if ($build->isEmpty()) {
throw new Exception(Exception::BUILD_NOT_FOUND);
}
$path = $build->getAttribute('path', '');
$device = $deviceForBuilds;
break;
case 'source':
$path = $deployment->getAttribute('path', '');
$device = $deviceForFunctions;
break;
}
if (!$device->exists($path)) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
@ -86,7 +105,7 @@ class Get extends Action
->addHeader('X-Peak', \memory_get_peak_usage())
->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"');
$size = $deviceForFunctions->getFileSize($path);
$size = $device->getFileSize($path);
$rangeHeader = $request->getHeader('range');
if (!empty($rangeHeader)) {
@ -108,13 +127,13 @@ class Get extends Action
->addHeader('Content-Length', $end - $start + 1)
->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT);
$response->send($deviceForFunctions->read($path, $start, ($end - $start + 1)));
$response->send($device->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(
$deviceForFunctions->read(
$device->read(
$path,
($i * MAX_OUTPUT_CHUNK_SIZE),
min(MAX_OUTPUT_CHUNK_SIZE, $size - ($i * MAX_OUTPUT_CHUNK_SIZE))
@ -123,7 +142,7 @@ class Get extends Action
);
}
} else {
$response->send($deviceForFunctions->read($path));
$response->send($device->read($path));
}
}
}

View file

@ -3,7 +3,6 @@
namespace Appwrite\Platform\Modules\Functions\Services;
use Appwrite\Platform\Modules\Functions\Http\Deployments\Builds\Create as CreateBuild;
use Appwrite\Platform\Modules\Functions\Http\Deployments\Builds\Download\Get as DownloadBuild;
use Appwrite\Platform\Modules\Functions\Http\Deployments\Builds\Update as UpdateBuild;
use Appwrite\Platform\Modules\Functions\Http\Deployments\Create as CreateDeployment;
use Appwrite\Platform\Modules\Functions\Http\Deployments\Delete as DeleteDeployment;
@ -65,7 +64,6 @@ class Http extends Service
$this->addAction(DownloadDeployment::getName(), new DownloadDeployment());
$this->addAction(CreateBuild::getName(), new CreateBuild());
$this->addAction(UpdateBuild::getName(), new UpdateBuild());
$this->addAction(DownloadBuild::getName(), new DownloadBuild());
// Executions
$this->addAction(CreateExecution::getName(), new CreateExecution());

View file

@ -1,133 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Deployments\Builds\Download;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\MethodType;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
use Utopia\Swoole\Request;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getDeploymentBuildDownload';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/build/download')
->desc('Download build')
->groups(['api', 'sites'])
->label('scope', 'sites.read')
->label('sdk', new Method(
namespace: 'sites',
name: 'getDeploymentBuildDownload',
description: <<<EOT
Get a site build content by its unique ID. The endpoint response return with a 'Content-Disposition: attachment' header that tells the browser to start downloading the file to user downloads directory.
EOT,
auth: [AuthType::KEY, AuthType::JWT],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_NONE
)
],
type: MethodType::LOCATION,
contentType: ContentType::ANY,
))
->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));
}
}
}

View file

@ -15,6 +15,7 @@ use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
use Utopia\Swoole\Request;
use Utopia\Validator\WhiteList;
class Get extends Action
{
@ -30,6 +31,7 @@ class Get extends Action
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/download')
->httpAlias('/v1/sites/:functionId/deployments/:deploymentId/build/download', [ 'type' => 'output' ])
->desc('Download deployment')
->groups(['api', 'sites'])
->label('scope', 'sites.read')
@ -51,14 +53,16 @@ class Get extends Action
))
->param('siteId', '', new UID(), 'Site ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->param('type', 'source', new WhiteList(['source', 'output']), 'Deployment file to download. Can be: "source", "output".', true)
->inject('response')
->inject('request')
->inject('dbForProject')
->inject('deviceForSites')
->inject('deviceForBuilds')
->callback([$this, 'action']);
}
public function action(string $siteId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceForSites)
public function action(string $siteId, string $deploymentId, string $type, Response $response, Request $request, Database $dbForProject, Device $deviceForSites, Device $deviceForBuilds)
{
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
@ -74,18 +78,33 @@ class Get extends Action
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$path = $deployment->getAttribute('path', '');
if (!$deviceForSites->exists($path)) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
switch ($type) {
case 'output':
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId'));
if ($build->isEmpty()) {
throw new Exception(Exception::BUILD_NOT_FOUND);
}
$path = $build->getAttribute('path', '');
$device = $deviceForBuilds;
break;
case 'source':
$path = $deployment->getAttribute('path', '');
$device = $deviceForSites;
break;
}
if (!$device->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"');
->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '-' . $type . '.tar.gz"');
$size = $deviceForSites->getFileSize($path);
$size = $device->getFileSize($path);
$rangeHeader = $request->getHeader('range');
if (!empty($rangeHeader)) {
@ -107,13 +126,13 @@ class Get extends Action
->addHeader('Content-Length', $end - $start + 1)
->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT);
$response->send($deviceForSites->read($path, $start, ($end - $start + 1)));
$response->send($device->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(
$deviceForSites->read(
$device->read(
$path,
($i * MAX_OUTPUT_CHUNK_SIZE),
min(MAX_OUTPUT_CHUNK_SIZE, $size - ($i * MAX_OUTPUT_CHUNK_SIZE))
@ -122,7 +141,7 @@ class Get extends Action
);
}
} else {
$response->send($deviceForSites->read($path));
$response->send($device->read($path));
}
}
}

View file

@ -3,7 +3,6 @@
namespace Appwrite\Platform\Modules\Sites\Services;
use Appwrite\Platform\Modules\Sites\Http\Deployments\Builds\Create as CreateBuild;
use Appwrite\Platform\Modules\Sites\Http\Deployments\Builds\Download\Get as DownloadBuild;
use Appwrite\Platform\Modules\Sites\Http\Deployments\Builds\Update as UpdateBuild;
use Appwrite\Platform\Modules\Sites\Http\Deployments\Create as CreateDeployment;
use Appwrite\Platform\Modules\Sites\Http\Deployments\Delete as DeleteDeployment;
@ -58,7 +57,6 @@ 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(CreateBuild::getName(), new CreateBuild());
$this->addAction(UpdateBuild::getName(), new UpdateBuild());

View file

@ -210,8 +210,24 @@ abstract class Format
break;
}
break;
case 'functions':
switch ($method) {
case 'getDeploymentDownload':
switch ($param) {
case 'type':
return 'DeploymentDownloadType';
}
break;
}
break;
case 'sites':
switch ($method) {
case 'getDeploymentDownload':
switch ($param) {
case 'type':
return 'DeploymentDownloadType';
}
break;
case 'getUsage':
case 'listUsage':
switch ($param) {

View file

@ -313,22 +313,14 @@ trait FunctionsBase
return $domain;
}
protected function getDeploymentDownload(string $functionId, string $deploymentId): mixed
protected function getDeploymentDownload(string $functionId, string $deploymentId, string $type): mixed
{
$response = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId . '/download', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
return $response;
}
protected function getBuildDownload(string $functionId, string $deploymentId): mixed
{
$response = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId . '/build/download', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
], $this->getHeaders()), [
'type' => $type
]);
return $response;
}

View file

@ -517,7 +517,7 @@ class FunctionsConsoleClientTest extends Scope
$this->assertNotEmpty($deploymentId);
$response = $this->getDeploymentDownload($functionId, $deploymentId);
$response = $this->getDeploymentDownload($functionId, $deploymentId, 'source');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('application/gzip', $response['headers']['content-type']);
$this->assertGreaterThan(0, $response['headers']['content-length']);
@ -525,7 +525,7 @@ class FunctionsConsoleClientTest extends Scope
$deploymentMd5 = \md5($response['body']);
$response = $this->getBuildDownload($functionId, $deploymentId);
$response = $this->getDeploymentDownload($functionId, $deploymentId, 'output');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('application/gzip', $response['headers']['content-type']);
$this->assertGreaterThan(0, $response['headers']['content-length']);

View file

@ -348,22 +348,14 @@ trait SitesBase
return $domain;
}
protected function getDeploymentDownload(string $siteId, string $deploymentId): mixed
protected function getDeploymentDownload(string $siteId, string $deploymentId, string $type): mixed
{
$response = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/deployments/' . $deploymentId . '/download', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
return $response;
}
protected function getBuildDownload(string $siteId, string $deploymentId): mixed
{
$response = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId . '/deployments/' . $deploymentId . '/build/download', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
], $this->getHeaders()), [
'type' => $type,
]);
return $response;
}

View file

@ -1657,7 +1657,7 @@ class SitesCustomServerTest extends Scope
$this->assertNotEmpty($deploymentId);
$response = $this->getDeploymentDownload($siteId, $deploymentId);
$response = $this->getDeploymentDownload($siteId, $deploymentId, 'source');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('application/gzip', $response['headers']['content-type']);
$this->assertGreaterThan(0, $response['headers']['content-length']);
@ -1665,7 +1665,7 @@ class SitesCustomServerTest extends Scope
$deploymentMd5 = \md5($response['body']);
$response = $this->getBuildDownload($siteId, $deploymentId);
$response = $this->getDeploymentDownload($siteId, $deploymentId, 'output');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('application/gzip', $response['headers']['content-type']);
$this->assertGreaterThan(0, $response['headers']['content-length']);