Merge pull request #9366 from appwrite/feat-site-screenshots

Feat: Site screenshots
This commit is contained in:
Matej Bačo 2025-02-21 22:11:03 +01:00 committed by GitHub
commit c8f4667b6a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 552 additions and 104 deletions

View file

@ -1528,7 +1528,29 @@ return [
'default' => false, 'default' => false,
'array' => false, 'array' => false,
'filters' => [], 'filters' => [],
] ],
[
'$id' => ID::custom('screenshotLight'), // File ID from 'screenshots' Console bucket
'type' => Database::VAR_STRING,
'format' => '',
'size' => 32,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('screenshotDark'), // File ID from 'screenshots' Console bucket
'type' => Database::VAR_STRING,
'format' => '',
'size' => 32,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
], ],
'indexes' => [ 'indexes' => [
[ [

View file

@ -539,4 +539,49 @@ return [
'providerVersion' => '0.2.*', 'providerVersion' => '0.2.*',
'variables' => [], 'variables' => [],
], ],
[
'key' => 'nextjs-starter',
'name' => 'Next.js starter website',
'useCases' => ['starter'],
'frameworks' => [
getFramework('NEXTJS', [
'providerRootDirectory' => './nextjs/starter',
]),
],
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.2.*',
'variables' => [],
],
[
'key' => 'nuxt-starter',
'name' => 'Nuxt starter website',
'useCases' => ['starter'],
'frameworks' => [
getFramework('NUXT', [
'providerRootDirectory' => './nuxt/starter',
]),
],
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.2.*',
'variables' => [],
],
[
'key' => 'sveltekit-starter',
'name' => 'SvelteKit starter website',
'useCases' => ['starter'],
'frameworks' => [
getFramework('SVELTEKIT', [
'providerRootDirectory' => './sveltekit/starter',
]),
],
'vcsProvider' => 'github',
'providerRepositoryId' => 'templates-for-sites',
'providerOwner' => 'appwrite',
'providerVersion' => '0.2.*',
'variables' => [],
],
]; ];

View file

@ -236,10 +236,9 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$projectId = $project->getId(); $projectId = $project->getId();
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$domain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; $domain = ID::unique() . "." . $sitesDomain;
$ruleId = md5($domain); $ruleId = md5($domain);
Authorization::skip(
$rule = Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([ fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId, '$id' => $ruleId,
'projectId' => $project->getId(), 'projectId' => $project->getId(),

View file

@ -4,6 +4,7 @@ require_once __DIR__ . '/../init.php';
use Ahc\Jwt\JWT; use Ahc\Jwt\JWT;
use Appwrite\Auth\Auth; use Appwrite\Auth\Auth;
use Appwrite\Auth\Key;
use Appwrite\Event\Certificate; use Appwrite\Event\Certificate;
use Appwrite\Event\Event; use Appwrite\Event\Event;
use Appwrite\Event\Func; use Appwrite\Event\Func;
@ -54,7 +55,7 @@ Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost'); Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname) function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey)
{ {
$utopia->getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml'); $utopia->getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml');
@ -269,11 +270,11 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
if ($type === 'function') { if ($type === 'function') {
$jwtExpiry = $resource->getAttribute('timeout', 900); $jwtExpiry = $resource->getAttribute('timeout', 900);
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0); $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0);
$apiKey = $jwtObj->encode([ $jwtKey = $jwtObj->encode([
'projectId' => $project->getId(), 'projectId' => $project->getId(),
'scopes' => $resource->getAttribute('scopes', []) 'scopes' => $resource->getAttribute('scopes', [])
]); ]);
$headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $apiKey; $headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $jwtKey;
$headers['x-appwrite-trigger'] = 'http'; $headers['x-appwrite-trigger'] = 'http';
$headers['x-appwrite-user-jwt'] = ''; $headers['x-appwrite-user-jwt'] = '';
} }
@ -448,18 +449,19 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
requestTimeout: 30 requestTimeout: 30
); );
// Branded banner for previews // Branded banner for previews
$transformation = new Transformation(); if (\is_null($apiKey) || $apiKey->isBannerDisabled() === false) {
$transformation->addAdapter(new Preview()); $transformation = new Transformation();
$transformation->setInput($executionResponse['body']); $transformation->addAdapter(new Preview());
$transformation->setTraits($executionResponse['headers']); $transformation->setInput($executionResponse['body']);
if ($type === 'deployment' && $transformation->transform()) { $transformation->setTraits($executionResponse['headers']);
$executionResponse['body'] = $transformation->getOutput(); if ($type === 'deployment' && $transformation->transform()) {
$executionResponse['body'] = $transformation->getOutput();
foreach ($executionResponse['headers'] as $key => $value) { foreach ($executionResponse['headers'] as $key => $value) {
if (\strtolower($key) === 'content-length') { if (\strtolower($key) === 'content-length') {
$executionResponse['headers'][$key] = \strlen($executionResponse['body']); $executionResponse['headers'][$key] = \strlen($executionResponse['body']);
}
} }
} }
} }
@ -585,7 +587,7 @@ App::init()
*/ */
App::init() App::init()
->groups(['database', 'functions', 'storage', 'messaging']) ->groups(['database', 'functions', 'messaging'])
->inject('project') ->inject('project')
->inject('request') ->inject('request')
->action(function (Document $project, Request $request) { ->action(function (Document $project, Request $request) {
@ -617,7 +619,8 @@ App::init()
->inject('queueForFunctions') ->inject('queueForFunctions')
->inject('isResourceBlocked') ->inject('isResourceBlocked')
->inject('previewHostname') ->inject('previewHostname')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, array $clients, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, callable $isResourceBlocked, string $previewHostname) { ->inject('apiKey')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, array $clients, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) {
/* /*
* Appwrite Router * Appwrite Router
*/ */
@ -625,7 +628,7 @@ App::init()
$mainDomain = System::getEnv('_APP_DOMAIN', ''); $mainDomain = System::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain // Only run Router when external domain
if ($host !== $mainDomain || !empty($previewHostname)) { if ($host !== $mainDomain || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname)) { if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
$utopia->getRoute()?->label('router', true); $utopia->getRoute()?->label('router', true);
} }
} }
@ -865,7 +868,8 @@ App::options()
->inject('geodb') ->inject('geodb')
->inject('isResourceBlocked') ->inject('isResourceBlocked')
->inject('previewHostname') ->inject('previewHostname')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname) { ->inject('apiKey')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) {
/* /*
* Appwrite Router * Appwrite Router
*/ */
@ -873,7 +877,7 @@ App::options()
$mainDomain = System::getEnv('_APP_DOMAIN', ''); $mainDomain = System::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain // Only run Router when external domain
if ($host !== $mainDomain || !empty($previewHostname)) { if ($host !== $mainDomain || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname)) { if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
$utopia->getRoute()?->label('router', true); $utopia->getRoute()?->label('router', true);
} }
} }
@ -1173,7 +1177,8 @@ App::get('/robots.txt')
->inject('geodb') ->inject('geodb')
->inject('isResourceBlocked') ->inject('isResourceBlocked')
->inject('previewHostname') ->inject('previewHostname')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname) { ->inject('apiKey')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) {
$host = $request->getHostname() ?? ''; $host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', ''); $mainDomain = System::getEnv('_APP_DOMAIN', '');
@ -1181,7 +1186,7 @@ App::get('/robots.txt')
$template = new View(__DIR__ . '/../views/general/robots.phtml'); $template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false)); $response->text($template->render(false));
} else { } else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname)) { if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
$utopia->getRoute()?->label('router', true); $utopia->getRoute()?->label('router', true);
} }
} }
@ -1203,7 +1208,8 @@ App::get('/humans.txt')
->inject('geodb') ->inject('geodb')
->inject('isResourceBlocked') ->inject('isResourceBlocked')
->inject('previewHostname') ->inject('previewHostname')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname) { ->inject('apiKey')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) {
$host = $request->getHostname() ?? ''; $host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', ''); $mainDomain = System::getEnv('_APP_DOMAIN', '');
@ -1211,7 +1217,7 @@ App::get('/humans.txt')
$template = new View(__DIR__ . '/../views/general/humans.phtml'); $template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false)); $response->text($template->render(false));
} else { } else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname)) { if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
$utopia->getRoute()?->label('router', true); $utopia->getRoute()?->label('router', true);
} }
} }

View file

@ -13,6 +13,7 @@ use Swoole\Table;
use Utopia\App; use Utopia\App;
use Utopia\Audit\Audit; use Utopia\Audit\Audit;
use Utopia\CLI\Console; use Utopia\CLI\Console;
use Utopia\Compression\Compression;
use Utopia\Config\Config; use Utopia\Config\Config;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\DateTime; use Utopia\Database\DateTime;
@ -249,8 +250,7 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
$audit->setup(); $audit->setup();
} }
if ($dbForPlatform->getDocument('buckets', 'default')->isEmpty() && if ($dbForPlatform->getDocument('buckets', 'default')->isEmpty()) {
!$dbForPlatform->exists($dbForPlatform->getDatabase(), 'bucket_1')) {
Console::info(" └── Creating default bucket..."); Console::info(" └── Creating default bucket...");
$dbForPlatform->createDocument('buckets', new Document([ $dbForPlatform->createDocument('buckets', new Document([
'$id' => ID::custom('default'), '$id' => ID::custom('default'),
@ -302,6 +302,59 @@ $http->on(Constant::EVENT_START, function (Server $http) use ($payloadSize, $reg
$dbForPlatform->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes); $dbForPlatform->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes);
} }
if ($dbForPlatform->getDocument('buckets', 'screenshots')->isEmpty()) {
Console::info(" └── Creating screenshots bucket...");
$dbForPlatform->createDocument('buckets', new Document([
'$id' => ID::custom('screenshots'),
'$collection' => ID::custom('buckets'),
'name' => 'Screenshots',
'maximumFileSize' => 5000000, // ~5MB
'allowedFileExtensions' => [ 'png' ],
'enabled' => true,
'compression' => Compression::GZIP,
'encryption' => false,
'antivirus' => false,
'fileSecurity' => true,
'$permissions' => [
Permission::create(Role::any()),
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'search' => 'buckets Screenshots',
]));
$bucket = $dbForPlatform->getDocument('buckets', 'screenshots');
Console::info(" └── Creating files collection for screenshots bucket...");
$files = $collections['buckets']['files'] ?? [];
if (empty($files)) {
throw new Exception('Files collection is not configured.');
}
$attributes = array_map(fn ($attr) => new Document([
'$id' => ID::custom($attr['$id']),
'type' => $attr['type'],
'size' => $attr['size'],
'required' => $attr['required'],
'signed' => $attr['signed'],
'array' => $attr['array'],
'filters' => $attr['filters'],
'default' => $attr['default'] ?? null,
'format' => $attr['format'] ?? ''
]), $files['attributes']);
$indexes = array_map(fn ($index) => new Document([
'$id' => ID::custom($index['$id']),
'type' => $index['type'],
'attributes' => $index['attributes'],
'lengths' => $index['lengths'],
'orders' => $index['orders'],
]), $files['indexes']);
$dbForPlatform->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes);
}
}); });
$pools->reclaim(); $pools->reclaim();

View file

@ -1961,16 +1961,24 @@ App::setResource(
fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false fn () => fn (Document $project, string $resourceType, ?string $resourceId) => false
); );
App::setResource('previewHostname', function (Request $request) { App::setResource('previewHostname', function (Request $request, ?Key $apiKey) {
$allowed = false;
if (App::isDevelopment()) { if (App::isDevelopment()) {
$host = $request->getQuery('appwrite-hostname') ?? ''; $allowed = true;
} elseif (!\is_null($apiKey) && $apiKey->getHostnameOverride() === true) {
$allowed = true;
}
if ($allowed) {
$host = $request->getQuery('appwrite-hostname', $request->getHeader('x-appwrite-hostname', ''));
if (!empty($host)) { if (!empty($host)) {
return $host; return $host;
} }
} }
return ''; return '';
}, ['request']); }, ['request', 'apiKey']);
App::setResource('apiKey', function (Request $request, Document $project): ?Key { App::setResource('apiKey', function (Request $request, Document $project): ?Key {
$key = $request->getHeader('x-appwrite-key'); $key = $request->getHeader('x-appwrite-key');

12
composer.lock generated
View file

@ -6190,16 +6190,16 @@
}, },
{ {
"name": "phpstan/phpdoc-parser", "name": "phpstan/phpdoc-parser",
"version": "2.0.2", "version": "2.1.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git", "url": "https://github.com/phpstan/phpdoc-parser.git",
"reference": "51087f87dcce2663e1fed4dfd4e56eccd580297e" "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/51087f87dcce2663e1fed4dfd4e56eccd580297e", "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68",
"reference": "51087f87dcce2663e1fed4dfd4e56eccd580297e", "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -6231,9 +6231,9 @@
"description": "PHPDoc parser with support for nullable, intersection and generic types", "description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": { "support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues", "issues": "https://github.com/phpstan/phpdoc-parser/issues",
"source": "https://github.com/phpstan/phpdoc-parser/tree/2.0.2" "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0"
}, },
"time": "2025-02-17T20:25:51+00:00" "time": "2025-02-19T13:28:12+00:00"
}, },
{ {
"name": "phpstan/phpstan", "name": "phpstan/phpstan",

View file

@ -435,6 +435,7 @@ services:
volumes: volumes:
- appwrite-functions:/storage/functions:rw - appwrite-functions:/storage/functions:rw
- appwrite-builds:/storage/builds:rw - appwrite-builds:/storage/builds:rw
- appwrite-uploads:/storage/uploads:rw
- ./app:/usr/src/code/app - ./app:/usr/src/code/app
- ./src:/usr/src/code/src - ./src:/usr/src/code/src
depends_on: depends_on:
@ -947,6 +948,12 @@ services:
environment: environment:
- _APP_ASSISTANT_OPENAI_API_KEY - _APP_ASSISTANT_OPENAI_API_KEY
appwrite-browser:
container_name: appwrite-browser
image: appwrite/browser:0.2.0
networks:
- appwrite
openruntimes-executor: openruntimes-executor:
container_name: openruntimes-executor container_name: openruntimes-executor
hostname: exc1 hostname: exc1

View file

@ -20,6 +20,9 @@ class Key
protected string $name, protected string $name,
protected bool $expired = false, protected bool $expired = false,
protected array $disabledMetrics = [], protected array $disabledMetrics = [],
protected bool $hostnameOverride = false,
protected bool $bannerDisabled = false,
protected bool $projectCheckDisabled = false,
) { ) {
} }
@ -58,6 +61,23 @@ class Key
return $this->disabledMetrics; return $this->disabledMetrics;
} }
public function getHostnameOverride(): bool
{
return $this->hostnameOverride;
}
public function isBannerDisabled(): bool
{
return $this->bannerDisabled;
}
public function isProjectCheckDisabled(): bool
{
return $this->projectCheckDisabled;
}
/** /**
* Decode the given secret key into a Key object, containing the project ID, type, role, scopes, and name. * Decode the given secret key into a Key object, containing the project ID, type, role, scopes, and name.
* Can be a stored API key or a dynamic key (JWT). * Can be a stored API key or a dynamic key (JWT).
@ -109,9 +129,12 @@ class Key
$name = $payload['name'] ?? 'Dynamic Key'; $name = $payload['name'] ?? 'Dynamic Key';
$projectId = $payload['projectId'] ?? ''; $projectId = $payload['projectId'] ?? '';
$disabledMetrics = $payload['disabledMetrics'] ?? []; $disabledMetrics = $payload['disabledMetrics'] ?? [];
$hostnameOverride = $payload['hostnameOverride'] ?? false;
$bannerDisabled = $payload['bannerDisabled'] ?? false;
$projectCheckDisabled = $payload['projectCheckDisabled'] ?? false;
$scopes = \array_merge($payload['scopes'] ?? [], $scopes); $scopes = \array_merge($payload['scopes'] ?? [], $scopes);
if ($projectId !== $project->getId()) { if (!$projectCheckDisabled && $projectId !== $project->getId()) {
return $guestKey; return $guestKey;
} }
@ -122,7 +145,10 @@ class Key
$scopes, $scopes,
$name, $name,
$expired, $expired,
$disabledMetrics $disabledMetrics,
$hostnameOverride,
$bannerDisabled,
$projectCheckDisabled
); );
case API_KEY_STANDARD: case API_KEY_STANDARD:
$key = $project->find( $key = $project->find(

View file

@ -172,14 +172,10 @@ class Base extends Action
'activate' => $activate, 'activate' => $activate,
])); ]));
// Preview deployments for sites
$projectId = $project->getId();
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$domain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; $domain = ID::unique() . "." . $sitesDomain;
$ruleId = md5($domain); $ruleId = md5($domain);
Authorization::skip(
$rule = Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([ fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId, '$id' => $ruleId,
'projectId' => $project->getId(), 'projectId' => $project->getId(),

View file

@ -7,6 +7,8 @@ use Appwrite\Event\Event;
use Appwrite\Event\Func; use Appwrite\Event\Func;
use Appwrite\Event\StatsUsage; use Appwrite\Event\StatsUsage;
use Appwrite\Messaging\Adapter\Realtime; use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Permission;
use Appwrite\Role;
use Appwrite\Utopia\Response\Model\Deployment; use Appwrite\Utopia\Response\Model\Deployment;
use Appwrite\Vcs\Comment; use Appwrite\Vcs\Comment;
use Exception; use Exception;
@ -24,9 +26,11 @@ use Utopia\Database\Exception\Structure;
use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\ID;
use Utopia\Database\Query; use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Authorization;
use Utopia\Fetch\Client as FetchClient;
use Utopia\Logger\Log; use Utopia\Logger\Log;
use Utopia\Platform\Action; use Utopia\Platform\Action;
use Utopia\Queue\Message; use Utopia\Queue\Message;
use Utopia\Storage\Compression\Compression;
use Utopia\Storage\Device; use Utopia\Storage\Device;
use Utopia\Storage\Device\Local; use Utopia\Storage\Device\Local;
use Utopia\System\System; use Utopia\System\System;
@ -56,9 +60,10 @@ class Builds extends Action
->inject('dbForProject') ->inject('dbForProject')
->inject('deviceForFunctions') ->inject('deviceForFunctions')
->inject('isResourceBlocked') ->inject('isResourceBlocked')
->inject('deviceForFiles')
->inject('log') ->inject('log')
->callback(fn ($message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, StatsUsage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, callable $isResourceBlocked, Log $log) => ->callback(fn ($message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, StatsUsage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, callable $isResourceBlocked, Device $deviceForFiles, Log $log) =>
$this->action($message, $project, $dbForPlatform, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $isResourceBlocked, $log)); $this->action($message, $project, $dbForPlatform, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $isResourceBlocked, $deviceForFiles, $log));
} }
/** /**
@ -71,11 +76,12 @@ class Builds extends Action
* @param Cache $cache * @param Cache $cache
* @param Database $dbForProject * @param Database $dbForProject
* @param Device $deviceForFunctions * @param Device $deviceForFunctions
* @param Device $deviceForFiles
* @param Log $log * @param Log $log
* @return void * @return void
* @throws \Utopia\Database\Exception * @throws \Utopia\Database\Exception
*/ */
public function action(Message $message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, StatsUsage $queueForStatsUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, callable $isResourceBlocked, Log $log): void public function action(Message $message, Document $project, Database $dbForPlatform, Event $queueForEvents, Func $queueForFunctions, StatsUsage $queueForStatsUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, callable $isResourceBlocked, Device $deviceForFiles, Log $log): void
{ {
$payload = $message->getPayload() ?? []; $payload = $message->getPayload() ?? [];
@ -96,7 +102,7 @@ class Builds extends Action
case BUILD_TYPE_RETRY: case BUILD_TYPE_RETRY:
Console::info('Creating build for deployment: ' . $deployment->getId()); Console::info('Creating build for deployment: ' . $deployment->getId());
$github = new GitHub($cache); $github = new GitHub($cache);
$this->buildDeployment($deviceForFunctions, $queueForFunctions, $queueForEvents, $queueForStatsUsage, $dbForPlatform, $dbForProject, $github, $project, $resource, $deployment, $template, $isResourceBlocked, $log); $this->buildDeployment($deviceForFunctions, $deviceForFiles, $queueForFunctions, $queueForEvents, $queueForStatsUsage, $dbForPlatform, $dbForProject, $github, $project, $resource, $deployment, $template, $isResourceBlocked, $log);
break; break;
default: default:
@ -106,6 +112,7 @@ class Builds extends Action
/** /**
* @param Device $deviceForFunctions * @param Device $deviceForFunctions
* @param Device $deviceForFiles
* @param Func $queueForFunctions * @param Func $queueForFunctions
* @param Event $queueForEvents * @param Event $queueForEvents
* @param StatsUsage $queueForStatsUsage * @param StatsUsage $queueForStatsUsage
@ -122,7 +129,7 @@ class Builds extends Action
* *
* @throws Exception * @throws Exception
*/ */
protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, StatsUsage $queueForStatsUsage, Database $dbForPlatform, Database $dbForProject, GitHub $github, Document $project, Document $resource, Document $deployment, Document $template, callable $isResourceBlocked, Log $log): void protected function buildDeployment(Device $deviceForFunctions, Device $deviceForFiles, Func $queueForFunctions, Event $queueForEvents, StatsUsage $queueForStatsUsage, Database $dbForPlatform, Database $dbForProject, GitHub $github, Document $project, Document $resource, Document $deployment, Document $template, callable $isResourceBlocked, Log $log): void
{ {
$resourceKey = match($resource->getCollection()) { $resourceKey = match($resource->getCollection()) {
'functions' => 'functionId', 'functions' => 'functionId',
@ -713,6 +720,109 @@ class Builds extends Action
Console::success("Build id: $buildId created"); Console::success("Build id: $buildId created");
if ($resource->getCollection() === 'sites') {
try {
$rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [
Query::equal("projectInternalId", [$project->getInternalId()]),
Query::equal("resourceType", ["deployment"]),
Query::equal("resourceInternalId", [$deployment->getInternalId()])
]));
if ($rule->isEmpty()) {
throw new \Exception("Rule for build not found");
}
$client = new FetchClient();
$client->addHeader('content-type', FetchClient::CONTENT_TYPE_APPLICATION_JSON);
$bucket = $dbForPlatform->getDocument('buckets', 'screenshots');
$configs = [
'screenshotLight' => [
'headers' => [ 'x-appwrite-hostname' => $rule->getAttribute('domain') ],
'url' => 'http://appwrite/?appwrite-preview=1&appwrite-theme=light',
'theme' => 'light'
],
'screenshotDark' => [
'headers' => [ 'x-appwrite-hostname' => $rule->getAttribute('domain') ],
'url' => 'http://appwrite/?appwrite-preview=1&appwrite-theme=dark',
'theme' => 'dark'
],
];
$jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 0);
$apiKey = $jwtObj->encode([
'hostnameOverride' => true,
'bannerDisabled' => true,
'projectCheckDisabled' => true
]);
// TODO: @Meldiron if becomes too slow, do concurrently
foreach ($configs as $key => $config) {
$config['headers'] = \array_merge($config['headers'] ?? [], [
'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey
]);
$response = $client->fetch(
url: 'http://appwrite-browser:3000/v1/screenshots',
method: 'POST',
body: $config
);
if ($response->getStatusCode() >= 400) {
throw new \Exception("Screenshot failed to generate: " . $response->getBody());
}
$screenshot = $response->getBody();
$fileId = ID::unique();
$fileName = $fileId . '.png';
$path = $deviceForFiles->getPath($fileName);
$path = str_ireplace($deviceForFiles->getRoot(), $deviceForFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $path); // Add bucket id to path after root
$success = $deviceForFiles->write($path, $screenshot, "image/png");
if (!$success) {
throw new \Exception("Screenshot failed to save");
}
$teamId = $project->getAttribute('teamId', '');
$file = new Document([
'$id' => $fileId,
'$permissions' => [
Permission::read(Role::team(ID::custom($teamId))),
],
'bucketId' => $bucket->getId(),
'bucketInternalId' => $bucket->getInternalId(),
'name' => $fileName,
'path' => $path,
'signature' => $deviceForFiles->getFileHash($path),
'mimeType' => $deviceForFiles->getFileMimeType($path),
'sizeOriginal' => \strlen($screenshot),
'sizeActual' => $deviceForFiles->getFileSize($path),
'algorithm' => Compression::GZIP,
'comment' => '',
'chunksTotal' => 1,
'chunksUploaded' => 1,
'openSSLVersion' => null,
'openSSLCipher' => null,
'openSSLTag' => null,
'openSSLIV' => null,
'search' => implode(' ', [$fileId, $fileName]),
'metadata' => ['content_type' => $deviceForFiles->getFileMimeType($path)],
]);
$file = Authorization::skip(fn () => $dbForPlatform->createDocument('bucket_' . $bucket->getInternalId(), $file));
$deployment->setAttribute($key, $fileId);
}
$dbForProject->updateDocument('deployments', $deployment->getId(), $deployment);
} catch (\Throwable $th) {
Console::warning("Screenshot failed to generate:");
Console::warning($th->getMessage());
Console::warning($th->getTraceAsString());
}
}
/** Set auto deploy */ /** Set auto deploy */
if ($deployment->getAttribute('activate') === true) { if ($deployment->getAttribute('activate') === true) {
$resource->setAttribute('deploymentInternalId', $deployment->getInternalId()); $resource->setAttribute('deploymentInternalId', $deployment->getInternalId());

View file

@ -10,11 +10,14 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse; use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response; use Appwrite\Utopia\Response;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID; use Utopia\Database\Validator\UID;
use Utopia\Platform\Action; use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP; use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device; use Utopia\Storage\Device;
use Utopia\System\System;
class Create extends Action class Create extends Action
{ {
@ -53,7 +56,9 @@ class Create extends Action
->param('siteId', '', new UID(), 'Site ID.') ->param('siteId', '', new UID(), 'Site ID.')
->param('deploymentId', '', new UID(), 'Deployment ID.') ->param('deploymentId', '', new UID(), 'Deployment ID.')
->inject('response') ->inject('response')
->inject('project')
->inject('dbForProject') ->inject('dbForProject')
->inject('dbForPlatform')
->inject('queueForEvents') ->inject('queueForEvents')
->inject('queueForBuilds') ->inject('queueForBuilds')
->inject('deviceForSites') ->inject('deviceForSites')
@ -61,7 +66,7 @@ class Create extends Action
->callback([$this, 'action']); ->callback([$this, 'action']);
} }
public function action(string $siteId, string $deploymentId, Response $response, Database $dbForProject, Event $queueForEvents, Build $queueForBuilds, Device $deviceForSites, Device $deviceForFunctions) public function action(string $siteId, string $deploymentId, Response $response, Document $project, Database $dbForProject, Database $dbForPlatform, Event $queueForEvents, Build $queueForBuilds, Device $deviceForSites, Device $deviceForFunctions)
{ {
$site = $dbForProject->getDocument('sites', $siteId); $site = $dbForProject->getDocument('sites', $siteId);
@ -97,6 +102,24 @@ class Create extends Action
'search' => implode(' ', [$deploymentId]), 'search' => implode(' ', [$deploymentId]),
])); ]));
// 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,
'resourceType' => 'deployment',
'resourceId' => $deploymentId,
'resourceInternalId' => $deployment->getInternalId(),
'status' => 'verified',
'certificateId' => '',
]))
);
$queueForBuilds $queueForBuilds
->setType(BUILD_TYPE_DEPLOYMENT) ->setType(BUILD_TYPE_DEPLOYMENT)
->setResource($site) ->setResource($site)

View file

@ -228,14 +228,10 @@ class Create extends Action
'type' => $type 'type' => $type
])); ]));
// Preview deployments for sites
$projectId = $project->getId();
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$domain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; $domain = ID::unique() . "." . $sitesDomain;
$ruleId = md5($domain); $ruleId = md5($domain);
Authorization::skip(
$rule = Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([ fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId, '$id' => $ruleId,
'projectId' => $project->getId(), 'projectId' => $project->getId(),
@ -283,14 +279,10 @@ class Create extends Action
'type' => $type 'type' => $type
])); ]));
// Preview deployments for sites
$projectId = $project->getId();
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$domain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; $domain = ID::unique() . "." . $sitesDomain;
$ruleId = md5($domain); $ruleId = md5($domain);
Authorization::skip(
$rule = Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([ fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId, '$id' => $ruleId,
'projectId' => $project->getId(), 'projectId' => $project->getId(),

View file

@ -139,18 +139,15 @@ class Create extends Base
'activate' => $activate, 'activate' => $activate,
])); ]));
// Preview deployments url
$projectId = $project->getId();
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$previewDomain = "{$deploymentId}-{$projectId}.{$sitesDomain}"; $domain = ID::unique() . "." . $sitesDomain;
$ruleId = md5($domain);
$rule = Authorization::skip( Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([ fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => \md5($previewDomain), '$id' => $ruleId,
'projectId' => $project->getId(), 'projectId' => $project->getId(),
'projectInternalId' => $project->getInternalId(), 'projectInternalId' => $project->getInternalId(),
'domain' => $previewDomain, 'domain' => $domain,
'resourceType' => 'deployment', 'resourceType' => 'deployment',
'resourceId' => $deploymentId, 'resourceId' => $deploymentId,
'resourceInternalId' => $deployment->getInternalId(), 'resourceInternalId' => $deployment->getInternalId(),

View file

@ -22,6 +22,7 @@ use Utopia\Database\Exception\Conflict;
use Utopia\Database\Exception\Restricted; use Utopia\Database\Exception\Restricted;
use Utopia\Database\Exception\Structure; use Utopia\Database\Exception\Structure;
use Utopia\Database\Query; use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization as ValidatorAuthorization;
use Utopia\DSN\DSN; use Utopia\DSN\DSN;
use Utopia\Logger\Log; use Utopia\Logger\Log;
use Utopia\Platform\Action; use Utopia\Platform\Action;
@ -92,13 +93,13 @@ class Deletes extends Action
$this->deleteProject($dbForPlatform, $getProjectDB, $deviceForFiles, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $certificates, $document); $this->deleteProject($dbForPlatform, $getProjectDB, $deviceForFiles, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $certificates, $document);
break; break;
case DELETE_TYPE_SITES: case DELETE_TYPE_SITES:
$this->deleteSite($dbForPlatform, $getProjectDB, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $document, $certificates, $project); $this->deleteSite($dbForPlatform, $getProjectDB, $deviceForSites, $deviceForFunctions, $deviceForBuilds, $deviceForFiles, $document, $certificates, $project);
break; break;
case DELETE_TYPE_FUNCTIONS: case DELETE_TYPE_FUNCTIONS:
$this->deleteFunction($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $certificates, $document, $project); $this->deleteFunction($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $certificates, $document, $project);
break; break;
case DELETE_TYPE_DEPLOYMENTS: case DELETE_TYPE_DEPLOYMENTS:
$this->deleteDeployment($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $document, $certificates, $project); $this->deleteDeployment($dbForPlatform, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $deviceForFiles, $document, $certificates, $project);
break; break;
case DELETE_TYPE_USERS: case DELETE_TYPE_USERS:
$this->deleteUser($getProjectDB, $document, $project); $this->deleteUser($getProjectDB, $document, $project);
@ -749,7 +750,7 @@ class Deletes extends Action
* @return void * @return void
* @throws Exception * @throws Exception
*/ */
private function deleteSite(Database $dbForPlatform, callable $getProjectDB, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForBuilds, Document $document, CertificatesAdapter $certificates, Document $project): void private function deleteSite(Database $dbForPlatform, callable $getProjectDB, Device $deviceForSites, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForFiles, Document $document, CertificatesAdapter $certificates, Document $project): void
{ {
$dbForProject = $getProjectDB($project); $dbForProject = $getProjectDB($project);
$siteId = $document->getId(); $siteId = $document->getId();
@ -783,9 +784,10 @@ class Deletes extends Action
$deploymentInternalIds = []; $deploymentInternalIds = [];
$this->deleteByGroup('deployments', [ $this->deleteByGroup('deployments', [
Query::equal('resourceInternalId', [$siteInternalId]) Query::equal('resourceInternalId', [$siteInternalId])
], $dbForProject, function (Document $document) use ($deviceForFunctions, &$deploymentInternalIds) { ], $dbForProject, function (Document $document) use ($deviceForFunctions, $deviceForFiles, $dbForPlatform, &$deploymentInternalIds) {
$deploymentInternalIds[] = $document->getInternalId(); $deploymentInternalIds[] = $document->getInternalId();
$this->deleteDeploymentFiles($deviceForFunctions, $document); $this->deleteDeploymentFiles($deviceForFunctions, $document);
$this->deleteDeploymentScreenshots($deviceForFiles, $dbForPlatform, $document);
}); });
/** /**
@ -928,6 +930,53 @@ class Deletes extends Action
$this->deleteRuntimes($getProjectDB, $document, $project); $this->deleteRuntimes($getProjectDB, $document, $project);
} }
private function deleteDeploymentScreenshots(Device $deviceForFiles, Database $dbForPlatform, Document $deployment): void
{
$screenshotIds = [];
if (!empty($deployment->getAttribute('screenshotLight', ''))) {
$screenshotIds[] = $deployment->getAttribute('screenshotLight', '');
}
if (!empty($deployment->getAttribute('screenshotDark', ''))) {
$screenshotIds[] = $deployment->getAttribute('screenshotDark', '');
}
if (empty($screenshotIds)) {
return;
}
Console::info("Deleting screenshots for deployment " . $deployment->getId());
$bucket = ValidatorAuthorization::skip(fn () => $dbForPlatform->getDocument('buckets', 'screenshots'));
if ($bucket->isEmpty()) {
Console::error('Failed to get bucket for deployment screenshots');
return;
}
foreach ($screenshotIds as $id) {
$file = ValidatorAuthorization::skip(fn () => $dbForPlatform->getDocument('bucket_' . $bucket->getInternalId(), $id));
if ($file->isEmpty()) {
Console::error('Failed to get deployment screenshot: ' . $id);
continue;
}
$path = $file->getAttribute('path', '');
try {
if ($deviceForFiles->delete($path, true)) {
Console::success('Deleted deployment screenshot: ' . $path);
} else {
Console::error('Failed to delete deployment screenshot: ' . $path);
}
} catch (\Throwable $th) {
Console::error('Failed to delete deployment screenshot: ' . $path);
Console::error('[Error] Type: ' . get_class($th));
Console::error('[Error] Message: ' . $th->getMessage());
Console::error('[Error] File: ' . $th->getFile());
Console::error('[Error] Line: ' . $th->getLine());
}
}
}
/** /**
* @param Device $device * @param Device $device
* @param Document $deployment * @param Document $deployment
@ -999,7 +1048,7 @@ class Deletes extends Action
* @return void * @return void
* @throws Exception * @throws Exception
*/ */
private function deleteDeployment(Database $dbForPlatform, callable $getProjectDB, Device $deviceForFunctions, Device $deviceForBuilds, Document $document, CertificatesAdapter $certificates, Document $project): void private function deleteDeployment(Database $dbForPlatform, callable $getProjectDB, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForFiles, Document $document, CertificatesAdapter $certificates, Document $project): void
{ {
$projectId = $project->getId(); $projectId = $project->getId();
$dbForProject = $getProjectDB($project); $dbForProject = $getProjectDB($project);
@ -1011,6 +1060,11 @@ class Deletes extends Action
*/ */
$this->deleteDeploymentFiles($deviceForFunctions, $document); //TODO: For sites, this should be deviceForSites $this->deleteDeploymentFiles($deviceForFunctions, $document); //TODO: For sites, this should be deviceForSites
/**
* Delete deployment screenshots
*/
$this->deleteDeploymentScreenshots($deviceForFiles, $dbForPlatform, $document);
/** /**
* Delete builds * Delete builds
*/ */

View file

@ -76,6 +76,18 @@ class Deployment extends Model
'default' => false, 'default' => false,
'example' => true, 'example' => true,
]) ])
->addRule('screenshotLight', [
'type' => self::TYPE_STRING,
'description' => 'Screenshot with light theme preference file ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('screenshotDark', [
'type' => self::TYPE_STRING,
'description' => 'Screenshot with dark theme preference file ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('status', [ ->addRule('status', [
'type' => self::TYPE_STRING, 'type' => self::TYPE_STRING,
'description' => 'The deployment status. Possible values are "processing", "building", "waiting", "ready", and "failed".', 'description' => 'The deployment status. Possible values are "processing", "building", "waiting", "ready", and "failed".',

View file

@ -51,6 +51,18 @@ trait FunctionsBase
$this->assertEquals('ready', $deployment['body']['status'], 'Deployment status is not ready, deployment: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); $this->assertEquals('ready', $deployment['body']['status'], 'Deployment status is not ready, deployment: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT));
}, 50000, 500); }, 50000, 500);
// Not === so multipart/form-data works fine too
if (($params['activate'] ?? false) == true) {
$this->assertEventually(function () use ($functionId, $deploymentId) {
$function = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]));
$this->assertEquals($deploymentId, $function['body']['deployment'], 'Deployment is not activated, deployment: ' . json_encode($function['body'], JSON_PRETTY_PRINT));
}, 100000, 500);
}
return $deploymentId; return $deploymentId;
} }

View file

@ -51,6 +51,18 @@ trait SitesBase
$this->assertEquals('ready', $deployment['body']['status'], 'Deployment status is not ready, deployment: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT)); $this->assertEquals('ready', $deployment['body']['status'], 'Deployment status is not ready, deployment: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT));
}, 100000, 500); }, 100000, 500);
// Not === so multipart/form-data works fine too
if (($params['activate'] ?? false) == true) {
$this->assertEventually(function () use ($siteId, $deploymentId) {
$site = $this->client->call(Client::METHOD_GET, '/sites/' . $siteId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]));
$this->assertEquals($deploymentId, $site['body']['deploymentId'], 'Deployment is not activated, deployment: ' . json_encode($site['body'], JSON_PRETTY_PRINT));
}, 100000, 500);
}
return $deploymentId; return $deploymentId;
} }

View file

@ -313,10 +313,7 @@ class SitesCustomServerTest extends Scope
$proxyClient = new Client(); $proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain); $proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ $response = $proxyClient->call(Client::METHOD_GET, '/');
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]));
$this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString("Env variable is Appwrite", $response['body']); $this->assertStringContainsString("Env variable is Appwrite", $response['body']);
@ -1249,19 +1246,13 @@ class SitesCustomServerTest extends Scope
$proxyClient = new Client(); $proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain); $proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ $response = $proxyClient->call(Client::METHOD_GET, '/');
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]));
$this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString("Astro Blog", $response['body']); $this->assertStringContainsString("Astro Blog", $response['body']);
$this->assertStringContainsString("Hello, Astronaut!", $response['body']); $this->assertStringContainsString("Hello, Astronaut!", $response['body']);
$response = $proxyClient->call(Client::METHOD_GET, '/about', array_merge([ $response = $proxyClient->call(Client::METHOD_GET, '/about');
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]));
$this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString("Astro Blog", $response['body']); $this->assertStringContainsString("Astro Blog", $response['body']);
@ -1300,10 +1291,7 @@ class SitesCustomServerTest extends Scope
$proxyClient = new Client(); $proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain); $proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ $response = $proxyClient->call(Client::METHOD_GET, '/');
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]));
$this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringNotContainsString("This domain is not connected to any Appwrite resource yet", $response['body']); $this->assertStringNotContainsString("This domain is not connected to any Appwrite resource yet", $response['body']);
@ -1350,10 +1338,7 @@ class SitesCustomServerTest extends Scope
$this->assertEquals(0, $rules['body']['total']); $this->assertEquals(0, $rules['body']['total']);
}, 50000, 500); }, 50000, 500);
$response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ $response = $proxyClient->call(Client::METHOD_GET, '/');
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]));
$this->assertEquals(401, $response['headers']['status-code']); $this->assertEquals(401, $response['headers']['status-code']);
$this->assertStringContainsString("This domain is not connected to any Appwrite resource yet", $response['body']); $this->assertStringContainsString("This domain is not connected to any Appwrite resource yet", $response['body']);
@ -1416,10 +1401,7 @@ class SitesCustomServerTest extends Scope
$proxyClient = new Client(); $proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain); $proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ $response = $proxyClient->call(Client::METHOD_GET, '/');
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]));
$this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString("Hello Appwrite", $response['body']); $this->assertStringContainsString("Hello Appwrite", $response['body']);
@ -1430,10 +1412,7 @@ class SitesCustomServerTest extends Scope
$proxyClient = new Client(); $proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $previewDomain); $proxyClient->setEndpoint('http://' . $previewDomain);
$response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ $response = $proxyClient->call(Client::METHOD_GET, '/');
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]));
$this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString("Hello Appwrite", $response['body']); $this->assertStringContainsString("Hello Appwrite", $response['body']);
@ -1500,5 +1479,77 @@ class SitesCustomServerTest extends Scope
$this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']); $this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']);
} }
public function testSiteScreenshot(): void
{
$siteId = $this->setupSite([
'siteId' => ID::unique(),
'name' => 'Themed site',
'framework' => 'other',
'adapter' => 'static',
'buildRuntime' => 'static-1',
'outputDirectory' => './',
'buildCommand' => '',
'installCommand' => '',
'fallbackFile' => '',
]);
$this->assertNotEmpty($siteId);
$domain = $this->setupSiteDomain($siteId);
$deploymentId = $this->setupDeployment($siteId, [
'code' => $this->packageSite('static-themed'),
'activate' => 'true'
]);
$this->assertNotEmpty($deploymentId);
$domain = $this->getSiteDomain($siteId);
$this->assertNotEmpty($domain);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString("Themed website", $response['body']);
$this->assertStringContainsString("@media (prefers-color-scheme: dark)", $response['body']);
$deployment = $this->getDeployment($siteId, $deploymentId);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertNotEmpty($deployment['body']['screenshotLight']);
$this->assertNotEmpty($deployment['body']['screenshotDark']);
$screenshotId = $deployment['body']['screenshotLight'];
$file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console&mode=admin", array_merge([
], $this->getHeaders()));
$this->assertEquals(200, $file['headers']['status-code']);
$this->assertNotEmpty(200, $file['body']);
$this->assertGreaterThan(1, $file['headers']['content-length']);
$this->assertEquals('image/png', $file['headers']['content-type']);
$screenshotHash = \md5($file['body']);
$this->assertNotEmpty($screenshotHash);
$screenshotId = $deployment['body']['screenshotDark'];
$file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console&mode=admin", array_merge([
], $this->getHeaders()));
$this->assertEquals(200, $file['headers']['status-code']);
$this->assertNotEmpty(200, $file['body']);
$this->assertGreaterThan(1, $file['headers']['content-length']);
$this->assertEquals('image/png', $file['headers']['content-type']);
$screenshotDarkHash = \md5($file['body']);
$this->assertNotEmpty($screenshotDarkHash);
$this->assertNotEquals($screenshotDarkHash, $screenshotHash);
$this->cleanupSite($siteId);
}
// TODO: Add tests for deletion of resources when site is deleted // TODO: Add tests for deletion of resources when site is deleted
} }

View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Themed website</title>
<style>
/* Light theme */
body {
background-color: #ffffff;
color: #000000;
}
/* Dark theme */
@media (prefers-color-scheme: dark) {
body {
background-color: #000000;
color: #ffffff;
}
}
</style>
</head>
<body></body>
</html>