Merge remote-tracking branch 'origin/1.7.x' into 1.8.x

# Conflicts:
#	composer.lock
#	src/Appwrite/Platform/Workers/Audits.php
This commit is contained in:
Jake Barnby
2025-06-09 20:08:41 -04:00
70 changed files with 813 additions and 617 deletions
+2
View File
@@ -124,6 +124,8 @@ class Exception extends \Exception
/** Membership */
public const MEMBERSHIP_NOT_FOUND = 'membership_not_found';
public const MEMBERSHIP_ALREADY_CONFIRMED = 'membership_already_confirmed';
public const MEMBERSHIP_DELETION_PROHIBITED = 'membership_deletion_prohibited';
public const MEMBERSHIP_DOWNGRADE_PROHIBITED = 'membership_downgrade_prohibited';
/** Avatars */
public const AVATAR_SET_NOT_FOUND = 'avatar_set_not_found';
+2 -2
View File
@@ -150,9 +150,9 @@ class V22 extends Migration
];
foreach ($indexes as $index) {
try {
$this->createIndexFromCollection($this->dbForProject, $id, $index);
$this->dbForProject->deleteIndex($id, $index);
} catch (Throwable $th) {
Console::warning("Failed to create index \"$index\" from {$id}: {$th->getMessage()}");
Console::warning("Failed to delete index \"$index\" from {$id}: {$th->getMessage()}");
}
}
$this->dbForProject->purgeCachedCollection($id);
@@ -105,9 +105,11 @@ class Get extends Action
$response
->setContentType('application/gzip')
->addHeader('Cache-Control', 'private, max-age=3888000') // 45 days
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Expires', '0')
->addHeader('Pragma', 'no-cache')
->addHeader('X-Peak', \memory_get_peak_usage())
->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"');
->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '-' . $type . '.tar.gz"');
$size = $device->getFileSize($path);
$rangeHeader = $request->getHeader('range');
@@ -14,6 +14,7 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\UID;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
@@ -65,15 +66,18 @@ class Create extends Action
->param('domain', null, new ValidatorDomain(), 'Domain name.')
->param('url', null, new URL(), 'Target URL of redirection')
->param('statusCode', null, new WhiteList([301, 302, 307, 308]), 'Status code of redirection')
->param('resourceId', '', new UID(), 'ID of parent resource.')
->param('resourceType', '', new WhiteList(['site', 'function']), 'Type of parent resource.')
->inject('response')
->inject('project')
->inject('queueForCertificates')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('dbForProject')
->callback([$this, 'action']);
}
public function action(string $domain, string $url, int $statusCode, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform)
public function action(string $domain, string $url, int $statusCode, string $resourceId, string $resourceType, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject)
{
$deniedDomains = [
'localhost',
@@ -116,6 +120,15 @@ class Create extends Action
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
}
$collection = match ($resourceType) {
'site' => 'sites',
'function' => 'functions'
};
$resource = $dbForProject->getDocument($collection, $resourceId);
if ($resource->isEmpty()) {
throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND);
}
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
@@ -164,6 +177,9 @@ class Create extends Action
'trigger' => 'manual',
'redirectUrl' => $url,
'redirectStatusCode' => $statusCode,
'deploymentResourceType' => $resourceType,
'deploymentResourceId' => $resource->getId(),
'deploymentResourceInternalId' => $resource->getInternalId(),
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain->get()]),
'owner' => $owner,
@@ -99,12 +99,14 @@ class Get extends Action
}
if (!$device->exists($path)) {
throw new Exception(Exception::BUILD_NOT_FOUND);
throw new Exception(Exception::DEPLOYMENT_NOT_FOUND);
}
$response
->setContentType('application/gzip')
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Expires', '0')
->addHeader('Pragma', 'no-cache')
->addHeader('X-Peak', \memory_get_peak_usage())
->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '-' . $type . '.tar.gz"');
+29 -28
View File
@@ -12,13 +12,13 @@ use Utopia\Database\Exception\Authorization;
use Utopia\Database\Exception\Structure;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Queue\Result\Commit;
use Utopia\Queue\Result\NoCommit;
use Utopia\System\System;
class Audits extends Action
{
protected const BATCH_SIZE_DEVELOPMENT = 1; // smaller batch size for development
protected const BATCH_SIZE_PRODUCTION = 5_000;
protected const BATCH_AGGREGATION_INTERVAL = 60; // in seconds
protected const int BATCH_AGGREGATION_INTERVAL = 60; // in seconds
private int $lastTriggeredTime = 0;
@@ -27,9 +27,7 @@ class Audits extends Action
protected function getBatchSize(): int
{
return System::getEnv('_APP_ENV', 'development') === 'development'
? self::BATCH_SIZE_DEVELOPMENT
: self::BATCH_SIZE_PRODUCTION;
return intval(System::getEnv('_APP_QUEUE_PREFETCH_COUNT', 1));
}
public static function getName(): string
@@ -57,13 +55,13 @@ class Audits extends Action
* @param Message $message
* @param callable $getProjectDB
* @param Document $project
* @return void
* @return Commit|NoCommit
* @throws Throwable
* @throws \Utopia\Database\Exception
* @throws Authorization
* @throws Structure
*/
public function action(Message $message, callable $getProjectDB, Document $project): void
public function action(Message $message, callable $getProjectDB, Document $project): Commit|NoCommit
{
$payload = $message->getPayload() ?? [];
@@ -123,29 +121,32 @@ class Audits extends Action
// Check if we should process the batch by checking both for the batch size and the elapsed time
$batchSize = $this->getBatchSize();
$shouldProcessBatch = \count($this->logs) >= $batchSize;
if (!$shouldProcessBatch && \count($this->logs) > 0) {
$logCount = array_reduce($this->logs, fn (int $current, $logs) => $current + count($logs['logs']), 0);
$shouldProcessBatch = $logCount >= $batchSize;
if (!$shouldProcessBatch && $logCount > 0) {
$shouldProcessBatch = (\time() - $this->lastTriggeredTime) >= self::BATCH_AGGREGATION_INTERVAL;
}
if ($shouldProcessBatch) {
try {
foreach ($this->logs as $sequence => $projectLogs) {
$dbForProject = $getProjectDB($projectLogs['project']);
Console::log('Processing batch with ' . count($projectLogs['logs']) . ' events');
$audit = new Audit($dbForProject);
$audit->logBatch($projectLogs['logs']);
Console::success('Audit logs processed successfully');
unset($this->logs[$sequence]);
}
} catch (Throwable $e) {
Console::error('Error processing audit logs: ' . $e->getMessage());
} finally {
$this->lastTriggeredTime = time();
}
if (!$shouldProcessBatch) {
return new NoCommit();
}
try {
foreach ($this->logs as $internalId => $projectLogs) {
$dbForProject = $getProjectDB($projectLogs['project']);
Console::log('Processing batch with ' . count($projectLogs['logs']) . ' events');
$audit = new Audit($dbForProject);
$audit->logBatch($projectLogs['logs']);
Console::success('Audit logs processed successfully');
unset($this->logs[$internalId]);
}
} catch (Throwable $e) {
Console::error('Error processing audit logs: ' . $e->getMessage());
}
$this->lastTriggeredTime = time();
return new Commit();
}
}
+17 -1
View File
@@ -113,6 +113,16 @@ abstract class Format
protected function getEnumName(string $service, string $method, string $param): ?string
{
switch ($service) {
case 'proxy':
switch ($method) {
case 'createRedirectRule':
switch ($param) {
case 'resourceType':
return 'ProxyResourceType';
}
break;
}
break;
case 'console':
switch ($method) {
case 'getResource':
@@ -441,7 +451,13 @@ abstract class Format
case 'proxy':
switch ($method) {
case 'createRedirectRule':
return ['Moved Permanently 301', 'Found 302', 'Temporary Redirect 307', 'Permanent Redirect 308'];
switch ($param) {
case 'statusCode':
return ['Moved Permanently 301', 'Found 302', 'Temporary Redirect 307', 'Permanent Redirect 308'];
case 'resourceType':
return ['Site', 'Function'];
}
break;
}
break;
case 'functions':
+12 -8
View File
@@ -87,7 +87,7 @@ class Comment
$i = 0;
foreach ($projects as $projectId => $project) {
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_DOMAIN');
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN'));
$text .= "## {$project['name']}\n\n";
$text .= "Project ID: `{$projectId}`\n\n";
@@ -103,10 +103,12 @@ class Comment
$text .= "| :- | :- | :- | :- | :- |\n";
foreach ($project['site'] as $siteId => $site) {
$imageStatus = in_array($site['status'], ['processing', 'building']) ? 'building' : $site['status'];
$extension = $site['status'] === 'building' ? 'gif' : 'png';
$pathLight = '/images/vcs/status-' . $site['status'] . '-light.' . $extension;
$pathDark = '/images/vcs/status-' . $site['status'] . '-dark.' . $extension;
$pathLight = '/images/vcs/status-' . $imageStatus . '-light.' . $extension;
$pathDark = '/images/vcs/status-' . $imageStatus . '-dark.' . $extension;
$status = match ($site['status']) {
'waiting' => $this->generatImage($pathLight, $pathDark, 'Queued', 85) . ' _Queued_',
@@ -149,10 +151,11 @@ class Comment
$text .= "| :- | :- | :- | :- |\n";
foreach ($project['function'] as $functionId => $function) {
$extension = $function['status'] === 'building' ? 'gif' : 'png';
$imageStatus = in_array($function['status'], ['processing', 'building']) ? 'building' : $function['status'];
$extension = $imageStatus === 'building' ? 'gif' : 'png';
$pathLight = '/images/vcs/status-' . $function['status'] . '-light.' . $extension;
$pathDark = '/images/vcs/status-' . $function['status'] . '-dark.' . $extension;
$pathLight = '/images/vcs/status-' . $imageStatus . '-light.' . $extension;
$pathDark = '/images/vcs/status-' . $imageStatus . '-dark.' . $extension;
$status = match ($function['status']) {
'waiting' => $this->generatImage($pathLight, $pathDark, 'Queued', 85) . ' _Queued_',
@@ -168,7 +171,8 @@ class Comment
$action = '[Authorize](' . $function['action']['url'] . ')';
}
$text .= "| &nbsp;**{$function['name']}**<br>`$functionId`";
$text .= "| &nbsp;**{$function['name']}**";
$text .= "| `{$functionId}`";
$text .= "| {$status}";
$text .= "| {$action}";
$text .= "|\n";
@@ -197,7 +201,7 @@ class Comment
public function generatImage(string $pathLight, string $pathDark, string $alt, int $width): string
{
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_DOMAIN');
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN'));
$imageLight = $protocol . '://' . $hostname . $pathLight;
$imageDark = $protocol . '://' . $hostname . $pathDark;