mirror of
https://github.com/appwrite/appwrite
synced 2026-05-21 16:08:22 +00:00
Merge pull request #11110 from appwrite/feat-async-screenshots
Feat: Async screenshots
This commit is contained in:
commit
445d7495a3
12 changed files with 450 additions and 204 deletions
|
|
@ -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 && \
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
|
|
|||
3
bin/worker-screenshots
Normal file
3
bin/worker-screenshots
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
exec php /usr/src/code/app/worker.php screenshots "$@"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
50
src/Appwrite/Event/Screenshot.php
Normal file
50
src/Appwrite/Event/Screenshot.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Event;
|
||||
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Queue\Publisher;
|
||||
use Utopia\System\System;
|
||||
|
||||
class Screenshot extends Event
|
||||
{
|
||||
protected string $deploymentId = '';
|
||||
|
||||
public function __construct(protected Publisher $publisher)
|
||||
{
|
||||
parent::__construct($publisher);
|
||||
|
||||
$this
|
||||
->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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 .= "[90m[$date] [90m[[0mappwrite[90m][97m Screenshot capturing started. [0m\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 .= "[90m[$date] [90m[[0mappwrite[90m][97m Screenshot capturing finished. [0m\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 .= "[90m[$date] [90m[[0mappwrite[90m][33m Screenshot capturing failed. Deployment will continue. [0m\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();
|
||||
|
|
|
|||
281
src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php
Normal file
281
src/Appwrite/Platform/Modules/Functions/Workers/Screenshots.php
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Functions\Workers;
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Appwrite\Event\Realtime;
|
||||
use Appwrite\Permission;
|
||||
use Appwrite\Role;
|
||||
use Exception;
|
||||
use Utopia\CLI\Console;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Query;
|
||||
use Utopia\Fetch\Client as FetchClient;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Queue\Message;
|
||||
use Utopia\Storage\Compression\Compression;
|
||||
use Utopia\Storage\Device;
|
||||
use Utopia\System\System;
|
||||
|
||||
use function Swoole\Coroutine\batch;
|
||||
|
||||
class Screenshots extends Action
|
||||
{
|
||||
public static function getName(): string
|
||||
{
|
||||
return 'screenshots';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this
|
||||
->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, "[90m[$date] [90m[[0mappwrite[90m][97m Screenshot capturing started. [0m\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, "[90m[$date] [90m[[0mappwrite[90m][97m Screenshot capturing finished. [0m\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, "[90m[$date] [90m[[0mappwrite[90m][33m Screenshot capturing failed. Deployment will continue. [0m\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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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(), [
|
||||
|
|
|
|||
Loading…
Reference in a new issue