diff --git a/Dockerfile b/Dockerfile index ecc5112cc4..ac8cff0884 100755 --- a/Dockerfile +++ b/Dockerfile @@ -77,6 +77,7 @@ RUN chmod +x /usr/local/bin/doctor && \ chmod +x /usr/local/bin/queue-count-success && \ chmod +x /usr/local/bin/worker-audits && \ chmod +x /usr/local/bin/worker-builds && \ + chmod +x /usr/local/bin/worker-screenshots && \ chmod +x /usr/local/bin/worker-certificates && \ chmod +x /usr/local/bin/worker-databases && \ chmod +x /usr/local/bin/worker-deletes && \ diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index 97ddf8391c..907ed54de8 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -11,6 +11,7 @@ use Appwrite\Event\Func; use Appwrite\Event\Mail; use Appwrite\Event\Messaging; use Appwrite\Event\Migration; +use Appwrite\Event\Screenshot; use Appwrite\Event\StatsResources; use Appwrite\Event\StatsUsage; use Appwrite\Event\Webhook; @@ -955,6 +956,7 @@ App::get('/v1/health/queue/failed/:name') System::getEnv('_APP_WEBHOOK_QUEUE_NAME', Event::WEBHOOK_QUEUE_NAME), System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME), System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME), + System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME), System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME), System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) ]), 'The name of the queue') @@ -972,6 +974,7 @@ App::get('/v1/health/queue/failed/:name') ->inject('queueForBuilds') ->inject('queueForMessaging') ->inject('queueForMigrations') + ->inject('queueForScreenshots') ->action(function ( string $name, int|string $threshold, @@ -987,7 +990,8 @@ App::get('/v1/health/queue/failed/:name') Certificate $queueForCertificates, Build $queueForBuilds, Messaging $queueForMessaging, - Migration $queueForMigrations + Migration $queueForMigrations, + Screenshot $queueForScreenshots, ) { $threshold = \intval($threshold); @@ -1003,6 +1007,7 @@ App::get('/v1/health/queue/failed/:name') System::getEnv('_APP_WEBHOOK_QUEUE_NAME', Event::WEBHOOK_QUEUE_NAME) => $queueForWebhooks, System::getEnv('_APP_CERTIFICATES_QUEUE_NAME', Event::CERTIFICATES_QUEUE_NAME) => $queueForCertificates, System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME) => $queueForBuilds, + System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME) => $queueForScreenshots, System::getEnv('_APP_MESSAGING_QUEUE_NAME', Event::MESSAGING_QUEUE_NAME) => $queueForMessaging, System::getEnv('_APP_MIGRATIONS_QUEUE_NAME', Event::MIGRATIONS_QUEUE_NAME) => $queueForMigrations, }; diff --git a/app/init/resources.php b/app/init/resources.php index d56354c14b..a3aa3ae47c 100644 --- a/app/init/resources.php +++ b/app/init/resources.php @@ -15,6 +15,7 @@ use Appwrite\Event\Mail; use Appwrite\Event\Messaging; use Appwrite\Event\Migration; use Appwrite\Event\Realtime; +use Appwrite\Event\Screenshot; use Appwrite\Event\StatsResources; use Appwrite\Event\StatsUsage; use Appwrite\Event\Webhook; @@ -129,6 +130,9 @@ App::setResource('queueForMails', function (Publisher $publisher) { App::setResource('queueForBuilds', function (Publisher $publisher) { return new Build($publisher); }, ['publisher']); +App::setResource('queueForScreenshots', function (Publisher $publisher) { + return new Screenshot($publisher); +}, ['publisher']); App::setResource('queueForDatabase', function (Publisher $publisher) { return new EventDatabase($publisher); }, ['publisher']); diff --git a/app/worker.php b/app/worker.php index 7868861cf4..3720fb85fe 100644 --- a/app/worker.php +++ b/app/worker.php @@ -14,6 +14,7 @@ use Appwrite\Event\Mail; use Appwrite\Event\Messaging; use Appwrite\Event\Migration; use Appwrite\Event\Realtime; +use Appwrite\Event\Screenshot; use Appwrite\Event\StatsUsage; use Appwrite\Event\Webhook; use Appwrite\Platform\Appwrite; @@ -307,6 +308,10 @@ Server::setResource('queueForBuilds', function (Publisher $publisher) { return new Build($publisher); }, ['publisher']); +Server::setResource('queueForScreenshots', function (Publisher $publisher) { + return new Screenshot($publisher); +}, ['publisher']); + Server::setResource('queueForDeletes', function (Publisher $publisher) { return new Delete($publisher); }, ['publisher']); @@ -544,9 +549,4 @@ $worker Console::error('[Error] Line: ' . $error->getLine()); }); -$worker->workerStart() - ->action(function () use ($workerName) { - Console::info("Worker $workerName started"); - }); - $worker->start(); diff --git a/bin/worker-screenshots b/bin/worker-screenshots new file mode 100644 index 0000000000..0252556075 --- /dev/null +++ b/bin/worker-screenshots @@ -0,0 +1,3 @@ +#!/bin/sh + +exec php /usr/src/code/app/worker.php screenshots "$@" \ No newline at end of file diff --git a/composer.json b/composer.json index d45d723430..131455d206 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,7 @@ "utopia-php/platform": "0.7.*", "utopia-php/pools": "0.8.*", "utopia-php/preloader": "0.2.*", - "utopia-php/queue": "0.11.*", + "utopia-php/queue": "0.15.*", "utopia-php/registry": "0.5.*", "utopia-php/storage": "0.18.*", "utopia-php/swoole": "0.8.*", @@ -100,12 +100,6 @@ "provide": { "ext-phpiredis": "*" }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/utopia-php/migration.git" - } - ], "config": { "platform": { "php": "8.3" @@ -115,4 +109,4 @@ "tbachert/spi": true } } -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 73abeb57f0..c047bf2e03 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "375a062e8675e7e6938c1d8cc7b61ecf", + "content-hash": "ff3172688b600aa3c560131c9d9c5588", "packages": [ { "name": "adhocore/jwt", @@ -4516,16 +4516,16 @@ }, { "name": "utopia-php/migration", - "version": "1.3.12", + "version": "1.3.13", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "1b8d5519c50630e4c0b6a79be615b70d5f23d2e4" + "reference": "c5e3f5e970e62e8f7db97b5b90baae2af800a715" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/1b8d5519c50630e4c0b6a79be615b70d5f23d2e4", - "reference": "1b8d5519c50630e4c0b6a79be615b70d5f23d2e4", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/c5e3f5e970e62e8f7db97b5b90baae2af800a715", + "reference": "c5e3f5e970e62e8f7db97b5b90baae2af800a715", "shasum": "" }, "require": { @@ -4551,25 +4551,7 @@ "Utopia\\Migration\\": "src/Migration" } }, - "autoload-dev": { - "psr-4": { - "Utopia\\Tests\\": "tests/Migration" - } - }, - "scripts": { - "test": [ - "./vendor/bin/phpunit" - ], - "lint": [ - "./vendor/bin/pint --test" - ], - "format": [ - "./vendor/bin/pint" - ], - "check": [ - "./vendor/bin/phpstan analyse --level 3 src tests --memory-limit 2G" - ] - }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -4582,10 +4564,10 @@ "utopia" ], "support": { - "source": "https://github.com/utopia-php/migration/tree/1.3.12", - "issues": "https://github.com/utopia-php/migration/issues" + "issues": "https://github.com/utopia-php/migration/issues", + "source": "https://github.com/utopia-php/migration/tree/1.3.13" }, - "time": "2026-01-07T06:07:33+00:00" + "time": "2026-01-07T14:48:05+00:00" }, { "name": "utopia-php/mongo", @@ -4700,16 +4682,16 @@ }, { "name": "utopia-php/platform", - "version": "0.7.13", + "version": "0.7.14", "source": { "type": "git", "url": "https://github.com/utopia-php/platform.git", - "reference": "77a863a920122e2c6a6bc6ee5548d366a3f4c6c7" + "reference": "9f18ce63f1425ae2dae57468200e4a5d1239d57b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/platform/zipball/77a863a920122e2c6a6bc6ee5548d366a3f4c6c7", - "reference": "77a863a920122e2c6a6bc6ee5548d366a3f4c6c7", + "url": "https://api.github.com/repos/utopia-php/platform/zipball/9f18ce63f1425ae2dae57468200e4a5d1239d57b", + "reference": "9f18ce63f1425ae2dae57468200e4a5d1239d57b", "shasum": "" }, "require": { @@ -4718,7 +4700,7 @@ "php": ">=8.0", "utopia-php/cli": "0.15.*", "utopia-php/framework": "0.33.*", - "utopia-php/queue": "0.11.*" + "utopia-php/queue": "0.15.*" }, "require-dev": { "laravel/pint": "1.*", @@ -4745,9 +4727,9 @@ ], "support": { "issues": "https://github.com/utopia-php/platform/issues", - "source": "https://github.com/utopia-php/platform/tree/0.7.13" + "source": "https://github.com/utopia-php/platform/tree/0.7.14" }, - "time": "2025-12-08T10:02:40+00:00" + "time": "2026-01-06T15:39:45+00:00" }, { "name": "utopia-php/pools", @@ -4856,22 +4838,22 @@ }, { "name": "utopia-php/queue", - "version": "0.11.3", + "version": "0.15.0", "source": { "type": "git", "url": "https://github.com/utopia-php/queue.git", - "reference": "f3b2623efe87595c9ed907b3efd587e77c622d3d" + "reference": "6abb268ba7ec00dea4e5201b007776ea1bce9242" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/queue/zipball/f3b2623efe87595c9ed907b3efd587e77c622d3d", - "reference": "f3b2623efe87595c9ed907b3efd587e77c622d3d", + "url": "https://api.github.com/repos/utopia-php/queue/zipball/6abb268ba7ec00dea4e5201b007776ea1bce9242", + "reference": "6abb268ba7ec00dea4e5201b007776ea1bce9242", "shasum": "" }, "require": { "php": ">=8.3", "php-amqplib/php-amqplib": "^3.7", - "utopia-php/cli": "0.15.*", + "utopia-php/console": "0.0.*", "utopia-php/fetch": "0.5.*", "utopia-php/framework": "0.33.*", "utopia-php/pools": "0.8.*", @@ -4916,9 +4898,9 @@ ], "support": { "issues": "https://github.com/utopia-php/queue/issues", - "source": "https://github.com/utopia-php/queue/tree/0.11.3" + "source": "https://github.com/utopia-php/queue/tree/0.15.0" }, - "time": "2025-12-19T10:56:22+00:00" + "time": "2026-01-06T12:41:51+00:00" }, { "name": "utopia-php/registry", diff --git a/docker-compose.yml b/docker-compose.yml index 14591db926..20c0ad8f79 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -466,14 +466,12 @@ services: - appwrite-functions:/storage/functions:rw - appwrite-sites:/storage/sites:rw - appwrite-builds:/storage/builds:rw - - appwrite-uploads:/storage/uploads:rw - ./app:/usr/src/code/app - ./src:/usr/src/code/src depends_on: - redis - mariadb environment: - - _APP_BROWSER_HOST - _APP_ENV - _APP_WORKER_PER_CORE - _APP_OPENSSL_KEY_V1 @@ -529,6 +527,65 @@ services: extra_hosts: - "host.docker.internal:host-gateway" + appwrite-worker-screenshots: + entrypoint: worker-screenshots + <<: *x-logging + container_name: appwrite-worker-screenshots + image: appwrite-dev + networks: + - appwrite + volumes: + - appwrite-uploads:/storage/uploads:rw + - ./app:/usr/src/code/app + - ./src:/usr/src/code/src + depends_on: + - redis + - mariadb + environment: + # Specific + - _APP_BROWSER_HOST + # Basic + - _APP_ENV + - _APP_WORKER_PER_CORE + - _APP_LOGGING_CONFIG + # Database + - _APP_OPENSSL_KEY_V1 + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS + - _APP_DATABASE_SHARED_TABLES + # Storage + - _APP_STORAGE_DEVICE + - _APP_STORAGE_S3_ACCESS_KEY + - _APP_STORAGE_S3_SECRET + - _APP_STORAGE_S3_REGION + - _APP_STORAGE_S3_BUCKET + - _APP_STORAGE_S3_ENDPOINT + - _APP_STORAGE_DO_SPACES_ACCESS_KEY + - _APP_STORAGE_DO_SPACES_SECRET + - _APP_STORAGE_DO_SPACES_REGION + - _APP_STORAGE_DO_SPACES_BUCKET + - _APP_STORAGE_BACKBLAZE_ACCESS_KEY + - _APP_STORAGE_BACKBLAZE_SECRET + - _APP_STORAGE_BACKBLAZE_REGION + - _APP_STORAGE_BACKBLAZE_BUCKET + - _APP_STORAGE_LINODE_ACCESS_KEY + - _APP_STORAGE_LINODE_SECRET + - _APP_STORAGE_LINODE_REGION + - _APP_STORAGE_LINODE_BUCKET + - _APP_STORAGE_WASABI_ACCESS_KEY + - _APP_STORAGE_WASABI_SECRET + - _APP_STORAGE_WASABI_REGION + - _APP_STORAGE_WASABI_BUCKET + extra_hosts: + - "host.docker.internal:host-gateway" + appwrite-worker-certificates: entrypoint: worker-certificates <<: *x-logging diff --git a/src/Appwrite/Event/Event.php b/src/Appwrite/Event/Event.php index c7bb22f715..9805ab7830 100644 --- a/src/Appwrite/Event/Event.php +++ b/src/Appwrite/Event/Event.php @@ -39,6 +39,9 @@ class Event public const BUILDS_QUEUE_NAME = 'v1-builds'; public const BUILDS_CLASS_NAME = 'BuildsV1'; + public const SCREENSHOTS_QUEUE_NAME = 'v1-screenshots'; + public const SCREENSHOTS_CLASS_NAME = 'ScreenshotsV1'; + public const MESSAGING_QUEUE_NAME = 'v1-messaging'; public const MESSAGING_CLASS_NAME = 'MessagingV1'; diff --git a/src/Appwrite/Event/Screenshot.php b/src/Appwrite/Event/Screenshot.php new file mode 100644 index 0000000000..acacf2b872 --- /dev/null +++ b/src/Appwrite/Event/Screenshot.php @@ -0,0 +1,50 @@ +setQueue(System::getEnv('_APP_SCREENSHOTS_QUEUE_NAME', Event::SCREENSHOTS_QUEUE_NAME)) + ->setClass(System::getEnv('_APP_SCREENSHOTS_CLASS_NAME', Event::SCREENSHOTS_CLASS_NAME)); + } + + public function setDeploymentId(string $deploymentId): self + { + $this->deploymentId = $deploymentId; + + return $this; + } + + protected function preparePayload(): array + { + $platform = $this->platform; + if (empty($platform)) { + $platform = Config::getParam('platform', []); + } + + return [ + 'project' => $this->project, + 'deploymentId' => $this->deploymentId, + 'platform' => $platform, + ]; + } + + public function reset(): self + { + $this->deploymentId = ''; + parent::reset(); + + return $this; + } +} diff --git a/src/Appwrite/Platform/Modules/Functions/Services/Workers.php b/src/Appwrite/Platform/Modules/Functions/Services/Workers.php index 61256b6bf9..2acb31ad8a 100644 --- a/src/Appwrite/Platform/Modules/Functions/Services/Workers.php +++ b/src/Appwrite/Platform/Modules/Functions/Services/Workers.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Functions\Services; use Appwrite\Platform\Modules\Functions\Workers\Builds; +use Appwrite\Platform\Modules\Functions\Workers\Screenshots; use Utopia\Platform\Service; class Workers extends Service @@ -11,5 +12,6 @@ class Workers extends Service { $this->type = Service::TYPE_WORKER; $this->addAction(Builds::getName(), new Builds()); + $this->addAction(Screenshots::getName(), new Screenshots()); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index e38a56bd2b..285f78319a 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -6,10 +6,9 @@ use Ahc\Jwt\JWT; use Appwrite\Event\Event; use Appwrite\Event\Func; use Appwrite\Event\Realtime; +use Appwrite\Event\Screenshot; use Appwrite\Event\StatsUsage; use Appwrite\Event\Webhook; -use Appwrite\Permission; -use Appwrite\Role; use Appwrite\Utopia\Response\Model\Deployment; use Appwrite\Vcs\Comment; use Exception; @@ -25,24 +24,19 @@ use Utopia\Database\Exception\Conflict; use Utopia\Database\Exception\Duplicate; use Utopia\Database\Exception\Restricted; use Utopia\Database\Exception\Structure; -use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; use Utopia\Detector\Detection\Rendering\SSR; use Utopia\Detector\Detection\Rendering\XStatic; use Utopia\Detector\Detector\Rendering; -use Utopia\Fetch\Client as FetchClient; use Utopia\Logger\Log; use Utopia\Platform\Action; use Utopia\Queue\Message; -use Utopia\Storage\Compression\Compression; use Utopia\Storage\Device; use Utopia\Storage\Device\Local; use Utopia\System\System; use Utopia\VCS\Adapter\Git\GitHub; -use function Swoole\Coroutine\batch; - class Builds extends Action { public static function getName(): string @@ -62,6 +56,7 @@ class Builds extends Action ->inject('project') ->inject('dbForPlatform') ->inject('queueForEvents') + ->inject('queueForScreenshots') ->inject('queueForWebhooks') ->inject('queueForFunctions') ->inject('queueForRealtime') @@ -83,6 +78,7 @@ class Builds extends Action * @param Document $project * @param Database $dbForPlatform * @param Event $queueForEvents + * @param Screenshot $queueForScreenshots * @param Webhook $queueForWebhooks * @param Func $queueForFunctions * @param Realtime $queueForRealtime @@ -103,6 +99,7 @@ class Builds extends Action Document $project, Database $dbForPlatform, Event $queueForEvents, + Screenshot $queueForScreenshots, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, @@ -143,6 +140,7 @@ class Builds extends Action $deviceForFunctions, $deviceForSites, $deviceForFiles, + $queueForScreenshots, $queueForWebhooks, $queueForFunctions, $queueForRealtime, @@ -172,6 +170,7 @@ class Builds extends Action * @param Device $deviceForFunctions * @param Device $deviceForSites * @param Device $deviceForFiles + * @param Screenshot $queueForScreenshots * @param Webhook $queueForWebhooks * @param Func $queueForFunctions * @param Realtime $queueForRealtime @@ -196,6 +195,7 @@ class Builds extends Action Device $deviceForFunctions, Device $deviceForSites, Device $deviceForFiles, + Screenshot $queueForScreenshots, Webhook $queueForWebhooks, Func $queueForFunctions, Realtime $queueForRealtime, @@ -913,187 +913,6 @@ class Builds extends Action Console::log('Build details stored'); $this->afterBuildSuccess($queueForRealtime, $dbForProject, $deployment, $runtime, $adapter); - $logs = $deployment->getAttribute('buildLogs', ''); - - /** Screenshot site */ - if ($resource->getCollection() === 'sites') { - Console::log('Site screenshot started'); - - $date = \date('H:i:s'); - $logs .= "[$date] [appwrite] Screenshot capturing started. \n"; - $deployment->setAttribute('buildLogs', $logs); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); - $queueForRealtime - ->setPayload($deployment->getArrayCopy()) - ->trigger(); - - try { - $rule = Authorization::skip(fn () => $dbForPlatform->findOne('rules', [ - Query::equal("projectInternalId", [$project->getSequence()]), - Query::equal("type", ["deployment"]), - Query::equal('deploymentInternalId', [$deployment->getSequence()]), - ])); - - if ($rule->isEmpty()) { - throw new \Exception("Rule for build not found"); - } - - $client = new FetchClient(); - $client->setTimeout(\intval($resource->getAttribute('timeout', '15')) * 1000); - $client->addHeader('content-type', FetchClient::CONTENT_TYPE_APPLICATION_JSON); - - $bucket = Authorization::skip(fn () => $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, - 'disabledMetrics' => [ - METRIC_EXECUTIONS, - METRIC_EXECUTIONS_COMPUTE, - METRIC_EXECUTIONS_MB_SECONDS, - METRIC_NETWORK_REQUESTS, - METRIC_NETWORK_INBOUND, - METRIC_NETWORK_OUTBOUND, - str_replace(["{resourceType}"], [RESOURCE_TYPE_SITES], METRIC_RESOURCE_TYPE_EXECUTIONS), - str_replace(["{resourceType}"], [RESOURCE_TYPE_SITES], METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE), - str_replace(["{resourceType}"], [RESOURCE_TYPE_SITES], METRIC_RESOURCE_TYPE_EXECUTIONS_MB_SECONDS), - str_replace(["{resourceType}", "{resourceInternalId}"], [RESOURCE_TYPE_SITES, $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS), - str_replace(["{resourceType}", "{resourceInternalId}"], [RESOURCE_TYPE_SITES, $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE), - str_replace(["{resourceType}", "{resourceInternalId}"], [RESOURCE_TYPE_SITES, $resource->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS), - ], - 'bannerDisabled' => true, - 'projectCheckDisabled' => true, - 'previewAuthDisabled' => true, - 'deploymentStatusIgnored' => true - ]); - - $screenshotError = null; - $screenshots = batch(\array_map(function ($key) use ($configs, $apiKey, $resource, $client, &$screenshotError) { - return function () use ($key, $configs, $apiKey, $resource, $client, &$screenshotError) { - try { - $config = $configs[$key]; - - $config['headers'] = \array_merge($config['headers'] ?? [], [ - 'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey - ]); - $config['sleep'] = 3000; - - $frameworks = Config::getParam('frameworks', []); - $framework = $frameworks[$resource->getAttribute('framework', '')] ?? null; - if (!is_null($framework)) { - $config['sleep'] = $framework['screenshotSleep']; - } - - $browserEndpoint = System::getEnv('_APP_BROWSER_HOST', 'http://appwrite-browser:3000/v1'); - $fetchResponse = $client->fetch( - url: $browserEndpoint . '/screenshots', - method: 'POST', - body: $config - ); - - if ($fetchResponse->getStatusCode() >= 400) { - throw new \Exception($fetchResponse->getBody()); - } - - $screenshot = $fetchResponse->getBody(); - - return ['key' => $key, 'screenshot' => $screenshot]; - } catch (\Throwable $th) { - $screenshotError = $th->getMessage(); - return; - } - }; - }, \array_keys($configs))); - - if (!\is_null($screenshotError)) { - throw new \Exception($screenshotError); - } - - $mimeType = "image/png"; - - foreach ($screenshots as $data) { - $key = $data['key']; - $screenshot = $data['screenshot']; - - $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, $mimeType); - - 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->getSequence(), - 'name' => $fileName, - 'path' => $path, - 'signature' => $deviceForFiles->getFileHash($path), - 'mimeType' => $mimeType, - 'sizeOriginal' => \strlen($screenshot), - 'sizeActual' => $deviceForFiles->getFileSize($path), - 'algorithm' => Compression::NONE, - 'comment' => '', - 'chunksTotal' => 1, - 'chunksUploaded' => 1, - 'openSSLVersion' => null, - 'openSSLCipher' => null, - 'openSSLTag' => null, - 'openSSLIV' => null, - 'search' => implode(' ', [$fileId, $fileName]), - 'metadata' => ['content_type' => $mimeType], - ]); - - Authorization::skip(fn () => $dbForPlatform->createDocument('bucket_' . $bucket->getSequence(), $file)); - - $deployment->setAttribute($key, $fileId); - } - - $logs = $deployment->getAttribute('buildLogs', ''); - $date = \date('H:i:s'); - $logs .= "[$date] [appwrite] Screenshot capturing finished. \n"; - - $deployment->setAttribute('buildLogs', $logs); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); - - $queueForRealtime - ->setPayload($deployment->getArrayCopy()) - ->trigger(); - } catch (\Throwable $th) { - Console::warning("Screenshot failed to generate:"); - Console::warning($th->getMessage()); - Console::warning($th->getTraceAsString()); - - $logs = $deployment->getAttribute('buildLogs', ''); - $date = \date('H:i:s'); - $logs .= "[$date] [appwrite] Screenshot capturing failed. Deployment will continue. \n"; - - $deployment->setAttribute('buildLogs', $logs); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); - } - - Console::log('Site screenshot finished'); - } $logs = $deployment->getAttribute('buildLogs', ''); $date = \date('H:i:s'); @@ -1114,6 +933,16 @@ class Builds extends Action $this->runGitAction('ready', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform); } + /** Screenshot site */ + if ($resource->getCollection() === 'sites') { + $queueForScreenshots + ->setDeploymentId($deployment->getId()) + ->setProject($project) + ->trigger(); + + Console::log('Site screenshot queued'); + } + /** Set auto deploy */ $activateBuild = false; if ($deployment->getAttribute('activate') === true) { @@ -1171,8 +1000,6 @@ class Builds extends Action 'live' => true, 'deploymentId' => $deployment->getId(), 'deploymentInternalId' => $deployment->getSequence(), - 'deploymentScreenshotDark' => $deployment->getAttribute('screenshotDark', ''), - 'deploymentScreenshotLight' => $deployment->getAttribute('screenshotLight', ''), 'deploymentCreatedAt' => $deployment->getCreatedAt(), ])); $queries = [ @@ -1265,9 +1092,10 @@ class Builds extends Action $endTime = DateTime::now(); $durationEnd = \microtime(true); - $deployment->setAttribute('buildEndedAt', $endTime); - $deployment->setAttribute('buildDuration', \intval(\ceil($durationEnd - $durationStart))); - $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $deployment); + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'buildEndedAt' => $endTime, + 'buildDuration' => \intval(\ceil($durationEnd - $durationStart)), + ])); $queueForRealtime ->setPayload($deployment->getArrayCopy()) ->trigger(); diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php b/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php new file mode 100644 index 0000000000..ca89532307 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php @@ -0,0 +1,281 @@ +desc('Screenshots worker') + ->groups(['screenshots']) + ->inject('message') + ->inject('queueForRealtime') + ->inject('dbForPlatform') + ->inject('dbForProject') + ->inject('project') + ->inject('deviceForFiles') + ->callback($this->action(...)); + } + + public function action( + Message $message, + Realtime $queueForRealtime, + Database $dbForPlatform, + Database $dbForProject, + Document $project, + Device $deviceForFiles + ): void { + Console::log('Screenshot action started'); + + $payload = $message->getPayload() ?? []; + + if (empty($payload)) { + throw new \Exception('Missing payload'); + } + + Console::log('Site screenshot started'); + + $deploymentId = $payload['deploymentId'] ?? null; + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + + if ($deployment->isEmpty()) { + throw new \Exception('Deployment not found'); + } + + $siteId = $deployment->getAttribute('resourceId'); + $site = $dbForProject->getDocument('sites', $siteId); + + if ($site->isEmpty()) { + throw new \Exception('Site not found'); + } + + // Realtime preparation + $event = "sites.[siteId].deployments.[deploymentId].update"; + $queueForRealtime + ->setSubscribers(['console']) + ->setProject($project) + ->setEvent($event) + ->setParam('siteId', $site->getId()) + ->setParam('deploymentId', $deployment->getId()); + + $date = \date('H:i:s'); + $this->appendToLogs($dbForProject, $deployment->getId(), $queueForRealtime, "[$date] [appwrite] Screenshot capturing started. \n"); + + try { + $rule = $dbForPlatform->findOne('rules', [ + Query::equal("projectInternalId", [$project->getSequence()]), + Query::equal("type", ["deployment"]), + Query::equal('deploymentInternalId', [$deployment->getSequence()]), + ]); + + if ($rule->isEmpty()) { + throw new \Exception("Rule for deployment not found"); + } + + $client = new FetchClient(); + $client->setTimeout(\intval($site->getAttribute('timeout', '15')) * 1000); + $client->addHeader('content-type', FetchClient::CONTENT_TYPE_APPLICATION_JSON); + + $bucket = $dbForPlatform->getDocument('buckets', 'screenshots'); + + if ($bucket->isEmpty()) { + throw new \Exception('Bucket not found'); + } + + $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, + 'disabledMetrics' => [ + METRIC_EXECUTIONS, + METRIC_EXECUTIONS_COMPUTE, + METRIC_EXECUTIONS_MB_SECONDS, + METRIC_NETWORK_REQUESTS, + METRIC_NETWORK_INBOUND, + METRIC_NETWORK_OUTBOUND, + str_replace(["{resourceType}"], [RESOURCE_TYPE_SITES], METRIC_RESOURCE_TYPE_EXECUTIONS), + str_replace(["{resourceType}"], [RESOURCE_TYPE_SITES], METRIC_RESOURCE_TYPE_EXECUTIONS_COMPUTE), + str_replace(["{resourceType}"], [RESOURCE_TYPE_SITES], METRIC_RESOURCE_TYPE_EXECUTIONS_MB_SECONDS), + str_replace(["{resourceType}", "{resourceInternalId}"], [RESOURCE_TYPE_SITES, $site->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS), + str_replace(["{resourceType}", "{resourceInternalId}"], [RESOURCE_TYPE_SITES, $site->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_COMPUTE), + str_replace(["{resourceType}", "{resourceInternalId}"], [RESOURCE_TYPE_SITES, $site->getSequence()], METRIC_RESOURCE_TYPE_ID_EXECUTIONS_MB_SECONDS), + ], + 'bannerDisabled' => true, + 'projectCheckDisabled' => true, + 'previewAuthDisabled' => true, + 'deploymentStatusIgnored' => true + ]); + + $screenshotError = null; + $screenshots = batch(\array_map(function ($key) use ($configs, $apiKey, $site, $client, &$screenshotError) { + return function () use ($key, $configs, $apiKey, $site, $client, &$screenshotError) { + try { + $config = $configs[$key]; + + $config['headers'] = \array_merge($config['headers'] ?? [], [ + 'x-appwrite-key' => API_KEY_DYNAMIC . '_' . $apiKey + ]); + $config['sleep'] = 3000; + + $frameworks = Config::getParam('frameworks', []); + $framework = $frameworks[$site->getAttribute('framework', '')] ?? null; + if (!is_null($framework)) { + $config['sleep'] = $framework['screenshotSleep']; + } + + $browserEndpoint = System::getEnv('_APP_BROWSER_HOST', 'http://appwrite-browser:3000/v1'); + $fetchResponse = $client->fetch( + url: $browserEndpoint . '/screenshots', + method: 'POST', + body: $config + ); + + if ($fetchResponse->getStatusCode() >= 400) { + throw new \Exception($fetchResponse->getBody()); + } + + $screenshot = $fetchResponse->getBody(); + + return ['key' => $key, 'screenshot' => $screenshot]; + } catch (\Throwable $th) { + $screenshotError = $th->getMessage(); + return; + } + }; + }, \array_keys($configs))); + + if (!\is_null($screenshotError)) { + throw new \Exception($screenshotError); + } + + $mimeType = "image/png"; + $updates = new Document([]); + + foreach ($screenshots as $data) { + $key = $data['key']; + $screenshot = $data['screenshot']; + + $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, $mimeType); + + 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->getSequence(), + 'name' => $fileName, + 'path' => $path, + 'signature' => $deviceForFiles->getFileHash($path), + 'mimeType' => $mimeType, + 'sizeOriginal' => \strlen($screenshot), + 'sizeActual' => $deviceForFiles->getFileSize($path), + 'algorithm' => Compression::NONE, + 'comment' => '', + 'chunksTotal' => 1, + 'chunksUploaded' => 1, + 'openSSLVersion' => null, + 'openSSLCipher' => null, + 'openSSLTag' => null, + 'openSSLIV' => null, + 'search' => implode(' ', [$fileId, $fileName]), + 'metadata' => ['content_type' => $mimeType], + ]); + + $dbForPlatform->createDocument('bucket_' . $bucket->getSequence(), $file); + + $updates->setAttribute($key, $fileId); + } + + $date = \date('H:i:s'); + $this->appendToLogs($dbForProject, $deployment->getId(), $queueForRealtime, "[$date] [appwrite] Screenshot capturing finished. \n"); + + // Apply screenshot properties + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), $updates); + + $queueForRealtime + ->setPayload($deployment->getArrayCopy()) + ->trigger(); + + $site = $dbForProject->updateDocument('sites', $site->getId(), new Document([ + 'deploymentScreenshotDark' => $deployment->getAttribute('screenshotDark', ''), + 'deploymentScreenshotLight' => $deployment->getAttribute('screenshotLight', ''), + ])); + } catch (\Throwable $th) { + Console::warning("Screenshot failed to generate:"); + Console::warning($th->getMessage()); + Console::warning($th->getTraceAsString()); + + $date = \date('H:i:s'); + $this->appendToLogs($dbForProject, $deployment->getId(), $queueForRealtime, "[$date] [appwrite] Screenshot capturing failed. Deployment will continue. \n"); + + throw $th; + } + } + + protected function appendToLogs(Database $dbForProject, string $deploymentId, Realtime $queueForRealtime, string $logs) + { + $deployment = $dbForProject->getDocument('deployments', $deploymentId); + + $buildLogs = $deployment->getAttribute('buildLogs', ''); + $buildLogs .= $logs; + + $deployment = $dbForProject->updateDocument('deployments', $deployment->getId(), new Document([ + 'buildLogs' => $buildLogs + ])); + + $queueForRealtime + ->setPayload($deployment->getArrayCopy()) + ->trigger(); + } +} diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php index b2d9af5a08..ed5c23b6c1 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Create.php @@ -385,6 +385,9 @@ class Create extends Action } $file = Authorization::skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, $file)); } + + // Trigger after create success hook + $this->afterCreateSuccess($file); } else { if ($file->isEmpty()) { $doc = new Document([ @@ -448,4 +451,17 @@ class Create extends Action ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic($file, Response::MODEL_FILE); } + + /** + * Hook to run after file is created successfully + * + * @param Document $file + * @return void + */ + protected function afterCreateSuccess(Document $file) + { + if (!($file instanceof Document)) { + throw new Exception('file must be an instance of document'); + } + } } diff --git a/tests/e2e/Services/Sites/SitesConsoleClientTest.php b/tests/e2e/Services/Sites/SitesConsoleClientTest.php index 2b75402b25..31cee13261 100644 --- a/tests/e2e/Services/Sites/SitesConsoleClientTest.php +++ b/tests/e2e/Services/Sites/SitesConsoleClientTest.php @@ -54,15 +54,22 @@ class SitesConsoleClientTest extends Scope $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']); + $deployment = null; + $site = null; + $this->assertEventually(function () use ($siteId, $deploymentId, &$deployment, &$site) { + $deployment = $this->getDeployment($siteId, $deploymentId); + $this->assertEquals(200, $deployment['headers']['status-code']); + $this->assertNotEmpty($deployment['body']['screenshotLight']); + $this->assertNotEmpty($deployment['body']['screenshotDark']); - $site = $this->getSite($siteId); - $this->assertEquals(200, $site['headers']['status-code']); - $this->assertEquals($deployment['body']['screenshotLight'], $site['body']['deploymentScreenshotLight']); - $this->assertEquals($deployment['body']['screenshotDark'], $site['body']['deploymentScreenshotDark']); + $site = $this->getSite($siteId); + $this->assertEquals(200, $site['headers']['status-code']); + $this->assertEquals($deployment['body']['screenshotLight'], $site['body']['deploymentScreenshotLight']); + $this->assertEquals($deployment['body']['screenshotDark'], $site['body']['deploymentScreenshotDark']); + }); + + $this->assertNotNull($site); + $this->assertNotNull($deployment); $screenshotId = $deployment['body']['screenshotLight']; $file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console", array_merge($this->getHeaders(), [