Merge branch '1.9.x' of https://github.com/appwrite/appwrite into users-skip-targets

This commit is contained in:
fogelito
2026-05-17 17:17:09 +03:00
86 changed files with 1970 additions and 779 deletions
+5
View File
@@ -4,6 +4,7 @@ require_once __DIR__ . '/init.php';
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Publisher\StatsResources as StatsResourcesPublisher;
@@ -285,6 +286,10 @@ $container->set('publisherForFunctions', fn (Publisher $publisher) => new Functi
$publisher,
new Queue(System::getEnv('_APP_FUNCTIONS_QUEUE_NAME', Event::FUNCTIONS_QUEUE_NAME), 'utopia-queue', Event::FUNCTIONS_QUEUE_TTL)
), ['publisher']);
$container->set('publisherForDatabase', fn (Publisher $publisherDatabases) => new DatabasePublisher(
$publisherDatabases,
new Queue(System::getEnv('_APP_DATABASE_QUEUE_NAME', Event::DATABASE_QUEUE_NAME))
), ['publisherDatabases']);
$container->set('publisherForDeletes', fn (Publisher $publisher) => new DeletePublisher(
$publisher,
new Queue(System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME))
+8 -2
View File
@@ -4,17 +4,23 @@
return [
"projects.read" => [
"description" => 'Access to read organization\'s projects',
"description" => 'Access to read organization projects',
"category" => "Projects",
],
"projects.write" => [
"description" =>
"Access to create, update, and delete projects in organization",
"Access to create, update, and delete organization projects",
"category" => "Projects",
],
"devKeys.read" => [
"description" => 'Access to read project\'s development keys',
"category" => "Other",
"deprecated" => true,
],
"devKeys.write" => [
"description" =>
"Access to create, update, and delete project\'s development keys",
"category" => "Other",
"deprecated" => true,
],
];
+2 -12
View File
@@ -4,7 +4,6 @@ use Appwrite\Auth\Key;
use Appwrite\Auth\MFA\Type\TOTP;
use Appwrite\Bus\Events\RequestCompleted;
use Appwrite\Event\Context\Audit as AuditContext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Audit as AuditMessage;
use Appwrite\Event\Message\Func as FunctionMessage;
@@ -564,7 +563,6 @@ Http::init()
->inject('user')
->inject('queueForEvents')
->inject('auditContext')
->inject('queueForDatabase')
->inject('usage')
->inject('publisherForFunctions')
->inject('dbForProject')
@@ -576,7 +574,7 @@ Http::init()
->inject('platform')
->inject('authorization')
->inject('cacheControlForStorage')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, EventDatabase $queueForDatabase, Context $usage, FunctionPublisher $publisherForFunctions, Database $dbForProject, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) {
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Context $usage, FunctionPublisher $publisherForFunctions, Database $dbForProject, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) {
$response->setUser($user);
$request->setUser($user);
@@ -622,9 +620,6 @@ Http::init()
$auditContext->user = $userClone;
}
/* Auto-set projects */
$queueForDatabase->setProject($project);
$useCache = $route->getLabel('cache', false);
$storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load');
if ($useCache) {
@@ -815,7 +810,6 @@ Http::shutdown()
->inject('publisherForAudits')
->inject('usage')
->inject('publisherForUsage')
->inject('queueForDatabase')
->inject('publisherForFunctions')
->inject('queueForWebhooks')
->inject('queueForRealtime')
@@ -826,7 +820,7 @@ Http::shutdown()
->inject('bus')
->inject('apiKey')
->inject('mode')
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, EventDatabase $queueForDatabase, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Audit $publisherForAudits, Context $usage, UsagePublisher $publisherForUsage, FunctionPublisher $publisherForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, Authorization $authorization, callable $timelimit, EventProcessor $eventProcessor, Bus $bus, ?Key $apiKey, string $mode) use ($parseLabel) {
$responsePayload = $response->getPayload();
@@ -973,10 +967,6 @@ Http::shutdown()
$publisherForAudits->enqueue(AuditMessage::fromContext($auditContext));
}
if (! empty($queueForDatabase->getType())) {
$queueForDatabase->trigger();
}
// Cache label
$useCache = $route->getLabel('cache', false);
if ($useCache) {
+4
View File
@@ -514,3 +514,7 @@ const CSV_ALLOWED_DATABASE_TYPES = [
DATABASE_TYPE_TABLESDB,
DATABASE_TYPE_VECTORSDB
];
const VCS_DEPLOYMENT_SKIP_PATTERNS = [
'[skip ci]',
];
+8
View File
@@ -240,6 +240,12 @@ $register->set('pools', function () {
'multiple' => true,
'schemes' => ['redis'],
],
'lock' => [
'type' => 'lock',
'dsns' => $fallbackForRedis,
'multiple' => false,
'schemes' => ['redis'],
],
];
$maxConnections = (int) System::getEnv('_APP_CONNECTIONS_MAX', 151);
@@ -369,6 +375,8 @@ $register->set('pools', function () {
}
return $adapter;
case 'lock':
return $resource();
default:
throw new Exception(Exception::GENERAL_SERVER_ERROR, "Server error: Missing adapter implementation.");
}
+16
View File
@@ -4,6 +4,7 @@ use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Audit as AuditPublisher;
use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Event\Publisher\Certificate as CertificatePublisher;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Event\Publisher\Execution as ExecutionPublisher;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
@@ -28,6 +29,7 @@ use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\DI\Container;
use Utopia\DSN\DSN;
use Utopia\Lock\Distributed;
use Utopia\Pools\Group;
use Utopia\Queue\Broker\Pool as BrokerPool;
use Utopia\Queue\Publisher;
@@ -126,6 +128,10 @@ $container->set('publisherForBuilds', fn (Publisher $publisher) => new BuildPubl
$publisher,
new Queue(System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME))
), ['publisher']);
$container->set('publisherForDatabase', fn (Publisher $publisherDatabases) => new DatabasePublisher(
$publisherDatabases,
new Queue(System::getEnv('_APP_DATABASE_QUEUE_NAME', Event::DATABASE_QUEUE_NAME))
), ['publisherDatabases']);
$container->set('publisherForDeletes', fn (Publisher $publisher) => new DeletePublisher(
$publisher,
new Queue(System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME))
@@ -243,6 +249,16 @@ $container->set('redis', function () {
return $redis;
});
$container->set('locks', function (Group $pools) {
return function (string $key, int $ttl, callable $callback, float $timeout = 0.0) use ($pools): mixed {
return $pools->get('lock')->use(function (\Redis $redis) use ($key, $ttl, $callback, $timeout) {
$lock = new Distributed($redis, $key, ttl: $ttl);
return $lock->withLock($callback, timeout: $timeout);
});
};
}, ['pools']);
$container->set('timelimit', function (\Redis $redis) {
return function (string $key, int $limit, int $time) use ($redis) {
return new TimeLimitRedis($key, $limit, $time, $redis);
-2
View File
@@ -5,7 +5,6 @@ use Ahc\Jwt\JWTException;
use Appwrite\Auth\Key;
use Appwrite\Databases\TransactionState;
use Appwrite\Event\Context\Audit as AuditContext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Func as FunctionMessage;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
@@ -107,7 +106,6 @@ return function (Container $context): void {
});
// Per-request queue resources (stateful, accumulate event data during request)
$context->set('queueForDatabase', fn (Publisher $publisher) => new EventDatabase($publisher), ['publisher']);
$context->set('queueForEvents', fn (Publisher $publisher) => new Event($publisher), ['publisher']);
$context->set('queueForWebhooks', fn (Publisher $publisher) => new Webhook($publisher), ['publisher']);
$context->set('queueForRealtime', fn () => new Realtime(), []);
-5
View File
@@ -1,6 +1,5 @@
<?php
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Realtime;
@@ -327,10 +326,6 @@ return function (Container $container): void {
return DateTime::addSeconds(new \DateTime(), -1 * (int) System::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', 1209600)); // 14 days
}, []);
$container->set('queueForDatabase', function (Publisher $publisher) {
return new EventDatabase($publisher);
}, ['publisher']);
$container->set('queueForEvents', function (Publisher $publisher) {
return new Event($publisher);
}, ['publisher']);
+2 -1
View File
@@ -72,10 +72,11 @@
"utopia-php/validators": "0.2.*",
"utopia-php/image": "0.8.*",
"utopia-php/locale": "0.8.*",
"utopia-php/lock": "0.2.*",
"utopia-php/logger": "0.8.*",
"utopia-php/messaging": "0.22.*",
"utopia-php/migration": "1.*",
"utopia-php/platform": "^1.0@RC",
"utopia-php/platform": "1.0.0-rc2",
"utopia-php/pools": "1.*",
"utopia-php/span": "1.1.*",
"utopia-php/preloader": "0.2.*",
Generated
+128 -78
View File
@@ -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": "9377e1b56bca8dbaf213ee3572ca15c0",
"content-hash": "035685d1335039f13e16d0532c874b21",
"packages": [
{
"name": "adhocore/jwt",
@@ -69,16 +69,16 @@
},
{
"name": "appwrite/appwrite",
"version": "23.1.0",
"version": "23.1.1",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-for-php.git",
"reference": "2f275921f10ceb7cff99f2d463f7328b296234fa"
"reference": "fd7c0f0bf5ddf334533534b20ed967cfb400f6ea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/2f275921f10ceb7cff99f2d463f7328b296234fa",
"reference": "2f275921f10ceb7cff99f2d463f7328b296234fa",
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/fd7c0f0bf5ddf334533534b20ed967cfb400f6ea",
"reference": "fd7c0f0bf5ddf334533534b20ed967cfb400f6ea",
"shasum": ""
},
"require": {
@@ -104,10 +104,10 @@
"support": {
"email": "team@appwrite.io",
"issues": "https://github.com/appwrite/sdk-for-php/issues",
"source": "https://github.com/appwrite/sdk-for-php/tree/23.1.0",
"source": "https://github.com/appwrite/sdk-for-php/tree/23.1.1",
"url": "https://appwrite.io/support"
},
"time": "2026-05-08T13:44:58+00:00"
"time": "2026-05-12T11:03:36+00:00"
},
{
"name": "appwrite/php-clamav",
@@ -4498,6 +4498,57 @@
},
"time": "2025-08-12T12:58:26+00:00"
},
{
"name": "utopia-php/lock",
"version": "0.2.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/lock.git",
"reference": "49317c9493d8f747e4299aa24c22862aa5f6e106"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/lock/zipball/49317c9493d8f747e4299aa24c22862aa5f6e106",
"reference": "49317c9493d8f747e4299aa24c22862aa5f6e106",
"shasum": ""
},
"require": {
"php": ">=8.3"
},
"require-dev": {
"laravel/pint": "1.*",
"phpstan/phpstan": "2.*",
"phpunit/phpunit": "11.*",
"swoole/ide-helper": "*"
},
"suggest": {
"ext-pcntl": "Required to run the File lock tests",
"ext-redis": "Required for the Distributed lock",
"ext-swoole": "Required for the Mutex and Semaphore locks (>=6.0)"
},
"type": "library",
"autoload": {
"psr-4": {
"Utopia\\Lock\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Appwrite Team",
"email": "team@appwrite.io"
}
],
"description": "Mutex, semaphore, file and distributed locks for PHP — one interface, four backends.",
"support": {
"issues": "https://github.com/utopia-php/lock/issues",
"source": "https://github.com/utopia-php/lock/tree/0.2.0"
},
"time": "2026-04-24T10:47:56+00:00"
},
{
"name": "utopia-php/logger",
"version": "0.8.0",
@@ -4555,16 +4606,16 @@
},
{
"name": "utopia-php/messaging",
"version": "0.22.0",
"version": "0.22.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/messaging.git",
"reference": "a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030"
"reference": "f99feceab575243f3a86ee2e90cd1a6407805def"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030",
"reference": "a6ac04fd204fb6a16bf8c75a84d0b9fc10aa5030",
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/f99feceab575243f3a86ee2e90cd1a6407805def",
"reference": "f99feceab575243f3a86ee2e90cd1a6407805def",
"shasum": ""
},
"require": {
@@ -4600,9 +4651,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/messaging/issues",
"source": "https://github.com/utopia-php/messaging/tree/0.22.0"
"source": "https://github.com/utopia-php/messaging/tree/0.22.2"
},
"time": "2026-04-02T04:09:19+00:00"
"time": "2026-05-14T08:51:26+00:00"
},
{
"name": "utopia-php/migration",
@@ -4722,26 +4773,26 @@
},
{
"name": "utopia-php/platform",
"version": "1.0.0-rc1",
"version": "1.0.0-rc2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/platform.git",
"reference": "36c0a8b2f3d96ca056d724701a302a127111e933"
"reference": "a67e5037007ee7fdca5359ab4577b82917e55452"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/platform/zipball/36c0a8b2f3d96ca056d724701a302a127111e933",
"reference": "36c0a8b2f3d96ca056d724701a302a127111e933",
"url": "https://api.github.com/repos/utopia-php/platform/zipball/a67e5037007ee7fdca5359ab4577b82917e55452",
"reference": "a67e5037007ee7fdca5359ab4577b82917e55452",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-redis": "*",
"php": ">=8.3",
"utopia-php/cli": "0.23.3",
"utopia-php/cli": "0.23.*",
"utopia-php/http": "^2.0@RC",
"utopia-php/queue": "0.18.2",
"utopia-php/servers": "0.4.0"
"utopia-php/queue": "0.18.*",
"utopia-php/servers": "0.4.*"
},
"require-dev": {
"laravel/pint": "1.2.*",
@@ -4767,9 +4818,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/platform/issues",
"source": "https://github.com/utopia-php/platform/tree/1.0.0-rc1"
"source": "https://github.com/utopia-php/platform/tree/1.0.0-rc2"
},
"time": "2026-05-05T15:09:27+00:00"
"time": "2026-05-15T06:19:20+00:00"
},
{
"name": "utopia-php/pools",
@@ -4925,16 +4976,16 @@
},
{
"name": "utopia-php/queue",
"version": "0.18.2",
"version": "0.18.3",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/queue.git",
"reference": "f85ca003c99ff475708c05466643d067403c0c22"
"reference": "141aad162b90728353f3aa834684b1f2affed045"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/queue/zipball/f85ca003c99ff475708c05466643d067403c0c22",
"reference": "f85ca003c99ff475708c05466643d067403c0c22",
"url": "https://api.github.com/repos/utopia-php/queue/zipball/141aad162b90728353f3aa834684b1f2affed045",
"reference": "141aad162b90728353f3aa834684b1f2affed045",
"shasum": ""
},
"require": {
@@ -4985,9 +5036,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/queue/issues",
"source": "https://github.com/utopia-php/queue/tree/0.18.2"
"source": "https://github.com/utopia-php/queue/tree/0.18.3"
},
"time": "2026-05-05T04:38:59+00:00"
"time": "2026-05-14T08:53:35+00:00"
},
{
"name": "utopia-php/registry",
@@ -5141,16 +5192,16 @@
},
{
"name": "utopia-php/storage",
"version": "2.0.2",
"version": "2.0.3",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/storage.git",
"reference": "64e132a3768e22243eda36fe4262da22fd204f3c"
"reference": "37129cf0bfcc03210172000e4388d4d3495ae013"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/storage/zipball/64e132a3768e22243eda36fe4262da22fd204f3c",
"reference": "64e132a3768e22243eda36fe4262da22fd204f3c",
"url": "https://api.github.com/repos/utopia-php/storage/zipball/37129cf0bfcc03210172000e4388d4d3495ae013",
"reference": "37129cf0bfcc03210172000e4388d4d3495ae013",
"shasum": ""
},
"require": {
@@ -5187,9 +5238,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/storage/issues",
"source": "https://github.com/utopia-php/storage/tree/2.0.2"
"source": "https://github.com/utopia-php/storage/tree/2.0.3"
},
"time": "2026-05-01T15:06:16+00:00"
"time": "2026-05-15T09:42:32+00:00"
},
{
"name": "utopia-php/system",
@@ -5304,16 +5355,16 @@
},
{
"name": "utopia-php/validators",
"version": "0.2.2",
"version": "0.2.3",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/validators.git",
"reference": "5d7d494e64457cd4eb67fdcfd9481f2c89796aa6"
"reference": "9770269c8ed8e6909934965fa8722103c7434c23"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/validators/zipball/5d7d494e64457cd4eb67fdcfd9481f2c89796aa6",
"reference": "5d7d494e64457cd4eb67fdcfd9481f2c89796aa6",
"url": "https://api.github.com/repos/utopia-php/validators/zipball/9770269c8ed8e6909934965fa8722103c7434c23",
"reference": "9770269c8ed8e6909934965fa8722103c7434c23",
"shasum": ""
},
"require": {
@@ -5343,22 +5394,22 @@
],
"support": {
"issues": "https://github.com/utopia-php/validators/issues",
"source": "https://github.com/utopia-php/validators/tree/0.2.2"
"source": "https://github.com/utopia-php/validators/tree/0.2.3"
},
"time": "2026-04-27T16:30:24+00:00"
"time": "2026-05-14T08:05:44+00:00"
},
{
"name": "utopia-php/vcs",
"version": "4.0.0",
"version": "4.1.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/vcs.git",
"reference": "c14ec4d1188e6cc2e8f5256a4b26e531e4f9ac4e"
"reference": "2850dbe975ee69b9466ee6df385fe1679394ce78"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/c14ec4d1188e6cc2e8f5256a4b26e531e4f9ac4e",
"reference": "c14ec4d1188e6cc2e8f5256a4b26e531e4f9ac4e",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/2850dbe975ee69b9466ee6df385fe1679394ce78",
"reference": "2850dbe975ee69b9466ee6df385fe1679394ce78",
"shasum": ""
},
"require": {
@@ -5392,9 +5443,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/vcs/issues",
"source": "https://github.com/utopia-php/vcs/tree/4.0.0"
"source": "https://github.com/utopia-php/vcs/tree/4.1.0"
},
"time": "2026-05-13T04:20:45+00:00"
"time": "2026-05-14T10:04:10+00:00"
},
{
"name": "utopia-php/websocket",
@@ -5587,16 +5638,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "1.29.2",
"version": "1.29.5",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "31248a984a4d478d20a780dda8f5897984ee4e8f"
"reference": "e670edcdfb9ffcec36125b1eb3e4473dce30b620"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/31248a984a4d478d20a780dda8f5897984ee4e8f",
"reference": "31248a984a4d478d20a780dda8f5897984ee4e8f",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/e670edcdfb9ffcec36125b1eb3e4473dce30b620",
"reference": "e670edcdfb9ffcec36125b1eb3e4473dce30b620",
"shasum": ""
},
"require": {
@@ -5632,9 +5683,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/1.29.2"
"source": "https://github.com/appwrite/sdk-generator/tree/1.29.5"
},
"time": "2026-05-13T04:47:38+00:00"
"time": "2026-05-15T06:49:05+00:00"
},
{
"name": "brianium/paratest",
@@ -6741,16 +6792,16 @@
},
{
"name": "phpunit/phpunit",
"version": "12.5.24",
"version": "12.5.25",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "d75dd30597caa80e72fad2ef7904601a30ef1046"
"reference": "792c2980442dfce319226b88fa845b8b6de3b333"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d75dd30597caa80e72fad2ef7904601a30ef1046",
"reference": "d75dd30597caa80e72fad2ef7904601a30ef1046",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/792c2980442dfce319226b88fa845b8b6de3b333",
"reference": "792c2980442dfce319226b88fa845b8b6de3b333",
"shasum": ""
},
"require": {
@@ -6819,7 +6870,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.24"
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.25"
},
"funding": [
{
@@ -6827,7 +6878,7 @@
"type": "other"
}
],
"time": "2026-05-01T04:21:04+00:00"
"time": "2026-05-13T03:56:57+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -7812,16 +7863,16 @@
},
{
"name": "symfony/console",
"version": "v8.0.9",
"version": "v8.0.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "7113778e2e91f4709cb3194a75dfa9c0d028d94d"
"reference": "3156577f46a38aa1b9323aad223de7a9cd426782"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/7113778e2e91f4709cb3194a75dfa9c0d028d94d",
"reference": "7113778e2e91f4709cb3194a75dfa9c0d028d94d",
"url": "https://api.github.com/repos/symfony/console/zipball/3156577f46a38aa1b9323aad223de7a9cd426782",
"reference": "3156577f46a38aa1b9323aad223de7a9cd426782",
"shasum": ""
},
"require": {
@@ -7878,7 +7929,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v8.0.9"
"source": "https://github.com/symfony/console/tree/v8.0.11"
},
"funding": [
{
@@ -7898,7 +7949,7 @@
"type": "tidelift"
}
],
"time": "2026-04-29T15:02:55+00:00"
"time": "2026-05-13T12:07:53+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -8232,16 +8283,16 @@
},
{
"name": "symfony/process",
"version": "v8.0.8",
"version": "v8.0.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc"
"reference": "26d89e459f037d2873300605d0a07e7a8ef84db0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc",
"reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc",
"url": "https://api.github.com/repos/symfony/process/zipball/26d89e459f037d2873300605d0a07e7a8ef84db0",
"reference": "26d89e459f037d2873300605d0a07e7a8ef84db0",
"shasum": ""
},
"require": {
@@ -8273,7 +8324,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v8.0.8"
"source": "https://github.com/symfony/process/tree/v8.0.11"
},
"funding": [
{
@@ -8293,20 +8344,20 @@
"type": "tidelift"
}
],
"time": "2026-03-30T15:14:47+00:00"
"time": "2026-05-11T16:56:32+00:00"
},
{
"name": "symfony/string",
"version": "v8.0.8",
"version": "v8.0.11",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "ae9488f874d7603f9d2dfbf120203882b645d963"
"reference": "39be2ad058a3c0bd558edca23e65f009865d75ff"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/ae9488f874d7603f9d2dfbf120203882b645d963",
"reference": "ae9488f874d7603f9d2dfbf120203882b645d963",
"url": "https://api.github.com/repos/symfony/string/zipball/39be2ad058a3c0bd558edca23e65f009865d75ff",
"reference": "39be2ad058a3c0bd558edca23e65f009865d75ff",
"shasum": ""
},
"require": {
@@ -8363,7 +8414,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v8.0.8"
"source": "https://github.com/symfony/string/tree/v8.0.11"
},
"funding": [
{
@@ -8383,7 +8434,7 @@
"type": "tidelift"
}
],
"time": "2026-03-30T15:14:47+00:00"
"time": "2026-05-13T12:07:53+00:00"
},
{
"name": "textalk/websocket",
@@ -8567,8 +8618,7 @@
"aliases": [],
"minimum-stability": "dev",
"stability-flags": {
"utopia-php/http": 5,
"utopia-php/platform": 5
"utopia-php/http": 5
},
"prefer-stable": true,
"prefer-lowest": false,
+51
View File
@@ -0,0 +1,51 @@
<?php
namespace Appwrite\Event\Message;
use Utopia\Database\Document;
final class Database extends Base
{
public function __construct(
public readonly ?Document $project = null,
public readonly ?Document $user = null,
public readonly string $type = '',
public readonly ?Document $table = null,
public readonly ?Document $row = null,
public readonly ?Document $collection = null,
public readonly ?Document $document = null,
public readonly ?Document $database = null,
public readonly array $events = [],
) {
}
public function toArray(): array
{
return [
'project' => $this->project?->getArrayCopy(),
'user' => $this->user?->getArrayCopy(),
'type' => $this->type,
'table' => $this->table?->getArrayCopy(),
'row' => $this->row?->getArrayCopy(),
'collection' => $this->collection?->getArrayCopy(),
'document' => $this->document?->getArrayCopy(),
'database' => $this->database?->getArrayCopy(),
'events' => $this->events,
];
}
public static function fromArray(array $data): static
{
return new self(
project: !empty($data['project']) ? new Document($data['project']) : null,
user: !empty($data['user']) ? new Document($data['user']) : null,
type: $data['type'] ?? '',
table: !empty($data['table']) ? new Document($data['table']) : null,
row: !empty($data['row']) ? new Document($data['row']) : null,
collection: !empty($data['collection']) ? new Document($data['collection']) : null,
document: !empty($data['document']) ? new Document($data['document']) : null,
database: !empty($data['database']) ? new Document($data['database']) : null,
events: $data['events'] ?? [],
);
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace Appwrite\Event\Publisher;
use Appwrite\Event\Message\Database as DatabaseMessage;
use Utopia\Database\Document;
use Utopia\DSN\DSN;
use Utopia\Queue\Publisher;
use Utopia\Queue\Queue;
readonly class Database extends Base
{
public function __construct(
Publisher $publisher,
protected Queue $queue,
) {
parent::__construct($publisher);
}
public function enqueue(DatabaseMessage $message, ?Queue $queue = null): string|bool
{
return $this->publish($queue ?? $this->getQueueFromProject($message->project), $message);
}
public function getSize(bool $failed = false, ?Queue $queue = null): int
{
return $this->getQueueSize($queue ?? $this->queue, $failed);
}
private function getQueueFromProject(?Document $project): Queue
{
$database = $project?->getAttribute('database', '');
if (empty($database)) {
return $this->queue;
}
try {
$dsn = new DSN($database);
} catch (\InvalidArgumentException) {
$dsn = new DSN('mysql://' . $database);
}
return new Queue($dsn->getHost());
}
}
@@ -0,0 +1,69 @@
<?php
namespace Appwrite\Platform\Modules\Console\Http\Scopes\Organization;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class XList extends Action
{
use HTTP;
public static function getName(): string
{
return 'listConsoleOrganizationScopes';
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/console/scopes/organization')
->desc('List organization scopes')
->groups(['api'])
->label('scope', 'public')
->label('sdk', new Method(
namespace: 'console',
group: 'console',
name: 'listOrganizationScopes',
description: 'List all scopes available for organization API keys, along with a description for each scope.',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_CONSOLE_KEY_SCOPE_LIST,
)
],
contentType: ContentType::JSON
))
->inject('response')
->callback($this->action(...));
}
public function action(Response $response): void
{
$scopesConfig = Config::getParam('organizationScopes', []);
$scopes = [];
foreach ($scopesConfig as $scopeId => $scope) {
$scopes[] = new Document([
'$id' => $scopeId,
'description' => $scope['description'] ?? '',
'category' => $scope['category'] ?? '',
'deprecated' => $scope['deprecated'] ?? false,
]);
}
$response->dynamic(new Document([
'total' => \count($scopes),
'scopes' => $scopes,
]), Response::MODEL_CONSOLE_KEY_SCOPE_LIST);
}
}
@@ -1,6 +1,6 @@
<?php
namespace Appwrite\Platform\Modules\Console\Http\Scopes\Key;
namespace Appwrite\Platform\Modules\Console\Http\Scopes\Project;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -0,0 +1,123 @@
<?php
namespace Appwrite\Platform\Modules\Console\Http\Templates\Email;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Template\Template;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Locale\Locale;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\System\System;
use Utopia\Validator\WhiteList;
class Get extends Action
{
use HTTP;
public static function getName(): string
{
return 'getConsoleEmailTemplate';
}
public function __construct()
{
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
->setHttpPath('/v1/console/templates/email/:templateId')
->desc('Get email template')
->groups(['api'])
->label('scope', 'public')
->label('sdk', new Method(
namespace: 'console',
group: null,
name: 'getEmailTemplate',
description: <<<EOT
Get the Appwrite built-in default email template for the specified type and locale. Always returns the unmodified default, ignoring any custom project overrides.
EOT,
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_EMAIL_TEMPLATE,
)
]
))
->param('templateId', '', new WhiteList(Config::getParam('locale-templates')['email'] ?? [], true), 'Email template type. Can be one of: ' . \implode(', ', Config::getParam('locale-templates')['email'] ?? []))
->param('locale', '', fn ($localeCodes) => new WhiteList($localeCodes), 'Template locale. If left empty, the fallback locale (en) will be used.', optional: true, injections: ['localeCodes'])
->inject('response')
->callback($this->action(...));
}
public function action(
string $templateId,
string $locale,
Response $response,
): void {
$locale = $locale ?: System::getEnv('_APP_LOCALE', 'en');
$localeObj = new Locale($locale);
$localeObj->setFallback(System::getEnv('_APP_LOCALE', 'en'));
$response->dynamic(new Document([
'templateId' => $templateId,
'locale' => $locale,
'subject' => $localeObj->getText('emails.' . $templateId . '.subject'),
'message' => $this->getDefaultMessage($templateId, $localeObj),
'senderName' => '',
'senderEmail' => '',
'replyToEmail' => '',
'replyToName' => '',
]), Response::MODEL_EMAIL_TEMPLATE);
}
private function getDefaultMessage(string $templateId, Locale $localeObj): string
{
$templateConfigs = [
'magicSession' => [
'file' => 'email-magic-url.tpl',
'placeholders' => ['optionButton', 'buttonText', 'optionUrl', 'clientInfo', 'securityPhrase']
],
'mfaChallenge' => [
'file' => 'email-mfa-challenge.tpl',
'placeholders' => ['description', 'clientInfo']
],
'otpSession' => [
'file' => 'email-otp.tpl',
'placeholders' => ['description', 'clientInfo', 'securityPhrase']
],
'sessionAlert' => [
'file' => 'email-session-alert.tpl',
'placeholders' => ['body', 'listDevice', 'listIpAddress', 'listCountry', 'footer']
],
];
$config = $templateConfigs[$templateId] ?? [
'file' => 'email-inner-base.tpl',
'placeholders' => ['buttonText', 'body', 'footer']
];
$templateString = file_get_contents(APP_CE_CONFIG_DIR . '/locale/templates/' . $config['file']);
$message = Template::fromString($templateString);
foreach ($config['placeholders'] as $param) {
$escapeHtml = !in_array($param, ['clientInfo', 'body', 'footer', 'description']);
if ($templateId === 'magicSession' && $param === 'securityPhrase') {
$message->setParam('{{securityPhrase}}', '');
continue;
}
$message->setParam("{{{$param}}}", $localeObj->getText("emails.{$templateId}.{$param}"), escapeHtml: $escapeHtml);
}
$message
->setParam('{{hello}}', $localeObj->getText("emails.{$templateId}.hello"))
->setParam('{{thanks}}', $localeObj->getText("emails.{$templateId}.thanks"))
->setParam('{{signature}}', $localeObj->getText("emails.{$templateId}.signature"));
return $message->render(useContent: true);
}
}
@@ -15,7 +15,9 @@ use Appwrite\Platform\Modules\Console\Http\Redirects\Recover\Get as RedirectReco
use Appwrite\Platform\Modules\Console\Http\Redirects\Register\Get as RedirectRegister;
use Appwrite\Platform\Modules\Console\Http\Redirects\Root\Get as RedirectRoot;
use Appwrite\Platform\Modules\Console\Http\Resources\Get as GetResourceAvailability;
use Appwrite\Platform\Modules\Console\Http\Scopes\Key\XList as ListKeyScopes;
use Appwrite\Platform\Modules\Console\Http\Scopes\Organization\XList as ListOrganizationScopes;
use Appwrite\Platform\Modules\Console\Http\Scopes\Project\XList as ListKeyScopes;
use Appwrite\Platform\Modules\Console\Http\Templates\Email\Get as GetEmailTemplate;
use Appwrite\Platform\Modules\Console\Http\Variables\Get as GetVariables;
use Utopia\Platform\Service;
@@ -30,8 +32,10 @@ class Http extends Service
$this->addAction(Web::getName(), new Web());
$this->addAction(GetVariables::getName(), new GetVariables());
$this->addAction(GetEmailTemplate::getName(), new GetEmailTemplate());
$this->addAction(ListOAuth2Providers::getName(), new ListOAuth2Providers());
$this->addAction(ListKeyScopes::getName(), new ListKeyScopes());
$this->addAction(ListOrganizationScopes::getName(), new ListOrganizationScopes());
$this->addAction(CreateAssistantQuery::getName(), new CreateAssistantQuery());
$this->addAction(GetResourceAvailability::getName(), new GetResourceAvailability());
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Database as DatabaseMessage;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response as UtopiaResponse;
@@ -312,7 +313,7 @@ abstract class Action extends UtopiaAction
};
}
protected function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): Document
protected function createAttribute(string $databaseId, string $collectionId, Document $attribute, Response $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): Document
{
$key = $attribute->getAttribute('key');
$type = $attribute->getAttribute('type', '');
@@ -464,20 +465,6 @@ abstract class Action extends UtopiaAction
$dbForProject->purgeCachedCollection('database_' . $db->getSequence() . '_collection_' . $relatedCollection->getSequence());
}
$queueForDatabase
->setType(DATABASE_TYPE_CREATE_ATTRIBUTE)
->setDatabase($db);
if ($this->isCollectionsAPI()) {
$queueForDatabase
->setDocument($attribute)
->setCollection($collection);
} else {
$queueForDatabase
->setRow($attribute)
->setTable($collection);
}
$queueForEvents
->setContext('database', $db)
->setParam('databaseId', $databaseId)
@@ -487,6 +474,18 @@ abstract class Action extends UtopiaAction
->setParam('columnId', $attribute->getId())
->setContext($this->getCollectionsEventsContext(), $collection);
$publisherForDatabase->enqueue(new DatabaseMessage(
project: $queueForEvents->getProject(),
user: $queueForEvents->getUser(),
type: DATABASE_TYPE_CREATE_ATTRIBUTE,
database: $db,
collection: $this->isCollectionsAPI() ? $collection : null,
document: $this->isCollectionsAPI() ? $attribute : null,
table: $this->isCollectionsAPI() ? null : $collection,
row: $this->isCollectionsAPI() ? null : $attribute,
events: Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams()),
));
$response->setStatusCode(SwooleResponse::STATUS_CODE_CREATED);
return $attribute;
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\BigInt;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -73,13 +73,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$min ??= \PHP_INT_MIN;
$max ??= \PHP_INT_MAX;
@@ -102,7 +102,7 @@ class Create extends Action
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_BIGINT_RANGE,
'formatOptions' => ['min' => $min, 'max' => $max],
]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$formatOptions = $attribute->getAttribute('formatOptions', []);
if (!empty($formatOptions)) {
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Boolean;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Deprecated;
@@ -68,13 +68,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?bool $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$attribute = $this->createAttribute($databaseId, $collectionId, new Document([
'key' => $key,
@@ -83,7 +83,7 @@ class Create extends Action
'required' => $required,
'default' => $default,
'array' => $array,
]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Datetime;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Deprecated;
@@ -69,13 +69,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$attribute = $this->createAttribute(
$databaseId,
@@ -91,7 +91,7 @@ class Create extends Action
]),
$response,
$dbForProject,
$queueForDatabase,
$publisherForDatabase,
$queueForEvents,
$authorization
);
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Database as DatabaseMessage;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -66,13 +67,13 @@ class Delete extends Action
->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Attribute Key.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($db->isEmpty()) {
@@ -129,20 +130,6 @@ class Delete extends Action
}
}
$queueForDatabase
->setDatabase($db)
->setType(DATABASE_TYPE_DELETE_ATTRIBUTE);
if ($this->isCollectionsAPI()) {
$queueForDatabase
->setRow($attribute)
->setTable($collection);
} else {
$queueForDatabase
->setDocument($attribute)
->setCollection($collection);
}
$type = $attribute->getAttribute('type');
$format = $attribute->getAttribute('format');
@@ -158,6 +145,18 @@ class Delete extends Action
->setPayload($response->output($attribute, $model))
->setContext($this->getCollectionsEventsContext(), $collection);
$publisherForDatabase->enqueue(new DatabaseMessage(
project: $queueForEvents->getProject(),
user: $queueForEvents->getUser(),
type: DATABASE_TYPE_DELETE_ATTRIBUTE,
database: $db,
collection: $this->isCollectionsAPI() ? null : $collection,
document: $this->isCollectionsAPI() ? null : $attribute,
table: $this->isCollectionsAPI() ? $collection : null,
row: $this->isCollectionsAPI() ? $attribute : null,
events: Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams()),
));
$response->noContent();
}
}
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Email;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Deprecated;
@@ -69,13 +69,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$attribute = $this->createAttribute(
$databaseId,
@@ -91,7 +91,7 @@ class Create extends Action
]),
$response,
$dbForProject,
$queueForDatabase,
$publisherForDatabase,
$queueForEvents,
$authorization
);
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Enum;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -72,13 +72,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, array $elements, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
if (!is_null($default) && !\in_array($default, $elements, true)) {
throw new Exception($this->getInvalidValueException(), 'Default value not found in elements');
@@ -99,7 +99,7 @@ class Create extends Action
]),
$response,
$dbForProject,
$queueForDatabase,
$publisherForDatabase,
$queueForEvents,
$authorization
);
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Float;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -73,13 +73,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?float $min, ?float $max, ?float $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$min ??= -PHP_FLOAT_MAX;
$max ??= PHP_FLOAT_MAX;
@@ -102,7 +102,7 @@ class Create extends Action
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_FLOAT_RANGE,
'formatOptions' => ['min' => $min, 'max' => $max],
]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$formatOptions = $attribute->getAttribute('formatOptions', []);
if (!empty($formatOptions)) {
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\IP;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Deprecated;
@@ -69,13 +69,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?string $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$attribute = $this->createAttribute(
$databaseId,
@@ -91,7 +91,7 @@ class Create extends Action
]),
$response,
$dbForProject,
$queueForDatabase,
$publisherForDatabase,
$queueForEvents,
$authorization
);
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Integer;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -73,13 +73,13 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?int $min, ?int $max, ?int $default, bool $array, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$min ??= \PHP_INT_MIN;
$max ??= \PHP_INT_MAX;
@@ -104,7 +104,7 @@ class Create extends Action
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_INT_RANGE,
'formatOptions' => ['min' => $min, 'max' => $max],
]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$formatOptions = $attribute->getAttribute('formatOptions', []);
if (!empty($formatOptions)) {
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Line;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -69,13 +69,13 @@ class Create extends Action
->param('default', null, new Nullable(new Spatial(Database::VAR_LINESTRING)), 'Default value for attribute when not provided, two-dimensional array of coordinate pairs, [[longitude, latitude], [longitude, latitude], …], listing the vertices of the line in order. Cannot be set when attribute is required.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
if (!$dbForProject->getAdapter()->getSupportForSpatialAttributes()) {
throw new Exception(Exception::GENERAL_FEATURE_UNSUPPORTED, 'Spatial columns are not supported by this database.');
@@ -86,7 +86,7 @@ class Create extends Action
'type' => Database::VAR_LINESTRING,
'required' => $required,
'default' => $default
]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Longtext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -67,7 +67,7 @@ class Create extends Action
->param('encrypt', false, new Boolean(), 'Toggle encryption for the attribute. Encryption enhances security by not storing any plain text values in the database. However, encrypted attributes cannot be queried.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
@@ -84,7 +84,7 @@ class Create extends Action
bool $encrypt,
UtopiaResponse $response,
Database $dbForProject,
EventDatabase $queueForDatabase,
DatabasePublisher $publisherForDatabase,
Event $queueForEvents,
array $plan,
Authorization $authorization
@@ -112,7 +112,7 @@ class Create extends Action
]),
$response,
$dbForProject,
$queueForDatabase,
$publisherForDatabase,
$queueForEvents,
$authorization
);
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Mediumtext;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -67,7 +67,7 @@ class Create extends Action
->param('encrypt', false, new Boolean(), 'Toggle encryption for the attribute. Encryption enhances security by not storing any plain text values in the database. However, encrypted attributes cannot be queried.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
@@ -84,7 +84,7 @@ class Create extends Action
bool $encrypt,
UtopiaResponse $response,
Database $dbForProject,
EventDatabase $queueForDatabase,
DatabasePublisher $publisherForDatabase,
Event $queueForEvents,
array $plan,
Authorization $authorization
@@ -112,7 +112,7 @@ class Create extends Action
]),
$response,
$dbForProject,
$queueForDatabase,
$publisherForDatabase,
$queueForEvents,
$authorization
);
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Point;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -69,13 +69,13 @@ class Create extends Action
->param('default', null, new Nullable(new Spatial(Database::VAR_POINT)), 'Default value for attribute when not provided, array of two numbers [longitude, latitude], representing a single coordinate. Cannot be set when attribute is required.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
if (!$dbForProject->getAdapter()->getSupportForSpatialAttributes()) {
throw new Exception(Exception::GENERAL_FEATURE_UNSUPPORTED, 'Spatial columns are not supported by this database.');
@@ -86,7 +86,7 @@ class Create extends Action
'type' => Database::VAR_POINT,
'required' => $required,
'default' => $default,
]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Polygon;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -69,13 +69,13 @@ class Create extends Action
->param('default', null, new Nullable(new Spatial(Database::VAR_POLYGON)), 'Default value for attribute when not provided, three-dimensional array where the outer array holds one or more linear rings, [[[longitude, latitude], …], …], the first ring is the exterior boundary, any additional rings are interior holes, and each ring must start and end with the same coordinate pair. Cannot be set when attribute is required.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, ?bool $required, ?array $default, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
if (!$dbForProject->getAdapter()->getSupportForSpatialAttributes()) {
throw new Exception(Exception::GENERAL_FEATURE_UNSUPPORTED, 'Spatial columns are not supported by this database.');
@@ -86,7 +86,7 @@ class Create extends Action
'type' => Database::VAR_POLYGON,
'required' => $required,
'default' => $default,
]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Relationship;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -81,13 +81,13 @@ class Create extends Action
], true), 'Constraints option', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $relatedCollectionId, string $type, bool $twoWay, ?string $key, ?string $twoWayKey, string $onDelete, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $relatedCollectionId, string $type, bool $twoWay, ?string $key, ?string $twoWayKey, string $onDelete, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
if (!$dbForProject->getAdapter()->getSupportForRelationships()) {
throw new Exception(Exception::GENERAL_FEATURE_UNSUPPORTED, 'Relationships are not supported by this database.');
@@ -159,7 +159,7 @@ class Create extends Action
'twoWayKey' => $twoWayKey,
'onDelete' => $onDelete,
]
]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
foreach ($attribute->getAttribute('options', []) as $k => $option) {
$attribute->setAttribute($k, $option);
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\String;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -75,7 +75,7 @@ class Create extends Action
->param('encrypt', false, new Boolean(), 'Toggle encryption for the attribute. Encryption enhances security by not storing any plain text values in the database. However, encrypted attributes cannot be queried.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
@@ -93,7 +93,7 @@ class Create extends Action
bool $encrypt,
UtopiaResponse $response,
Database $dbForProject,
EventDatabase $queueForDatabase,
DatabasePublisher $publisherForDatabase,
Event $queueForEvents,
array $plan,
Authorization $authorization
@@ -134,7 +134,7 @@ class Create extends Action
]),
$response,
$dbForProject,
$queueForDatabase,
$publisherForDatabase,
$queueForEvents,
$authorization
);
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Text;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -67,7 +67,7 @@ class Create extends Action
->param('encrypt', false, new Boolean(), 'Toggle encryption for the attribute. Encryption enhances security by not storing any plain text values in the database. However, encrypted attributes cannot be queried.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
@@ -84,7 +84,7 @@ class Create extends Action
bool $encrypt,
UtopiaResponse $response,
Database $dbForProject,
EventDatabase $queueForDatabase,
DatabasePublisher $publisherForDatabase,
Event $queueForEvents,
array $plan,
Authorization $authorization
@@ -112,7 +112,7 @@ class Create extends Action
]),
$response,
$dbForProject,
$queueForDatabase,
$publisherForDatabase,
$queueForEvents,
$authorization
);
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\URL;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Deprecated;
@@ -69,7 +69,7 @@ class Create extends Action
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -84,7 +84,7 @@ class Create extends Action
bool $array,
UtopiaResponse $response,
Database $dbForProject,
EventDatabase $queueForDatabase,
DatabasePublisher $publisherForDatabase,
Event $queueForEvents,
Authorization $authorization
): void {
@@ -96,7 +96,7 @@ class Create extends Action
'default' => $default,
'array' => $array,
'format' => APP_DATABASE_ATTRIBUTE_URL,
]), $response, $dbForProject, $queueForDatabase, $queueForEvents, $authorization);
]), $response, $dbForProject, $publisherForDatabase, $queueForEvents, $authorization);
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
@@ -2,8 +2,8 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Varchar;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Attributes\Action;
use Appwrite\SDK\AuthType;
@@ -70,7 +70,7 @@ class Create extends Action
->param('encrypt', false, new Boolean(), 'Toggle encryption for the attribute. Encryption enhances security by not storing any plain text values in the database. However, encrypted attributes cannot be queried.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
@@ -88,7 +88,7 @@ class Create extends Action
bool $encrypt,
UtopiaResponse $response,
Database $dbForProject,
EventDatabase $queueForDatabase,
DatabasePublisher $publisherForDatabase,
Event $queueForEvents,
array $plan,
Authorization $authorization
@@ -129,7 +129,7 @@ class Create extends Action
]),
$response,
$dbForProject,
$queueForDatabase,
$publisherForDatabase,
$queueForEvents,
$authorization
);
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Database as DatabaseMessage;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -63,13 +64,13 @@ class Delete extends Action
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$database = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
if ($database->isEmpty()) {
@@ -89,22 +90,22 @@ class Delete extends Action
$dbForDatabases = $getDatabasesDB($database);
$dbForDatabases->purgeCachedCollection('database_' . $database->getSequence() . '_collection_' . $collection->getSequence());
$queueForDatabase
->setType(DATABASE_TYPE_DELETE_COLLECTION)
->setDatabase($database);
if ($this->isCollectionsAPI()) {
$queueForDatabase->setCollection($collection);
} else {
$queueForDatabase->setTable($collection);
}
$queueForEvents
->setParam('databaseId', $databaseId)
->setContext('database', $database)
->setParam($this->getEventsParamKey(), $collection->getId())
->setPayload($response->output($collection, $this->getResponseModel()));
$publisherForDatabase->enqueue(new DatabaseMessage(
project: $queueForEvents->getProject(),
user: $queueForEvents->getUser(),
type: DATABASE_TYPE_DELETE_COLLECTION,
database: $database,
collection: $this->isCollectionsAPI() ? $collection : null,
table: $this->isCollectionsAPI() ? null : $collection,
events: Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams()),
));
$response->noContent();
}
}
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Database as DatabaseMessage;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -78,13 +79,13 @@ class Create extends Action
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, array $lengths, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, string $type, array $attributes, array $orders, array $lengths, UtopiaResponse $response, Database $dbForProject, callable $getDatabasesDB, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@@ -228,20 +229,6 @@ class Create extends Action
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
$queueForDatabase
->setType(DATABASE_TYPE_CREATE_INDEX)
->setDatabase($db);
if ($this->isCollectionsAPI()) {
$queueForDatabase
->setCollection($collection)
->setDocument($index);
} else {
$queueForDatabase
->setTable($collection)
->setRow($index);
}
$queueForEvents
->setContext('database', $db)
->setParam('databaseId', $databaseId)
@@ -250,6 +237,18 @@ class Create extends Action
->setParam('tableId', $collection->getId())
->setContext($this->getCollectionsEventsContext(), $collection);
$publisherForDatabase->enqueue(new DatabaseMessage(
project: $queueForEvents->getProject(),
user: $queueForEvents->getUser(),
type: DATABASE_TYPE_CREATE_INDEX,
database: $db,
collection: $this->isCollectionsAPI() ? $collection : null,
document: $this->isCollectionsAPI() ? $index : null,
table: $this->isCollectionsAPI() ? null : $collection,
row: $this->isCollectionsAPI() ? null : $index,
events: Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams()),
));
$response
->setStatusCode(SwooleResponse::STATUS_CODE_ACCEPTED)
->dynamic($index, $this->getResponseModel());
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases\Collections\Indexes;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Database as DatabaseMessage;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -69,13 +70,13 @@ class Delete extends Action
->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Index Key.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
}
public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents, Authorization $authorization): void
public function action(string $databaseId, string $collectionId, string $key, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents, Authorization $authorization): void
{
$db = $authorization->skip(fn () => $dbForProject->getDocument('databases', $databaseId));
@@ -103,20 +104,6 @@ class Delete extends Action
$dbForProject->purgeCachedDocument('database_' . $db->getSequence(), $collectionId);
$queueForDatabase
->setType(DATABASE_TYPE_DELETE_INDEX)
->setDatabase($db);
if ($this->isCollectionsAPI()) {
$queueForDatabase
->setCollection($collection)
->setDocument($index);
} else {
$queueForDatabase
->setTable($collection)
->setRow($index);
}
$queueForEvents
->setContext('database', $db)
->setParam('databaseId', $databaseId)
@@ -126,6 +113,18 @@ class Delete extends Action
->setContext($this->getCollectionsEventsContext(), $collection)
->setPayload($response->output($index, $this->getResponseModel()));
$publisherForDatabase->enqueue(new DatabaseMessage(
project: $queueForEvents->getProject(),
user: $queueForEvents->getUser(),
type: DATABASE_TYPE_DELETE_INDEX,
database: $db,
collection: $this->isCollectionsAPI() ? $collection : null,
document: $this->isCollectionsAPI() ? $index : null,
table: $this->isCollectionsAPI() ? null : $collection,
row: $this->isCollectionsAPI() ? null : $index,
events: Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams()),
));
$response->noContent();
}
}
@@ -2,8 +2,9 @@
namespace Appwrite\Platform\Modules\Databases\Http\Databases;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Message\Database as DatabaseMessage;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -58,12 +59,12 @@ class Delete extends Action
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
public function action(string $databaseId, UtopiaResponse $response, Database $dbForProject, EventDatabase $queueForDatabase, Event $queueForEvents): void
public function action(string $databaseId, UtopiaResponse $response, Database $dbForProject, DatabasePublisher $publisherForDatabase, Event $queueForEvents): void
{
$database = $dbForProject->getDocument('databases', $databaseId);
@@ -78,14 +79,18 @@ class Delete extends Action
$dbForProject->purgeCachedDocument('databases', $database->getId());
$dbForProject->purgeCachedCollection('databases_' . $database->getSequence());
$queueForDatabase
->setType(DATABASE_TYPE_DELETE_DATABASE)
->setDatabase($database);
$queueForEvents
->setParam('databaseId', $database->getId())
->setPayload($response->output($database, UtopiaResponse::MODEL_DATABASE));
$publisherForDatabase->enqueue(new DatabaseMessage(
project: $queueForEvents->getProject(),
user: $queueForEvents->getUser(),
type: DATABASE_TYPE_DELETE_DATABASE,
database: $database,
events: Event::generateEvents($queueForEvents->getEvent(), $queueForEvents->getParams()),
));
$response->noContent();
}
}
@@ -54,7 +54,7 @@ class Delete extends CollectionDelete
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -65,7 +65,7 @@ class Create extends IndexCreate
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -59,7 +59,7 @@ class Delete extends IndexDelete
->param('key', '', new Key(), 'Index Key.')
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -48,7 +48,7 @@ class Delete extends DatabaseDelete
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('usage')
->callback($this->action(...));
@@ -48,7 +48,7 @@ class Delete extends DatabaseDelete
->param('databaseId', '', fn (Database $dbForProject) => new UID($dbForProject->getAdapter()->getMaxUIDLength()), 'Database ID.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->callback($this->action(...));
}
@@ -62,7 +62,7 @@ class Create extends BigIntCreate
->param('array', false, new Boolean(), 'Is column an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -59,7 +59,7 @@ class Create extends BooleanCreate
->param('array', false, new Boolean(), 'Is column an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -60,7 +60,7 @@ class Create extends DatetimeCreate
->param('array', false, new Boolean(), 'Is column an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -57,7 +57,7 @@ class Delete extends AttributesDelete
->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Column Key.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -60,7 +60,7 @@ class Create extends EmailCreate
->param('array', false, new Boolean(), 'Is column an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -62,7 +62,7 @@ class Create extends EnumCreate
->param('array', false, new Boolean(), 'Is column an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -62,7 +62,7 @@ class Create extends FloatCreate
->param('array', false, new Boolean(), 'Is column an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -60,7 +60,7 @@ class Create extends IPCreate
->param('array', false, new Boolean(), 'Is column an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -62,7 +62,7 @@ class Create extends IntegerCreate
->param('array', false, new Boolean(), 'Is column an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -59,7 +59,7 @@ class Create extends LineCreate
->param('default', null, new Nullable(new Spatial(Database::VAR_LINESTRING)), 'Default value for column when not provided, two-dimensional array of coordinate pairs, [[longitude, latitude], [longitude, latitude], …], listing the vertices of the line in order. Cannot be set when column is required.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -60,7 +60,7 @@ class Create extends LongtextCreate
->param('encrypt', false, new Boolean(), 'Toggle encryption for the column. Encryption enhances security by not storing any plain text values in the database. However, encrypted columns cannot be queried.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
@@ -60,7 +60,7 @@ class Create extends MediumtextCreate
->param('encrypt', false, new Boolean(), 'Toggle encryption for the column. Encryption enhances security by not storing any plain text values in the database. However, encrypted columns cannot be queried.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
@@ -59,7 +59,7 @@ class Create extends PointCreate
->param('default', null, new Nullable(new Spatial(Database::VAR_POINT)), 'Default value for column when not provided, array of two numbers [longitude, latitude], representing a single coordinate. Cannot be set when column is required.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -59,7 +59,7 @@ class Create extends PolygonCreate
->param('default', null, new Nullable(new Spatial(Database::VAR_POLYGON)), 'Default value for column when not provided, three-dimensional array where the outer array holds one or more linear rings, [[[longitude, latitude], …], …], the first ring is the exterior boundary, any additional rings are interior holes, and each ring must start and end with the same coordinate pair. Cannot be set when column is required.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -71,7 +71,7 @@ class Create extends RelationshipCreate
], true), 'Constraints option', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -69,7 +69,7 @@ class Create extends StringCreate
->param('encrypt', false, new Boolean(), 'Toggle encryption for the column. Encryption enhances security by not storing any plain text values in the database. However, encrypted columns cannot be queried.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
@@ -60,7 +60,7 @@ class Create extends TextCreate
->param('encrypt', false, new Boolean(), 'Toggle encryption for the column. Encryption enhances security by not storing any plain text values in the database. However, encrypted columns cannot be queried.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
@@ -60,7 +60,7 @@ class Create extends URLCreate
->param('array', false, new Boolean(), 'Is column an array?', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -63,7 +63,7 @@ class Create extends VarcharCreate
->param('encrypt', false, new Boolean(), 'Toggle encryption for the column. Encryption enhances security by not storing any plain text values in the database. However, encrypted columns cannot be queried.', true)
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('plan')
->inject('authorization')
@@ -55,7 +55,7 @@ class Delete extends CollectionDelete
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -65,7 +65,7 @@ class Create extends IndexCreate
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -60,7 +60,7 @@ class Delete extends IndexDelete
->param('key', '', fn (Database $dbForProject) => new Key(false, $dbForProject->getAdapter()->getMaxUIDLength()), 'Index Key.', false, ['dbForProject'])
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -54,7 +54,7 @@ class Delete extends CollectionDelete
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -65,7 +65,7 @@ class Create extends IndexCreate
->inject('response')
->inject('dbForProject')
->inject('getDatabasesDB')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -59,7 +59,7 @@ class Delete extends IndexDelete
->param('key', '', new Key(), 'Index Key.')
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('authorization')
->callback($this->action(...));
@@ -47,7 +47,7 @@ class Delete extends DatabaseDelete
->param('databaseId', '', new UID(), 'Database ID.')
->inject('response')
->inject('dbForProject')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('queueForEvents')
->inject('usage')
->callback($this->action(...));
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Modules\Databases\Workers;
use Appwrite\Event\Message\Database as DatabaseMessage;
use Appwrite\Event\Realtime;
use Exception;
use Utopia\Console;
@@ -60,10 +61,11 @@ class Databases extends Action
throw new Exception('Missing payload');
}
$type = $payload['type'];
$document = new Document($payload['row'] ?? $payload['document'] ?? []);
$collection = new Document($payload['table'] ?? $payload['collection'] ?? []);
$database = new Document($payload['database'] ?? []);
$databaseMessage = DatabaseMessage::fromArray($payload);
$type = $databaseMessage->type;
$document = $databaseMessage->row ?? $databaseMessage->document ?? new Document();
$collection = $databaseMessage->table ?? $databaseMessage->collection ?? new Document();
$database = $databaseMessage->database ?? new Document();
/**
* @var Database $dbForDatabases
*/
@@ -21,6 +21,7 @@ use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Request;
use Utopia\Lock\Exception\Contention as LockContention;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
@@ -92,6 +93,7 @@ class Create extends Action
->inject('plan')
->inject('authorization')
->inject('platform')
->inject('locks')
->callback($this->action(...));
}
@@ -111,7 +113,8 @@ class Create extends Action
BuildPublisher $publisherForBuilds,
array $plan,
Authorization $authorization,
array $platform
array $platform,
callable $locks
) {
$activate = \strval($activate) === 'true' || \strval($activate) === '1';
@@ -193,20 +196,38 @@ class Create extends Action
// Save to storage
$fileSize ??= $deviceForLocal->getFileSize($fileTmpName);
$path = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION));
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
$lockKey = 'functions:deployment:' . $project->getId() . ':' . $functionId . ':' . $deploymentId;
$metadata = ['content_type' => $deviceForLocal->getFileMimeType($fileTmpName)];
if (!$deployment->isEmpty()) {
$chunks = $deployment->getAttribute('sourceChunksTotal', 1);
$uploaded = $deployment->getAttribute('sourceChunksUploaded', 0);
$metadata = $deployment->getAttribute('sourceMetadata', []);
$completed = false;
if ($uploaded === $chunks) {
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
return;
}
try {
$locks($lockKey, 600, function () use (&$chunks, $dbForProject, $deploymentId, &$metadata, &$completed, $response): void {
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if (!$deployment->isEmpty()) {
$chunks = $deployment->getAttribute('sourceChunksTotal', 1);
$uploaded = $deployment->getAttribute('sourceChunksUploaded', 0);
$metadata = $deployment->getAttribute('sourceMetadata', []);
if ($uploaded === $chunks) {
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
$completed = true;
return;
}
}
}, timeout: 120.0);
} catch (LockContention) {
$response->addHeader('Retry-After', '5');
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Deployment upload is busy. Try again.');
}
if ($completed) {
return;
}
$chunksUploaded = $deviceForFunctions->upload($fileTmpName, $path, $chunk, $chunks, $metadata);
@@ -217,118 +238,144 @@ class Create extends Action
$type = $request->getHeader('x-sdk-language') === 'cli' ? 'cli' : 'manual';
if ($chunksUploaded === $chunks) {
if ($activate) {
// Remove deploy for all other deployments.
$activeDeployments = $dbForProject->find('deployments', [
Query::equal('activate', [true]),
Query::equal('resourceId', [$functionId]),
Query::equal('resourceType', ['functions'])
]);
try {
$locks($lockKey, 600, function () use ($activate, &$chunks, $chunksUploaded, $commands, $dbForProject, $deploymentId, $deviceForFunctions, $entrypoint, $fileSize, &$function, $functionId, $path, &$metadata, $platform, $project, $publisherForBuilds, $queueForEvents, $response, $type): void {
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
$uploaded = 0;
foreach ($activeDeployments as $activeDeployment) {
$activeDeployment->setAttribute('activate', false);
$dbForProject->updateDocument('deployments', $activeDeployment->getId(), new Document([
'activate' => false,
]));
if (!$deployment->isEmpty()) {
$chunks = $deployment->getAttribute('sourceChunksTotal', 1);
$uploaded = $deployment->getAttribute('sourceChunksUploaded', 0);
$metadata = \array_merge($deployment->getAttribute('sourceMetadata', []), $metadata);
if ($uploaded === $chunks) {
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
return;
}
}
}
$fileSize = $deviceForFunctions->getFileSize($path);
$chunksUploaded = max($uploaded, $chunksUploaded);
if ($deployment->isEmpty()) {
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceInternalId' => $function->getSequence(),
'resourceId' => $function->getId(),
'resourceType' => 'functions',
'entrypoint' => $entrypoint,
'buildCommands' => $commands,
'startCommand' => $function->getAttribute('startCommand', ''),
'sourcePath' => $path,
'sourceSize' => $fileSize,
'totalSize' => $fileSize,
'sourceChunksTotal' => $chunks,
'sourceChunksUploaded' => $chunksUploaded,
'activate' => $activate,
'sourceMetadata' => $metadata,
'type' => $type
]));
if ($chunksUploaded === $chunks && $uploaded < $chunks) {
if ($activate) {
// Remove deploy for all other deployments.
$activeDeployments = $dbForProject->find('deployments', [
Query::equal('activate', [true]),
Query::equal('resourceId', [$functionId]),
Query::equal('resourceType', ['functions'])
]);
$function = $dbForProject->updateDocument('functions', $function->getId(), new Document([
'latestDeploymentId' => $deployment->getId(),
'latestDeploymentInternalId' => $deployment->getSequence(),
'latestDeploymentCreatedAt' => $deployment->getCreatedAt(),
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
]));
} else {
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([
'sourceSize' => $fileSize,
'sourceChunksUploaded' => $chunksUploaded,
'sourceMetadata' => $metadata,
]));
}
foreach ($activeDeployments as $activeDeployment) {
$dbForProject->updateDocument('deployments', $activeDeployment->getId(), new Document([
'activate' => false,
]));
}
}
// Start the build
$publisherForBuilds->enqueue(new BuildMessage(
project: $project,
resource: $function,
deployment: $deployment,
type: BUILD_TYPE_DEPLOYMENT,
platform: $platform,
));
} else {
if ($deployment->isEmpty()) {
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceInternalId' => $function->getSequence(),
'resourceId' => $function->getId(),
'resourceType' => 'functions',
'entrypoint' => $entrypoint,
'buildCommands' => $commands,
'startCommand' => $function->getAttribute('startCommand', ''),
'sourcePath' => $path,
'sourceSize' => $fileSize,
'totalSize' => $fileSize,
'sourceChunksTotal' => $chunks,
'sourceChunksUploaded' => $chunksUploaded,
'activate' => $activate,
'sourceMetadata' => $metadata,
'type' => $type
]));
$fileSize = $deviceForFunctions->getFileSize($path);
$function = $dbForProject->updateDocument('functions', $function->getId(), new Document([
'latestDeploymentId' => $deployment->getId(),
'latestDeploymentInternalId' => $deployment->getSequence(),
'latestDeploymentCreatedAt' => $deployment->getCreatedAt(),
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
]));
} else {
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([
'sourceChunksUploaded' => $chunksUploaded,
'sourceMetadata' => $metadata,
]));
}
if ($deployment->isEmpty()) {
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceInternalId' => $function->getSequence(),
'resourceId' => $function->getId(),
'resourceType' => 'functions',
'entrypoint' => $entrypoint,
'buildCommands' => $commands,
'startCommand' => $function->getAttribute('startCommand', ''),
'sourcePath' => $path,
'sourceSize' => $fileSize,
'totalSize' => $fileSize,
'sourceChunksTotal' => $chunks,
'sourceChunksUploaded' => $chunksUploaded,
'activate' => $activate,
'sourceMetadata' => $metadata,
'type' => $type
]));
$function = $dbForProject->updateDocument('functions', $function->getId(), new Document([
'latestDeploymentId' => $deployment->getId(),
'latestDeploymentInternalId' => $deployment->getSequence(),
'latestDeploymentCreatedAt' => $deployment->getCreatedAt(),
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
]));
} else {
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([
'sourceSize' => $fileSize,
'sourceChunksUploaded' => $chunksUploaded,
'sourceMetadata' => $metadata,
]));
}
// Start the build
$publisherForBuilds->enqueue(new BuildMessage(
project: $project,
resource: $function,
deployment: $deployment,
type: BUILD_TYPE_DEPLOYMENT,
platform: $platform,
));
} else {
if ($deployment->isEmpty()) {
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceInternalId' => $function->getSequence(),
'resourceId' => $function->getId(),
'resourceType' => 'functions',
'entrypoint' => $entrypoint,
'buildCommands' => $commands,
'startCommand' => $function->getAttribute('startCommand', ''),
'sourcePath' => $path,
'sourceSize' => $fileSize,
'totalSize' => $fileSize,
'sourceChunksTotal' => $chunks,
'sourceChunksUploaded' => $chunksUploaded,
'activate' => $activate,
'sourceMetadata' => $metadata,
'type' => $type
]));
$function = $dbForProject->updateDocument('functions', $function->getId(), new Document([
'latestDeploymentId' => $deployment->getId(),
'latestDeploymentInternalId' => $deployment->getSequence(),
'latestDeploymentCreatedAt' => $deployment->getCreatedAt(),
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
]));
} else {
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([
'sourceChunksUploaded' => $chunksUploaded,
'sourceMetadata' => $metadata,
]));
}
}
$metadata = null;
if ($chunksUploaded === $chunks) {
$queueForEvents
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
}
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}, timeout: 120.0);
} catch (LockContention) {
$response->addHeader('Retry-After', '5');
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Deployment upload is busy. Try again.');
}
$metadata = null;
$queueForEvents
->setParam('functionId', $function->getId())
->setParam('deploymentId', $deployment->getId());
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}
}
@@ -2,7 +2,7 @@
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Databases;
use Appwrite\Event\Database;
use Appwrite\Event\Publisher\Database;
use Appwrite\Platform\Modules\Health\Http\Health\Queue\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
@@ -10,6 +10,7 @@ use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Document;
use Utopia\Queue\Queue;
use Utopia\Validator\Integer;
use Utopia\Validator\Text;
@@ -44,15 +45,15 @@ class Get extends Base
))
->param('name', 'database_db_main', new Text(256), 'Queue name for which to check the queue size', true)
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('response')
->callback($this->action(...));
}
public function action(string $name, int|string $threshold, Database $queueForDatabase, Response $response): void
public function action(string $name, int|string $threshold, Database $publisherForDatabase, Response $response): void
{
$threshold = (int) $threshold;
$size = $queueForDatabase->setQueue($name)->getSize();
$size = $publisherForDatabase->getSize(queue: new Queue($name));
$this->assertQueueThreshold($size, $threshold);
@@ -2,11 +2,11 @@
namespace Appwrite\Platform\Modules\Health\Http\Health\Queue\Failed;
use Appwrite\Event\Database;
use Appwrite\Event\Event;
use Appwrite\Event\Publisher\Audit;
use Appwrite\Event\Publisher\Build as BuildPublisher;
use Appwrite\Event\Publisher\Certificate;
use Appwrite\Event\Publisher\Database as DatabasePublisher;
use Appwrite\Event\Publisher\Delete as DeletePublisher;
use Appwrite\Event\Publisher\Func as FunctionPublisher;
use Appwrite\Event\Publisher\Mail as MailPublisher;
@@ -74,7 +74,7 @@ class Get extends Base
]), 'The name of the queue')
->param('threshold', 5000, new Integer(true), 'Queue size threshold. When hit (equal or higher), endpoint returns server error. Default value is 5000.', true)
->inject('response')
->inject('queueForDatabase')
->inject('publisherForDatabase')
->inject('publisherForDeletes')
->inject('publisherForAudits')
->inject('publisherForMails')
@@ -94,7 +94,7 @@ class Get extends Base
string $name,
int|string $threshold,
Response $response,
Database $queueForDatabase,
DatabasePublisher $publisherForDatabase,
DeletePublisher $publisherForDeletes,
Audit $publisherForAudits,
MailPublisher $publisherForMails,
@@ -111,7 +111,7 @@ class Get extends Base
$threshold = (int) $threshold;
$queue = match ($name) {
System::getEnv('_APP_DATABASE_QUEUE_NAME', Event::DATABASE_QUEUE_NAME) => $queueForDatabase,
System::getEnv('_APP_DATABASE_QUEUE_NAME', Event::DATABASE_QUEUE_NAME) => $publisherForDatabase,
System::getEnv('_APP_DELETE_QUEUE_NAME', Event::DELETE_QUEUE_NAME) => $publisherForDeletes,
System::getEnv('_APP_AUDITS_QUEUE_NAME', Event::AUDITS_QUEUE_NAME) => $publisherForAudits,
System::getEnv('_APP_MAILS_QUEUE_NAME', Event::MAILS_QUEUE_NAME) => $publisherForMails,
@@ -21,6 +21,7 @@ use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Request;
use Utopia\Lock\Exception\Contention as LockContention;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
@@ -90,6 +91,7 @@ class Create extends Action
->inject('plan')
->inject('authorization')
->inject('platform')
->inject('locks')
->callback($this->action(...));
}
@@ -112,6 +114,7 @@ class Create extends Action
array $plan,
Authorization $authorization,
array $platform,
callable $locks,
) {
$activate = \strval($activate) === 'true' || \strval($activate) === '1';
@@ -193,20 +196,38 @@ class Create extends Action
// Save to storage
$fileSize ??= $deviceForLocal->getFileSize($fileTmpName);
$path = $deviceForSites->getPath($deploymentId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION));
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
$lockKey = 'sites:deployment:' . $project->getId() . ':' . $siteId . ':' . $deploymentId;
$metadata = ['content_type' => $deviceForLocal->getFileMimeType($fileTmpName)];
if (!$deployment->isEmpty()) {
$chunks = $deployment->getAttribute('sourceChunksTotal', 1);
$uploaded = $deployment->getAttribute('sourceChunksUploaded', 0);
$metadata = $deployment->getAttribute('sourceMetadata', []);
$completed = false;
if ($uploaded === $chunks) {
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
return;
}
try {
$locks($lockKey, 600, function () use (&$chunks, $dbForProject, $deploymentId, &$metadata, &$completed, $response): void {
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if (!$deployment->isEmpty()) {
$chunks = $deployment->getAttribute('sourceChunksTotal', 1);
$uploaded = $deployment->getAttribute('sourceChunksUploaded', 0);
$metadata = $deployment->getAttribute('sourceMetadata', []);
if ($uploaded === $chunks) {
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
$completed = true;
return;
}
}
}, timeout: 120.0);
} catch (LockContention) {
$response->addHeader('Retry-After', '5');
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Deployment upload is busy. Try again.');
}
if ($completed) {
return;
}
$chunksUploaded = $deviceForSites->upload($fileTmpName, $path, $chunk, $chunks, $metadata);
@@ -225,184 +246,208 @@ class Create extends Action
$commands[] = $buildCommand;
}
if ($chunksUploaded === $chunks) {
if ($activate) {
// Remove deploy for all other deployments.
$activeDeployments = $dbForProject->find('deployments', [
Query::equal('activate', [true]),
Query::equal('resourceId', [$siteId]),
Query::equal('resourceType', ['sites'])
]);
try {
$locks($lockKey, 600, function () use ($activate, $authorization, $commands, &$chunks, $chunksUploaded, $dbForPlatform, $dbForProject, $deploymentId, $deviceForSites, $fileSize, &$metadata, $outputDirectory, $path, $platform, $project, $publisherForBuilds, $queueForEvents, $response, &$site, $siteId, $type): void {
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
$uploaded = 0;
foreach ($activeDeployments as $activeDeployment) {
$activeDeployment->setAttribute('activate', false);
$dbForProject->updateDocument('deployments', $activeDeployment->getId(), new Document(['activate' => false]));
if (!$deployment->isEmpty()) {
$chunks = $deployment->getAttribute('sourceChunksTotal', 1);
$uploaded = $deployment->getAttribute('sourceChunksUploaded', 0);
$metadata = \array_merge($deployment->getAttribute('sourceMetadata', []), $metadata);
if ($uploaded === $chunks) {
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
return;
}
}
}
$fileSize = $deviceForSites->getFileSize($path);
$chunksUploaded = max($uploaded, $chunksUploaded);
if ($deployment->isEmpty()) {
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceInternalId' => $site->getSequence(),
'resourceId' => $site->getId(),
'resourceType' => 'sites',
'buildCommands' => \implode(' && ', $commands),
'startCommand' => $site->getAttribute('startCommand', ''),
'buildOutput' => $outputDirectory,
'adapter' => $site->getAttribute('adapter', ''),
'fallbackFile' => $site->getAttribute('fallbackFile', ''),
'sourcePath' => $path,
'sourceSize' => $fileSize,
'totalSize' => $fileSize,
'sourceChunksTotal' => $chunks,
'sourceChunksUploaded' => $chunksUploaded,
'activate' => $activate,
'sourceMetadata' => $metadata,
'type' => $type,
]));
if ($chunksUploaded === $chunks && $uploaded < $chunks) {
if ($activate) {
// Remove deploy for all other deployments.
$activeDeployments = $dbForProject->find('deployments', [
Query::equal('activate', [true]),
Query::equal('resourceId', [$siteId]),
Query::equal('resourceType', ['sites'])
]);
$site = $site
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('sites', $site->getId(), new Document([
'latestDeploymentId' => $deployment->getId(),
'latestDeploymentInternalId' => $deployment->getSequence(),
'latestDeploymentCreatedAt' => $deployment->getCreatedAt(),
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
]));
foreach ($activeDeployments as $activeDeployment) {
$dbForProject->updateDocument('deployments', $activeDeployment->getId(), new Document(['activate' => false]));
}
}
$sitesDomain = $platform['sitesDomain'];
$domain = ID::unique() . "." . $sitesDomain;
$fileSize = $deviceForSites->getFileSize($path);
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$ruleId = $isMd5 ? md5($domain) : ID::unique();
if ($deployment->isEmpty()) {
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceInternalId' => $site->getSequence(),
'resourceId' => $site->getId(),
'resourceType' => 'sites',
'buildCommands' => \implode(' && ', $commands),
'startCommand' => $site->getAttribute('startCommand', ''),
'buildOutput' => $outputDirectory,
'adapter' => $site->getAttribute('adapter', ''),
'fallbackFile' => $site->getAttribute('fallbackFile', ''),
'sourcePath' => $path,
'sourceSize' => $fileSize,
'totalSize' => $fileSize,
'sourceChunksTotal' => $chunks,
'sourceChunksUploaded' => $chunksUploaded,
'activate' => $activate,
'sourceMetadata' => $metadata,
'type' => $type,
]));
$authorization->skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getSequence(),
'domain' => $domain,
'type' => 'deployment',
'trigger' => 'deployment',
'deploymentId' => $deployment->isEmpty() ? '' : $deployment->getId(),
'deploymentInternalId' => $deployment->isEmpty() ? '' : $deployment->getSequence(),
'deploymentResourceType' => 'site',
'deploymentResourceId' => $site->getId(),
'deploymentResourceInternalId' => $site->getSequence(),
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
'owner' => 'Appwrite',
'region' => $project->getAttribute('region')
]))
);
} else {
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([
'sourceSize' => $fileSize,
'sourceChunksUploaded' => $chunksUploaded,
'sourceMetadata' => $metadata,
]));
}
$site = $site
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('sites', $site->getId(), new Document([
'latestDeploymentId' => $deployment->getId(),
'latestDeploymentInternalId' => $deployment->getSequence(),
'latestDeploymentCreatedAt' => $deployment->getCreatedAt(),
'latestDeploymentStatus' => $deployment->getAttribute('status', ''),
]));
// Start the build
$publisherForBuilds->enqueue(new BuildMessage(
project: $project,
resource: $site,
deployment: $deployment,
type: BUILD_TYPE_DEPLOYMENT,
platform: $platform,
));
} else {
if ($deployment->isEmpty()) {
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceInternalId' => $site->getSequence(),
'resourceId' => $site->getId(),
'resourceType' => 'sites',
'buildCommands' => \implode(' && ', $commands),
'startCommand' => $site->getAttribute('startCommand', ''),
'buildOutput' => $outputDirectory,
'adapter' => $site->getAttribute('adapter', ''),
'fallbackFile' => $site->getAttribute('fallbackFile', ''),
'sourcePath' => $path,
'sourceSize' => $fileSize,
'totalSize' => $fileSize,
'sourceChunksTotal' => $chunks,
'sourceChunksUploaded' => $chunksUploaded,
'activate' => $activate,
'sourceMetadata' => $metadata,
'type' => $type,
]));
$sitesDomain = $platform['sitesDomain'];
$domain = ID::unique() . "." . $sitesDomain;
$site = $site
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('sites', $site->getId(), new Document([
'latestDeploymentId' => $site->getAttribute('latestDeploymentId'),
'latestDeploymentInternalId' => $site->getAttribute('latestDeploymentInternalId'),
'latestDeploymentCreatedAt' => $site->getAttribute('latestDeploymentCreatedAt'),
'latestDeploymentStatus' => $site->getAttribute('latestDeploymentStatus'),
]));
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$ruleId = $isMd5 ? md5($domain) : ID::unique();
$sitesDomain = $platform['sitesDomain'];
$domain = ID::unique() . "." . $sitesDomain;
$ruleId = md5($domain);
$authorization->skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getSequence(),
'domain' => $domain,
'type' => 'deployment',
'trigger' => 'deployment',
'deploymentId' => $deployment->isEmpty() ? '' : $deployment->getId(),
'deploymentInternalId' => $deployment->isEmpty() ? '' : $deployment->getSequence(),
'deploymentResourceType' => 'site',
'deploymentResourceId' => $site->getId(),
'deploymentResourceInternalId' => $site->getSequence(),
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
'owner' => 'Appwrite',
'region' => $project->getAttribute('region')
]))
);
} else {
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([
'sourceChunksUploaded' => $chunksUploaded,
'sourceMetadata' => $metadata,
]));
}
$authorization->skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getSequence(),
'domain' => $domain,
'type' => 'deployment',
'trigger' => 'deployment',
'deploymentId' => $deployment->isEmpty() ? '' : $deployment->getId(),
'deploymentInternalId' => $deployment->isEmpty() ? '' : $deployment->getSequence(),
'deploymentResourceType' => 'site',
'deploymentResourceId' => $site->getId(),
'deploymentResourceInternalId' => $site->getSequence(),
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
'owner' => 'Appwrite',
'region' => $project->getAttribute('region')
]))
);
} else {
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([
'sourceSize' => $fileSize,
'sourceChunksUploaded' => $chunksUploaded,
'sourceMetadata' => $metadata,
]));
}
// Start the build
$publisherForBuilds->enqueue(new BuildMessage(
project: $project,
resource: $site,
deployment: $deployment,
type: BUILD_TYPE_DEPLOYMENT,
platform: $platform,
));
} else {
if ($deployment->isEmpty()) {
$deployment = $dbForProject->createDocument('deployments', new Document([
'$id' => $deploymentId,
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'resourceInternalId' => $site->getSequence(),
'resourceId' => $site->getId(),
'resourceType' => 'sites',
'buildCommands' => \implode(' && ', $commands),
'startCommand' => $site->getAttribute('startCommand', ''),
'buildOutput' => $outputDirectory,
'adapter' => $site->getAttribute('adapter', ''),
'fallbackFile' => $site->getAttribute('fallbackFile', ''),
'sourcePath' => $path,
'sourceSize' => $fileSize,
'totalSize' => $fileSize,
'sourceChunksTotal' => $chunks,
'sourceChunksUploaded' => $chunksUploaded,
'activate' => $activate,
'sourceMetadata' => $metadata,
'type' => $type,
]));
$site = $site
->setAttribute('latestDeploymentId', $deployment->getId())
->setAttribute('latestDeploymentInternalId', $deployment->getSequence())
->setAttribute('latestDeploymentCreatedAt', $deployment->getCreatedAt())
->setAttribute('latestDeploymentStatus', $deployment->getAttribute('status', ''));
$dbForProject->updateDocument('sites', $site->getId(), new Document([
'latestDeploymentId' => $site->getAttribute('latestDeploymentId'),
'latestDeploymentInternalId' => $site->getAttribute('latestDeploymentInternalId'),
'latestDeploymentCreatedAt' => $site->getAttribute('latestDeploymentCreatedAt'),
'latestDeploymentStatus' => $site->getAttribute('latestDeploymentStatus'),
]));
$sitesDomain = $platform['sitesDomain'];
$domain = ID::unique() . "." . $sitesDomain;
$ruleId = md5($domain);
$authorization->skip(
fn () => $dbForPlatform->createDocument('rules', new Document([
'$id' => $ruleId,
'projectId' => $project->getId(),
'projectInternalId' => $project->getSequence(),
'domain' => $domain,
'type' => 'deployment',
'trigger' => 'deployment',
'deploymentId' => $deployment->isEmpty() ? '' : $deployment->getId(),
'deploymentInternalId' => $deployment->isEmpty() ? '' : $deployment->getSequence(),
'deploymentResourceType' => 'site',
'deploymentResourceId' => $site->getId(),
'deploymentResourceInternalId' => $site->getSequence(),
'status' => 'verified',
'certificateId' => '',
'search' => implode(' ', [$ruleId, $domain]),
'owner' => 'Appwrite',
'region' => $project->getAttribute('region')
]))
);
} else {
$deployment = $dbForProject->updateDocument('deployments', $deploymentId, new Document([
'sourceChunksUploaded' => $chunksUploaded,
'sourceMetadata' => $metadata,
]));
}
}
$metadata = null;
if ($chunksUploaded === $chunks) {
$queueForEvents
->setParam('siteId', $site->getId())
->setParam('deploymentId', $deployment->getId());
}
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}, timeout: 120.0);
} catch (LockContention) {
$response->addHeader('Retry-After', '5');
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'Deployment upload is busy. Try again.');
}
$metadata = null;
$queueForEvents
->setParam('siteId', $site->getId())
->setParam('deploymentId', $deployment->getId());
$response
->setStatusCode(Response::STATUS_CODE_ACCEPTED)
->dynamic($deployment, Response::MODEL_DEPLOYMENT);
}
}
@@ -29,6 +29,7 @@ use Utopia\Database\Validator\Authorization\Input;
use Utopia\Database\Validator\Permissions;
use Utopia\Database\Validator\UID;
use Utopia\Http\Adapter\Swoole\Request;
use Utopia\Lock\Exception\Contention as LockContention;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Storage\Device;
@@ -86,12 +87,13 @@ class Create extends Action
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('project')
->inject('user')
->inject('queueForEvents')
->inject('mode')
->inject('deviceForFiles')
->inject('deviceForLocal')
->inject('authorization')
->inject('locks')
->callback($this->action(...));
}
@@ -103,12 +105,13 @@ class Create extends Action
Request $request,
Response $response,
Database $dbForProject,
Document $project,
User $user,
Event $queueForEvents,
string $mode,
Device $deviceForFiles,
Device $deviceForLocal,
Authorization $authorization
Authorization $authorization,
callable $locks
) {
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
@@ -234,189 +237,242 @@ class Create extends Action
$path = $deviceForFiles->getPath($fileId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION));
$path = str_ireplace($deviceForFiles->getRoot(), $deviceForFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $path); // Add bucket id to path after root
$file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId);
$lockKey = 'storage:file:' . $project->getId() . ':' . $bucket->getId() . ':' . $fileId;
$metadata = ['content_type' => $deviceForLocal->getFileMimeType($fileTmpName)];
if (!$file->isEmpty()) {
$chunks = $file->getAttribute('chunksTotal', 1);
$uploaded = $file->getAttribute('chunksUploaded', 0);
$metadata = $file->getAttribute('metadata', []);
$completed = false;
if ($uploaded === $chunks) {
if (empty($contentRange)) {
throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS);
$mergeUploadMetadata = function (array $stored, array $current): array {
$merged = \array_merge($stored, $current);
if (isset($stored['parts']) || isset($current['parts'])) {
$parts = $stored['parts'] ?? [];
foreach (($current['parts'] ?? []) as $part => $value) {
$parts[(int) $part] = $value;
}
\ksort($parts);
$merged['parts'] = $parts;
$merged['chunks'] = \count($parts);
}
return $merged;
};
try {
$locks($lockKey, 600, function () use ($bucket, &$chunks, $contentRange, $dbForProject, $deviceForFiles, $fileId, $fileName, $fileSize, &$metadata, $path, $permissions, $response, &$completed): void {
$file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId);
if (!$file->isEmpty()) {
$chunks = $file->getAttribute('chunksTotal', 1);
$uploaded = $file->getAttribute('chunksUploaded', 0);
$metadata = $file->getAttribute('metadata', []);
if ($uploaded === $chunks) {
if (empty($contentRange)) {
throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS);
}
$response
->setStatusCode(Response::STATUS_CODE_OK)
->dynamic($file, Response::MODEL_FILE);
$completed = true;
return;
}
}
$response
->setStatusCode(Response::STATUS_CODE_OK)
->dynamic($file, Response::MODEL_FILE);
return;
}
if ($file->isEmpty()) {
$deviceForFiles->prepareUpload($path, $metadata['content_type'] ?? '', $chunks, $metadata);
if (!empty($contentRange)) {
$doc = new Document([
'$id' => ID::custom($fileId),
'$permissions' => $permissions,
'bucketId' => $bucket->getId(),
'bucketInternalId' => $bucket->getSequence(),
'name' => $fileName,
'path' => $path,
'signature' => '',
'mimeType' => '',
'sizeOriginal' => $fileSize,
'sizeActual' => 0,
'algorithm' => '',
'comment' => '',
'chunksTotal' => $chunks,
'chunksUploaded' => 0,
'search' => implode(' ', [$fileId, $fileName]),
'metadata' => $metadata,
]);
try {
$dbForProject->createDocument('bucket_' . $bucket->getSequence(), $doc);
} catch (DuplicateException) {
throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS);
} catch (NotFoundException) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
}
}
}, timeout: 120.0);
} catch (LockContention) {
$response->addHeader('Retry-After', '5');
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'File upload is busy. Try again.');
}
$chunksUploaded = $deviceForFiles->upload($fileTmpName, $path, $chunk, $chunks, $metadata);
if (empty($chunksUploaded)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed uploading file');
if ($completed) {
return;
}
if ($chunksUploaded === $chunks) {
if (System::getEnv('_APP_STORAGE_ANTIVIRUS') === 'enabled' && $bucket->getAttribute('antivirus', true) && $fileSize <= APP_LIMIT_ANTIVIRUS && $deviceForFiles->getType() === Storage::DEVICE_LOCAL) {
$antivirus = new Network(
System::getEnv('_APP_STORAGE_ANTIVIRUS_HOST', 'clamav'),
(int) System::getEnv('_APP_STORAGE_ANTIVIRUS_PORT', 3310)
);
$finalizeUpload = function (int $chunksUploaded) use ($authorization, $bucket, &$chunks, $contentRange, $dbForProject, $deviceForFiles, $fileId, $fileName, $fileSize, &$metadata, $mergeUploadMetadata, $path, $permissions, $queueForEvents, $response): void {
$file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId);
$uploaded = 0;
if (!$antivirus->fileScan($path)) {
$deviceForFiles->delete($path);
throw new Exception(Exception::STORAGE_INVALID_FILE);
if (!$file->isEmpty()) {
$chunks = $file->getAttribute('chunksTotal', 1);
$uploaded = $file->getAttribute('chunksUploaded', 0);
$metadata = $mergeUploadMetadata($file->getAttribute('metadata', []), $metadata);
if ($uploaded === $chunks) {
if (empty($contentRange)) {
throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS);
}
$response
->setStatusCode(Response::STATUS_CODE_OK)
->dynamic($file, Response::MODEL_FILE);
return;
}
}
$mimeType = $deviceForFiles->getFileMimeType($path); // Get mime-type before compression and encryption
$fileHash = $deviceForFiles->getFileHash($path); // Get file hash before compression and encryption
$data = '';
$iv = '';
$tag = null;
// Compression
$algorithm = $bucket->getAttribute('compression', Compression::NONE);
if ($fileSize <= APP_STORAGE_READ_BUFFER && $algorithm != Compression::NONE) {
$data = $deviceForFiles->read($path);
switch ($algorithm) {
case Compression::ZSTD:
$compressor = new Zstd();
break;
case Compression::GZIP:
default:
$compressor = new GZIP();
break;
}
$data = $compressor->compress($data);
} else {
// reset the algorithm to none as we do not compress the file
// if file size exceedes the APP_STORAGE_READ_BUFFER
// regardless the bucket compression algoorithm
$algorithm = Compression::NONE;
}
$chunksUploaded = max($uploaded, $chunksUploaded, (int) ($metadata['chunks'] ?? 0));
if ($bucket->getAttribute('encryption', true) && $fileSize <= APP_STORAGE_READ_BUFFER) {
if (empty($data)) {
if ($chunksUploaded === $chunks && $uploaded < $chunks) {
$deviceForFiles->finalizeUpload($path, $chunks, $metadata);
if (System::getEnv('_APP_STORAGE_ANTIVIRUS') === 'enabled' && $bucket->getAttribute('antivirus', true) && $fileSize <= APP_LIMIT_ANTIVIRUS && $deviceForFiles->getType() === Storage::DEVICE_LOCAL) {
$antivirus = new Network(
System::getEnv('_APP_STORAGE_ANTIVIRUS_HOST', 'clamav'),
(int) System::getEnv('_APP_STORAGE_ANTIVIRUS_PORT', 3310)
);
if (!$antivirus->fileScan($path)) {
$deviceForFiles->delete($path);
throw new Exception(Exception::STORAGE_INVALID_FILE);
}
}
$mimeType = $deviceForFiles->getFileMimeType($path); // Get mime-type before compression and encryption
$fileHash = $deviceForFiles->getFileHash($path); // Get file hash before compression and encryption
$data = '';
$iv = '';
$tag = null;
// Compression
$algorithm = $bucket->getAttribute('compression', Compression::NONE);
if ($fileSize <= APP_STORAGE_READ_BUFFER && $algorithm != Compression::NONE) {
$data = $deviceForFiles->read($path);
switch ($algorithm) {
case Compression::ZSTD:
$compressor = new Zstd();
break;
case Compression::GZIP:
default:
$compressor = new GZIP();
break;
}
$data = $compressor->compress($data);
} else {
// reset the algorithm to none as we do not compress the file
// if file size exceedes the APP_STORAGE_READ_BUFFER
// regardless the bucket compression algoorithm
$algorithm = Compression::NONE;
}
$key = System::getEnv('_APP_OPENSSL_KEY_V1');
$iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM));
$data = OpenSSL::encrypt($data, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag);
}
if (!empty($data)) {
if (!$deviceForFiles->write($path, $data, $mimeType)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to save file');
if ($bucket->getAttribute('encryption', true) && $fileSize <= APP_STORAGE_READ_BUFFER) {
if (empty($data)) {
$data = $deviceForFiles->read($path);
}
$key = System::getEnv('_APP_OPENSSL_KEY_V1');
$iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM));
$data = OpenSSL::encrypt($data, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag);
}
}
$sizeActual = $deviceForFiles->getFileSize($path);
$openSSLVersion = null;
$openSSLCipher = null;
$openSSLTag = null;
$openSSLIV = null;
if ($bucket->getAttribute('encryption', true) && $fileSize <= APP_STORAGE_READ_BUFFER) {
$openSSLVersion = '1';
$openSSLCipher = OpenSSL::CIPHER_AES_128_GCM;
$openSSLTag = \bin2hex($tag);
$openSSLIV = \bin2hex($iv);
}
if ($file->isEmpty()) {
$doc = new Document([
'$id' => $fileId,
'$permissions' => $permissions,
'bucketId' => $bucket->getId(),
'bucketInternalId' => $bucket->getSequence(),
'name' => $fileName,
'path' => $path,
'signature' => $fileHash,
'mimeType' => $mimeType,
'sizeOriginal' => $fileSize,
'sizeActual' => $sizeActual,
'algorithm' => $algorithm,
'comment' => '',
'chunksTotal' => $chunks,
'chunksUploaded' => $chunksUploaded,
'openSSLVersion' => $openSSLVersion,
'openSSLCipher' => $openSSLCipher,
'openSSLTag' => $openSSLTag,
'openSSLIV' => $openSSLIV,
'search' => implode(' ', [$fileId, $fileName]),
'metadata' => $metadata,
]);
try {
$file = $dbForProject->createDocument('bucket_' . $bucket->getSequence(), $doc);
} catch (DuplicateException) {
throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS);
} catch (NotFoundException) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
if (!empty($data)) {
if (!$deviceForFiles->write($path, $data, $mimeType)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to save file');
}
}
$sizeActual = $deviceForFiles->getFileSize($path);
$openSSLVersion = null;
$openSSLCipher = null;
$openSSLTag = null;
$openSSLIV = null;
if ($bucket->getAttribute('encryption', true) && $fileSize <= APP_STORAGE_READ_BUFFER) {
$openSSLVersion = '1';
$openSSLCipher = OpenSSL::CIPHER_AES_128_GCM;
$openSSLTag = \bin2hex($tag);
$openSSLIV = \bin2hex($iv);
}
if ($file->isEmpty()) {
$doc = new Document([
'$id' => $fileId,
'$permissions' => $permissions,
'bucketId' => $bucket->getId(),
'bucketInternalId' => $bucket->getSequence(),
'name' => $fileName,
'path' => $path,
'signature' => $fileHash,
'mimeType' => $mimeType,
'sizeOriginal' => $fileSize,
'sizeActual' => $sizeActual,
'algorithm' => $algorithm,
'comment' => '',
'chunksTotal' => $chunks,
'chunksUploaded' => $chunksUploaded,
'openSSLVersion' => $openSSLVersion,
'openSSLCipher' => $openSSLCipher,
'openSSLTag' => $openSSLTag,
'openSSLIV' => $openSSLIV,
'search' => implode(' ', [$fileId, $fileName]),
'metadata' => $metadata,
]);
try {
$file = $dbForProject->createDocument('bucket_' . $bucket->getSequence(), $doc);
} catch (DuplicateException) {
throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS);
} catch (NotFoundException) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
} else {
/**
* Skip authorization in updateDocument.
* Without this, the file creation will fail when user doesn't have update permission.
* However as with chunk upload even if we are updating, we are essentially creating a file
* adding it's new chunk so we rely on the create-permission check performed earlier.
*/
$file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, new Document([
'$permissions' => $permissions,
'signature' => $fileHash,
'mimeType' => $mimeType,
'sizeActual' => $sizeActual,
'algorithm' => $algorithm,
'openSSLVersion' => $openSSLVersion,
'openSSLCipher' => $openSSLCipher,
'openSSLTag' => $openSSLTag,
'openSSLIV' => $openSSLIV,
'metadata' => $metadata,
'chunksUploaded' => $chunksUploaded,
])));
}
// Trigger after create success hook
$this->afterCreateSuccess($file);
} else {
$file = $file
->setAttribute('$permissions', $permissions)
->setAttribute('signature', $fileHash)
->setAttribute('mimeType', $mimeType)
->setAttribute('sizeActual', $sizeActual)
->setAttribute('algorithm', $algorithm)
->setAttribute('openSSLVersion', $openSSLVersion)
->setAttribute('openSSLCipher', $openSSLCipher)
->setAttribute('openSSLTag', $openSSLTag)
->setAttribute('openSSLIV', $openSSLIV)
->setAttribute('metadata', $metadata)
->setAttribute('chunksUploaded', $chunksUploaded);
/**
* Skip authorization in updateDocument.
* Without this, the file creation will fail when user doesn't have update permission.
* However as with chunk upload even if we are updating, we are essentially creating a file
* adding it's new chunk so we rely on the create-permission check performed earlier.
*/
$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([
'$id' => ID::custom($fileId),
'$permissions' => $permissions,
'bucketId' => $bucket->getId(),
'bucketInternalId' => $bucket->getSequence(),
'name' => $fileName,
'path' => $path,
'signature' => '',
'mimeType' => '',
'sizeOriginal' => $fileSize,
'sizeActual' => 0,
'algorithm' => '',
'comment' => '',
'chunksTotal' => $chunks,
'chunksUploaded' => $chunksUploaded,
'search' => implode(' ', [$fileId, $fileName]),
'metadata' => $metadata,
]);
try {
$file = $dbForProject->createDocument('bucket_' . $bucket->getSequence(), $doc);
} catch (DuplicateException) {
throw new Exception(Exception::STORAGE_FILE_ALREADY_EXISTS);
} catch (NotFoundException) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
} else {
$file = $file
->setAttribute('chunksUploaded', $chunksUploaded)
->setAttribute('metadata', $metadata);
/**
* Skip authorization in updateDocument.
* Without this, the file creation will fail when user doesn't have update permission.
@@ -424,23 +480,41 @@ class Create extends Action
* adding it's new chunk so we rely on the create-permission check performed earlier.
*/
try {
$file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, $file));
$file = $authorization->skip(fn () => $dbForProject->updateDocument('bucket_' . $bucket->getSequence(), $fileId, new Document([
'chunksUploaded' => $chunksUploaded,
'metadata' => $metadata,
])));
} catch (NotFoundException) {
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
}
}
if ($chunksUploaded === $chunks) {
$queueForEvents
->setParam('bucketId', $bucket->getId())
->setParam('fileId', $file->getId())
->setContext('bucket', $bucket);
}
$metadata = null; // was causing leaks as it was passed by reference
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($file, Response::MODEL_FILE);
};
try {
$chunksUploaded = $deviceForFiles->uploadChunk($fileTmpName, $path, $chunk, $chunks, $metadata);
if (empty($chunksUploaded)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed uploading file');
}
$locks($lockKey, 600, fn () => $finalizeUpload($chunksUploaded), timeout: 120.0);
} catch (LockContention) {
$response->addHeader('Retry-After', '5');
throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED, 'File upload is busy. Try again.');
}
$queueForEvents
->setParam('bucketId', $bucket->getId())
->setParam('fileId', $file->getId())
->setContext('bucket', $bucket);
$metadata = null; // was causing leaks as it was passed by reference
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($file, Response::MODEL_FILE);
}
/**
@@ -131,7 +131,6 @@ class Get extends Action
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
}
/* @type Document $bucket */
$bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
$isAPIKey = $user->isApp($authorization->getRoles());
@@ -155,7 +154,6 @@ class Get extends Action
if ($fileSecurity && !$valid && !$isToken) {
$file = $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId);
} else {
/* @type Document $file */
$file = $authorization->skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getSequence(), $fileId));
}
@@ -21,6 +21,7 @@ use Utopia\Database\Validator\Authorization;
use Utopia\DSN\DSN;
use Utopia\Span\Span;
use Utopia\System\System;
use Utopia\Validator\Contains;
use Utopia\VCS\Adapter\Git\GitHub;
use Utopia\VCS\Exception\RepositoryNotFound;
@@ -95,6 +96,13 @@ trait Deployment
$resource = $authorization->skip(fn () => $dbForProject->getDocument($resourceCollection, $resourceId));
$resourceInternalId = $resource->getSequence();
$validator = new Contains(VCS_DEPLOYMENT_SKIP_PATTERNS);
if ($validator->isValid($providerCommitMessage)) {
Span::add("{$logBase}.build.skipped.reason", $validator->getDescription());
Span::add("{$logBase}.build.skipped", 'true');
continue;
}
$deploymentId = ID::unique();
$repositoryId = $repository->getId();
$repositoryInternalId = $repository->getSequence();
@@ -561,4 +569,5 @@ trait Deployment
{
return System::getEnv('_APP_BUILDS_QUEUE_NAME', Event::BUILDS_QUEUE_NAME);
}
}
@@ -466,6 +466,14 @@ abstract class Format
return 'ConsoleResourceValue';
}
break;
case 'getEmailTemplate':
switch ($param) {
case 'templateId':
return 'ProjectEmailTemplateId';
case 'locale':
return 'ProjectEmailTemplateLocale';
}
break;
}
break;
case 'account':
@@ -175,4 +175,49 @@ class ConsoleConsoleClientTest extends Scope
$this->assertNotNull($usersRead);
$this->assertEquals('Access to read users', $usersRead['description']);
}
public function testListOrganizationScopes(): void
{
$response = $this->client->call(Client::METHOD_GET, '/console/scopes/organization', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['total']);
$this->assertIsArray($response['body']['scopes']);
$this->assertGreaterThan(0, $response['body']['total']);
$this->assertEquals($response['body']['total'], \count($response['body']['scopes']));
$scopeIds = \array_column($response['body']['scopes'], '$id');
// Well-known scopes must be present
$this->assertContains('projects.read', $scopeIds);
$this->assertContains('projects.write', $scopeIds);
// Every scope has the expected shape
foreach ($response['body']['scopes'] as $scope) {
$this->assertArrayHasKey('$id', $scope);
$this->assertIsString($scope['$id']);
$this->assertNotEmpty($scope['$id']);
$this->assertArrayHasKey('description', $scope);
$this->assertIsString($scope['description']);
$this->assertNotEmpty($scope['description']);
$this->assertArrayHasKey('deprecated', $scope);
$this->assertIsBool($scope['deprecated']);
$this->assertArrayHasKey('category', $scope);
$this->assertIsString($scope['category']);
}
// A specific scope has the expected description
$projectsRead = null;
foreach ($response['body']['scopes'] as $scope) {
if ($scope['$id'] === 'projects.read') {
$projectsRead = $scope;
break;
}
}
$this->assertNotNull($projectsRead);
$this->assertEquals('Access to read organization projects', $projectsRead['description']);
}
}
@@ -74,4 +74,35 @@ class ConsoleCustomServerTest extends Scope
$this->assertArrayHasKey('deprecated', $usersRead);
$this->assertIsBool($usersRead['deprecated']);
}
public function testListOrganizationScopes(): void
{
// Public endpoint: must succeed without admin authentication. Drop the
// headers from getHeaders() and only pass project + content-type.
$response = $this->client->call(Client::METHOD_GET, '/console/scopes/organization', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertIsInt($response['body']['total']);
$this->assertIsArray($response['body']['scopes']);
$this->assertGreaterThan(0, $response['body']['total']);
$scopeIds = \array_column($response['body']['scopes'], '$id');
$this->assertContains('projects.read', $scopeIds);
$projectsRead = null;
foreach ($response['body']['scopes'] as $scope) {
if ($scope['$id'] === 'projects.read') {
$projectsRead = $scope;
break;
}
}
$this->assertNotNull($projectsRead);
$this->assertIsString($projectsRead['description']);
$this->assertNotEmpty($projectsRead['description']);
$this->assertArrayHasKey('deprecated', $projectsRead);
$this->assertIsBool($projectsRead['deprecated']);
}
}
@@ -1199,6 +1199,144 @@ class FunctionsCustomServerTest extends Scope
}, 120000, 500);
}
public function testCreateDeploymentParallelChunksLargeFile(): void
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test Parallel Chunk Deployment',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 10,
]);
$deploymentId = ID::unique();
$tmpDirectory = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'appwrite-parallel-function-deployment-' . $deploymentId;
mkdir($tmpDirectory);
try {
copy(__DIR__ . '/../../../resources/functions/basic/index.js', $tmpDirectory . DIRECTORY_SEPARATOR . 'index.js');
file_put_contents($tmpDirectory . DIRECTORY_SEPARATOR . 'large.bin', random_bytes(20 * 1024 * 1024));
$source = $tmpDirectory . DIRECTORY_SEPARATOR . 'code.tar.gz';
Console::execute('cd ' . $tmpDirectory . ' && tar --exclude code.tar.gz -czf code.tar.gz .', '', $this->stdout, $this->stderr);
$totalSize = filesize($source);
$chunkSize = 5 * 1024 * 1024;
$chunksTotal = (int) ceil($totalSize / $chunkSize);
$this->assertGreaterThanOrEqual(4, $chunksTotal, 'Test deployment must span at least 4 chunks');
$requests = [];
$sourceHandle = fopen($source, 'rb');
$this->assertNotFalse($sourceHandle, 'Could not open deployment package');
try {
for ($i = 0; $i < $chunksTotal; $i++) {
$start = $i * $chunkSize;
$end = min($start + $chunkSize, $totalSize) - 1;
$length = $end - $start + 1;
$chunkPath = $tmpDirectory . DIRECTORY_SEPARATOR . 'chunk-' . $i . '.part';
fseek($sourceHandle, $start);
file_put_contents($chunkPath, fread($sourceHandle, $length));
$requests[] = [
'headers' => [
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
'x-appwrite-id' => $deploymentId,
'content-range' => 'bytes ' . $start . '-' . $end . '/' . $totalSize,
],
'chunkPath' => $chunkPath,
];
}
} finally {
fclose($sourceHandle);
}
$responses = [];
$endpoint = parse_url($this->client->getEndpoint());
$scheme = $endpoint['scheme'] ?? 'http';
$host = $endpoint['host'] ?? 'appwrite';
$port = $endpoint['port'] ?? ($scheme === 'https' ? 443 : 80);
$basePath = rtrim($endpoint['path'] ?? '', '/');
\Swoole\Coroutine\run(function () use ($basePath, $functionId, $host, $port, $requests, $scheme, &$responses): void {
$wg = new \Swoole\Coroutine\WaitGroup();
foreach ($requests as $index => $request) {
$wg->add();
\Swoole\Coroutine::create(function () use ($basePath, $functionId, $host, $index, $port, $request, &$responses, $scheme, $wg): void {
try {
for ($attempt = 0; $attempt < 3; $attempt++) {
$client = new \Swoole\Coroutine\Http\Client($host, (int) $port, $scheme === 'https');
$client->set([
'timeout' => 300,
'ssl_verify_peer' => false,
'ssl_verify_host' => false,
]);
$client->setHeaders($request['headers']);
$client->setMethod(Client::METHOD_POST);
$client->setData([
'entrypoint' => 'index.js',
'activate' => true,
]);
$client->addFile($request['chunkPath'], 'code', 'application/x-gzip', 'code.tar.gz');
$client->execute($basePath . '/functions/' . $functionId . '/deployments');
$responses[$index] = [
'body' => $client->body,
'error' => $client->errMsg,
'headers' => $client->headers ?? [],
'statusCode' => $client->statusCode,
];
$client->close();
if ($responses[$index]['statusCode'] !== 429) {
break;
}
$retryAfter = (float) ($responses[$index]['headers']['retry-after'] ?? 0.1);
\Swoole\Coroutine::sleep(max($retryAfter, 0.1));
}
} finally {
$wg->done();
}
});
}
$wg->wait();
});
ksort($responses);
foreach ($responses as $response) {
$this->assertSame('', $response['error']);
$this->assertContains($response['statusCode'], [202], (string) $response['body']);
}
$this->assertEventually(function () use ($functionId, $deploymentId) {
$deployment = $this->getDeployment($functionId, $deploymentId);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertEquals('ready', $deployment['body']['status']);
$this->assertEquals($deploymentId, $deployment['body']['$id']);
}, 120000, 500);
} finally {
$this->cleanupFunction($functionId);
if (is_dir($tmpDirectory)) {
foreach (glob($tmpDirectory . DIRECTORY_SEPARATOR . '*') ?: [] as $file) {
unlink($file);
}
rmdir($tmpDirectory);
}
}
}
public function testUpdateDeployment(): void
{
$data = $this->setupTestDeployment();
@@ -1147,6 +1147,115 @@ trait TemplatesBase
return $this->client->call(Client::METHOD_PATCH, '/project/templates/email', $headers, $params);
}
// Console email template (default) tests
public function testGetConsoleEmailTemplate(): void
{
$response = $this->getConsoleEmailTemplate('verification', 'en');
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame('verification', $response['body']['templateId']);
$this->assertSame('en', $response['body']['locale']);
$this->assertNotEmpty($response['body']['subject']);
$this->assertNotEmpty($response['body']['message']);
$this->assertSame('', $response['body']['senderName']);
$this->assertSame('', $response['body']['senderEmail']);
$this->assertSame('', $response['body']['replyToEmail']);
$this->assertSame('', $response['body']['replyToName']);
}
public function testGetConsoleEmailTemplateIgnoresCustomOverride(): void
{
$this->ensureSMTPEnabled();
// Set a custom override on the project template.
$this->updateEmailTemplate(
templateId: 'recovery',
locale: 'en',
subject: 'Custom subject',
message: 'Custom message',
senderName: 'Custom Sender',
senderEmail: 'custom@appwrite.io',
);
// Console endpoint must always return the built-in default, not the override.
$response = $this->getConsoleEmailTemplate('recovery', 'en');
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame('recovery', $response['body']['templateId']);
$this->assertNotSame('Custom subject', $response['body']['subject']);
$this->assertSame('', $response['body']['senderName']);
$this->assertSame('', $response['body']['senderEmail']);
}
public function testGetConsoleEmailTemplateDefaultLocale(): void
{
$response = $this->getConsoleEmailTemplate('magicSession');
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame('en', $response['body']['locale']);
$this->assertNotEmpty($response['body']['subject']);
}
public function testGetConsoleEmailTemplateNonDefaultLocale(): void
{
$response = $this->getConsoleEmailTemplate('verification', 'fr');
$this->assertSame(200, $response['headers']['status-code']);
$this->assertSame('verification', $response['body']['templateId']);
$this->assertSame('fr', $response['body']['locale']);
$this->assertNotEmpty($response['body']['subject']);
$this->assertNotEmpty($response['body']['message']);
}
public function testGetConsoleEmailTemplateAllTypes(): void
{
$types = [
'verification',
'magicSession',
'recovery',
'invitation',
'mfaChallenge',
'sessionAlert',
'otpSession',
];
foreach ($types as $type) {
$response = $this->getConsoleEmailTemplate($type, 'en');
$this->assertSame(200, $response['headers']['status-code'], "type={$type}");
$this->assertNotEmpty($response['body']['subject'], "type={$type} must have subject");
$this->assertNotEmpty($response['body']['message'], "type={$type} must have message");
}
}
public function testGetConsoleEmailTemplateInvalidTemplateId(): void
{
$response = $this->getConsoleEmailTemplate('invalidTemplate', 'en');
$this->assertSame(400, $response['headers']['status-code']);
}
public function testGetConsoleEmailTemplateInvalidLocale(): void
{
$response = $this->getConsoleEmailTemplate('recovery', 'not-a-locale');
$this->assertSame(400, $response['headers']['status-code']);
}
protected function getConsoleEmailTemplate(string $templateId, ?string $locale = null): mixed
{
$params = [];
if ($locale !== null) {
$params['locale'] = $locale;
}
return $this->client->call(Client::METHOD_GET, '/console/templates/email/' . $templateId, [
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
], $params);
}
protected function ensureSMTPEnabled(): void
{
$this->client->call(
@@ -1351,6 +1351,145 @@ class SitesCustomServerTest extends Scope
$this->cleanupSite($siteId);
}
public function testCreateDeploymentParallelChunksLargeFile(): void
{
$siteId = $this->setupSite([
'buildRuntime' => 'node-22',
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site Parallel Chunk Deployment',
'outputDirectory' => './',
'providerBranch' => 'main',
'providerRootDirectory' => './',
'siteId' => ID::unique()
]);
$deploymentId = ID::unique();
$tmpDirectory = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'appwrite-parallel-site-deployment-' . $deploymentId;
mkdir($tmpDirectory);
try {
file_put_contents($tmpDirectory . DIRECTORY_SEPARATOR . 'index.html', '<html><body>Hello World</body></html>');
file_put_contents($tmpDirectory . DIRECTORY_SEPARATOR . 'large.bin', random_bytes(20 * 1024 * 1024));
$source = $tmpDirectory . DIRECTORY_SEPARATOR . 'code.tar.gz';
Console::execute('cd ' . $tmpDirectory . ' && tar --exclude code.tar.gz -czf code.tar.gz .', '', $this->stdout, $this->stderr);
$totalSize = filesize($source);
$chunkSize = 5 * 1024 * 1024;
$chunksTotal = (int) ceil($totalSize / $chunkSize);
$this->assertGreaterThanOrEqual(4, $chunksTotal, 'Test deployment must span at least 4 chunks');
$requests = [];
$sourceHandle = fopen($source, 'rb');
$this->assertNotFalse($sourceHandle, 'Could not open deployment package');
try {
for ($i = 0; $i < $chunksTotal; $i++) {
$start = $i * $chunkSize;
$end = min($start + $chunkSize, $totalSize) - 1;
$length = $end - $start + 1;
$chunkPath = $tmpDirectory . DIRECTORY_SEPARATOR . 'chunk-' . $i . '.part';
fseek($sourceHandle, $start);
file_put_contents($chunkPath, fread($sourceHandle, $length));
$requests[] = [
'headers' => [
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
'x-appwrite-id' => $deploymentId,
'content-range' => 'bytes ' . $start . '-' . $end . '/' . $totalSize,
],
'chunkPath' => $chunkPath,
];
}
} finally {
fclose($sourceHandle);
}
$responses = [];
$endpoint = parse_url($this->client->getEndpoint());
$scheme = $endpoint['scheme'] ?? 'http';
$host = $endpoint['host'] ?? 'appwrite';
$port = $endpoint['port'] ?? ($scheme === 'https' ? 443 : 80);
$basePath = rtrim($endpoint['path'] ?? '', '/');
\Swoole\Coroutine\run(function () use ($basePath, $host, $port, $requests, $scheme, $siteId, &$responses): void {
$wg = new \Swoole\Coroutine\WaitGroup();
foreach ($requests as $index => $request) {
$wg->add();
\Swoole\Coroutine::create(function () use ($basePath, $host, $index, $port, $request, &$responses, $scheme, $siteId, $wg): void {
try {
for ($attempt = 0; $attempt < 3; $attempt++) {
$client = new \Swoole\Coroutine\Http\Client($host, (int) $port, $scheme === 'https');
$client->set([
'timeout' => 300,
'ssl_verify_peer' => false,
'ssl_verify_host' => false,
]);
$client->setHeaders($request['headers']);
$client->setMethod(Client::METHOD_POST);
$client->setData([
'activate' => true,
]);
$client->addFile($request['chunkPath'], 'code', 'application/x-gzip', 'code.tar.gz');
$client->execute($basePath . '/sites/' . $siteId . '/deployments');
$responses[$index] = [
'body' => $client->body,
'error' => $client->errMsg,
'headers' => $client->headers ?? [],
'statusCode' => $client->statusCode,
];
$client->close();
if ($responses[$index]['statusCode'] !== 429) {
break;
}
$retryAfter = (float) ($responses[$index]['headers']['retry-after'] ?? 0.1);
\Swoole\Coroutine::sleep(max($retryAfter, 0.1));
}
} finally {
$wg->done();
}
});
}
$wg->wait();
});
ksort($responses);
foreach ($responses as $response) {
$this->assertSame('', $response['error']);
$this->assertContains($response['statusCode'], [202], (string) $response['body']);
}
$this->assertEventually(function () use ($siteId, $deploymentId) {
$deployment = $this->getDeployment($siteId, $deploymentId);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertEquals('ready', $deployment['body']['status']);
$this->assertEquals($deploymentId, $deployment['body']['$id']);
}, 120000, 500);
} finally {
$this->cleanupSite($siteId);
if (is_dir($tmpDirectory)) {
foreach (glob($tmpDirectory . DIRECTORY_SEPARATOR . '*') ?: [] as $file) {
unlink($file);
}
rmdir($tmpDirectory);
}
}
}
public function testCreateDeployment()
{
$siteId = $this->setupSite([
+179 -1
View File
@@ -391,7 +391,7 @@ trait StorageBase
'bucketId' => ID::unique(),
'name' => 'Test Bucket 2',
'fileSecurity' => true,
'maximumFileSize' => 6000000000, //6GB
'maximumFileSize' => 6000000001,
'allowedFileExtensions' => ["jpg", "png"],
'permissions' => [
Permission::read(Role::any()),
@@ -1436,6 +1436,184 @@ trait StorageBase
]);
}
public function testCreateBucketFileParallelChunksLargeFile(): void
{
$totalSize = 20 * 1024 * 1024;
$chunkSize = 5 * 1024 * 1024;
$chunksTotal = (int) ceil($totalSize / $chunkSize);
$this->assertGreaterThanOrEqual(4, $chunksTotal, 'Test file must span at least 4 chunks');
$bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'bucketId' => ID::unique(),
'name' => 'Test Bucket Parallel Chunk Upload',
'fileSecurity' => true,
'maximumFileSize' => $totalSize,
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals(201, $bucket['headers']['status-code']);
$bucketId = $bucket['body']['$id'];
$fileId = ID::unique();
$tmpDirectory = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'appwrite-parallel-upload-' . $fileId;
$source = $tmpDirectory . DIRECTORY_SEPARATOR . 'large-parallel-upload.bin';
mkdir($tmpDirectory);
try {
$handle = fopen($source, 'wb');
$this->assertNotFalse($handle, 'Could not create test file');
$remaining = $totalSize;
$block = str_repeat(hash('sha256', $fileId, binary: true), 1024);
while ($remaining > 0) {
$bytes = substr($block, 0, min(strlen($block), $remaining));
fwrite($handle, $bytes);
$remaining -= strlen($bytes);
}
fclose($handle);
$requests = [];
$sourceHandle = fopen($source, 'rb');
$this->assertNotFalse($sourceHandle, 'Could not open test file');
for ($i = 0; $i < $chunksTotal; $i++) {
$start = $i * $chunkSize;
$end = min($start + $chunkSize, $totalSize) - 1;
$length = $end - $start + 1;
$chunkPath = $tmpDirectory . DIRECTORY_SEPARATOR . 'chunk-' . $i . '.part';
fseek($sourceHandle, $start);
file_put_contents($chunkPath, fread($sourceHandle, $length));
$requests[] = [
'headers' => [
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
'content-range' => 'bytes ' . $start . '-' . $end . '/' . $totalSize,
],
'chunkPath' => $chunkPath,
];
}
fclose($sourceHandle);
$responses = [];
$endpoint = parse_url($this->client->getEndpoint());
$scheme = $endpoint['scheme'] ?? 'http';
$host = $endpoint['host'] ?? 'appwrite';
$port = $endpoint['port'] ?? ($scheme === 'https' ? 443 : 80);
$basePath = rtrim($endpoint['path'] ?? '', '/');
\Swoole\Coroutine\run(function () use ($basePath, $bucketId, $fileId, $host, $port, $requests, $scheme, &$responses): void {
$wg = new \Swoole\Coroutine\WaitGroup();
foreach ($requests as $index => $request) {
$wg->add();
\Swoole\Coroutine::create(function () use ($basePath, $bucketId, $fileId, $host, $index, $port, $request, &$responses, $scheme, $wg): void {
try {
for ($attempt = 0; $attempt < 3; $attempt++) {
$client = new \Swoole\Coroutine\Http\Client($host, (int) $port, $scheme === 'https');
$client->set([
'timeout' => 300,
'ssl_verify_peer' => false,
'ssl_verify_host' => false,
]);
$client->setHeaders($request['headers']);
$client->setMethod(Client::METHOD_POST);
$client->setData([
'fileId' => $fileId,
'permissions[0]' => Permission::read(Role::any()),
'permissions[1]' => Permission::delete(Role::any()),
]);
$client->addFile($request['chunkPath'], 'file', 'application/octet-stream', 'large-parallel-upload.bin');
$client->execute($basePath . '/storage/buckets/' . $bucketId . '/files');
$responses[$index] = [
'body' => $client->body,
'error' => $client->errMsg,
'headers' => $client->headers ?? [],
'statusCode' => $client->statusCode,
];
$client->close();
if ($responses[$index]['statusCode'] !== 429) {
break;
}
$retryAfter = (float) ($responses[$index]['headers']['retry-after'] ?? 0.1);
\Swoole\Coroutine::sleep(max($retryAfter, 0.1));
}
} finally {
$wg->done();
}
});
}
$wg->wait();
});
ksort($responses);
foreach ($responses as $response) {
$this->assertSame('', $response['error']);
$this->assertContains($response['statusCode'], [200, 201], (string) $response['body']);
}
$uploadedFile = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]));
$this->assertEquals(200, $uploadedFile['headers']['status-code']);
$this->assertEquals($chunksTotal, $uploadedFile['body']['chunksTotal']);
$this->assertEquals($chunksTotal, $uploadedFile['body']['chunksUploaded']);
$download = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId . '/download', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]));
$this->assertEquals(200, $download['headers']['status-code']);
$this->assertEquals($totalSize, strlen($download['body']));
$this->assertEquals(hash_file('sha256', $source), hash('sha256', $download['body']));
} finally {
if (isset($bucketId)) {
$this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucketId . '/files/' . $fileId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]));
$this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucketId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
}
foreach (glob($tmpDirectory . DIRECTORY_SEPARATOR . '*') ?: [] as $file) {
unlink($file);
}
if (is_dir($tmpDirectory)) {
rmdir($tmpDirectory);
}
}
}
public function testDeleteBucketFile(): void
{
// Create a fresh file just for deletion testing (not using cache since we delete it)