mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
454 lines
21 KiB
PHP
454 lines
21 KiB
PHP
<?php
|
|
|
|
namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
|
|
|
|
use Appwrite\Event\Event;
|
|
use Appwrite\Event\Message\Build as BuildMessage;
|
|
use Appwrite\Event\Publisher\Build as BuildPublisher;
|
|
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\Document;
|
|
use Utopia\Database\Helpers\ID;
|
|
use Utopia\Database\Helpers\Permission;
|
|
use Utopia\Database\Helpers\Role;
|
|
use Utopia\Database\Query;
|
|
use Utopia\Database\Validator\Authorization;
|
|
use Utopia\Database\Validator\UID;
|
|
use Utopia\Http\Adapter\Swoole\Request;
|
|
use Utopia\Lock\Exception\Contention as LockContention;
|
|
use Utopia\Platform\Action;
|
|
use Utopia\Platform\Scope\HTTP;
|
|
use Utopia\Storage\Device;
|
|
use Utopia\Storage\Validator\File;
|
|
use Utopia\Storage\Validator\FileExt;
|
|
use Utopia\Storage\Validator\FileSize;
|
|
use Utopia\Storage\Validator\Upload;
|
|
use Utopia\System\System;
|
|
use Utopia\Validator\Boolean;
|
|
use Utopia\Validator\Nullable;
|
|
use Utopia\Validator\Text;
|
|
|
|
class Create extends Action
|
|
{
|
|
use HTTP;
|
|
|
|
public static function getName()
|
|
{
|
|
return 'createDeployment';
|
|
}
|
|
|
|
public function __construct()
|
|
{
|
|
$this
|
|
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
|
|
->setHttpPath('/v1/sites/:siteId/deployments')
|
|
->desc('Create deployment')
|
|
->groups(['api', 'sites'])
|
|
->label('scope', 'sites.write')
|
|
->label('resourceType', RESOURCE_TYPE_SITES)
|
|
->label('event', 'sites.[siteId].deployments.[deploymentId].create')
|
|
->label('audits.event', 'deployment.create')
|
|
->label('audits.resource', 'site/{request.siteId}')
|
|
->label('sdk', new Method(
|
|
namespace: 'sites',
|
|
group: 'deployments',
|
|
name: 'createDeployment',
|
|
description: <<<EOT
|
|
Create a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the site's deployment to use your new deployment ID.
|
|
EOT,
|
|
auth: [AuthType::ADMIN, AuthType::KEY],
|
|
responses: [
|
|
new SDKResponse(
|
|
code: Response::STATUS_CODE_ACCEPTED,
|
|
model: Response::MODEL_DEPLOYMENT,
|
|
)
|
|
],
|
|
requestType: ContentType::MULTIPART,
|
|
type: MethodType::UPLOAD,
|
|
packaging: true,
|
|
))
|
|
->param('siteId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Site ID.', false, ['dbForProject'])
|
|
->param('installCommand', null, new Nullable(new Text(8192, 0)), 'Install Commands.', true)
|
|
->param('buildCommand', null, new Nullable(new Text(8192, 0)), 'Build Commands.', true)
|
|
->param('outputDirectory', null, new Nullable(new Text(8192, 0)), 'Output Directory.', true)
|
|
->param('code', [], new File(), 'Gzip file with your code package. When used with the Appwrite CLI, pass the path to your code directory, and the CLI will automatically package your code. Use a path that is within the current directory.', skipValidation: true)
|
|
->param('activate', false, new Boolean(true), 'Automatically activate the deployment when it is finished building.', true)
|
|
->inject('request')
|
|
->inject('response')
|
|
->inject('dbForProject')
|
|
->inject('dbForPlatform')
|
|
->inject('project')
|
|
->inject('queueForEvents')
|
|
->inject('deviceForSites')
|
|
->inject('deviceForLocal')
|
|
->inject('publisherForBuilds')
|
|
->inject('plan')
|
|
->inject('authorization')
|
|
->inject('platform')
|
|
->inject('locks')
|
|
->callback($this->action(...));
|
|
}
|
|
|
|
public function action(
|
|
string $siteId,
|
|
?string $installCommand,
|
|
?string $buildCommand,
|
|
?string $outputDirectory,
|
|
mixed $code,
|
|
mixed $activate,
|
|
Request $request,
|
|
Response $response,
|
|
Database $dbForProject,
|
|
Database $dbForPlatform,
|
|
Document $project,
|
|
Event $queueForEvents,
|
|
Device $deviceForSites,
|
|
Device $deviceForLocal,
|
|
BuildPublisher $publisherForBuilds,
|
|
array $plan,
|
|
Authorization $authorization,
|
|
array $platform,
|
|
callable $locks,
|
|
) {
|
|
$activate = \strval($activate) === 'true' || \strval($activate) === '1';
|
|
|
|
$site = $dbForProject->getDocument('sites', $siteId);
|
|
|
|
if ($site->isEmpty()) {
|
|
throw new Exception(Exception::SITE_NOT_FOUND);
|
|
}
|
|
|
|
if ($installCommand === null) {
|
|
$installCommand = $site->getAttribute('installCommand', '');
|
|
}
|
|
|
|
if ($buildCommand === null) {
|
|
$buildCommand = $site->getAttribute('buildCommand', '');
|
|
}
|
|
|
|
if ($outputDirectory === null) {
|
|
$outputDirectory = $site->getAttribute('outputDirectory', '');
|
|
}
|
|
|
|
$file = $request->getFiles('code');
|
|
|
|
// GraphQL multipart spec adds files with index keys
|
|
if (empty($file)) {
|
|
$file = $request->getFiles(0);
|
|
}
|
|
|
|
if (empty($file)) {
|
|
throw new Exception(Exception::STORAGE_FILE_EMPTY, 'No file sent');
|
|
}
|
|
|
|
$siteSizeLimit = (int) System::getEnv('_APP_COMPUTE_SIZE_LIMIT', '30000000');
|
|
|
|
if (isset($plan['deploymentSize'])) {
|
|
$siteSizeLimit = $plan['deploymentSize'] * 1000 * 1000;
|
|
}
|
|
|
|
$fileExt = new FileExt([FileExt::TYPE_GZIP]);
|
|
$fileSizeValidator = new FileSize($siteSizeLimit);
|
|
$upload = new Upload();
|
|
|
|
// Make sure we handle a single file and multiple files the same way
|
|
$fileName = (\is_array($file['name']) && isset($file['name'][0])) ? $file['name'][0] : $file['name'];
|
|
$fileTmpName = (\is_array($file['tmp_name']) && isset($file['tmp_name'][0])) ? $file['tmp_name'][0] : $file['tmp_name'];
|
|
$fileSize = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
|
|
|
|
if (!$fileExt->isValid($file['name'])) { // Check if file type is allowed
|
|
throw new Exception(Exception::STORAGE_FILE_TYPE_UNSUPPORTED);
|
|
}
|
|
|
|
$contentRange = $request->getHeader('content-range');
|
|
$deploymentId = ID::unique();
|
|
$chunk = 1;
|
|
$chunks = 1;
|
|
|
|
if (!empty($contentRange)) {
|
|
$start = $request->getContentRangeStart();
|
|
$end = $request->getContentRangeEnd();
|
|
$fileSize = $request->getContentRangeSize();
|
|
$deploymentId = $request->getHeader('x-appwrite-id', $deploymentId);
|
|
// TODO make `end >= $fileSize` in next breaking version
|
|
if (is_null($start) || is_null($end) || is_null($fileSize) || $end > $fileSize) {
|
|
throw new Exception(Exception::STORAGE_INVALID_CONTENT_RANGE);
|
|
}
|
|
|
|
$chunks = (int) ceil($fileSize / APP_LIMIT_UPLOAD_CHUNK_SIZE);
|
|
$chunk = (int) ($start / APP_LIMIT_UPLOAD_CHUNK_SIZE) + 1;
|
|
}
|
|
|
|
if (!$fileSizeValidator->isValid($fileSize) && $siteSizeLimit !== 0) { // Check if file size is exceeding allowed limit
|
|
throw new Exception(Exception::STORAGE_INVALID_FILE_SIZE);
|
|
}
|
|
|
|
if (!$upload->isValid($fileTmpName)) {
|
|
throw new Exception(Exception::STORAGE_INVALID_FILE);
|
|
}
|
|
|
|
// Save to storage
|
|
$fileSize ??= $deviceForLocal->getFileSize($fileTmpName);
|
|
$path = $deviceForSites->getPath($deploymentId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION));
|
|
|
|
$lockKey = 'sites:deployment:' . $project->getId() . ':' . $siteId . ':' . $deploymentId;
|
|
|
|
$metadata = ['content_type' => $deviceForLocal->getFileMimeType($fileTmpName)];
|
|
$completed = false;
|
|
|
|
try {
|
|
$locks($lockKey, 600, function () use (&$chunks, $dbForProject, $deploymentId, &$metadata, &$completed, $response): void {
|
|
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
|
|
|
|
if (!$deployment->isEmpty()) {
|
|
$chunks = $deployment->getAttribute('sourceChunksTotal', 1);
|
|
$uploaded = $deployment->getAttribute('sourceChunksUploaded', 0);
|
|
$metadata = $deployment->getAttribute('sourceMetadata', []);
|
|
|
|
if ($uploaded === $chunks) {
|
|
$response
|
|
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
|
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
|
|
|
|
$completed = true;
|
|
return;
|
|
}
|
|
}
|
|
}, timeout: 120.0);
|
|
} catch (LockContention) {
|
|
$response->addHeader('Retry-After', '5');
|
|
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Deployment upload is busy. Try again.');
|
|
}
|
|
|
|
if ($completed) {
|
|
return;
|
|
}
|
|
|
|
$chunksUploaded = $deviceForSites->upload($fileTmpName, $path, $chunk, $chunks, $metadata);
|
|
|
|
if (empty($chunksUploaded)) {
|
|
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed moving file');
|
|
}
|
|
|
|
$type = $request->getHeader('x-sdk-language') === 'cli' ? 'cli' : 'manual';
|
|
|
|
$commands = [];
|
|
if (!empty($installCommand)) {
|
|
$commands[] = $installCommand;
|
|
}
|
|
if (!empty($buildCommand)) {
|
|
$commands[] = $buildCommand;
|
|
}
|
|
|
|
try {
|
|
$locks($lockKey, 600, function () use ($activate, $authorization, $commands, &$chunks, $chunksUploaded, $dbForPlatform, $dbForProject, $deploymentId, $deviceForSites, $fileSize, &$metadata, $outputDirectory, $path, $platform, $project, $publisherForBuilds, $queueForEvents, $response, &$site, $siteId, $type): void {
|
|
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
|
|
$uploaded = 0;
|
|
|
|
if (!$deployment->isEmpty()) {
|
|
$chunks = $deployment->getAttribute('sourceChunksTotal', 1);
|
|
$uploaded = $deployment->getAttribute('sourceChunksUploaded', 0);
|
|
$metadata = \array_merge($deployment->getAttribute('sourceMetadata', []), $metadata);
|
|
|
|
if ($uploaded === $chunks) {
|
|
$response
|
|
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
|
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
|
|
return;
|
|
}
|
|
}
|
|
|
|
$chunksUploaded = max($uploaded, $chunksUploaded);
|
|
|
|
if ($chunksUploaded === $chunks && $uploaded < $chunks) {
|
|
if ($activate) {
|
|
// Remove deploy for all other deployments.
|
|
$activeDeployments = $dbForProject->find('deployments', [
|
|
Query::equal('activate', [true]),
|
|
Query::equal('resourceId', [$siteId]),
|
|
Query::equal('resourceType', ['sites'])
|
|
]);
|
|
|
|
foreach ($activeDeployments as $activeDeployment) {
|
|
$dbForProject->updateDocument('deployments', $activeDeployment->getId(), new Document(['activate' => false]));
|
|
}
|
|
}
|
|
|
|
$fileSize = $deviceForSites->getFileSize($path);
|
|
|
|
if ($deployment->isEmpty()) {
|
|
$deployment = $dbForProject->createDocument('deployments', new Document([
|
|
'$id' => $deploymentId,
|
|
'$permissions' => [
|
|
Permission::read(Role::any()),
|
|
Permission::update(Role::any()),
|
|
Permission::delete(Role::any()),
|
|
],
|
|
'resourceInternalId' => $site->getSequence(),
|
|
'resourceId' => $site->getId(),
|
|
'resourceType' => 'sites',
|
|
'buildCommands' => \implode(' && ', $commands),
|
|
'startCommand' => $site->getAttribute('startCommand', ''),
|
|
'buildOutput' => $outputDirectory,
|
|
'adapter' => $site->getAttribute('adapter', ''),
|
|
'fallbackFile' => $site->getAttribute('fallbackFile', ''),
|
|
'sourcePath' => $path,
|
|
'sourceSize' => $fileSize,
|
|
'totalSize' => $fileSize,
|
|
'sourceChunksTotal' => $chunks,
|
|
'sourceChunksUploaded' => $chunksUploaded,
|
|
'activate' => $activate,
|
|
'sourceMetadata' => $metadata,
|
|
'type' => $type,
|
|
]));
|
|
|
|
$site = $site
|
|
->setAttribute('latestDeploymentId', $deployment->getId())
|
|
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
|
|
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
|
|
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
|
|
$dbForProject->updateDocument('sites', $site->getId(), new Document([
|
|
'latestDeploymentId' => $deployment->getId(),
|
|
'latestDeploymentInternalId' => $deployment->getSequence(),
|
|
'latestDeploymentCreatedAt' => $deployment->getCreatedAt(),
|
|
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
|
|
]));
|
|
|
|
$sitesDomain = $platform['sitesDomain'];
|
|
$domain = ID::unique() . "." . $sitesDomain;
|
|
|
|
// TODO: (@Meldiron) Remove after 1.7.x migration
|
|
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
|
|
$ruleId = $isMd5 ? md5($domain) : ID::unique();
|
|
|
|
$authorization->skip(
|
|
fn () => $dbForPlatform->createDocument('rules', new Document([
|
|
'$id' => $ruleId,
|
|
'projectId' => $project->getId(),
|
|
'projectInternalId' => $project->getSequence(),
|
|
'domain' => $domain,
|
|
'type' => 'deployment',
|
|
'trigger' => 'deployment',
|
|
'deploymentId' => $deployment->isEmpty() ? '' : $deployment->getId(),
|
|
'deploymentInternalId' => $deployment->isEmpty() ? '' : $deployment->getSequence(),
|
|
'deploymentResourceType' => 'site',
|
|
'deploymentResourceId' => $site->getId(),
|
|
'deploymentResourceInternalId' => $site->getSequence(),
|
|
'status' => 'verified',
|
|
'certificateId' => '',
|
|
'search' => implode(' ', [$ruleId, $domain]),
|
|
'owner' => 'Appwrite',
|
|
'region' => $project->getAttribute('region')
|
|
]))
|
|
);
|
|
} else {
|
|
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([
|
|
'sourceSize' => $fileSize,
|
|
'sourceChunksUploaded' => $chunksUploaded,
|
|
'sourceMetadata' => $metadata,
|
|
]));
|
|
}
|
|
|
|
// Start the build
|
|
$publisherForBuilds->enqueue(new BuildMessage(
|
|
project: $project,
|
|
resource: $site,
|
|
deployment: $deployment,
|
|
type: BUILD_TYPE_DEPLOYMENT,
|
|
platform: $platform,
|
|
));
|
|
} else {
|
|
if ($deployment->isEmpty()) {
|
|
$deployment = $dbForProject->createDocument('deployments', new Document([
|
|
'$id' => $deploymentId,
|
|
'$permissions' => [
|
|
Permission::read(Role::any()),
|
|
Permission::update(Role::any()),
|
|
Permission::delete(Role::any()),
|
|
],
|
|
'resourceInternalId' => $site->getSequence(),
|
|
'resourceId' => $site->getId(),
|
|
'resourceType' => 'sites',
|
|
'buildCommands' => \implode(' && ', $commands),
|
|
'startCommand' => $site->getAttribute('startCommand', ''),
|
|
'buildOutput' => $outputDirectory,
|
|
'adapter' => $site->getAttribute('adapter', ''),
|
|
'fallbackFile' => $site->getAttribute('fallbackFile', ''),
|
|
'sourcePath' => $path,
|
|
'sourceSize' => $fileSize,
|
|
'totalSize' => $fileSize,
|
|
'sourceChunksTotal' => $chunks,
|
|
'sourceChunksUploaded' => $chunksUploaded,
|
|
'activate' => $activate,
|
|
'sourceMetadata' => $metadata,
|
|
'type' => $type,
|
|
]));
|
|
|
|
$site = $site
|
|
->setAttribute('latestDeploymentId', $deployment->getId())
|
|
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
|
|
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
|
|
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
|
|
$dbForProject->updateDocument('sites', $site->getId(), new Document([
|
|
'latestDeploymentId' => $site->getAttribute('latestDeploymentId'),
|
|
'latestDeploymentInternalId' => $site->getAttribute('latestDeploymentInternalId'),
|
|
'latestDeploymentCreatedAt' => $site->getAttribute('latestDeploymentCreatedAt'),
|
|
'latestDeploymentStatus' => $site->getAttribute('latestDeploymentStatus'),
|
|
]));
|
|
|
|
$sitesDomain = $platform['sitesDomain'];
|
|
$domain = ID::unique() . "." . $sitesDomain;
|
|
$ruleId = md5($domain);
|
|
$authorization->skip(
|
|
fn () => $dbForPlatform->createDocument('rules', new Document([
|
|
'$id' => $ruleId,
|
|
'projectId' => $project->getId(),
|
|
'projectInternalId' => $project->getSequence(),
|
|
'domain' => $domain,
|
|
'type' => 'deployment',
|
|
'trigger' => 'deployment',
|
|
'deploymentId' => $deployment->isEmpty() ? '' : $deployment->getId(),
|
|
'deploymentInternalId' => $deployment->isEmpty() ? '' : $deployment->getSequence(),
|
|
'deploymentResourceType' => 'site',
|
|
'deploymentResourceId' => $site->getId(),
|
|
'deploymentResourceInternalId' => $site->getSequence(),
|
|
'status' => 'verified',
|
|
'certificateId' => '',
|
|
'search' => implode(' ', [$ruleId, $domain]),
|
|
'owner' => 'Appwrite',
|
|
'region' => $project->getAttribute('region')
|
|
]))
|
|
);
|
|
} else {
|
|
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([
|
|
'sourceChunksUploaded' => $chunksUploaded,
|
|
'sourceMetadata' => $metadata,
|
|
]));
|
|
}
|
|
}
|
|
|
|
$metadata = null;
|
|
|
|
if ($chunksUploaded === $chunks) {
|
|
$queueForEvents
|
|
->setParam('siteId', $site->getId())
|
|
->setParam('deploymentId', $deployment->getId());
|
|
}
|
|
|
|
$response
|
|
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
|
|
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
|
|
}, timeout: 120.0);
|
|
} catch (LockContention) {
|
|
$response->addHeader('Retry-After', '5');
|
|
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Deployment upload is busy. Try again.');
|
|
}
|
|
}
|
|
}
|