Resolve merge conflicts

This commit is contained in:
Khushboo Verma 2025-03-05 11:38:28 +05:30
commit 9ee9a8e0ed
57 changed files with 437 additions and 855 deletions

View file

@ -68,6 +68,7 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/sdks && \
chmod +x /usr/local/bin/specs && \
chmod +x /usr/local/bin/ssl && \
chmod +x /usr/local/bin/screenshot && \
chmod +x /usr/local/bin/test && \
chmod +x /usr/local/bin/upgrade && \
chmod +x /usr/local/bin/vars && \

View file

@ -121,7 +121,8 @@ return [
'key' => 'template-for-onelink',
'name' => 'Onelink template',
'useCases' => ['starter'],
'demoImage' => $url . '/console/images/sites/templates/template-for-onelink.png',
'screenshotDark' => $url . '/images/sites/templates/template-for-onelink-dark.png',
'screenshotLight' => $url . '/images/sites/templates/template-for-onelink-light.png',
'frameworks' => [
getFramework('NUXT', [
'providerRootDirectory' => './onelink',
@ -140,7 +141,8 @@ return [
'key' => 'starter-for-svelte',
'name' => 'Svelte starter',
'useCases' => ['starter'],
'demoImage' => $url . '/console/images/sites/templates/starter-for-svelte.png',
'screenshotDark' => $url . '/images/sites/templates/starter-for-svelte-dark.png',
'screenshotLight' => $url . '/images/sites/templates/starter-for-svelte-light.png',
'frameworks' => [
getFramework('SVELTEKIT', [
'providerRootDirectory' => './',
@ -181,7 +183,8 @@ return [
'key' => 'starter-for-react',
'name' => 'React starter',
'useCases' => ['starter'],
'demoImage' => $url . '/console/images/sites/templates/starter-for-react.png',
'screenshotDark' => $url . '/images/sites/templates/starter-for-react-dark.png',
'screenshotLight' => $url . '/images/sites/templates/starter-for-react-light.png',
'frameworks' => [
getFramework('REACT', [
'providerRootDirectory' => './',
@ -222,7 +225,8 @@ return [
'key' => 'starter-for-vue',
'name' => 'Vue starter',
'useCases' => ['starter'],
'demoImage' => $url . '/console/images/sites/templates/starter-for-vue.png',
'screenshotDark' => $url . '/images/sites/templates/starter-for-vue-dark.png',
'screenshotLight' => $url . '/images/sites/templates/starter-for-vue-light.png',
'frameworks' => [
getFramework('VUE', [
'providerRootDirectory' => './',
@ -263,7 +267,8 @@ return [
'key' => 'starter-for-react-native',
'name' => 'React Native starter',
'useCases' => ['starter'],
'demoImage' => $url . '/console/images/sites/templates/starter-for-react-native.png',
'screenshotDark' => $url . '/images/sites/templates/starter-for-react-native-dark.png',
'screenshotLight' => $url . '/images/sites/templates/starter-for-react-native-light.png',
'frameworks' => [
getFramework('REACT', [
'providerRootDirectory' => './',
@ -305,7 +310,8 @@ return [
'key' => 'starter-for-nextjs',
'name' => 'Next.js starter',
'useCases' => ['starter'],
'demoImage' => $url . '/console/images/sites/templates/starter-for-nextjs.png',
'screenshotDark' => $url . '/images/sites/templates/starter-for-nextjs-dark.png',
'screenshotLight' => $url . '/images/sites/templates/starter-for-nextjs-light.png',
'frameworks' => [
getFramework('NEXTJS', [
'providerRootDirectory' => './',
@ -346,7 +352,8 @@ return [
'key' => 'starter-for-nuxt',
'name' => 'Nuxt starter',
'useCases' => ['starter'],
'demoImage' => $url . '/console/images/sites/templates/starter-for-nuxt.png',
'screenshotDark' => $url . '/images/sites/templates/starter-for-nuxt-dark.png',
'screenshotLight' => $url . '/images/sites/templates/starter-for-nuxt-light.png',
'frameworks' => [
getFramework('NUXT', [
'providerRootDirectory' => './',
@ -387,7 +394,8 @@ return [
'key' => 'template-for-event',
'name' => 'Event template',
'useCases' => ['starter'],
'demoImage' => $url . '/console/images/sites/templates/template-for-event.png',
'screenshotDark' => $url . '/images/sites/templates/template-for-event-dark.png',
'screenshotLight' => $url . '/images/sites/templates/template-for-event-light.png',
'frameworks' => [
getFramework('NEXTJS', [
'providerRootDirectory' => './',
@ -422,7 +430,8 @@ return [
'key' => 'template-for-portfolio',
'name' => 'Portfolio template',
'useCases' => ['starter'],
'demoImage' => $url . '/console/images/sites/templates/template-for-portfolio.png',
'screenshotDark' => $url . '/images/sites/templates/template-for-portfolio-dark.png',
'screenshotLight' => $url . '/images/sites/templates/template-for-portfolio-light.png',
'frameworks' => [
getFramework('NEXTJS', [
'providerRootDirectory' => './',
@ -438,7 +447,8 @@ return [
'key' => 'template-for-store',
'name' => 'Store template',
'useCases' => ['starter'],
'demoImage' => $url . '/console/images/sites/templates/template-for-store.png',
'screenshotDark' => $url . '/images/sites/templates/template-for-store-dark.png',
'screenshotLight' => $url . '/images/sites/templates/template-for-store-light.png',
'frameworks' => [
getFramework('SVELTEKIT', [
'providerRootDirectory' => './',
@ -479,7 +489,8 @@ return [
'key' => 'template-for-blog',
'name' => 'Blog template',
'useCases' => ['starter'],
'demoImage' => $url . '/console/images/sites/templates/template-for-blog.png',
'screenshotDark' => $url . '/images/sites/templates/template-for-blog-dark.png',
'screenshotLight' => $url . '/images/sites/templates/template-for-blog-light.png',
'frameworks' => [
getFramework('SVELTEKIT', [
'providerRootDirectory' => './',
@ -495,7 +506,8 @@ return [
'key' => 'astro-starter',
'name' => 'Astro starter',
'useCases' => ['starter'],
'demoImage' => '',
'screenshotDark' => $url . '/images/sites/templates/astro-starter-dark.png',
'screenshotLight' => $url . '/images/sites/templates/astro-starter-light.png',
'frameworks' => [
getFramework('ASTRO', [
'providerRootDirectory' => './astro/starter',
@ -511,7 +523,8 @@ return [
'key' => 'remix-starter',
'name' => 'Remix starter',
'useCases' => ['starter'],
'demoImage' => '',
'screenshotDark' => $url . '/images/sites/templates/remix-starter-dark.png',
'screenshotLight' => $url . '/images/sites/templates/remix-starter-light.png',
'frameworks' => [
getFramework('REMIX', [
'providerRootDirectory' => './remix/starter',
@ -527,7 +540,8 @@ return [
'key' => 'flutter-starter',
'name' => 'Flutter starter',
'useCases' => ['starter'],
'demoImage' => '',
'screenshotDark' => $url . '/images/sites/templates/flutter-starter-dark.png',
'screenshotLight' => $url . '/images/sites/templates/flutter-starter-light.png',
'frameworks' => [
getFramework('FLUTTER', [
'providerRootDirectory' => './flutter/starter',
@ -543,6 +557,8 @@ return [
'key' => 'nextjs-starter',
'name' => 'Next.js starter website',
'useCases' => ['starter'],
'screenshotDark' => $url . '/images/sites/templates/nextjs-starter-dark.png',
'screenshotLight' => $url . '/images/sites/templates/nextjs-starter-light.png',
'frameworks' => [
getFramework('NEXTJS', [
'providerRootDirectory' => './nextjs/starter',
@ -558,6 +574,8 @@ return [
'key' => 'nuxt-starter',
'name' => 'Nuxt starter website',
'useCases' => ['starter'],
'screenshotDark' => $url . '/images/sites/templates/nuxt-starter-dark.png',
'screenshotLight' => $url . '/images/sites/templates/nuxt-starter-light.png',
'frameworks' => [
getFramework('NUXT', [
'providerRootDirectory' => './nuxt/starter',
@ -573,6 +591,8 @@ return [
'key' => 'sveltekit-starter',
'name' => 'SvelteKit starter website',
'useCases' => ['starter'],
'screenshotDark' => $url . '/images/sites/templates/sveltekit-starter-dark.png',
'screenshotLight' => $url . '/images/sites/templates/sveltekit-starter-light.png',
'frameworks' => [
getFramework('SVELTEKIT', [
'providerRootDirectory' => './sveltekit/starter',

View file

@ -29,6 +29,8 @@ use Utopia\Pools\Group;
use Utopia\Swoole\Files;
use Utopia\System\System;
Files::load(__DIR__.'/../public');
const DOMAIN_SYNC_TIMER = 30; // 30 seconds
$domains = new Table(1_000_000); // 1 million rows

View file

@ -1977,6 +1977,7 @@ App::setResource('previewHostname', function (Request $request, ?Key $apiKey) {
}
}
return '';
}, ['request', 'apiKey']);

3
bin/screenshot Executable file
View file

@ -0,0 +1,3 @@
#!/bin/sh
php /usr/src/code/app/cli.php screenshot $@

View file

@ -953,7 +953,7 @@ services:
appwrite-browser:
container_name: appwrite-browser
image: appwrite/browser:0.2.0
image: appwrite/browser:0.2.1
networks:
- appwrite
@ -962,7 +962,7 @@ services:
hostname: exc1
<<: *x-logging
stop_signal: SIGINT
image: openruntimes/executor:0.7.6
image: openruntimes/executor:0.7.7
restart: unless-stopped
networks:
- appwrite

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

View file

@ -1,112 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Deployments\Builds;
use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createDeploymentBuild';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId/build')
->httpAlias('/v1/functions/:functionId/deployments/:deploymentId/builds/:buildId')
->desc('Rebuild deployment')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('event', 'functions.[functionId].deployments.[deploymentId].update')
->label('audits.event', 'deployment.update')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
name: 'createBuild',
description: <<<EOT
Create a new build for an existing function deployment. This endpoint allows you to rebuild a deployment with the updated function configuration, including its entrypoint and build commands if they have been modified. The build process will be queued and executed asynchronously. The original deployment's code will be preserved and used for the new build.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
]
))
->param('functionId', '', new UID(), 'Function ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->param('buildId', '', new UID(), 'Build unique ID.', true) // added as optional param for backward compatibility
->inject('response')
->inject('dbForProject')
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('deviceForFunctions')
->callback([$this, 'action']);
}
public function action(string $functionId, string $deploymentId, string $buildId, Response $response, Database $dbForProject, Event $queueForEvents, Build $queueForBuilds, Device $deviceForFunctions)
{
$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);
}
$path = $deployment->getAttribute('path');
if (empty($path) || !$deviceForFunctions->exists($path)) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$deploymentId = ID::unique();
$destination = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
$deviceForFunctions->transfer($path, $destination, $deviceForFunctions);
$deployment->removeAttribute('$internalId');
$deployment = $dbForProject->createDocument('deployments', $deployment->setAttributes([
'$internalId' => '',
'$id' => $deploymentId,
'buildId' => '',
'buildInternalId' => '',
'path' => $destination,
'entrypoint' => $function->getAttribute('entrypoint'),
'commands' => $function->getAttribute('commands', ''),
'search' => implode(' ', [$deploymentId, $function->getAttribute('entrypoint')]),
]));
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($function)
->setDeployment($deployment);
$queueForEvents
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
$response->noContent();
}
}

View file

@ -1,134 +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('resourceType', RESOURCE_TYPE_FUNCTIONS)
->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

@ -1,136 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Functions\Http\Deployments\Builds;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Executor\Executor;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateDeploymentBuild';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/functions/:functionId/deployments/:deploymentId/build')
->desc('Cancel deployment')
->groups(['api', 'functions'])
->label('scope', 'functions.write')
->label('resourceType', RESOURCE_TYPE_FUNCTIONS)
->label('audits.event', 'deployment.update')
->label('audits.resource', 'function/{request.functionId}')
->label('sdk', new Method(
namespace: 'functions',
name: 'updateDeploymentBuild',
description: <<<EOT
Cancel an ongoing function deployment build. If the build is already in progress, it will be stopped and marked as canceled. If the build hasn't started yet, it will be marked as canceled without executing. You cannot cancel builds that have already completed (status 'ready') or failed. The response includes the final build status and details.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_BUILD,
)
]
))
->param('functionId', '', new UID(), 'Function ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('queueForEvents')
->callback([$this, 'action']);
}
public function action(string $functionId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents)
{
$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);
}
$build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
if ($build->isEmpty()) {
$buildId = ID::unique();
$build = $dbForProject->createDocument('builds', new Document([
'$id' => $buildId,
'$permissions' => [],
'startTime' => DateTime::now(),
'deploymentInternalId' => $deployment->getInternalId(),
'deploymentId' => $deployment->getId(),
'status' => 'canceled',
'path' => '',
'runtime' => $function->getAttribute('runtime'),
'source' => $deployment->getAttribute('path', ''),
'sourceType' => '',
'logs' => '',
'duration' => 0,
'size' => 0
]));
$deployment->setAttribute('buildId', $build->getId());
$deployment->setAttribute('buildInternalId', $build->getInternalId());
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
} else {
if (\in_array($build->getAttribute('status'), ['ready', 'failed'])) {
throw new Exception(Exception::BUILD_ALREADY_COMPLETED);
}
$startTime = new \DateTime($build->getAttribute('startTime'));
$endTime = new \DateTime('now');
$duration = $endTime->getTimestamp() - $startTime->getTimestamp();
$build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttributes([
'endTime' => DateTime::now(),
'duration' => $duration,
'status' => 'canceled'
]));
}
$dbForProject->purgeCachedDocument('deployments', $deployment->getId());
try {
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
$executor->deleteRuntime($project->getId(), $deploymentId . "-build");
} catch (\Throwable $th) {
// Don't throw if the deployment doesn't exist
if ($th->getCode() !== 404) {
throw $th;
}
}
$queueForEvents
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
$response->dynamic($build, Response::MODEL_BUILD);
}
}

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

@ -740,6 +740,7 @@ class Builds extends Action
}
$client = new FetchClient();
$client->setTimeout(\intval($resource->getAttribute('timeout', '15')));
$client->addHeader('content-type', FetchClient::CONTENT_TYPE_APPLICATION_JSON);
$bucket = Authorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots'));

View file

@ -1,136 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Deployments\Builds;
use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
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\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
use Utopia\System\System;
class Create extends Action
{
use HTTP;
public static function getName()
{
return 'createDeploymentBuild';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST)
->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/build')
->desc('Rebuild deployment')
->groups(['api', 'sites'])
->label('scope', 'sites.write')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('event', 'sites.[siteId].deployments.[deploymentId].update')
->label('audits.event', 'deployment.update')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk', new Method(
namespace: 'sites',
name: 'createDeploymentBuild',
description: <<<EOT
Create a new build for an existing site deployment. This endpoint allows you to rebuild a deployment with the updated site configuration, including its commands and output directory if they have been modified. The build process will be queued and executed asynchronously. The original deployment's code will be preserved and used for the new build.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_NOCONTENT,
model: Response::MODEL_NONE,
)
]
))
->param('siteId', '', new UID(), 'Site ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->inject('response')
->inject('project')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('queueForEvents')
->inject('queueForBuilds')
->inject('deviceForSites')
->callback([$this, 'action']);
}
public function action(string $siteId, string $deploymentId, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform, Event $queueForEvents, Build $queueForBuilds, Device $deviceForSites)
{
$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);
}
$path = $deployment->getAttribute('path');
if (empty($path) || !$deviceForSites->exists($path)) {
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$deploymentId = ID::unique();
$destination = $deviceForSites->getPath($deploymentId . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION));
$deviceForSites->transfer($path, $destination, $deviceForSites);
$deployment->removeAttribute('$internalId');
$deployment = $dbForProject->createDocument('deployments', $deployment->setAttributes([
'$internalId' => '',
'$id' => $deploymentId,
'buildId' => '',
'buildInternalId' => '',
'path' => $destination,
'buildCommand' => $site->getAttribute('buildCommand', ''),
'installCommand' => $site->getAttribute('installCommand', ''),
'outputDirectory' => $site->getAttribute('outputDirectory', ''),
'search' => implode(' ', [$deploymentId]),
'screenshotLight' => '',
'screenshotDark' => ''
]));
// Preview deployments for sites
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$domain = ID::unique() . "." . $sitesDomain;
$ruleId = md5($domain);
Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(),
'domain' => $domain,
'type' => 'deployment',
'value' => $deployment->getId(),
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
]))
);
$queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($site)
->setDeployment($deployment);
$queueForEvents
->setParam('siteId', $site->getId())
->setParam('deploymentId', $deployment->getId());
$response->noContent();
}
}

View file

@ -1,134 +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('resourceType', RESOURCE_TYPE_SITES)
->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

@ -1,136 +0,0 @@
<?php
namespace Appwrite\Platform\Modules\Sites\Http\Deployments\Builds;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Executor\Executor;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateDeploymentBuild';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/sites/:siteId/deployments/:deploymentId/build')
->desc('Cancel deployment')
->groups(['api', 'sites'])
->label('scope', 'sites.write')
->label('resourceType', RESOURCE_TYPE_SITES)
->label('audits.event', 'deployment.update')
->label('audits.resource', 'site/{request.siteId}')
->label('sdk', new Method(
namespace: 'sites',
name: 'updateDeploymentBuild',
description: <<<EOT
Cancel an ongoing site deployment build. If the build is already in progress, it will be stopped and marked as canceled. If the build hasn't started yet, it will be marked as canceled without executing. You cannot cancel builds that have already completed (status 'ready') or failed. The response includes the final build status and details.
EOT,
auth: [AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_BUILD,
)
]
))
->param('siteId', '', new UID(), 'Site ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.')
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('queueForEvents')
->callback([$this, 'action']);
}
public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject, Document $project, Event $queueForEvents)
{
$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);
}
$build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', '')));
if ($build->isEmpty()) {
$buildId = ID::unique();
$build = $dbForProject->createDocument('builds', new Document([
'$id' => $buildId,
'$permissions' => [],
'startTime' => DateTime::now(),
'deploymentInternalId' => $deployment->getInternalId(),
'deploymentId' => $deployment->getId(),
'status' => 'canceled',
'path' => '',
'runtime' => $site->getAttribute('framework'),
'source' => $deployment->getAttribute('path', ''),
'sourceType' => '',
'logs' => '',
'duration' => 0,
'size' => 0
]));
$deployment->setAttribute('buildId', $build->getId());
$deployment->setAttribute('buildInternalId', $build->getInternalId());
$deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
} else {
if (\in_array($build->getAttribute('status'), ['ready', 'failed'])) {
throw new Exception(Exception::BUILD_ALREADY_COMPLETED);
}
$startTime = new \DateTime($build->getAttribute('startTime'));
$endTime = new \DateTime('now');
$duration = $endTime->getTimestamp() - $startTime->getTimestamp();
$build = $dbForProject->updateDocument('builds', $build->getId(), $build->setAttributes([
'endTime' => DateTime::now(),
'duration' => $duration,
'status' => 'canceled'
]));
}
$dbForProject->purgeCachedDocument('deployments', $deployment->getId());
try {
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
$executor->deleteRuntime($project->getId(), $deploymentId . "-build");
} catch (\Throwable $th) {
// Don't throw if the deployment doesn't exist
if ($th->getCode() !== 404) {
throw $th;
}
}
$queueForEvents
->setParam('siteId', $site->getId())
->setParam('deploymentId', $deployment->getId());
$response->dynamic($build, Response::MODEL_BUILD);
}
}

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')
@ -52,14 +54,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()) {
@ -75,18 +79,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)) {
@ -108,13 +127,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))
@ -123,7 +142,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

@ -10,6 +10,7 @@ use Appwrite\Platform\Tasks\QueueRetry;
use Appwrite\Platform\Tasks\ScheduleExecutions;
use Appwrite\Platform\Tasks\ScheduleFunctions;
use Appwrite\Platform\Tasks\ScheduleMessages;
use Appwrite\Platform\Tasks\Screenshot;
use Appwrite\Platform\Tasks\SDKs;
use Appwrite\Platform\Tasks\Specs;
use Appwrite\Platform\Tasks\SSL;
@ -32,6 +33,7 @@ class Tasks extends Service
->addAction(QueueRetry::getName(), new QueueRetry())
->addAction(SDKs::getName(), new SDKs())
->addAction(SSL::getName(), new SSL())
->addAction(Screenshot::getName(), new Screenshot())
->addAction(ScheduleFunctions::getName(), new ScheduleFunctions())
->addAction(ScheduleExecutions::getName(), new ScheduleExecutions())
->addAction(ScheduleMessages::getName(), new ScheduleMessages())

View file

@ -0,0 +1,300 @@
<?php
namespace Appwrite\Platform\Tasks;
use Appwrite\ID;
use Tests\E2E\Client;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Platform\Action;
use Utopia\Validator\Text;
class Screenshot extends Action
{
public static function getName(): string
{
return 'screenshot';
}
public function __construct()
{
$this
->desc('Create Site template screenshot')
->param('templateId', '', new Text(128), 'Template ID.')
->callback(fn (string $templateId) => $this->action($templateId));
}
public function action(string $templateId): void
{
$templates = Config::getParam('site-templates', []);
$allowedTemplates = \array_filter($templates, function ($item) use ($templateId) {
return $item['key'] === $templateId;
});
$template = \array_shift($allowedTemplates);
if (empty($template)) {
throw new \Exception("Template {$templateId} not found. Find correct ID in app/config/site-templates.php");
}
Console::info("Found: " . $template['name']);
$client = new Client();
$client->setEndpoint('http://localhost/v1');
$client->addHeader('origin', 'http://localhost');
// Register
$email = uniqid() . 'user@localhost.test';
$password = 'password';
$user = $client->call(Client::METHOD_POST, '/account', [
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
], [
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
]);
if ($user['headers']['status-code'] !== 201) {
Console::error(\json_encode($user));
throw new \Exception("Failed to register user");
}
Console::info("User created");
// Login
$session = $client->call(Client::METHOD_POST, '/account/sessions/email', [
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
], [
'email' => $email,
'password' => $password,
]);
if ($session['headers']['status-code'] !== 201) {
Console::error(\json_encode($session));
throw new \Exception("Failed to login user");
}
Console::info("Session created");
$secret = $session['cookies']['a_session_console'];
$cookieConsole = 'a_session_console=' . $secret;
// Create organization
$team = $client->call(Client::METHOD_POST, '/teams', [
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
'cookie' => $cookieConsole
], [
'teamId' => ID::unique(),
'name' => 'Demo Project Team',
]);
if ($team['headers']['status-code'] !== 201) {
Console::error(\json_encode($team));
throw new \Exception("Failed to create team");
}
Console::info("Team created");
$projectName = 'Demo Project';
$projectId = ID::unique();
// Create project
$project = $client->call(Client::METHOD_POST, '/projects', [
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
'cookie' => $cookieConsole
], [
'projectId' => $projectId,
'region' => 'default',
'name' => $projectName,
'teamId' => $team['body']['$id'],
'description' => 'Demo Project Description',
'url' => 'https://appwrite.io',
]);
if ($project['headers']['status-code'] !== 201) {
Console::error(\json_encode($project));
throw new \Exception("Failed to create project");
}
Console::info("Project created");
$projectId = $project['body']['$id'];
$framework = $template['frameworks'][0];
// Create site
$site = $client->call(Client::METHOD_POST, '/sites', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-mode' => 'admin',
'cookie' => $cookieConsole
], [
'siteId' => ID::unique(),
'name' => $template["name"],
'framework' => $framework['key'],
'adapter' => $framework['adapter'],
'buildCommand' => $framework['buildCommand'],
'buildRuntime' => $framework['buildRuntime'],
'fallbackFile' => $framework['fallbackFile'],
'installCommand' => $framework['installCommand'],
'outputDirectory' => $framework['outputDirectory'],
'providerRootDirectory' => $framework['providerRootDirectory'],
'timeout' => 60
]);
if ($site['headers']['status-code'] !== 201) {
Console::error(\json_encode($site));
throw new \Exception("Failed to create site");
}
Console::info("Site created");
$siteId = $site['body']['$id'];
// Create variables
if (!empty($template['variables'] ?? [])) {
foreach ($template['variables'] as $variable) {
if (empty($variable['value'] ?? '')) {
if (($variable['required'] ?? false) === true) {
throw new \Exception("Missing required variable: {$variable['name']}");
}
continue;
}
$value = $variable['value'];
$value = \str_replace('{projectName}', $projectName, $value);
$value = \str_replace('{projectId}', $projectId, $value);
$value = \str_replace('{apiEndpoint}', 'http://localhost/v1', $value);
$response = $client->call(Client::METHOD_POST, '/sites/' . $siteId . '/variables', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-mode' => 'admin',
'cookie' => $cookieConsole
], [
'key' => $variable['name'],
'value' => $value
]);
if ($response['headers']['status-code'] !== 201) {
Console::error(\json_encode($response));
throw new \Exception("Failed to create variable");
}
}
Console::info("Variables created");
}
// Create deployment
$deployment = $client->call(Client::METHOD_POST, '/sites/' . $siteId . '/deployments/template', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-mode' => 'admin',
'cookie' => $cookieConsole
], [
'owner' => $template['providerOwner'],
'repository' => $template['providerRepositoryId'],
'rootDirectory' => $framework['providerRootDirectory'],
'version' => $template['providerVersion'],
'activate' => true,
]);
if ($deployment['headers']['status-code'] !== 202) {
Console::error(\json_encode($deployment));
throw new \Exception("Failed to create deployment");
}
Console::info("Deployment created");
$deploymentId = $deployment['body']['$id'];
// Await screenshot
$attempts = 50;
$sleep = 5;
$idLight = '';
$idDark = '';
Console::log("Awaiting deployment (every $sleep seconds, $attempts attempts)");
for ($i = 0; $i < $attempts; $i++) {
$deployment = $client->call(Client::METHOD_GET, '/sites/' . $siteId . '/deployments/' . $deploymentId, [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-mode' => 'admin',
'cookie' => $cookieConsole
]);
if ($deployment['headers']['status-code'] !== 200) {
Console::error(\json_encode($deployment));
throw new \Exception("Failed to get deployment");
}
if ($deployment['body']['status'] === 'failed') {
Console::error(\json_encode($deployment));
throw new \Exception("Deployment build failed");
}
if ($deployment['body']['status'] !== 'ready') {
Console::log("Deployment not ready yet, status: " . $deployment['body']['status']);
\sleep($sleep);
continue;
}
if (empty($deployment['body']['screenshotLight'])) {
Console::log("Light screenshot not available yet");
\sleep($sleep);
continue;
}
if (empty($deployment['body']['screenshotDark'])) {
Console::log("Dark screenshot not available yet");
\sleep($sleep);
continue;
}
$idLight = $deployment['body']['screenshotLight'];
$idDark = $deployment['body']['screenshotDark'];
break;
}
if (empty($idLight) || empty($idDark)) {
Console::error(\json_encode($deployment));
throw new \Exception("Failed to get deployment screenshot");
}
Console::info("Screenshots created");
$themes = [
[ 'fileId' => $idLight, 'suffix' => 'light' ],
[ 'fileId' => $idDark, 'suffix' => 'dark' ]
];
foreach ($themes as $theme) {
$file = $client->call(Client::METHOD_GET, '/storage/buckets/screenshots/files/' . $theme['fileId'] . '/download', [
'x-appwrite-project' => 'console',
'x-appwrite-mode' => 'admin',
'cookie' => $cookieConsole
]);
if ($file['headers']['status-code'] !== 200) {
Console::error(\json_encode($file));
throw new \Exception("Failed to download {$theme['suffix']} screenshot");
}
$path = "/usr/src/code/public/images/sites/templates/{$template['key']}-{$theme['suffix']}.png";
if (!\file_put_contents($path, $file['body'])) {
throw new \Exception("Failed to save {$theme['suffix']} screenshot");
}
}
Console::success("Screenshots saved");
}
}

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

@ -28,11 +28,17 @@ class TemplateSite extends Model
'default' => '',
'example' => 'https://nextjs-starter.appwrite.network/',
])
->addRule('demoImage', [
->addRule('screenshotDark', [
'type' => self::TYPE_STRING,
'description' => 'File URL with preview screenshot.',
'description' => 'File URL with preview screenshot in dark theme preference.',
'default' => '',
'example' => 'https://cloud.appwrite.io/console/images/sites/templates/nextjs-starter.png',
'example' => 'https://cloud.appwrite.io/images/sites/templates/template-for-blog-dark.png',
])
->addRule('screenshotLight', [
'type' => self::TYPE_STRING,
'description' => 'File URL with preview screenshot in light theme preference.',
'default' => '',
'example' => 'https://cloud.appwrite.io/images/sites/templates/template-for-blog-light.png',
])
->addRule('useCases', [
'type' => self::TYPE_STRING,

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']);