From 26af9b7463732ba48039b4654041fe80725a5b0b Mon Sep 17 00:00:00 2001 From: shimon Date: Wed, 14 Feb 2024 23:05:01 +0200 Subject: [PATCH 01/38] usage logs updates --- src/Appwrite/Platform/Workers/Usage.php | 1 + src/Appwrite/Platform/Workers/UsageDump.php | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Usage.php b/src/Appwrite/Platform/Workers/Usage.php index 49f334490..35c0e02ce 100644 --- a/src/Appwrite/Platform/Workers/Usage.php +++ b/src/Appwrite/Platform/Workers/Usage.php @@ -76,6 +76,7 @@ class Usage extends Action } $this->stats[$projectId]['project'] = $project; + $this->stats[$projectId]['receivedAt'] = DateTime::now(); foreach ($payload['metrics'] ?? [] as $metric) { $this->keys++; if (!isset($this->stats[$projectId]['keys'][$metric['key']])) { diff --git a/src/Appwrite/Platform/Workers/UsageDump.php b/src/Appwrite/Platform/Workers/UsageDump.php index f56357898..9f2e269d5 100644 --- a/src/Appwrite/Platform/Workers/UsageDump.php +++ b/src/Appwrite/Platform/Workers/UsageDump.php @@ -59,12 +59,12 @@ class UsageDump extends Action foreach ($payload['stats'] ?? [] as $stats) { $project = new Document($stats['project'] ?? []); $numberOfKeys = !empty($stats['keys']) ? count($stats['keys']) : 0; - + $receivedAt = $stats['receivedAt'] ?? 'NONE'; if ($numberOfKeys === 0) { continue; } - console::log('[' . DateTime::now() . '] ProjectId [' . $project->getInternalId() . '] Database [' . $project['database'] . '] ' . $numberOfKeys . ' keys'); + console::log('[' . DateTime::now() . '] ProjectId [' . $project->getInternalId() . '] receivedAt [' . $receivedAt . '] ' . $numberOfKeys . ' keys'); try { $dbForProject = $getProjectDB($project); From dae01f707773ec5edbb714c7980e4bd6cb945613 Mon Sep 17 00:00:00 2001 From: shimon Date: Wed, 14 Feb 2024 23:07:25 +0200 Subject: [PATCH 02/38] usage logs updates --- src/Appwrite/Platform/Workers/UsageDump.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Workers/UsageDump.php b/src/Appwrite/Platform/Workers/UsageDump.php index 9f2e269d5..5a4324fd3 100644 --- a/src/Appwrite/Platform/Workers/UsageDump.php +++ b/src/Appwrite/Platform/Workers/UsageDump.php @@ -64,7 +64,7 @@ class UsageDump extends Action continue; } - console::log('[' . DateTime::now() . '] ProjectId [' . $project->getInternalId() . '] receivedAt [' . $receivedAt . '] ' . $numberOfKeys . ' keys'); + console::log('[' . DateTime::now() . '] ProjectId [' . $project->getInternalId() . '] ReceivedAt [' . $receivedAt . '] ' . $numberOfKeys . ' keys'); try { $dbForProject = $getProjectDB($project); From 00db19257d0d0b49745d31102f1240f6810ea214 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Thu, 15 Feb 2024 11:57:24 +0530 Subject: [PATCH 03/38] Add health storage check --- app/controllers/api/health.php | 37 +++++++++++++++++++ docs/references/health/get-storage.md | 1 + .../Health/HealthCustomServerTest.php | 18 +++++++++ 3 files changed, 56 insertions(+) create mode 100644 docs/references/health/get-storage.md diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index a85f9da32..2f637be42 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -702,6 +702,43 @@ App::get('/v1/health/storage/local') $response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS); }); +App::get('/v1/health/storage') + ->desc('Get storage') + ->groups(['api', 'health']) + ->label('scope', 'health.read') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'health') + ->label('sdk.method', 'getStorage') + ->label('sdk.description', '/docs/references/health/get-storage.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_HEALTH_STATUS) + ->inject('response') + ->inject('deviceFiles') + ->action(function (Response $response, Device $deviceFiles) { + + $checkStart = \microtime(true); + + if (!$deviceFiles->write($deviceFiles->getPath('health.txt'), 'test', '')) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed writing test file'); + } + + if ($deviceFiles->read($deviceFiles->getPath('health.txt')) !== 'test') { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed reading test file'); + } + + if (!$deviceFiles->delete($deviceFiles->getPath('health.txt'))) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed deleting test file'); + } + + $output = [ + 'status' => 'pass', + 'ping' => \round((\microtime(true) - $checkStart) / 1000) + ]; + + $response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS); + }); + App::get('/v1/health/anti-virus') ->desc('Get antivirus') ->groups(['api', 'health']) diff --git a/docs/references/health/get-storage.md b/docs/references/health/get-storage.md new file mode 100644 index 000000000..ea73e8802 --- /dev/null +++ b/docs/references/health/get-storage.md @@ -0,0 +1 @@ +Check the Appwrite storage device is up and connection is successful. \ No newline at end of file diff --git a/tests/e2e/Services/Health/HealthCustomServerTest.php b/tests/e2e/Services/Health/HealthCustomServerTest.php index c817222c4..dde89a6a7 100644 --- a/tests/e2e/Services/Health/HealthCustomServerTest.php +++ b/tests/e2e/Services/Health/HealthCustomServerTest.php @@ -407,6 +407,24 @@ class HealthCustomServerTest extends Scope return []; } + public function testStorageSuccess(): array + { + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_GET, '/health/storage', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('pass', $response['body']['status']); + $this->assertIsInt($response['body']['ping']); + $this->assertLessThan(100, $response['body']['ping']); + + return []; + } + public function testStorageAntiVirusSuccess(): array { /** From 2a5f21e9827d92db3d4919d4d8fe69b112ac3f54 Mon Sep 17 00:00:00 2001 From: shimon Date: Thu, 15 Feb 2024 17:12:37 +0200 Subject: [PATCH 04/38] Addressed comments --- app/worker.php | 5 +++-- src/Appwrite/Event/Mail.php | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/worker.php b/app/worker.php index 4cf0edbae..0bf5ae89e 100644 --- a/app/worker.php +++ b/app/worker.php @@ -253,7 +253,6 @@ try { Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine()); } - $worker = $platform->getWorker(); $worker @@ -268,7 +267,8 @@ $worker ->inject('error') ->inject('logger') ->inject('log') - ->action(function (Throwable $error, ?Logger $logger, Log $log) { + ->inject('project') + ->action(function (Throwable $error, ?Logger $logger, Log $log, Document $project) { $version = App::getEnv('_APP_VERSION', 'UNKNOWN'); if ($error instanceof PDOException) { @@ -284,6 +284,7 @@ $worker $log->setAction('appwrite-queue-' . App::getEnv('QUEUE')); $log->addTag('verboseType', get_class($error)); $log->addTag('code', $error->getCode()); + $log->addTag('projectId', $project->getInternalId() ?? 'n/a'); $log->addExtra('file', $error->getFile()); $log->addExtra('line', $error->getLine()); $log->addExtra('trace', $error->getTraceAsString()); diff --git a/src/Appwrite/Event/Mail.php b/src/Appwrite/Event/Mail.php index 9973dae40..ca8fe7d20 100644 --- a/src/Appwrite/Event/Mail.php +++ b/src/Appwrite/Event/Mail.php @@ -365,6 +365,7 @@ class Mail extends Event $client = new Client($this->queue, $this->connection); return $client->enqueue([ + 'project' => $this->project, 'recipient' => $this->recipient, 'name' => $this->name, 'subject' => $this->subject, From ed4319314af0610b2a353ac074245ccee0b8dd38 Mon Sep 17 00:00:00 2001 From: shimon Date: Thu, 15 Feb 2024 17:15:10 +0200 Subject: [PATCH 05/38] Addressed comments --- app/worker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/worker.php b/app/worker.php index 0bf5ae89e..de12bba6c 100644 --- a/app/worker.php +++ b/app/worker.php @@ -284,7 +284,7 @@ $worker $log->setAction('appwrite-queue-' . App::getEnv('QUEUE')); $log->addTag('verboseType', get_class($error)); $log->addTag('code', $error->getCode()); - $log->addTag('projectId', $project->getInternalId() ?? 'n/a'); + $log->addTag('projectId', $project->getId() ?? 'n/a'); $log->addExtra('file', $error->getFile()); $log->addExtra('line', $error->getLine()); $log->addExtra('trace', $error->getTraceAsString()); From 3b1233eaa9d7e4ba705e6073697c21d6e747dffe Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Sun, 18 Feb 2024 18:28:41 +0530 Subject: [PATCH 06/38] Add deviceFunctions and deviceBuilds as well --- app/controllers/api/health.php | 36 ++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index 2f637be42..2cbf8add3 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -21,6 +21,20 @@ use Utopia\Validator\Multiple; use Utopia\Validator\Text; use Utopia\Validator\WhiteList; +$checkStorageHealth = function (Device $device) { + if (!$device->write($device->getPath('health.txt'), 'test', '')) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed writing test file to ' . $device->getRoot()); + } + + if ($device->read($device->getPath('health.txt')) !== 'test') { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed reading test file from ' . $device->getRoot()); + } + + if (!$device->delete($device->getPath('health.txt'))) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed deleting test file from ' . $device->getRoot()); + } +}; + App::get('/v1/health') ->desc('Get HTTP') ->groups(['api', 'health']) @@ -715,20 +729,18 @@ App::get('/v1/health/storage') ->label('sdk.response.model', Response::MODEL_HEALTH_STATUS) ->inject('response') ->inject('deviceFiles') - ->action(function (Response $response, Device $deviceFiles) { - + ->inject('deviceFunctions') + ->inject('deviceBuilds') + ->action(function (Response $response, Device $deviceFiles, Device $deviceFunctions, Device $deviceBuilds) use ($checkStorageHealth) { + $devices = [$deviceFiles, $deviceFunctions, $deviceBuilds]; $checkStart = \microtime(true); - if (!$deviceFiles->write($deviceFiles->getPath('health.txt'), 'test', '')) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed writing test file'); - } - - if ($deviceFiles->read($deviceFiles->getPath('health.txt')) !== 'test') { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed reading test file'); - } - - if (!$deviceFiles->delete($deviceFiles->getPath('health.txt'))) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed deleting test file'); + try { + foreach ($devices as $device) { + $checkStorageHealth($device); + } + } catch (Exception $e) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage()); } $output = [ From e4a86d9777db3542f5b5de11da2772ac8975c7c1 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Mon, 19 Feb 2024 09:19:54 +0530 Subject: [PATCH 07/38] Remove global function --- app/controllers/api/health.php | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index 2cbf8add3..c007d629b 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -21,20 +21,6 @@ use Utopia\Validator\Multiple; use Utopia\Validator\Text; use Utopia\Validator\WhiteList; -$checkStorageHealth = function (Device $device) { - if (!$device->write($device->getPath('health.txt'), 'test', '')) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed writing test file to ' . $device->getRoot()); - } - - if ($device->read($device->getPath('health.txt')) !== 'test') { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed reading test file from ' . $device->getRoot()); - } - - if (!$device->delete($device->getPath('health.txt'))) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed deleting test file from ' . $device->getRoot()); - } -}; - App::get('/v1/health') ->desc('Get HTTP') ->groups(['api', 'health']) @@ -731,16 +717,22 @@ App::get('/v1/health/storage') ->inject('deviceFiles') ->inject('deviceFunctions') ->inject('deviceBuilds') - ->action(function (Response $response, Device $deviceFiles, Device $deviceFunctions, Device $deviceBuilds) use ($checkStorageHealth) { + ->action(function (Response $response, Device $deviceFiles, Device $deviceFunctions, Device $deviceBuilds) { $devices = [$deviceFiles, $deviceFunctions, $deviceBuilds]; $checkStart = \microtime(true); - try { - foreach ($devices as $device) { - $checkStorageHealth($device); + foreach ($devices as $device) { + if (!$device->write($device->getPath('health.txt'), 'test', '')) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed writing test file to ' . $device->getRoot()); + } + + if ($device->read($device->getPath('health.txt')) !== 'test') { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed reading test file from ' . $device->getRoot()); + } + + if (!$device->delete($device->getPath('health.txt'))) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed deleting test file from ' . $device->getRoot()); } - } catch (Exception $e) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, $e->getMessage()); } $output = [ From 2ab6156db4f9a9c454931b2dcf7a59714df2a916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 19 Feb 2024 14:08:15 +0000 Subject: [PATCH 08/38] Add smarter traefik load balancing with health check --- .env | 2 +- app/controllers/api/health.php | 10 +++++++++- app/controllers/web/home.php | 1 + app/http.php | 7 +++++++ docker-compose.yml | 9 ++++++--- 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/.env b/.env index b915f9151..56fde8a4f 100644 --- a/.env +++ b/.env @@ -1,6 +1,6 @@ _APP_ENV=development _APP_LOCALE=en -_APP_WORKER_PER_CORE=6 +_APP_WORKER_PER_CORE=1 _APP_CONSOLE_WHITELIST_ROOT=disabled _APP_CONSOLE_WHITELIST_EMAILS= _APP_CONSOLE_WHITELIST_IPS= diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index a85f9da32..f6ab3e8ee 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -51,8 +51,16 @@ App::get('/v1/health/version') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_HEALTH_VERSION) + ->inject('http') ->inject('response') - ->action(function (Response $response) { + ->action(function (mixed $http, Response $response) { + $stats = $http->stats(); + \var_dump(\gethostname() . ': ' . $stats['idle_worker_num'] . '/' . $stats['worker_num']); + + if(($stats['idle_worker_num'] ?? 0) <= 1) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Can not process more requests at the moment.'); + } + $response->dynamic(new Document([ 'version' => APP_VERSION_STABLE ]), Response::MODEL_HEALTH_VERSION); }); diff --git a/app/controllers/web/home.php b/app/controllers/web/home.php index 27b2614c3..5a8a7e1a9 100644 --- a/app/controllers/web/home.php +++ b/app/controllers/web/home.php @@ -10,6 +10,7 @@ App::get('/versions') ->label('scope', 'public') ->inject('response') ->action(function (Response $response) { + \sleep(60); $platforms = Config::getParam('platforms'); $versions = [ diff --git a/app/http.php b/app/http.php index 5b32d8f13..4b740516e 100644 --- a/app/http.php +++ b/app/http.php @@ -327,4 +327,11 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo } }); +$register->set('http', function () use ($http) { + return $http; +}); +App::setResource('http', function ($register) { + return $register->get('http'); +}, ['register']); + $http->start(); diff --git a/docker-compose.yml b/docker-compose.yml index de71e3937..8e017c3e5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,7 +44,9 @@ services: - appwrite appwrite: - container_name: appwrite + deploy: + mode: replicated + replicas: 3 <<: *x-logging image: appwrite-dev build: @@ -53,8 +55,6 @@ services: DEBUG: false TESTING: true VERSION: dev - ports: - - 9501:80 networks: - appwrite labels: @@ -62,6 +62,9 @@ services: - "traefik.constraint-label-stack=appwrite" - "traefik.docker.network=appwrite" - "traefik.http.services.appwrite_api.loadbalancer.server.port=80" + - "traefik.http.services.appwrite_api.loadbalancer.healthcheck.path=/v1/health/version" + - "traefik.http.services.appwrite_api.loadbalancer.healthcheck.interval=3s" + - "traefik.http.services.appwrite_api.loadbalancer.healthcheck.timeout=2s" #http - traefik.http.routers.appwrite_api_http.entrypoints=appwrite_web - traefik.http.routers.appwrite_api_http.rule=PathPrefix(`/`) From 505b4815378622a1c3988c26e087c2fcc8dc6832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 19 Feb 2024 14:10:35 +0000 Subject: [PATCH 09/38] Revert "Add smarter traefik load balancing with health check" This reverts commit 2ab6156db4f9a9c454931b2dcf7a59714df2a916. --- .env | 2 +- app/controllers/api/health.php | 10 +--------- app/controllers/web/home.php | 1 - app/http.php | 7 ------- docker-compose.yml | 9 +++------ 5 files changed, 5 insertions(+), 24 deletions(-) diff --git a/.env b/.env index 56fde8a4f..b915f9151 100644 --- a/.env +++ b/.env @@ -1,6 +1,6 @@ _APP_ENV=development _APP_LOCALE=en -_APP_WORKER_PER_CORE=1 +_APP_WORKER_PER_CORE=6 _APP_CONSOLE_WHITELIST_ROOT=disabled _APP_CONSOLE_WHITELIST_EMAILS= _APP_CONSOLE_WHITELIST_IPS= diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index f6ab3e8ee..a85f9da32 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -51,16 +51,8 @@ App::get('/v1/health/version') ->label('sdk.response.code', Response::STATUS_CODE_OK) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_HEALTH_VERSION) - ->inject('http') ->inject('response') - ->action(function (mixed $http, Response $response) { - $stats = $http->stats(); - \var_dump(\gethostname() . ': ' . $stats['idle_worker_num'] . '/' . $stats['worker_num']); - - if(($stats['idle_worker_num'] ?? 0) <= 1) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Can not process more requests at the moment.'); - } - + ->action(function (Response $response) { $response->dynamic(new Document([ 'version' => APP_VERSION_STABLE ]), Response::MODEL_HEALTH_VERSION); }); diff --git a/app/controllers/web/home.php b/app/controllers/web/home.php index 5a8a7e1a9..27b2614c3 100644 --- a/app/controllers/web/home.php +++ b/app/controllers/web/home.php @@ -10,7 +10,6 @@ App::get('/versions') ->label('scope', 'public') ->inject('response') ->action(function (Response $response) { - \sleep(60); $platforms = Config::getParam('platforms'); $versions = [ diff --git a/app/http.php b/app/http.php index 4b740516e..5b32d8f13 100644 --- a/app/http.php +++ b/app/http.php @@ -327,11 +327,4 @@ $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swo } }); -$register->set('http', function () use ($http) { - return $http; -}); -App::setResource('http', function ($register) { - return $register->get('http'); -}, ['register']); - $http->start(); diff --git a/docker-compose.yml b/docker-compose.yml index 8e017c3e5..de71e3937 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,9 +44,7 @@ services: - appwrite appwrite: - deploy: - mode: replicated - replicas: 3 + container_name: appwrite <<: *x-logging image: appwrite-dev build: @@ -55,6 +53,8 @@ services: DEBUG: false TESTING: true VERSION: dev + ports: + - 9501:80 networks: - appwrite labels: @@ -62,9 +62,6 @@ services: - "traefik.constraint-label-stack=appwrite" - "traefik.docker.network=appwrite" - "traefik.http.services.appwrite_api.loadbalancer.server.port=80" - - "traefik.http.services.appwrite_api.loadbalancer.healthcheck.path=/v1/health/version" - - "traefik.http.services.appwrite_api.loadbalancer.healthcheck.interval=3s" - - "traefik.http.services.appwrite_api.loadbalancer.healthcheck.timeout=2s" #http - traefik.http.routers.appwrite_api_http.entrypoints=appwrite_web - traefik.http.routers.appwrite_api_http.rule=PathPrefix(`/`) From 1577be91721ce6d04a6a8a33b3607f407868632a Mon Sep 17 00:00:00 2001 From: shimon Date: Mon, 19 Feb 2024 21:36:14 +0200 Subject: [PATCH 10/38] usage/usage-dump queue health endpoints --- app/controllers/api/health.php | 55 +++++++++++++++++++ .../references/health/get-queue-usage-dump.md | 1 + docs/references/health/get-queue-usage.md | 1 + .../Health/HealthCustomServerTest.php | 48 ++++++++++++++++ 4 files changed, 105 insertions(+) create mode 100644 docs/references/health/get-queue-usage-dump.md create mode 100644 docs/references/health/get-queue-usage.md diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index a85f9da32..f727492d3 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -659,6 +659,60 @@ App::get('/v1/health/queue/functions') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); +App::get('/v1/health/queue/usage') + ->desc('Get usage queue') + ->groups(['api', 'health']) + ->label('scope', 'health.read') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'health') + ->label('sdk.method', 'getQueueUsage') + ->label('sdk.description', '/docs/references/health/get-queue-usage.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_HEALTH_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('queue') + ->inject('response') + ->action(function (int|string $threshold, Connection $queue, Response $response) { + $threshold = \intval($threshold); + + $client = new Client(Event::USAGE_QUEUE_NAME, $queue); + $size = $client->getQueueSize(); + + if ($size >= $threshold) { + throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}."); + } + + $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); + }); + +App::get('/v1/health/queue/usage-dump') + ->desc('Get usage dump queue') + ->groups(['api', 'health']) + ->label('scope', 'health.read') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'health') + ->label('sdk.method', 'getQueueUsage') + ->label('sdk.description', '/docs/references/health/get-queue-usage-dump.md') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_HEALTH_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('queue') + ->inject('response') + ->action(function (int|string $threshold, Connection $queue, Response $response) { + $threshold = \intval($threshold); + + $client = new Client(Event::USAGE_DUMP_QUEUE_NAME, $queue); + $size = $client->getQueueSize(); + + if ($size >= $threshold) { + throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}."); + } + + $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); + }); + App::get('/v1/health/storage/local') ->desc('Get local storage') ->groups(['api', 'health']) @@ -755,6 +809,7 @@ App::get('/v1/health/queue/failed/:name') Event::MAILS_QUEUE_NAME, Event::FUNCTIONS_QUEUE_NAME, Event::USAGE_QUEUE_NAME, + Event::USAGE_DUMP_QUEUE_NAME, Event::WEBHOOK_CLASS_NAME, Event::CERTIFICATES_QUEUE_NAME, Event::BUILDS_QUEUE_NAME, diff --git a/docs/references/health/get-queue-usage-dump.md b/docs/references/health/get-queue-usage-dump.md new file mode 100644 index 000000000..3c95da1b8 --- /dev/null +++ b/docs/references/health/get-queue-usage-dump.md @@ -0,0 +1 @@ +Get the number of projects containing metrics that are waiting to be processed in the Appwrite internal queue server. \ No newline at end of file diff --git a/docs/references/health/get-queue-usage.md b/docs/references/health/get-queue-usage.md new file mode 100644 index 000000000..8e5b64e64 --- /dev/null +++ b/docs/references/health/get-queue-usage.md @@ -0,0 +1 @@ +Get the number of metrics that are waiting to be processed in the Appwrite internal queue server. \ No newline at end of file diff --git a/tests/e2e/Services/Health/HealthCustomServerTest.php b/tests/e2e/Services/Health/HealthCustomServerTest.php index c817222c4..1ea0020e3 100644 --- a/tests/e2e/Services/Health/HealthCustomServerTest.php +++ b/tests/e2e/Services/Health/HealthCustomServerTest.php @@ -494,4 +494,52 @@ class HealthCustomServerTest extends Scope return []; } + + public function testUsageSuccess() + { + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_GET, '/health/queue/usage', 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']['size']); + $this->assertLessThan(100, $response['body']['size']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/health/queue/usage?threshold=0', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + $this->assertEquals(503, $response['headers']['status-code']); + } + + public function testUsageDumpSuccess() + { + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_GET, '/health/queue/usage-dump', 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']['size']); + $this->assertLessThan(100, $response['body']['size']); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_GET, '/health/queue/usage-dump?threshold=0', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + $this->assertEquals(503, $response['headers']['status-code']); + } } From c2ae330888571ed91ce00eef0f8bcb13121a4a6d Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 20 Feb 2024 09:14:48 +0545 Subject: [PATCH 11/38] fix bandwidth usage --- app/controllers/api/project.php | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/project.php b/app/controllers/api/project.php index 2269cb81c..47b94d309 100644 --- a/app/controllers/api/project.php +++ b/app/controllers/api/project.php @@ -144,9 +144,30 @@ App::get('/v1/project/usage') ]; }, $dbForProject->find('buckets')); + // merge network inbound + outbound + $projectBandwidth = []; + foreach ($usage[METRIC_NETWORK_INBOUND] as $item) { + $projectBandwidth[$item['date']] ??= 0; + $projectBandwidth[$item['date']] += $item['value']; + } + + foreach ($usage[METRIC_NETWORK_OUTBOUND] as $item) { + $projectBandwidth[$item['date']] ??= 0; + $projectBandwidth[$item['date']] += $item['value']; + } + + + $network = []; + foreach ($projectBandwidth as $date => $value) { + $network[] = [ + 'date' => $date, + 'value' => $value + ]; + } + $response->dynamic(new Document([ 'requests' => ($usage[METRIC_NETWORK_REQUESTS]), - 'network' => ($usage[METRIC_NETWORK_INBOUND] + $usage[METRIC_NETWORK_OUTBOUND]), + 'network' => $network, 'users' => ($usage[METRIC_USERS]), 'executions' => ($usage[METRIC_EXECUTIONS]), 'executionsTotal' => $total[METRIC_EXECUTIONS], From d60e7c641f4c714b3a4fb9536c1aa5dc85b1cdc5 Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Tue, 20 Feb 2024 15:56:25 +0000 Subject: [PATCH 12/38] Add ARM64 to docker publish --- .github/workflows/publish.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0fc3fc2df..195eefebc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,6 +17,9 @@ jobs: fetch-depth: 2 submodules: recursive + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Login to Docker Hub uses: docker/login-action@v2 with: @@ -35,11 +38,11 @@ jobs: uses: docker/build-push-action@v4 with: context: . - platforms: linux/amd64 + platforms: linux/amd64,linux/arm64 build-args: | VERSION=${{ steps.meta.outputs.version }} VITE_APPWRITE_GROWTH_ENDPOINT=https://growth.appwrite.io/v1 VITE_GA_PROJECT=G-L7G2B6PLDS VITE_CONSOLE_MODE=cloud push: true - tags: ${{ steps.meta.outputs.tags }} \ No newline at end of file + tags: ${{ steps.meta.outputs.tags }} From cc8705253e8ba6c95dc85059dd63624fb5de388e Mon Sep 17 00:00:00 2001 From: Bradley Schofield Date: Wed, 21 Feb 2024 10:12:20 +0000 Subject: [PATCH 13/38] Add Docker Buildx for multiplatform --- .github/workflows/publish.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 195eefebc..f914e662d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -20,6 +20,9 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub uses: docker/login-action@v2 with: From 0dd2889de517c0a76b3b7c762ff3dacaef30bae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 21 Feb 2024 14:51:21 +0000 Subject: [PATCH 14/38] Fix double router curl --- app/controllers/general.php | 263 ++++++++++++++++++++++++++++++------ 1 file changed, 221 insertions(+), 42 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index bfec8b46b..e54d6c21e 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -12,6 +12,7 @@ use Swoole\Http\Request as SwooleRequest; use Utopia\Cache\Cache; use Utopia\Pools\Group; use Appwrite\Utopia\Request; +use MaxMind\Db\Reader; use Appwrite\Utopia\Response; use Appwrite\Utopia\View; use Appwrite\Extend\Exception as AppwriteException; @@ -19,6 +20,9 @@ use Utopia\Config\Config; use Utopia\Domains\Domain; use Appwrite\Auth\Auth; use Appwrite\Event\Certificate; +use Appwrite\Event\Event; +use Appwrite\Event\Usage; +use Appwrite\ID; use Appwrite\Network\Validator\Origin; use Appwrite\Utopia\Response\Filters\V11 as ResponseV11; use Appwrite\Utopia\Response\Filters\V12 as ResponseV12; @@ -38,6 +42,7 @@ use Appwrite\Utopia\Request\Filters\V13 as RequestV13; use Appwrite\Utopia\Request\Filters\V14 as RequestV14; use Appwrite\Utopia\Request\Filters\V15 as RequestV15; use Appwrite\Utopia\Request\Filters\V16 as RequestV16; +use Executor\Executor; use Utopia\Validator\Text; use Utopia\Validator\WhiteList; @@ -45,7 +50,11 @@ Config::setParam('domainVerification', false); Config::setParam('cookieDomain', 'localhost'); Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); -function router(App $utopia, Database $dbForConsole, SwooleRequest $swooleRequest, Request $request, Response $response) + +->inject('geodb') +Reader $geodb + +function router(App $utopia, Database $dbForConsole, Database $dbForProject, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, Usage $queueForUsage) { $utopia->getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml'); @@ -117,59 +126,214 @@ function router(App $utopia, Database $dbForConsole, SwooleRequest $swooleReques $path .= '?' . $query; } + + $body = $swooleRequest->getContent() ?? ''; + $method = $swooleRequest->server['request_method']; + $requestHeaders = $request->getHeaders(); - $body = \json_encode([ - 'async' => false, - 'body' => $swooleRequest->getContent() ?? '', - 'method' => $swooleRequest->server['request_method'], - 'path' => $path, - 'headers' => $requestHeaders + $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); + + if ($function->isEmpty() || !$function->getAttribute('enabled')) { + throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND); + } + + $version = $function->getAttribute('version', 'v2'); + $runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []); + + $runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null; + + if (\is_null($runtime)) { + throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported'); + } + + $deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', ''))); + + if ($deployment->getAttribute('resourceId') !== $function->getId()) { + throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); + } + + if ($deployment->isEmpty()) { + throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); + } + + /** Check if build has completed */ + $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); + if ($build->isEmpty()) { + throw new AppwriteException(AppwriteException::BUILD_NOT_FOUND); + } + + if ($build->getAttribute('status') !== 'ready') { + throw new AppwriteException(AppwriteException::BUILD_NOT_READY); + } + + $permissions = $function->getAttribute('execute'); + + if (!(\in_array('any', $permissions)) && (\in_array('guests', $permissions))) { + throw new AppwriteException(AppwriteException::USER_UNAUTHORIZED, 'To execute function using domain, execute permissions must include "any" or "guests"'); + } + + $headers = \array_merge([], $requestHeaders); + $headers['x-appwrite-trigger'] = 'http'; + $headers['x-appwrite-user-id'] = ''; + $headers['x-appwrite-user-jwt'] = ''; + $headers['x-appwrite-country-code'] = ''; + $headers['x-appwrite-continent-code'] = ''; + $headers['x-appwrite-continent-eu'] = 'false'; + + $ip = $headers['x-real-ip'] ?? ''; + if (!empty($ip)) { + $record = $geodb->get($ip); + + if ($record) { + $eu = Config::getParam('locale-eu'); + + $headers['x-appwrite-country-code'] = $record['country']['iso_code'] ?? ''; + $headers['x-appwrite-continent-code'] = $record['continent']['code'] ?? ''; + $headers['x-appwrite-continent-eu'] = (\in_array($record['country']['iso_code'], $eu)) ? 'true' : 'false'; + } + } + + $headersFiltered = []; + foreach ($headers as $key => $value) { + if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_REQUEST)) { + $headersFiltered[] = ['name' => $key, 'value' => $value]; + } + } + + $executionId = ID::unique(); + + $execution = new Document([ + '$id' => $executionId, + '$permissions' => [], + 'functionInternalId' => $function->getInternalId(), + 'functionId' => $function->getId(), + 'deploymentInternalId' => $deployment->getInternalId(), + 'deploymentId' => $deployment->getId(), + 'trigger' => 'http', // http / schedule / event + 'status' => 'processing', // waiting / processing / completed / failed + 'responseStatusCode' => 0, + 'responseHeaders' => [], + 'requestPath' => $path, + 'requestMethod' => $method, + 'requestHeaders' => $headersFiltered, + 'errors' => '', + 'logs' => '', + 'duration' => 0.0, + 'search' => implode(' ', [$functionId, $executionId]), ]); - $headers = [ - 'Content-Type: application/json', - 'Content-Length: ' . \strlen($body), - 'X-Appwrite-Project: ' . $projectId - ]; + $queueForEvents + ->setParam('functionId', $function->getId()) + ->setParam('executionId', $execution->getId()) + ->setContext('function', $function); - $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, "http://localhost/v1/functions/{$functionId}/executions"); - \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); - \curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - \curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - // \curl_setopt($ch, CURLOPT_HEADER, true); - \curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); - \curl_setopt($ch, CURLOPT_TIMEOUT, 30); + $durationStart = \microtime(true); - $executionResponse = \curl_exec($ch); - $statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE); - $error = \curl_error($ch); - $errNo = \curl_errno($ch); + $vars = []; - \curl_close($ch); - - if ($errNo !== 0) { - throw new AppwriteException(AppwriteException::GENERAL_ARGUMENT_INVALID, "Internal error: " . $error); + // V2 vars + if ($version === 'v2') { + $vars = \array_merge($vars, [ + 'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '', + 'APPWRITE_FUNCTION_DATA' => $body ?? '', + 'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '', + 'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? '' + ]); } - if ($statusCode >= 400) { - $error = \json_decode($executionResponse, true)['message']; - throw new AppwriteException(AppwriteException::GENERAL_ARGUMENT_INVALID, "Execution error: " . $error); + // Shared vars + foreach ($function->getAttribute('varsProject', []) as $var) { + $vars[$var->getAttribute('key')] = $var->getAttribute('value', ''); } - $execution = \json_decode($executionResponse, true); + // Function vars + foreach ($function->getAttribute('vars', []) as $var) { + $vars[$var->getAttribute('key')] = $var->getAttribute('value', ''); + } - $contentType = 'text/plain'; - foreach ($execution['responseHeaders'] as $header) { - if (\strtolower($header['name']) === 'content-type') { - $contentType = $header['value']; + // Appwrite vars + $vars = \array_merge($vars, [ + 'APPWRITE_FUNCTION_ID' => $functionId, + 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name'), + 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(), + 'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(), + 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '', + 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '', + ]); + + /** Execute function */ + $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); + try { + $version = $function->getAttribute('version', 'v2'); + $command = $runtime['startCommand']; + $command = $version === 'v2' ? '' : 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $command . '"'; + $executionResponse = $executor->createExecution( + projectId: $project->getId(), + deploymentId: $deployment->getId(), + body: \strlen($body) > 0 ? $body : null, + variables: $vars, + timeout: $function->getAttribute('timeout', 0), + image: $runtime['image'], + source: $build->getAttribute('path', ''), + entrypoint: $deployment->getAttribute('entrypoint', ''), + version: $version, + path: $path, + method: $method, + headers: $headers, + runtimeEntrypoint: $command, + requestTimeout: 30 + ); + + $headersFiltered = []; + foreach ($executionResponse['headers'] as $key => $value) { + if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_RESPONSE)) { + $headersFiltered[] = ['name' => $key, 'value' => $value]; + } } - $response->setHeader($header['name'], $header['value']); + /** Update execution status */ + $status = $executionResponse['statusCode'] >= 400 ? 'failed' : 'completed'; + $execution->setAttribute('status', $status); + $execution->setAttribute('responseStatusCode', $executionResponse['statusCode']); + $execution->setAttribute('responseHeaders', $headersFiltered); + $execution->setAttribute('logs', $executionResponse['logs']); + $execution->setAttribute('errors', $executionResponse['errors']); + $execution->setAttribute('duration', $executionResponse['duration']); + } catch (\Throwable $th) { + $durationEnd = \microtime(true); + + $execution + ->setAttribute('duration', $durationEnd - $durationStart) + ->setAttribute('status', 'failed') + ->setAttribute('responseStatusCode', 500) + ->setAttribute('errors', $th->getMessage() . '\nError Code: ' . $th->getCode()); + Console::error($th->getMessage()); + } finally { + $queueForUsage + ->addMetric(METRIC_EXECUTIONS, 1) + ->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1) + ->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000)) // per project + ->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function + ; } + if ($function->getAttribute('logging')) { + /** @var Document $execution */ + $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); + } + + $execution->setAttribute('logs', ''); + $execution->setAttribute('errors', ''); + + $headers = []; + foreach (($executionResponse['headers'] ?? []) as $key => $value) { + $headers[] = ['name' => $key, 'value' => $value]; + } + + $execution->setAttribute('responseBody', $executionResponse['body'] ?? ''); + $execution->setAttribute('responseHeaders', $headers); + $body = $execution['responseBody'] ?? ''; $encodingKey = \array_search('x-open-runtimes-encoding', \array_column($execution['responseHeaders'], 'name')); @@ -179,6 +343,15 @@ function router(App $utopia, Database $dbForConsole, SwooleRequest $swooleReques } } + $contentType = 'text/plain'; + foreach ($execution['responseHeaders'] as $header) { + if (\strtolower($header['name']) === 'content-type') { + $contentType = $header['value']; + } + + $response->setHeader($header['name'], $header['value']); + } + $response ->setContentType($contentType) ->setStatusCode($execution['responseStatusCode'] ?? 200) @@ -205,13 +378,16 @@ App::init() ->inject('console') ->inject('project') ->inject('dbForConsole') + ->inject('dbForProject') ->inject('user') ->inject('locale') ->inject('localeCodes') ->inject('clients') ->inject('servers') ->inject('queueForCertificates') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Document $user, Locale $locale, array $localeCodes, array $clients, array $servers, Certificate $queueForCertificates) { + ->inject('queueForEvents') + ->inject('queueForUsage') + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Database $dbForProject, Document $user, Locale $locale, array $localeCodes, array $clients, array $servers, Certificate $queueForCertificates, Event $queueForEvents, Usage $queueForUsage) { /* * Appwrite Router */ @@ -220,7 +396,7 @@ App::init() $mainDomain = App::getEnv('_APP_DOMAIN', ''); // Only run Router when external domain if ($host !== $mainDomain) { - if (router($utopia, $dbForConsole, $swooleRequest, $request, $response)) { + if (router($utopia, $dbForConsole, $dbForProject, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage)) { return; } } @@ -571,7 +747,10 @@ App::options() ->inject('request') ->inject('response') ->inject('dbForConsole') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole) { + ->inject('dbForProject') + ->inject('queueForEvents') + ->inject('queueForUsage') + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, Database $dbForProject, Event $queueForEvents, Usage $queueForUsage) { /* * Appwrite Router */ @@ -579,7 +758,7 @@ App::options() $mainDomain = App::getEnv('_APP_DOMAIN', ''); // Only run Router when external domain if ($host !== $mainDomain) { - if (router($utopia, $dbForConsole, $swooleRequest, $request, $response)) { + if (router($utopia, $dbForConsole, $dbForProject, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage)) { return; } } From e3c261b9e49076787b9b08112d6a51a3466a085a Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:24:55 +0000 Subject: [PATCH 15/38] fix: msg91 --- composer.json | 2 +- composer.lock | 48 ++++++++++----------- src/Appwrite/Platform/Workers/Messaging.php | 31 +++++++++---- 3 files changed, 48 insertions(+), 33 deletions(-) diff --git a/composer.json b/composer.json index 41baf29d9..795fb4ad3 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ "utopia-php/image": "0.5.*", "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.3.*", - "utopia-php/messaging": "0.2.*", + "utopia-php/messaging": "0.3.*", "utopia-php/migration": "0.3.*", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.5.*", diff --git a/composer.lock b/composer.lock index ca39c7d08..d30ad5f44 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fd03f97115d752d1a94b533ccf570109", + "content-hash": "a8c299cb631eb98bbcf0a58a815f74ac", "packages": [ { "name": "adhocore/jwt", @@ -1551,16 +1551,16 @@ }, { "name": "utopia-php/messaging", - "version": "0.2.0", + "version": "0.3.0", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "2d0f474a106bb1da285f85e105c29b46085d3a43" + "reference": "d488223876f88f97bb76fd6681fed0df80558f62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/2d0f474a106bb1da285f85e105c29b46085d3a43", - "reference": "2d0f474a106bb1da285f85e105c29b46085d3a43", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/d488223876f88f97bb76fd6681fed0df80558f62", + "reference": "d488223876f88f97bb76fd6681fed0df80558f62", "shasum": "" }, "require": { @@ -1593,9 +1593,9 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/0.2.0" + "source": "https://github.com/utopia-php/messaging/tree/0.3.0" }, - "time": "2023-09-14T20:48:42+00:00" + "time": "2023-11-14T21:02:37+00:00" }, { "name": "utopia-php/migration", @@ -2417,16 +2417,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.36.2", + "version": "0.36.4", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "0aa67479d75f0e0cb7b60454031534d7f0abaece" + "reference": "8d932098009d62d37dda73cfe4ebc11f83e21405" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/0aa67479d75f0e0cb7b60454031534d7f0abaece", - "reference": "0aa67479d75f0e0cb7b60454031534d7f0abaece", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/8d932098009d62d37dda73cfe4ebc11f83e21405", + "reference": "8d932098009d62d37dda73cfe4ebc11f83e21405", "shasum": "" }, "require": { @@ -2462,9 +2462,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/0.36.2" + "source": "https://github.com/appwrite/sdk-generator/tree/0.36.4" }, - "time": "2024-01-19T01:04:35+00:00" + "time": "2024-02-20T16:36:15+00:00" }, { "name": "doctrine/deprecations", @@ -3047,16 +3047,16 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.8.0", + "version": "1.8.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "fad452781b3d774e3337b0c0b245dd8e5a4455fc" + "reference": "bc3dc91a5e9b14aa06d1d9e90647c5c5a2cc5353" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/fad452781b3d774e3337b0c0b245dd8e5a4455fc", - "reference": "fad452781b3d774e3337b0c0b245dd8e5a4455fc", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/bc3dc91a5e9b14aa06d1d9e90647c5c5a2cc5353", + "reference": "bc3dc91a5e9b14aa06d1d9e90647c5c5a2cc5353", "shasum": "" }, "require": { @@ -3099,9 +3099,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.1" }, - "time": "2024-01-11T11:49:22+00:00" + "time": "2024-01-18T19:15:27+00:00" }, { "name": "phpspec/prophecy", @@ -4657,16 +4657,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.8.1", + "version": "3.9.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "14f5fff1e64118595db5408e946f3a22c75807f7" + "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/14f5fff1e64118595db5408e946f3a22c75807f7", - "reference": "14f5fff1e64118595db5408e946f3a22c75807f7", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/d63cee4890a8afaf86a22e51ad4d97c91dd4579b", + "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b", "shasum": "" }, "require": { @@ -4733,7 +4733,7 @@ "type": "open_collective" } ], - "time": "2024-01-11T20:47:48+00:00" + "time": "2024-02-16T15:06:51+00:00" }, { "name": "swoole/ide-helper", diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index 1f3e29c8d..741414898 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -88,14 +88,29 @@ class Messaging extends Action return; } - $sms = match ($this->dsn->getHost()) { - 'mock' => new Mock($this->user, $this->secret), // used for tests - 'twilio' => new Twilio($this->user, $this->secret), - 'text-magic' => new TextMagic($this->user, $this->secret), - 'telesign' => new Telesign($this->user, $this->secret), - 'msg91' => new Msg91($this->user, $this->secret), - 'vonage' => new Vonage($this->user, $this->secret), - default => null + + switch ($this->dsn->getHost()) { + case 'mock': + $sms = new Mock($this->user, $this->secret); // used for tests + break; + case 'twilio': + $sms = new Twilio($this->user, $this->secret); + break; + case 'text-magic': + $sms = new TextMagic($this->user, $this->secret); + break; + case 'telesign': + $sms = new Telesign($this->user, $this->secret); + break; + case 'msg91': + $sms = new Msg91($this->user, $this->secret); + $sms->setTemplate($this->dsn->getParam('template')); + break; + case 'vonage': + $sms = new Vonage($this->user, $this->secret); + break; + default: + $sms = null; }; if (empty(App::getEnv('_APP_SMS_PROVIDER'))) { From b2e1c162d25addd2bd7b3a4b97e5b96877fb6471 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:27:37 +0000 Subject: [PATCH 16/38] chore: fmt --- src/Appwrite/Platform/Workers/Messaging.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index 741414898..89d028b1a 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -88,14 +88,14 @@ class Messaging extends Action return; } - + switch ($this->dsn->getHost()) { case 'mock': $sms = new Mock($this->user, $this->secret); // used for tests - break; + break; case 'twilio': $sms = new Twilio($this->user, $this->secret); - break; + break; case 'text-magic': $sms = new TextMagic($this->user, $this->secret); break; From 6567cff5dc2550ab279c8511219a9740cd3eb13f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 21 Feb 2024 17:03:48 +0000 Subject: [PATCH 17/38] Fix missing todo --- app/controllers/general.php | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index e54d6c21e..4486aa6d6 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -9,8 +9,6 @@ use Utopia\Logger\Logger; use Utopia\Logger\Log; use Utopia\Logger\Log\User; use Swoole\Http\Request as SwooleRequest; -use Utopia\Cache\Cache; -use Utopia\Pools\Group; use Appwrite\Utopia\Request; use MaxMind\Db\Reader; use Appwrite\Utopia\Response; @@ -50,11 +48,7 @@ Config::setParam('domainVerification', false); Config::setParam('cookieDomain', 'localhost'); Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); - -->inject('geodb') -Reader $geodb - -function router(App $utopia, Database $dbForConsole, Database $dbForProject, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, Usage $queueForUsage) +function router(App $utopia, Database $dbForConsole, Database $dbForProject, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) { $utopia->getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml'); @@ -387,7 +381,8 @@ App::init() ->inject('queueForCertificates') ->inject('queueForEvents') ->inject('queueForUsage') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Database $dbForProject, Document $user, Locale $locale, array $localeCodes, array $clients, array $servers, Certificate $queueForCertificates, Event $queueForEvents, Usage $queueForUsage) { + ->inject('geodb') + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Database $dbForProject, Document $user, Locale $locale, array $localeCodes, array $clients, array $servers, Certificate $queueForCertificates, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) { /* * Appwrite Router */ @@ -396,7 +391,7 @@ App::init() $mainDomain = App::getEnv('_APP_DOMAIN', ''); // Only run Router when external domain if ($host !== $mainDomain) { - if (router($utopia, $dbForConsole, $dbForProject, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage)) { + if (router($utopia, $dbForConsole, $dbForProject, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) { return; } } @@ -750,7 +745,8 @@ App::options() ->inject('dbForProject') ->inject('queueForEvents') ->inject('queueForUsage') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, Database $dbForProject, Event $queueForEvents, Usage $queueForUsage) { + ->inject('geodb') + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, Database $dbForProject, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) { /* * Appwrite Router */ @@ -758,7 +754,7 @@ App::options() $mainDomain = App::getEnv('_APP_DOMAIN', ''); // Only run Router when external domain if ($host !== $mainDomain) { - if (router($utopia, $dbForConsole, $dbForProject, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage)) { + if (router($utopia, $dbForConsole, $dbForProject, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) { return; } } From b03783c588f042805800886ad524962f265b0c5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 21 Feb 2024 18:14:24 +0100 Subject: [PATCH 18/38] Fix bigs until works --- app/controllers/general.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 4486aa6d6..d9f6e5f28 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -48,7 +48,7 @@ Config::setParam('domainVerification', false); Config::setParam('cookieDomain', 'localhost'); Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); -function router(App $utopia, Database $dbForConsole, Database $dbForProject, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) +function router(App $utopia, Database $dbForConsole, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) { $utopia->getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml'); @@ -126,6 +126,10 @@ function router(App $utopia, Database $dbForConsole, Database $dbForProject, Swo $requestHeaders = $request->getHeaders(); + $project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId)); + + $dbForProject = $getProjectDB($project); + $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); if ($function->isEmpty() || !$function->getAttribute('enabled')) { @@ -372,7 +376,7 @@ App::init() ->inject('console') ->inject('project') ->inject('dbForConsole') - ->inject('dbForProject') + ->inject('getProjectDB') ->inject('user') ->inject('locale') ->inject('localeCodes') @@ -382,7 +386,7 @@ App::init() ->inject('queueForEvents') ->inject('queueForUsage') ->inject('geodb') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Database $dbForProject, Document $user, Locale $locale, array $localeCodes, array $clients, array $servers, Certificate $queueForCertificates, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) { + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, callable $getProjectDB, Document $user, Locale $locale, array $localeCodes, array $clients, array $servers, Certificate $queueForCertificates, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) { /* * Appwrite Router */ @@ -391,7 +395,7 @@ App::init() $mainDomain = App::getEnv('_APP_DOMAIN', ''); // Only run Router when external domain if ($host !== $mainDomain) { - if (router($utopia, $dbForConsole, $dbForProject, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) { + if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) { return; } } @@ -742,11 +746,11 @@ App::options() ->inject('request') ->inject('response') ->inject('dbForConsole') - ->inject('dbForProject') + ->inject('getProjectDB') ->inject('queueForEvents') ->inject('queueForUsage') ->inject('geodb') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, Database $dbForProject, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) { + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) { /* * Appwrite Router */ @@ -754,7 +758,7 @@ App::options() $mainDomain = App::getEnv('_APP_DOMAIN', ''); // Only run Router when external domain if ($host !== $mainDomain) { - if (router($utopia, $dbForConsole, $dbForProject, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) { + if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) { return; } } From a8098831a366532b61eef591602555774b6a30ef Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Thu, 22 Feb 2024 12:21:48 +0530 Subject: [PATCH 19/38] Revert "usage/usage-dump queue health endpoints" --- .env | 2 +- app/config/collections.php | 28 +--- app/controllers/api/account.php | 2 +- app/controllers/api/avatars.php | 2 +- app/controllers/api/databases.php | 12 +- app/controllers/api/functions.php | 8 +- app/controllers/api/health.php | 55 -------- app/controllers/api/project.php | 8 +- app/controllers/api/storage.php | 9 +- app/controllers/api/users.php | 9 +- app/controllers/shared/api.php | 35 +++-- app/init.php | 2 +- docker-compose.yml | 30 +++-- .../references/health/get-queue-usage-dump.md | 1 - docs/references/health/get-queue-usage.md | 1 - src/Appwrite/Event/Delete.php | 15 --- src/Appwrite/Platform/Tasks/CalcTierStats.php | 2 +- .../Platform/Tasks/CreateInfMetric.php | 6 +- src/Appwrite/Platform/Workers/Deletes.php | 122 ++++++++++++++---- src/Appwrite/Platform/Workers/Hamster.php | 2 +- src/Appwrite/Platform/Workers/Usage.php | 24 ++-- src/Appwrite/Platform/Workers/UsageDump.php | 6 +- .../Specification/Format/OpenAPI3.php | 6 +- .../Specification/Format/Swagger2.php | 6 +- .../Health/HealthCustomServerTest.php | 48 ------- 25 files changed, 188 insertions(+), 253 deletions(-) delete mode 100644 docs/references/health/get-queue-usage-dump.md delete mode 100644 docs/references/health/get-queue-usage.md diff --git a/.env b/.env index 2373b618c..b915f9151 100644 --- a/.env +++ b/.env @@ -78,7 +78,7 @@ _APP_MAINTENANCE_RETENTION_CACHE=2592000 _APP_MAINTENANCE_RETENTION_EXECUTION=1209600 _APP_MAINTENANCE_RETENTION_ABUSE=86400 _APP_MAINTENANCE_RETENTION_AUDIT=1209600 -_APP_USAGE_AGGREGATION_INTERVAL=30 +_APP_USAGE_AGGREGATION_INTERVAL=20 _APP_MAINTENANCE_RETENTION_USAGE_HOURLY=8640000 _APP_MAINTENANCE_RETENTION_SCHEDULES=86400 _APP_USAGE_STATS=enabled diff --git a/app/config/collections.php b/app/config/collections.php index 3bc58eff5..e2f3c11be 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -34,28 +34,6 @@ $commonCollections = [ 'array' => false, 'filters' => [], ], - [ - '$id' => 'resourceType', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => 255, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => ID::custom('mimeType'), - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => 255, // https://tools.ietf.org/html/rfc4288#section-4.2 - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], [ '$id' => 'accessedAt', 'type' => Database::VAR_DATETIME, @@ -1349,10 +1327,10 @@ $commonCollections = [ ] ], - 'stats' => [ + 'stats_v2' => [ '$collection' => ID::custom(Database::METADATA), - '$id' => ID::custom('stats'), - 'name' => 'stats', + '$id' => ID::custom('stats_v2'), + 'name' => 'stats_v2', 'attributes' => [ [ '$id' => ID::custom('metric'), diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 17b298447..68d2a544d 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -1791,7 +1791,7 @@ App::get('/v1/account/logs') } $response->dynamic(new Document([ - 'total' => $audit->countLogsByUser($user->getInternalId()), + 'total' => $audit->countLogsByUser($user->getId()), 'logs' => $output, ]), Response::MODEL_LOG_LIST); }); diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index 5f2ea4c2d..e0d967eb0 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -76,7 +76,7 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro } if (empty($gitHubSession)) { - throw new Exception(Exception::USER_SESSION_NOT_FOUND, 'GitHub session not found.'); + throw new Exception(Exception::GENERAL_UNKNOWN, 'GitHub session not found.'); } $provider = $gitHubSession->getAttribute('provider', ''); diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 5fdb2edb2..8684a7fae 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -3563,7 +3563,7 @@ App::get('/v1/databases/usage') Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { - $result = $dbForProject->findOne('stats', [ + $result = $dbForProject->findOne('stats_v2', [ Query::equal('metric', [$metric]), Query::equal('period', ['inf']) ]); @@ -3571,7 +3571,7 @@ App::get('/v1/databases/usage') $stats[$metric]['total'] = $result['value'] ?? 0; $limit = $days['limit']; $period = $days['period']; - $results = $dbForProject->find('stats', [ + $results = $dbForProject->find('stats_v2', [ Query::equal('metric', [$metric]), Query::equal('period', [$period]), Query::limit($limit), @@ -3647,7 +3647,7 @@ App::get('/v1/databases/:databaseId/usage') Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { - $result = $dbForProject->findOne('stats', [ + $result = $dbForProject->findOne('stats_v2', [ Query::equal('metric', [$metric]), Query::equal('period', ['inf']) ]); @@ -3655,7 +3655,7 @@ App::get('/v1/databases/:databaseId/usage') $stats[$metric]['total'] = $result['value'] ?? 0; $limit = $days['limit']; $period = $days['period']; - $results = $dbForProject->find('stats', [ + $results = $dbForProject->find('stats_v2', [ Query::equal('metric', [$metric]), Query::equal('period', [$period]), Query::limit($limit), @@ -3733,7 +3733,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/usage') Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { - $result = $dbForProject->findOne('stats', [ + $result = $dbForProject->findOne('stats_v2', [ Query::equal('metric', [$metric]), Query::equal('period', ['inf']) ]); @@ -3741,7 +3741,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/usage') $stats[$metric]['total'] = $result['value'] ?? 0; $limit = $days['limit']; $period = $days['period']; - $results = $dbForProject->find('stats', [ + $results = $dbForProject->find('stats_v2', [ Query::equal('metric', [$metric]), Query::equal('period', [$period]), Query::limit($limit), diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 45df54b70..d3913d180 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -484,7 +484,7 @@ App::get('/v1/functions/:functionId/usage') Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { - $result = $dbForProject->findOne('stats', [ + $result = $dbForProject->findOne('stats_v2', [ Query::equal('metric', [$metric]), Query::equal('period', ['inf']) ]); @@ -492,7 +492,7 @@ App::get('/v1/functions/:functionId/usage') $stats[$metric]['total'] = $result['value'] ?? 0; $limit = $days['limit']; $period = $days['period']; - $results = $dbForProject->find('stats', [ + $results = $dbForProject->find('stats_v2', [ Query::equal('metric', [$metric]), Query::equal('period', [$period]), Query::limit($limit), @@ -576,7 +576,7 @@ App::get('/v1/functions/usage') Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $metric) { - $result = $dbForProject->findOne('stats', [ + $result = $dbForProject->findOne('stats_v2', [ Query::equal('metric', [$metric]), Query::equal('period', ['inf']) ]); @@ -584,7 +584,7 @@ App::get('/v1/functions/usage') $stats[$metric]['total'] = $result['value'] ?? 0; $limit = $days['limit']; $period = $days['period']; - $results = $dbForProject->find('stats', [ + $results = $dbForProject->find('stats_v2', [ Query::equal('metric', [$metric]), Query::equal('period', [$period]), Query::limit($limit), diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index f7fea04fe..c007d629b 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -659,60 +659,6 @@ App::get('/v1/health/queue/functions') $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); }, ['response']); -App::get('/v1/health/queue/usage') - ->desc('Get usage queue') - ->groups(['api', 'health']) - ->label('scope', 'health.read') - ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) - ->label('sdk.namespace', 'health') - ->label('sdk.method', 'getQueueUsage') - ->label('sdk.description', '/docs/references/health/get-queue-usage.md') - ->label('sdk.response.code', Response::STATUS_CODE_OK) - ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_HEALTH_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('queue') - ->inject('response') - ->action(function (int|string $threshold, Connection $queue, Response $response) { - $threshold = \intval($threshold); - - $client = new Client(Event::USAGE_QUEUE_NAME, $queue); - $size = $client->getQueueSize(); - - if ($size >= $threshold) { - throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}."); - } - - $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); - }); - -App::get('/v1/health/queue/usage-dump') - ->desc('Get usage dump queue') - ->groups(['api', 'health']) - ->label('scope', 'health.read') - ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) - ->label('sdk.namespace', 'health') - ->label('sdk.method', 'getQueueUsage') - ->label('sdk.description', '/docs/references/health/get-queue-usage-dump.md') - ->label('sdk.response.code', Response::STATUS_CODE_OK) - ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_HEALTH_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('queue') - ->inject('response') - ->action(function (int|string $threshold, Connection $queue, Response $response) { - $threshold = \intval($threshold); - - $client = new Client(Event::USAGE_DUMP_QUEUE_NAME, $queue); - $size = $client->getQueueSize(); - - if ($size >= $threshold) { - throw new Exception(Exception::HEALTH_QUEUE_SIZE_EXCEEDED, "Queue size threshold hit. Current size is {$size} and threshold is {$threshold}."); - } - - $response->dynamic(new Document([ 'size' => $size ]), Response::MODEL_HEALTH_QUEUE); - }); - App::get('/v1/health/storage/local') ->desc('Get local storage') ->groups(['api', 'health']) @@ -850,7 +796,6 @@ App::get('/v1/health/queue/failed/:name') Event::MAILS_QUEUE_NAME, Event::FUNCTIONS_QUEUE_NAME, Event::USAGE_QUEUE_NAME, - Event::USAGE_DUMP_QUEUE_NAME, Event::WEBHOOK_CLASS_NAME, Event::CERTIFICATES_QUEUE_NAME, Event::BUILDS_QUEUE_NAME, diff --git a/app/controllers/api/project.php b/app/controllers/api/project.php index a067c4558..47b94d309 100644 --- a/app/controllers/api/project.php +++ b/app/controllers/api/project.php @@ -73,7 +73,7 @@ App::get('/v1/project/usage') Authorization::skip(function () use ($dbForProject, $firstDay, $lastDay, $period, $metrics, &$total, &$stats) { foreach ($metrics['total'] as $metric) { - $result = $dbForProject->findOne('stats', [ + $result = $dbForProject->findOne('stats_v2', [ Query::equal('metric', [$metric]), Query::equal('period', ['inf']) ]); @@ -81,7 +81,7 @@ App::get('/v1/project/usage') } foreach ($metrics['period'] as $metric) { - $results = $dbForProject->find('stats', [ + $results = $dbForProject->find('stats_v2', [ Query::equal('metric', [$metric]), Query::equal('period', [$period]), Query::greaterThanEqual('time', $firstDay), @@ -116,7 +116,7 @@ App::get('/v1/project/usage') $id = $function->getId(); $name = $function->getAttribute('name'); $metric = str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS); - $value = $dbForProject->findOne('stats', [ + $value = $dbForProject->findOne('stats_v2', [ Query::equal('metric', [$metric]), Query::equal('period', ['inf']) ]); @@ -132,7 +132,7 @@ App::get('/v1/project/usage') $id = $bucket->getId(); $name = $bucket->getAttribute('name'); $metric = str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES_STORAGE); - $value = $dbForProject->findOne('stats', [ + $value = $dbForProject->findOne('stats_v2', [ Query::equal('metric', [$metric]), Query::equal('period', ['inf']) ]); diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index e4798fb51..efe251546 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -1467,7 +1467,6 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId') if ($deviceDeleted) { $queueForDeletes ->setType(DELETE_TYPE_CACHE_BY_RESOURCE) - ->setResourceType('bucket/' . $bucket->getId()) ->setResource('file/' . $fileId) ; @@ -1525,7 +1524,7 @@ App::get('/v1/storage/usage') $total = []; Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) { foreach ($metrics as $metric) { - $result = $dbForProject->findOne('stats', [ + $result = $dbForProject->findOne('stats_v2', [ Query::equal('metric', [$metric]), Query::equal('period', ['inf']) ]); @@ -1533,7 +1532,7 @@ App::get('/v1/storage/usage') $stats[$metric]['total'] = $result['value'] ?? 0; $limit = $days['limit']; $period = $days['period']; - $results = $dbForProject->find('stats', [ + $results = $dbForProject->find('stats_v2', [ Query::equal('metric', [$metric]), Query::equal('period', [$period]), Query::limit($limit), @@ -1610,7 +1609,7 @@ App::get('/v1/storage/:bucketId/usage') Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) { foreach ($metrics as $metric) { - $result = $dbForProject->findOne('stats', [ + $result = $dbForProject->findOne('stats_v2', [ Query::equal('metric', [$metric]), Query::equal('period', ['inf']) ]); @@ -1618,7 +1617,7 @@ App::get('/v1/storage/:bucketId/usage') $stats[$metric]['total'] = $result['value'] ?? 0; $limit = $days['limit']; $period = $days['period']; - $results = $dbForProject->find('stats', [ + $results = $dbForProject->find('stats_v2', [ Query::equal('metric', [$metric]), Query::equal('period', [$period]), Query::limit($limit), diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index bbeef96f1..5ce2263f4 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -615,9 +615,6 @@ App::get('/v1/users/:userId/logs') $output[$i] = new Document([ 'event' => $log['event'], - 'userId' => ID::custom($log['data']['userId']), - 'userEmail' => $log['data']['userEmail'] ?? null, - 'userName' => $log['data']['userName'] ?? null, 'ip' => $log['ip'], 'time' => $log['time'], 'osCode' => $os['osCode'], @@ -646,7 +643,7 @@ App::get('/v1/users/:userId/logs') } $response->dynamic(new Document([ - 'total' => $audit->countLogsByUser($user->getInternalId()), + 'total' => $audit->countLogsByUser($user->getId()), 'logs' => $output, ]), Response::MODEL_LOG_LIST); }); @@ -1266,7 +1263,7 @@ App::get('/v1/users/usage') Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { foreach ($metrics as $count => $metric) { - $result = $dbForProject->findOne('stats', [ + $result = $dbForProject->findOne('stats_v2', [ Query::equal('metric', [$metric]), Query::equal('period', ['inf']) ]); @@ -1274,7 +1271,7 @@ App::get('/v1/users/usage') $stats[$metric]['total'] = $result['value'] ?? 0; $limit = $days['limit']; $period = $days['period']; - $results = $dbForProject->find('stats', [ + $results = $dbForProject->find('stats_v2', [ Query::equal('metric', [$metric]), Query::equal('period', [$period]), Query::limit($limit), diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 0423741e6..df6ec002c 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -252,22 +252,24 @@ App::init() ; $useCache = $route->getLabel('cache', false); + if ($useCache) { $key = md5($request->getURI() . implode('*', $request->getParams()) . '*' . APP_CACHE_BUSTER); - $cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key)); $cache = new Cache( new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId()) ); $timestamp = 60 * 60 * 24 * 30; $data = $cache->load($key, $timestamp); - if (!empty($data) && !$cacheLog->isEmpty()) { - $parts = explode('/', $cacheLog->getAttribute('resourceType')); + if (!empty($data)) { + $data = json_decode($data, true); + $parts = explode('/', $data['resourceType']); $type = $parts[0] ?? null; if ($type === 'bucket') { $bucketId = $parts[1] ?? null; - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + + $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); $isAPIKey = Auth::isAppUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); @@ -279,12 +281,11 @@ App::init() $fileSecurity = $bucket->getAttribute('fileSecurity', false); $validator = new Authorization(Database::PERMISSION_READ); $valid = $validator->isValid($bucket->getRead()); - if (!$fileSecurity && !$valid) { throw new Exception(Exception::USER_UNAUTHORIZED); } - $parts = explode('/', $cacheLog->getAttribute('resource')); + $parts = explode('/', $data['resource']); $fileId = $parts[1] ?? null; if ($fileSecurity && !$valid) { @@ -301,8 +302,8 @@ App::init() $response ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $timestamp) . ' GMT') ->addHeader('X-Appwrite-Cache', 'hit') - ->setContentType($cacheLog->getAttribute('mimeType')) - ->send($data) + ->setContentType($data['contentType']) + ->send(base64_decode($data['payload'])) ; } else { $response->addHeader('X-Appwrite-Cache', 'miss'); @@ -533,6 +534,7 @@ App::shutdown() if ($useCache) { $resource = $resourceType = null; $data = $response->getPayload(); + if (!empty($data['payload'])) { $pattern = $route->getLabel('cache.resource', null); if (!empty($pattern)) { @@ -544,17 +546,22 @@ App::shutdown() $resourceType = $parseLabel($pattern, $responsePayload, $requestParams, $user); } - $key = md5($request->getURI() . '*' . implode('*', $request->getParams())) . '*' . APP_CACHE_BUSTER; - $signature = md5($data['payload']); - $cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key)); + $key = md5($request->getURI() . implode('*', $request->getParams()) . '*' . APP_CACHE_BUSTER); + $data = json_encode([ + 'resourceType' => $resourceType, + 'resource' => $resource, + 'contentType' => $response->getContentType(), + 'payload' => base64_encode($data['payload']), + ]) ; + + $signature = md5($data); + $cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key)); $accessedAt = $cacheLog->getAttribute('accessedAt', ''); $now = DateTime::now(); if ($cacheLog->isEmpty()) { Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([ '$id' => $key, 'resource' => $resource, - 'resourceType' => $resourceType, - 'mimeType' => $response->getContentType(), 'accessedAt' => $now, 'signature' => $signature, ]))); @@ -567,7 +574,7 @@ App::shutdown() $cache = new Cache( new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId()) ); - $cache->save($key, $data['payload']); + $cache->save($key, $data); } } } diff --git a/app/init.php b/app/init.php index f27da88d2..80c3670ee 100644 --- a/app/init.php +++ b/app/init.php @@ -112,7 +112,7 @@ const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return const APP_KEY_ACCCESS = 24 * 60 * 60; // 24 hours const APP_USER_ACCCESS = 24 * 60 * 60; // 24 hours const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours -const APP_CACHE_BUSTER = 330; +const APP_CACHE_BUSTER = 329; const APP_VERSION_STABLE = '1.4.13'; const APP_DATABASE_ATTRIBUTE_EMAIL = 'email'; const APP_DATABASE_ATTRIBUTE_ENUM = 'enum'; diff --git a/docker-compose.yml b/docker-compose.yml index ed912dc8e..de71e3937 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -928,7 +928,6 @@ services: # - appwrite # volumes: # - appwrite-uploads:/storage/uploads - # Dev Tools Start ------------------------------------------------------------------------------------------ # # The Appwrite Team uses the following tools to help debug, monitor and diagnose the Appwrite stack @@ -937,9 +936,8 @@ services: # # MailCatcher - An SMTP server. Catches all system emails and displays them in a nice UI. # RequestCatcher - An HTTP server. Catches all system https calls and displays them using a simple HTTP API. Used to debug & tests webhooks and HTTP tasks - # Redis Insight - A nice UI for exploring Redis data - # Adminer - A nice UI for exploring MariaDB data - # GraphQl Explorer - A nice UI for exploring GraphQL API + # RedisCommander - A nice UI for exploring Redis data + # Webgrind - A nice UI for exploring and debugging code-level stuff maildev: # used mainly for dev tests image: appwrite/mailcatcher:1.0.0 @@ -969,15 +967,21 @@ services: networks: - appwrite - redis-insight: - image: redis/redisinsight:latest - restart: unless-stopped - networks: - - appwrite - environment: - - REDIS_HOSTS=redis - ports: - - "8081:5540" + # redis-commander: + # image: rediscommander/redis-commander:latest + # restart: unless-stopped + # networks: + # - appwrite + # environment: + # - REDIS_HOSTS=redis + # ports: + # - "8081:8081" + # webgrind: + # image: 'jokkedk/webgrind:latest' + # volumes: + # - './debug:/tmp' + # ports: + # - '3001:80' graphql-explorer: container_name: appwrite-graphql-explorer diff --git a/docs/references/health/get-queue-usage-dump.md b/docs/references/health/get-queue-usage-dump.md deleted file mode 100644 index 3c95da1b8..000000000 --- a/docs/references/health/get-queue-usage-dump.md +++ /dev/null @@ -1 +0,0 @@ -Get the number of projects containing metrics that are waiting to be processed in the Appwrite internal queue server. \ No newline at end of file diff --git a/docs/references/health/get-queue-usage.md b/docs/references/health/get-queue-usage.md deleted file mode 100644 index 8e5b64e64..000000000 --- a/docs/references/health/get-queue-usage.md +++ /dev/null @@ -1 +0,0 @@ -Get the number of metrics that are waiting to be processed in the Appwrite internal queue server. \ No newline at end of file diff --git a/src/Appwrite/Event/Delete.php b/src/Appwrite/Event/Delete.php index 064fbcefa..57300feb7 100644 --- a/src/Appwrite/Event/Delete.php +++ b/src/Appwrite/Event/Delete.php @@ -10,7 +10,6 @@ class Delete extends Event { protected string $type = ''; protected ?Document $document = null; - protected ?string $resourceType = null; protected ?string $resource = null; protected ?string $datetime = null; protected ?string $hourlyUsageRetentionDatetime = null; @@ -108,19 +107,6 @@ class Delete extends Event return $this; } - /** - * Sets the resource type for the delete event. - * - * @param string $resourceType - * @return self - */ - public function setResourceType(string $resourceType): self - { - $this->resourceType = $resourceType; - - return $this; - } - /** * Returns the set document for the delete event. * @@ -147,7 +133,6 @@ class Delete extends Event 'type' => $this->type, 'document' => $this->document, 'resource' => $this->resource, - 'resourceType' => $this->resourceType, 'datetime' => $this->datetime, 'hourlyUsageRetentionDatetime' => $this->hourlyUsageRetentionDatetime ]); diff --git a/src/Appwrite/Platform/Tasks/CalcTierStats.php b/src/Appwrite/Platform/Tasks/CalcTierStats.php index 05a28b418..2c904973a 100644 --- a/src/Appwrite/Platform/Tasks/CalcTierStats.php +++ b/src/Appwrite/Platform/Tasks/CalcTierStats.php @@ -270,7 +270,7 @@ class CalcTierStats extends Action $limit = $periods[$range]['limit']; $period = $periods[$range]['period']; - $requestDocs = $dbForProject->find('stats', [ + $requestDocs = $dbForProject->find('stats_v2', [ Query::equal('metric', [$metric]), Query::equal('period', [$period]), Query::limit($limit), diff --git a/src/Appwrite/Platform/Tasks/CreateInfMetric.php b/src/Appwrite/Platform/Tasks/CreateInfMetric.php index 4b3f0e89f..49b852ff6 100644 --- a/src/Appwrite/Platform/Tasks/CreateInfMetric.php +++ b/src/Appwrite/Platform/Tasks/CreateInfMetric.php @@ -167,8 +167,8 @@ class CreateInfMetric extends Action try { $id = \md5("_inf_{$metric}"); - $dbForProject->deleteDocument('stats', $id); - $dbForProject->createDocument('stats', new Document([ + $dbForProject->deleteDocument('stats_v2', $id); + $dbForProject->createDocument('stats_v2', new Document([ '$id' => $id, 'metric' => $metric, 'period' => 'inf', @@ -190,7 +190,7 @@ class CreateInfMetric extends Action protected function getFromMetric(database $dbForProject, string $metric): int|float { - return $dbForProject->sum('stats', 'value', [ + return $dbForProject->sum('stats_v2', 'value', [ Query::equal('metric', [ $metric, ]), diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 37975979c..96cb2d80c 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -68,13 +68,18 @@ class Deletes extends Action $datetime = $payload['datetime'] ?? null; $hourlyUsageRetentionDatetime = $payload['hourlyUsageRetentionDatetime'] ?? null; $resource = $payload['resource'] ?? null; - $resourceType = $payload['resourceType'] ?? null; $document = new Document($payload['document'] ?? []); $project = new Document($payload['project'] ?? []); switch (strval($type)) { case DELETE_TYPE_DOCUMENT: switch ($document->getCollection()) { + case DELETE_TYPE_DATABASES: + $this->deleteDatabase($getProjectDB, $document, $project); + break; + case DELETE_TYPE_COLLECTIONS: + $this->deleteCollection($getProjectDB, $document, $project); + break; case DELETE_TYPE_PROJECTS: $this->deleteProject($dbForConsole, $getProjectDB, $getFilesDevice, $getFunctionsDevice, $getBuildsDevice, $getCacheDevice, $document); break; @@ -103,6 +108,10 @@ class Deletes extends Action $this->deleteRule($dbForConsole, $document); break; default: + if (\str_starts_with($document->getCollection(), 'database_')) { + $this->deleteCollection($getProjectDB, $document, $project); + break; + } Console::error('No lazy delete operation available for document of type: ' . $document->getCollection()); break; } @@ -136,7 +145,7 @@ class Deletes extends Action $this->deleteUsageStats($project, $getProjectDB, $hourlyUsageRetentionDatetime); break; case DELETE_TYPE_CACHE_BY_RESOURCE: - $this->deleteCacheByResource($project, $getProjectDB, $resource, $resourceType); + $this->deleteCacheByResource($project, $getProjectDB, $resource); break; case DELETE_TYPE_CACHE_BY_TIMESTAMP: $this->deleteCacheByDate($project, $getProjectDB, $datetime); @@ -194,37 +203,32 @@ class Deletes extends Action * @param string $resource * @return void * @throws Authorization - * @param string|null $resourceType - * @throws Exception */ - private function deleteCacheByResource(Document $project, callable $getProjectDB, string $resource, string $resourceType = null): void + private function deleteCacheByResource(Document $project, callable $getProjectDB, string $resource): void { $projectId = $project->getId(); $dbForProject = $getProjectDB($project); + $document = $dbForProject->findOne('cache', [Query::equal('resource', [$resource])]); - $cache = new Cache( - new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $projectId) - ); + if ($document) { + $cache = new Cache( + new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $projectId) + ); - $query[] = Query::equal('resource', [$resource]); - if (!empty($resourceType)) { - $query[] = Query::equal('resourceType', [$resourceType]); - } + $this->deleteById( + $document, + $dbForProject, + function ($document) use ($cache, $projectId) { + $path = APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $projectId . DIRECTORY_SEPARATOR . $document->getId(); - $this->deleteByGroup( - 'cache', - $query, - $dbForProject, - function (Document $document) use ($cache, $projectId) { - $path = APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $projectId . DIRECTORY_SEPARATOR . $document->getId(); - - if ($cache->purge($document->getId())) { - Console::success('Deleting cache file: ' . $path); - } else { - Console::error('Failed to delete cache file: ' . $path); + if ($cache->purge($document->getId())) { + Console::success('Deleting cache file: ' . $path); + } else { + Console::error('Failed to delete cache file: ' . $path); + } } - } - ); + ); + } } /** @@ -264,6 +268,72 @@ class Deletes extends Action ); } + /** + * @param callable $getProjectDB + * @param Document $document + * @param Document $project + * @return void + * @throws Exception + */ + private function deleteDatabase(callable $getProjectDB, Document $document, Document $project): void + { + $databaseId = $document->getId(); + $dbForProject = $getProjectDB($project); + + $this->deleteByGroup('database_' . $document->getInternalId(), [], $dbForProject, function ($document) use ($getProjectDB, $project) { + $this->deleteCollection($getProjectDB, $document, $project); + }); + + $dbForProject->deleteCollection('database_' . $document->getInternalId()); + $this->deleteAuditLogsByResource($getProjectDB, 'database/' . $databaseId, $project); + } + + /** + * @param callable $getProjectDB + * @param Document $document teams document + * @param Document $project + * @return void + * @throws Exception + */ + private function deleteCollection(callable $getProjectDB, Document $document, Document $project): void + { + $collectionId = $document->getId(); + $collectionInternalId = $document->getInternalId(); + $databaseId = $document->getAttribute('databaseId'); + $databaseInternalId = $document->getAttribute('databaseInternalId'); + + $dbForProject = $getProjectDB($project); + + $relationships = \array_filter( + $document->getAttribute('attributes'), + fn ($attribute) => $attribute['type'] === Database::VAR_RELATIONSHIP + ); + + foreach ($relationships as $relationship) { + if (!$relationship['twoWay']) { + continue; + } + $relatedCollection = $dbForProject->getDocument('database_' . $databaseInternalId, $relationship['relatedCollection']); + $dbForProject->deleteDocument('attributes', $databaseInternalId . '_' . $relatedCollection->getInternalId() . '_' . $relationship['twoWayKey']); + $dbForProject->deleteCachedDocument('database_' . $databaseInternalId, $relatedCollection->getId()); + $dbForProject->deleteCachedCollection('database_' . $databaseInternalId . '_collection_' . $relatedCollection->getInternalId()); + } + + $dbForProject->deleteCollection('database_' . $databaseInternalId . '_collection_' . $document->getInternalId()); + + $this->deleteByGroup('attributes', [ + Query::equal('databaseInternalId', [$databaseInternalId]), + Query::equal('collectionInternalId', [$collectionInternalId]) + ], $dbForProject); + + $this->deleteByGroup('indexes', [ + Query::equal('databaseInternalId', [$databaseInternalId]), + Query::equal('collectionInternalId', [$collectionInternalId]) + ], $dbForProject); + + $this->deleteAuditLogsByResource($getProjectDB, 'database/' . $databaseId . '/collection/' . $collectionId, $project); + } + /** * @param Database $dbForConsole * @param callable $getProjectDB @@ -275,7 +345,7 @@ class Deletes extends Action { $dbForProject = $getProjectDB($project); // Delete Usage stats - $this->deleteByGroup('stats', [ + $this->deleteByGroup('stats_v2', [ Query::lessThan('time', $hourlyUsageRetentionDatetime), Query::equal('period', ['1h']), ], $dbForProject); diff --git a/src/Appwrite/Platform/Workers/Hamster.php b/src/Appwrite/Platform/Workers/Hamster.php index 6239f842e..0fb705d0f 100644 --- a/src/Appwrite/Platform/Workers/Hamster.php +++ b/src/Appwrite/Platform/Workers/Hamster.php @@ -286,7 +286,7 @@ class Hamster extends Action $limit = $periodValue['limit']; $period = $periodValue['period']; - $requestDocs = $dbForProject->find('stats', [ + $requestDocs = $dbForProject->find('stats_v2', [ Query::equal('period', [$period]), Query::equal('metric', [$metric]), Query::limit($limit), diff --git a/src/Appwrite/Platform/Workers/Usage.php b/src/Appwrite/Platform/Workers/Usage.php index 718dca851..35c0e02ce 100644 --- a/src/Appwrite/Platform/Workers/Usage.php +++ b/src/Appwrite/Platform/Workers/Usage.php @@ -130,8 +130,8 @@ class Usage extends Action } break; case $document->getCollection() === 'databases': // databases - $collections = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getInternalId(), METRIC_DATABASE_ID_COLLECTIONS))); - $documents = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getInternalId(), METRIC_DATABASE_ID_DOCUMENTS))); + $collections = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getInternalId(), METRIC_DATABASE_ID_COLLECTIONS))); + $documents = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getInternalId(), METRIC_DATABASE_ID_DOCUMENTS))); if (!empty($collections['value'])) { $metrics[] = [ 'key' => METRIC_COLLECTIONS, @@ -149,7 +149,7 @@ class Usage extends Action case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections $parts = explode('_', $document->getCollection()); $databaseInternalId = $parts[1] ?? 0; - $documents = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $document->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS))); + $documents = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace(['{databaseInternalId}', '{collectionInternalId}'], [$databaseInternalId, $document->getInternalId()], METRIC_DATABASE_ID_COLLECTION_ID_DOCUMENTS))); if (!empty($documents['value'])) { $metrics[] = [ @@ -164,8 +164,8 @@ class Usage extends Action break; case $document->getCollection() === 'buckets': - $files = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{bucketInternalId}', $document->getInternalId(), METRIC_BUCKET_ID_FILES))); - $storage = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{bucketInternalId}', $document->getInternalId(), METRIC_BUCKET_ID_FILES_STORAGE))); + $files = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{bucketInternalId}', $document->getInternalId(), METRIC_BUCKET_ID_FILES))); + $storage = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{bucketInternalId}', $document->getInternalId(), METRIC_BUCKET_ID_FILES_STORAGE))); if (!empty($files['value'])) { $metrics[] = [ @@ -183,13 +183,13 @@ class Usage extends Action break; case $document->getCollection() === 'functions': - $deployments = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $document->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS))); - $deploymentsStorage = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $document->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE))); - $builds = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_BUILDS))); - $buildsStorage = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_BUILDS_STORAGE))); - $buildsCompute = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE))); - $executions = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS))); - $executionsCompute = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE))); + $deployments = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $document->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS))); + $deploymentsStorage = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $document->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS_STORAGE))); + $builds = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_BUILDS))); + $buildsStorage = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_BUILDS_STORAGE))); + $buildsCompute = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_BUILDS_COMPUTE))); + $executions = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS))); + $executionsCompute = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE))); if (!empty($deployments['value'])) { $metrics[] = [ diff --git a/src/Appwrite/Platform/Workers/UsageDump.php b/src/Appwrite/Platform/Workers/UsageDump.php index 1d296a929..5a4324fd3 100644 --- a/src/Appwrite/Platform/Workers/UsageDump.php +++ b/src/Appwrite/Platform/Workers/UsageDump.php @@ -78,7 +78,7 @@ class UsageDump extends Action $id = \md5("{$time}_{$period}_{$key}"); try { - $dbForProject->createDocument('stats', new Document([ + $dbForProject->createDocument('stats_v2', new Document([ '$id' => $id, 'period' => $period, 'time' => $time, @@ -89,14 +89,14 @@ class UsageDump extends Action } catch (Duplicate $th) { if ($value < 0) { $dbForProject->decreaseDocumentAttribute( - 'stats', + 'stats_v2', $id, 'value', abs($value) ); } else { $dbForProject->increaseDocumentAttribute( - 'stats', + 'stats_v2', $id, 'value', $value diff --git a/src/Appwrite/Specification/Format/OpenAPI3.php b/src/Appwrite/Specification/Format/OpenAPI3.php index ce8623563..863966eed 100644 --- a/src/Appwrite/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/Specification/Format/OpenAPI3.php @@ -295,7 +295,7 @@ class OpenAPI3 extends Format switch ((!empty($validator)) ? \get_class($validator) : '') { case 'Utopia\Validator\Text': $node['schema']['type'] = $validator->getType(); - $node['schema']['x-example'] = '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; + $node['schema']['x-example'] = '[' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . ']'; break; case 'Utopia\Validator\Boolean': $node['schema']['type'] = $validator->getType(); @@ -303,14 +303,14 @@ class OpenAPI3 extends Format break; case 'Utopia\Database\Validator\UID': $node['schema']['type'] = $validator->getType(); - $node['schema']['x-example'] = '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; + $node['schema']['x-example'] = '[' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . ']'; break; case 'Appwrite\Utopia\Database\Validator\CustomId': if ($route->getLabel('sdk.methodType', '') === 'upload') { $node['schema']['x-upload-id'] = true; } $node['schema']['type'] = $validator->getType(); - $node['schema']['x-example'] = '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; + $node['schema']['x-example'] = '[' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . ']'; break; case 'Utopia\Database\Validator\DatetimeValidator': $node['schema']['type'] = $validator->getType(); diff --git a/src/Appwrite/Specification/Format/Swagger2.php b/src/Appwrite/Specification/Format/Swagger2.php index a458c30aa..c6ae7a7ff 100644 --- a/src/Appwrite/Specification/Format/Swagger2.php +++ b/src/Appwrite/Specification/Format/Swagger2.php @@ -297,7 +297,7 @@ class Swagger2 extends Format switch ((!empty($validator)) ? \get_class($validator) : '') { case 'Utopia\Validator\Text': $node['type'] = $validator->getType(); - $node['x-example'] = '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; + $node['x-example'] = '[' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . ']'; break; case 'Utopia\Validator\Boolean': $node['type'] = $validator->getType(); @@ -308,11 +308,11 @@ class Swagger2 extends Format $node['x-upload-id'] = true; } $node['type'] = $validator->getType(); - $node['x-example'] = '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; + $node['x-example'] = '[' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . ']'; break; case 'Utopia\Database\Validator\UID': $node['type'] = $validator->getType(); - $node['x-example'] = '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; + $node['x-example'] = '[' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . ']'; break; case 'Utopia\Database\Validator\DatetimeValidator': $node['type'] = $validator->getType(); diff --git a/tests/e2e/Services/Health/HealthCustomServerTest.php b/tests/e2e/Services/Health/HealthCustomServerTest.php index 091b3bb78..dde89a6a7 100644 --- a/tests/e2e/Services/Health/HealthCustomServerTest.php +++ b/tests/e2e/Services/Health/HealthCustomServerTest.php @@ -512,52 +512,4 @@ class HealthCustomServerTest extends Scope return []; } - - public function testUsageSuccess() - { - /** - * Test for SUCCESS - */ - $response = $this->client->call(Client::METHOD_GET, '/health/queue/usage', 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']['size']); - $this->assertLessThan(100, $response['body']['size']); - - /** - * Test for FAILURE - */ - $response = $this->client->call(Client::METHOD_GET, '/health/queue/usage?threshold=0', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - $this->assertEquals(503, $response['headers']['status-code']); - } - - public function testUsageDumpSuccess() - { - /** - * Test for SUCCESS - */ - $response = $this->client->call(Client::METHOD_GET, '/health/queue/usage-dump', 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']['size']); - $this->assertLessThan(100, $response['body']['size']); - - /** - * Test for FAILURE - */ - $response = $this->client->call(Client::METHOD_GET, '/health/queue/usage-dump?threshold=0', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), []); - $this->assertEquals(503, $response['headers']['status-code']); - } } From d2787c85553154597d6955d064deb85c316a2b44 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Thu, 22 Feb 2024 12:26:01 +0530 Subject: [PATCH 20/38] Revert "Fix: Router CURL" --- app/controllers/general.php | 275 +++++++----------------------------- 1 file changed, 48 insertions(+), 227 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index d9f6e5f28..bfec8b46b 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -9,8 +9,9 @@ use Utopia\Logger\Logger; use Utopia\Logger\Log; use Utopia\Logger\Log\User; use Swoole\Http\Request as SwooleRequest; +use Utopia\Cache\Cache; +use Utopia\Pools\Group; use Appwrite\Utopia\Request; -use MaxMind\Db\Reader; use Appwrite\Utopia\Response; use Appwrite\Utopia\View; use Appwrite\Extend\Exception as AppwriteException; @@ -18,9 +19,6 @@ use Utopia\Config\Config; use Utopia\Domains\Domain; use Appwrite\Auth\Auth; use Appwrite\Event\Certificate; -use Appwrite\Event\Event; -use Appwrite\Event\Usage; -use Appwrite\ID; use Appwrite\Network\Validator\Origin; use Appwrite\Utopia\Response\Filters\V11 as ResponseV11; use Appwrite\Utopia\Response\Filters\V12 as ResponseV12; @@ -40,7 +38,6 @@ use Appwrite\Utopia\Request\Filters\V13 as RequestV13; use Appwrite\Utopia\Request\Filters\V14 as RequestV14; use Appwrite\Utopia\Request\Filters\V15 as RequestV15; use Appwrite\Utopia\Request\Filters\V16 as RequestV16; -use Executor\Executor; use Utopia\Validator\Text; use Utopia\Validator\WhiteList; @@ -48,7 +45,7 @@ Config::setParam('domainVerification', false); Config::setParam('cookieDomain', 'localhost'); Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); -function router(App $utopia, Database $dbForConsole, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) +function router(App $utopia, Database $dbForConsole, SwooleRequest $swooleRequest, Request $request, Response $response) { $utopia->getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml'); @@ -120,226 +117,49 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo $path .= '?' . $query; } - - $body = $swooleRequest->getContent() ?? ''; - $method = $swooleRequest->server['request_method']; - $requestHeaders = $request->getHeaders(); - $project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId)); - - $dbForProject = $getProjectDB($project); - - $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); - - if ($function->isEmpty() || !$function->getAttribute('enabled')) { - throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND); - } - - $version = $function->getAttribute('version', 'v2'); - $runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []); - - $runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null; - - if (\is_null($runtime)) { - throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported'); - } - - $deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', ''))); - - if ($deployment->getAttribute('resourceId') !== $function->getId()) { - throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); - } - - if ($deployment->isEmpty()) { - throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); - } - - /** Check if build has completed */ - $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); - if ($build->isEmpty()) { - throw new AppwriteException(AppwriteException::BUILD_NOT_FOUND); - } - - if ($build->getAttribute('status') !== 'ready') { - throw new AppwriteException(AppwriteException::BUILD_NOT_READY); - } - - $permissions = $function->getAttribute('execute'); - - if (!(\in_array('any', $permissions)) && (\in_array('guests', $permissions))) { - throw new AppwriteException(AppwriteException::USER_UNAUTHORIZED, 'To execute function using domain, execute permissions must include "any" or "guests"'); - } - - $headers = \array_merge([], $requestHeaders); - $headers['x-appwrite-trigger'] = 'http'; - $headers['x-appwrite-user-id'] = ''; - $headers['x-appwrite-user-jwt'] = ''; - $headers['x-appwrite-country-code'] = ''; - $headers['x-appwrite-continent-code'] = ''; - $headers['x-appwrite-continent-eu'] = 'false'; - - $ip = $headers['x-real-ip'] ?? ''; - if (!empty($ip)) { - $record = $geodb->get($ip); - - if ($record) { - $eu = Config::getParam('locale-eu'); - - $headers['x-appwrite-country-code'] = $record['country']['iso_code'] ?? ''; - $headers['x-appwrite-continent-code'] = $record['continent']['code'] ?? ''; - $headers['x-appwrite-continent-eu'] = (\in_array($record['country']['iso_code'], $eu)) ? 'true' : 'false'; - } - } - - $headersFiltered = []; - foreach ($headers as $key => $value) { - if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_REQUEST)) { - $headersFiltered[] = ['name' => $key, 'value' => $value]; - } - } - - $executionId = ID::unique(); - - $execution = new Document([ - '$id' => $executionId, - '$permissions' => [], - 'functionInternalId' => $function->getInternalId(), - 'functionId' => $function->getId(), - 'deploymentInternalId' => $deployment->getInternalId(), - 'deploymentId' => $deployment->getId(), - 'trigger' => 'http', // http / schedule / event - 'status' => 'processing', // waiting / processing / completed / failed - 'responseStatusCode' => 0, - 'responseHeaders' => [], - 'requestPath' => $path, - 'requestMethod' => $method, - 'requestHeaders' => $headersFiltered, - 'errors' => '', - 'logs' => '', - 'duration' => 0.0, - 'search' => implode(' ', [$functionId, $executionId]), + $body = \json_encode([ + 'async' => false, + 'body' => $swooleRequest->getContent() ?? '', + 'method' => $swooleRequest->server['request_method'], + 'path' => $path, + 'headers' => $requestHeaders ]); - $queueForEvents - ->setParam('functionId', $function->getId()) - ->setParam('executionId', $execution->getId()) - ->setContext('function', $function); + $headers = [ + 'Content-Type: application/json', + 'Content-Length: ' . \strlen($body), + 'X-Appwrite-Project: ' . $projectId + ]; - $durationStart = \microtime(true); + $ch = \curl_init(); + \curl_setopt($ch, CURLOPT_URL, "http://localhost/v1/functions/{$functionId}/executions"); + \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); + \curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + \curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + // \curl_setopt($ch, CURLOPT_HEADER, true); + \curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); + \curl_setopt($ch, CURLOPT_TIMEOUT, 30); - $vars = []; + $executionResponse = \curl_exec($ch); + $statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = \curl_error($ch); + $errNo = \curl_errno($ch); - // V2 vars - if ($version === 'v2') { - $vars = \array_merge($vars, [ - 'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '', - 'APPWRITE_FUNCTION_DATA' => $body ?? '', - 'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '', - 'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? '' - ]); + \curl_close($ch); + + if ($errNo !== 0) { + throw new AppwriteException(AppwriteException::GENERAL_ARGUMENT_INVALID, "Internal error: " . $error); } - // Shared vars - foreach ($function->getAttribute('varsProject', []) as $var) { - $vars[$var->getAttribute('key')] = $var->getAttribute('value', ''); + if ($statusCode >= 400) { + $error = \json_decode($executionResponse, true)['message']; + throw new AppwriteException(AppwriteException::GENERAL_ARGUMENT_INVALID, "Execution error: " . $error); } - // Function vars - foreach ($function->getAttribute('vars', []) as $var) { - $vars[$var->getAttribute('key')] = $var->getAttribute('value', ''); - } - - // Appwrite vars - $vars = \array_merge($vars, [ - 'APPWRITE_FUNCTION_ID' => $functionId, - 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name'), - 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(), - 'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(), - 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '', - 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '', - ]); - - /** Execute function */ - $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); - try { - $version = $function->getAttribute('version', 'v2'); - $command = $runtime['startCommand']; - $command = $version === 'v2' ? '' : 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $command . '"'; - $executionResponse = $executor->createExecution( - projectId: $project->getId(), - deploymentId: $deployment->getId(), - body: \strlen($body) > 0 ? $body : null, - variables: $vars, - timeout: $function->getAttribute('timeout', 0), - image: $runtime['image'], - source: $build->getAttribute('path', ''), - entrypoint: $deployment->getAttribute('entrypoint', ''), - version: $version, - path: $path, - method: $method, - headers: $headers, - runtimeEntrypoint: $command, - requestTimeout: 30 - ); - - $headersFiltered = []; - foreach ($executionResponse['headers'] as $key => $value) { - if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_RESPONSE)) { - $headersFiltered[] = ['name' => $key, 'value' => $value]; - } - } - - /** Update execution status */ - $status = $executionResponse['statusCode'] >= 400 ? 'failed' : 'completed'; - $execution->setAttribute('status', $status); - $execution->setAttribute('responseStatusCode', $executionResponse['statusCode']); - $execution->setAttribute('responseHeaders', $headersFiltered); - $execution->setAttribute('logs', $executionResponse['logs']); - $execution->setAttribute('errors', $executionResponse['errors']); - $execution->setAttribute('duration', $executionResponse['duration']); - } catch (\Throwable $th) { - $durationEnd = \microtime(true); - - $execution - ->setAttribute('duration', $durationEnd - $durationStart) - ->setAttribute('status', 'failed') - ->setAttribute('responseStatusCode', 500) - ->setAttribute('errors', $th->getMessage() . '\nError Code: ' . $th->getCode()); - Console::error($th->getMessage()); - } finally { - $queueForUsage - ->addMetric(METRIC_EXECUTIONS, 1) - ->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1) - ->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000)) // per project - ->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function - ; - } - - if ($function->getAttribute('logging')) { - /** @var Document $execution */ - $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); - } - - $execution->setAttribute('logs', ''); - $execution->setAttribute('errors', ''); - - $headers = []; - foreach (($executionResponse['headers'] ?? []) as $key => $value) { - $headers[] = ['name' => $key, 'value' => $value]; - } - - $execution->setAttribute('responseBody', $executionResponse['body'] ?? ''); - $execution->setAttribute('responseHeaders', $headers); - - $body = $execution['responseBody'] ?? ''; - - $encodingKey = \array_search('x-open-runtimes-encoding', \array_column($execution['responseHeaders'], 'name')); - if ($encodingKey !== false) { - if (($execution['responseHeaders'][$encodingKey]['value'] ?? '') === 'base64') { - $body = \base64_decode($body); - } - } + $execution = \json_decode($executionResponse, true); $contentType = 'text/plain'; foreach ($execution['responseHeaders'] as $header) { @@ -350,6 +170,15 @@ function router(App $utopia, Database $dbForConsole, callable $getProjectDB, Swo $response->setHeader($header['name'], $header['value']); } + $body = $execution['responseBody'] ?? ''; + + $encodingKey = \array_search('x-open-runtimes-encoding', \array_column($execution['responseHeaders'], 'name')); + if ($encodingKey !== false) { + if (($execution['responseHeaders'][$encodingKey]['value'] ?? '') === 'base64') { + $body = \base64_decode($body); + } + } + $response ->setContentType($contentType) ->setStatusCode($execution['responseStatusCode'] ?? 200) @@ -376,17 +205,13 @@ App::init() ->inject('console') ->inject('project') ->inject('dbForConsole') - ->inject('getProjectDB') ->inject('user') ->inject('locale') ->inject('localeCodes') ->inject('clients') ->inject('servers') ->inject('queueForCertificates') - ->inject('queueForEvents') - ->inject('queueForUsage') - ->inject('geodb') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, callable $getProjectDB, Document $user, Locale $locale, array $localeCodes, array $clients, array $servers, Certificate $queueForCertificates, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) { + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Document $user, Locale $locale, array $localeCodes, array $clients, array $servers, Certificate $queueForCertificates) { /* * Appwrite Router */ @@ -395,7 +220,7 @@ App::init() $mainDomain = App::getEnv('_APP_DOMAIN', ''); // Only run Router when external domain if ($host !== $mainDomain) { - if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) { + if (router($utopia, $dbForConsole, $swooleRequest, $request, $response)) { return; } } @@ -746,11 +571,7 @@ App::options() ->inject('request') ->inject('response') ->inject('dbForConsole') - ->inject('getProjectDB') - ->inject('queueForEvents') - ->inject('queueForUsage') - ->inject('geodb') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) { + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole) { /* * Appwrite Router */ @@ -758,7 +579,7 @@ App::options() $mainDomain = App::getEnv('_APP_DOMAIN', ''); // Only run Router when external domain if ($host !== $mainDomain) { - if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) { + if (router($utopia, $dbForConsole, $swooleRequest, $request, $response)) { return; } } From c04f54da647aead11c0d9adeea7c4077e5f78546 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Thu, 22 Feb 2024 15:10:18 +0530 Subject: [PATCH 21/38] Revert "Revert "Fix: Router CURL"" --- app/controllers/general.php | 267 ++++++++++++++++++++++++++++++------ 1 file changed, 223 insertions(+), 44 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index bfec8b46b..d9f6e5f28 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -9,9 +9,8 @@ use Utopia\Logger\Logger; use Utopia\Logger\Log; use Utopia\Logger\Log\User; use Swoole\Http\Request as SwooleRequest; -use Utopia\Cache\Cache; -use Utopia\Pools\Group; use Appwrite\Utopia\Request; +use MaxMind\Db\Reader; use Appwrite\Utopia\Response; use Appwrite\Utopia\View; use Appwrite\Extend\Exception as AppwriteException; @@ -19,6 +18,9 @@ use Utopia\Config\Config; use Utopia\Domains\Domain; use Appwrite\Auth\Auth; use Appwrite\Event\Certificate; +use Appwrite\Event\Event; +use Appwrite\Event\Usage; +use Appwrite\ID; use Appwrite\Network\Validator\Origin; use Appwrite\Utopia\Response\Filters\V11 as ResponseV11; use Appwrite\Utopia\Response\Filters\V12 as ResponseV12; @@ -38,6 +40,7 @@ use Appwrite\Utopia\Request\Filters\V13 as RequestV13; use Appwrite\Utopia\Request\Filters\V14 as RequestV14; use Appwrite\Utopia\Request\Filters\V15 as RequestV15; use Appwrite\Utopia\Request\Filters\V16 as RequestV16; +use Executor\Executor; use Utopia\Validator\Text; use Utopia\Validator\WhiteList; @@ -45,7 +48,7 @@ Config::setParam('domainVerification', false); Config::setParam('cookieDomain', 'localhost'); Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); -function router(App $utopia, Database $dbForConsole, SwooleRequest $swooleRequest, Request $request, Response $response) +function router(App $utopia, Database $dbForConsole, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) { $utopia->getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml'); @@ -117,59 +120,218 @@ function router(App $utopia, Database $dbForConsole, SwooleRequest $swooleReques $path .= '?' . $query; } + + $body = $swooleRequest->getContent() ?? ''; + $method = $swooleRequest->server['request_method']; + $requestHeaders = $request->getHeaders(); - $body = \json_encode([ - 'async' => false, - 'body' => $swooleRequest->getContent() ?? '', - 'method' => $swooleRequest->server['request_method'], - 'path' => $path, - 'headers' => $requestHeaders + $project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId)); + + $dbForProject = $getProjectDB($project); + + $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId)); + + if ($function->isEmpty() || !$function->getAttribute('enabled')) { + throw new AppwriteException(AppwriteException::FUNCTION_NOT_FOUND); + } + + $version = $function->getAttribute('version', 'v2'); + $runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []); + + $runtime = (isset($runtimes[$function->getAttribute('runtime', '')])) ? $runtimes[$function->getAttribute('runtime', '')] : null; + + if (\is_null($runtime)) { + throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $function->getAttribute('runtime', '') . '" is not supported'); + } + + $deployment = Authorization::skip(fn () => $dbForProject->getDocument('deployments', $function->getAttribute('deployment', ''))); + + if ($deployment->getAttribute('resourceId') !== $function->getId()) { + throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); + } + + if ($deployment->isEmpty()) { + throw new AppwriteException(AppwriteException::DEPLOYMENT_NOT_FOUND, 'Deployment not found. Create a deployment before trying to execute a function'); + } + + /** Check if build has completed */ + $build = Authorization::skip(fn () => $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''))); + if ($build->isEmpty()) { + throw new AppwriteException(AppwriteException::BUILD_NOT_FOUND); + } + + if ($build->getAttribute('status') !== 'ready') { + throw new AppwriteException(AppwriteException::BUILD_NOT_READY); + } + + $permissions = $function->getAttribute('execute'); + + if (!(\in_array('any', $permissions)) && (\in_array('guests', $permissions))) { + throw new AppwriteException(AppwriteException::USER_UNAUTHORIZED, 'To execute function using domain, execute permissions must include "any" or "guests"'); + } + + $headers = \array_merge([], $requestHeaders); + $headers['x-appwrite-trigger'] = 'http'; + $headers['x-appwrite-user-id'] = ''; + $headers['x-appwrite-user-jwt'] = ''; + $headers['x-appwrite-country-code'] = ''; + $headers['x-appwrite-continent-code'] = ''; + $headers['x-appwrite-continent-eu'] = 'false'; + + $ip = $headers['x-real-ip'] ?? ''; + if (!empty($ip)) { + $record = $geodb->get($ip); + + if ($record) { + $eu = Config::getParam('locale-eu'); + + $headers['x-appwrite-country-code'] = $record['country']['iso_code'] ?? ''; + $headers['x-appwrite-continent-code'] = $record['continent']['code'] ?? ''; + $headers['x-appwrite-continent-eu'] = (\in_array($record['country']['iso_code'], $eu)) ? 'true' : 'false'; + } + } + + $headersFiltered = []; + foreach ($headers as $key => $value) { + if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_REQUEST)) { + $headersFiltered[] = ['name' => $key, 'value' => $value]; + } + } + + $executionId = ID::unique(); + + $execution = new Document([ + '$id' => $executionId, + '$permissions' => [], + 'functionInternalId' => $function->getInternalId(), + 'functionId' => $function->getId(), + 'deploymentInternalId' => $deployment->getInternalId(), + 'deploymentId' => $deployment->getId(), + 'trigger' => 'http', // http / schedule / event + 'status' => 'processing', // waiting / processing / completed / failed + 'responseStatusCode' => 0, + 'responseHeaders' => [], + 'requestPath' => $path, + 'requestMethod' => $method, + 'requestHeaders' => $headersFiltered, + 'errors' => '', + 'logs' => '', + 'duration' => 0.0, + 'search' => implode(' ', [$functionId, $executionId]), ]); - $headers = [ - 'Content-Type: application/json', - 'Content-Length: ' . \strlen($body), - 'X-Appwrite-Project: ' . $projectId - ]; + $queueForEvents + ->setParam('functionId', $function->getId()) + ->setParam('executionId', $execution->getId()) + ->setContext('function', $function); - $ch = \curl_init(); - \curl_setopt($ch, CURLOPT_URL, "http://localhost/v1/functions/{$functionId}/executions"); - \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); - \curl_setopt($ch, CURLOPT_POSTFIELDS, $body); - \curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - // \curl_setopt($ch, CURLOPT_HEADER, true); - \curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); - \curl_setopt($ch, CURLOPT_TIMEOUT, 30); + $durationStart = \microtime(true); - $executionResponse = \curl_exec($ch); - $statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE); - $error = \curl_error($ch); - $errNo = \curl_errno($ch); + $vars = []; - \curl_close($ch); - - if ($errNo !== 0) { - throw new AppwriteException(AppwriteException::GENERAL_ARGUMENT_INVALID, "Internal error: " . $error); + // V2 vars + if ($version === 'v2') { + $vars = \array_merge($vars, [ + 'APPWRITE_FUNCTION_TRIGGER' => $headers['x-appwrite-trigger'] ?? '', + 'APPWRITE_FUNCTION_DATA' => $body ?? '', + 'APPWRITE_FUNCTION_USER_ID' => $headers['x-appwrite-user-id'] ?? '', + 'APPWRITE_FUNCTION_JWT' => $headers['x-appwrite-user-jwt'] ?? '' + ]); } - if ($statusCode >= 400) { - $error = \json_decode($executionResponse, true)['message']; - throw new AppwriteException(AppwriteException::GENERAL_ARGUMENT_INVALID, "Execution error: " . $error); + // Shared vars + foreach ($function->getAttribute('varsProject', []) as $var) { + $vars[$var->getAttribute('key')] = $var->getAttribute('value', ''); } - $execution = \json_decode($executionResponse, true); + // Function vars + foreach ($function->getAttribute('vars', []) as $var) { + $vars[$var->getAttribute('key')] = $var->getAttribute('value', ''); + } - $contentType = 'text/plain'; - foreach ($execution['responseHeaders'] as $header) { - if (\strtolower($header['name']) === 'content-type') { - $contentType = $header['value']; + // Appwrite vars + $vars = \array_merge($vars, [ + 'APPWRITE_FUNCTION_ID' => $functionId, + 'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name'), + 'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(), + 'APPWRITE_FUNCTION_PROJECT_ID' => $project->getId(), + 'APPWRITE_FUNCTION_RUNTIME_NAME' => $runtime['name'] ?? '', + 'APPWRITE_FUNCTION_RUNTIME_VERSION' => $runtime['version'] ?? '', + ]); + + /** Execute function */ + $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); + try { + $version = $function->getAttribute('version', 'v2'); + $command = $runtime['startCommand']; + $command = $version === 'v2' ? '' : 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $command . '"'; + $executionResponse = $executor->createExecution( + projectId: $project->getId(), + deploymentId: $deployment->getId(), + body: \strlen($body) > 0 ? $body : null, + variables: $vars, + timeout: $function->getAttribute('timeout', 0), + image: $runtime['image'], + source: $build->getAttribute('path', ''), + entrypoint: $deployment->getAttribute('entrypoint', ''), + version: $version, + path: $path, + method: $method, + headers: $headers, + runtimeEntrypoint: $command, + requestTimeout: 30 + ); + + $headersFiltered = []; + foreach ($executionResponse['headers'] as $key => $value) { + if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_RESPONSE)) { + $headersFiltered[] = ['name' => $key, 'value' => $value]; + } } - $response->setHeader($header['name'], $header['value']); + /** Update execution status */ + $status = $executionResponse['statusCode'] >= 400 ? 'failed' : 'completed'; + $execution->setAttribute('status', $status); + $execution->setAttribute('responseStatusCode', $executionResponse['statusCode']); + $execution->setAttribute('responseHeaders', $headersFiltered); + $execution->setAttribute('logs', $executionResponse['logs']); + $execution->setAttribute('errors', $executionResponse['errors']); + $execution->setAttribute('duration', $executionResponse['duration']); + } catch (\Throwable $th) { + $durationEnd = \microtime(true); + + $execution + ->setAttribute('duration', $durationEnd - $durationStart) + ->setAttribute('status', 'failed') + ->setAttribute('responseStatusCode', 500) + ->setAttribute('errors', $th->getMessage() . '\nError Code: ' . $th->getCode()); + Console::error($th->getMessage()); + } finally { + $queueForUsage + ->addMetric(METRIC_EXECUTIONS, 1) + ->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS), 1) + ->addMetric(METRIC_EXECUTIONS_COMPUTE, (int)($execution->getAttribute('duration') * 1000)) // per project + ->addMetric(str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE), (int)($execution->getAttribute('duration') * 1000)) // per function + ; } + if ($function->getAttribute('logging')) { + /** @var Document $execution */ + $execution = Authorization::skip(fn () => $dbForProject->createDocument('executions', $execution)); + } + + $execution->setAttribute('logs', ''); + $execution->setAttribute('errors', ''); + + $headers = []; + foreach (($executionResponse['headers'] ?? []) as $key => $value) { + $headers[] = ['name' => $key, 'value' => $value]; + } + + $execution->setAttribute('responseBody', $executionResponse['body'] ?? ''); + $execution->setAttribute('responseHeaders', $headers); + $body = $execution['responseBody'] ?? ''; $encodingKey = \array_search('x-open-runtimes-encoding', \array_column($execution['responseHeaders'], 'name')); @@ -179,6 +341,15 @@ function router(App $utopia, Database $dbForConsole, SwooleRequest $swooleReques } } + $contentType = 'text/plain'; + foreach ($execution['responseHeaders'] as $header) { + if (\strtolower($header['name']) === 'content-type') { + $contentType = $header['value']; + } + + $response->setHeader($header['name'], $header['value']); + } + $response ->setContentType($contentType) ->setStatusCode($execution['responseStatusCode'] ?? 200) @@ -205,13 +376,17 @@ App::init() ->inject('console') ->inject('project') ->inject('dbForConsole') + ->inject('getProjectDB') ->inject('user') ->inject('locale') ->inject('localeCodes') ->inject('clients') ->inject('servers') ->inject('queueForCertificates') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Document $user, Locale $locale, array $localeCodes, array $clients, array $servers, Certificate $queueForCertificates) { + ->inject('queueForEvents') + ->inject('queueForUsage') + ->inject('geodb') + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, callable $getProjectDB, Document $user, Locale $locale, array $localeCodes, array $clients, array $servers, Certificate $queueForCertificates, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) { /* * Appwrite Router */ @@ -220,7 +395,7 @@ App::init() $mainDomain = App::getEnv('_APP_DOMAIN', ''); // Only run Router when external domain if ($host !== $mainDomain) { - if (router($utopia, $dbForConsole, $swooleRequest, $request, $response)) { + if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) { return; } } @@ -571,7 +746,11 @@ App::options() ->inject('request') ->inject('response') ->inject('dbForConsole') - ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole) { + ->inject('getProjectDB') + ->inject('queueForEvents') + ->inject('queueForUsage') + ->inject('geodb') + ->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole, callable $getProjectDB, Event $queueForEvents, Usage $queueForUsage, Reader $geodb) { /* * Appwrite Router */ @@ -579,7 +758,7 @@ App::options() $mainDomain = App::getEnv('_APP_DOMAIN', ''); // Only run Router when external domain if ($host !== $mainDomain) { - if (router($utopia, $dbForConsole, $swooleRequest, $request, $response)) { + if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) { return; } } From 141cdf4c822ff7a6c9f46b339d62d5802f49dc72 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Fri, 23 Feb 2024 20:10:25 +0530 Subject: [PATCH 22/38] Fix content-type of file --- app/controllers/api/health.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index c007d629b..d9faf998e 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -722,7 +722,7 @@ App::get('/v1/health/storage') $checkStart = \microtime(true); foreach ($devices as $device) { - if (!$device->write($device->getPath('health.txt'), 'test', '')) { + if (!$device->write($device->getPath('health.txt'), 'test', 'text/plain')) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed writing test file to ' . $device->getRoot()); } From ca1c83c2cf68a839f11a470034ff003959ee3e29 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sat, 24 Feb 2024 14:33:38 +0000 Subject: [PATCH 23/38] method to reset attachment --- src/Appwrite/Event/Mail.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Appwrite/Event/Mail.php b/src/Appwrite/Event/Mail.php index ca8fe7d20..bbd05a018 100644 --- a/src/Appwrite/Event/Mail.php +++ b/src/Appwrite/Event/Mail.php @@ -338,6 +338,14 @@ class Mail extends Event return $this; } + /** + * Set attachment + * @param string $content + * @param string $filename + * @param string $encoding + * @param string $type + * @return self + */ public function setAttachment(string $content, string $filename, string $encoding = 'base64', string $type = 'plain/text') { $this->attachment = [ @@ -349,11 +357,27 @@ class Mail extends Event return $this; } + /** + * Get attachment + * + * @return array + */ public function getAttachment(): array { return $this->attachment; } + /** + * Reset attachment + * + * @return self + */ + public function resetAttachment(): self + { + $this->attachment = []; + return $this; + } + /** * Executes the event and sends it to the mails worker. * From ea484e593b9325e99e1dd45ed8a374595074d7c1 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sat, 24 Feb 2024 14:49:17 +0000 Subject: [PATCH 24/38] format --- src/Appwrite/Event/Mail.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Event/Mail.php b/src/Appwrite/Event/Mail.php index bbd05a018..ac021e71a 100644 --- a/src/Appwrite/Event/Mail.php +++ b/src/Appwrite/Event/Mail.php @@ -359,7 +359,7 @@ class Mail extends Event /** * Get attachment - * + * * @return array */ public function getAttachment(): array @@ -369,7 +369,7 @@ class Mail extends Event /** * Reset attachment - * + * * @return self */ public function resetAttachment(): self From d91a0e30481b0ade36ec837c866baf038124b91a Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sat, 24 Feb 2024 14:56:53 +0000 Subject: [PATCH 25/38] reset variables --- src/Appwrite/Event/Mail.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Appwrite/Event/Mail.php b/src/Appwrite/Event/Mail.php index ac021e71a..6ded33e02 100644 --- a/src/Appwrite/Event/Mail.php +++ b/src/Appwrite/Event/Mail.php @@ -378,6 +378,23 @@ class Mail extends Event return $this; } + /** + * Reset + * + * @return self + */ + public function reset(): self + { + $this->project = null; + $this->recipient = ''; + $this->name = ''; + $this->subject = ''; + $this->body = ''; + $this->variables = []; + $this->bodyTemplate = ''; + $this->attachment = []; + } + /** * Executes the event and sends it to the mails worker. * From 9fe0a5a31b71f4d2016ff7c2f355596c07adeb26 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sat, 24 Feb 2024 21:03:18 +0545 Subject: [PATCH 26/38] Update Mail.php --- src/Appwrite/Event/Mail.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Appwrite/Event/Mail.php b/src/Appwrite/Event/Mail.php index 6ded33e02..9bdbf6044 100644 --- a/src/Appwrite/Event/Mail.php +++ b/src/Appwrite/Event/Mail.php @@ -393,6 +393,7 @@ class Mail extends Event $this->variables = []; $this->bodyTemplate = ''; $this->attachment = []; + return $this; } /** From e7a847a417f891e0a5d217bc56899be25b3ece25 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sat, 24 Feb 2024 19:01:00 +0100 Subject: [PATCH 27/38] Fixed ID class namespace --- app/controllers/general.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index d9f6e5f28..87008a7c4 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -20,7 +20,6 @@ use Appwrite\Auth\Auth; use Appwrite\Event\Certificate; use Appwrite\Event\Event; use Appwrite\Event\Usage; -use Appwrite\ID; use Appwrite\Network\Validator\Origin; use Appwrite\Utopia\Response\Filters\V11 as ResponseV11; use Appwrite\Utopia\Response\Filters\V12 as ResponseV12; @@ -33,6 +32,7 @@ use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Query; +use Utopia\Database\Helpers\ID; use Utopia\Database\Validator\Authorization; use Utopia\Validator\Hostname; use Appwrite\Utopia\Request\Filters\V12 as RequestV12; From b78725e650dbcf797ba9dba2f25dc69459f6f90a Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Sat, 24 Feb 2024 19:12:27 +0100 Subject: [PATCH 28/38] Hide error messages from HTML templates --- app/controllers/general.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 87008a7c4..e945189b2 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -936,10 +936,10 @@ App::error() ->setParam('development', App::isDevelopment()) ->setParam('projectName', $project->getAttribute('name')) ->setParam('projectURL', $project->getAttribute('url')) - ->setParam('message', $error->getMessage()) - ->setParam('type', $type) - ->setParam('code', $code) - ->setParam('trace', $trace); + ->setParam('message', $output['message'] ?? '') + ->setParam('type', $output['type'] ?? '') + ->setParam('code', $output['code'] ?? '') + ->setParam('trace', $output['trace'] ?? []); $response->html($layout->render()); } From bbcb5e8a6e5602ee0d1b11443f28ec2aa0ca5988 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Mon, 26 Feb 2024 17:41:19 +0530 Subject: [PATCH 29/38] Change status code from 503 to 403 --- app/config/errors.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config/errors.php b/app/config/errors.php index 7dedc373e..9e1e80662 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -31,7 +31,7 @@ return [ Exception::GENERAL_SERVICE_DISABLED => [ 'name' => Exception::GENERAL_SERVICE_DISABLED, 'description' => 'The requested service is disabled. You can enable the service from the Appwrite console.', - 'code' => 503, + 'code' => 403, ], Exception::GENERAL_UNAUTHORIZED_SCOPE => [ 'name' => Exception::GENERAL_UNAUTHORIZED_SCOPE, From 260928942b628e6ca4a3b33346a13ff75c63eca3 Mon Sep 17 00:00:00 2001 From: Khushboo Verma <43381712+vermakhushboo@users.noreply.github.com> Date: Mon, 26 Feb 2024 17:58:57 +0530 Subject: [PATCH 30/38] Update tests --- tests/e2e/Services/Projects/ProjectsConsoleClientTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 4a3e25ba2..574ff7dcc 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -1850,7 +1850,7 @@ class ProjectsConsoleClientTest extends Scope 'x-appwrite-project' => $id, ], $this->getHeaders())); - $this->assertEquals(503, $response['headers']['status-code']); + $this->assertEquals(403, $response['headers']['status-code']); $response = $this->client->call(Client::METHOD_POST, '/teams', array_merge([ 'content-type' => 'application/json', @@ -1860,7 +1860,7 @@ class ProjectsConsoleClientTest extends Scope 'name' => 'Arsenal' ]); - $this->assertEquals(503, $response['headers']['status-code']); + $this->assertEquals(403, $response['headers']['status-code']); // Cleanup From 84f47dc1d218e234f3f55f6dad7ca4a6d91af71e Mon Sep 17 00:00:00 2001 From: shimon Date: Wed, 28 Feb 2024 20:21:35 +0200 Subject: [PATCH 31/38] adding limit to queue retry --- src/Appwrite/Platform/Tasks/QueueRetry.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/QueueRetry.php b/src/Appwrite/Platform/Tasks/QueueRetry.php index cba68f993..bae5a59c4 100644 --- a/src/Appwrite/Platform/Tasks/QueueRetry.php +++ b/src/Appwrite/Platform/Tasks/QueueRetry.php @@ -7,6 +7,7 @@ use Utopia\CLI\Console; use Utopia\Platform\Action; use Utopia\Queue\Client; use Utopia\Queue\Connection; +use Utopia\Validator\Integer; use Utopia\Validator\WhiteList; class QueueRetry extends Action @@ -35,15 +36,17 @@ class QueueRetry extends Action Event::MIGRATIONS_QUEUE_NAME, Event::HAMSTER_CLASS_NAME ]), 'Queue name') + ->param('limit', 0, new Integer(true), 'jobs limit', true) ->inject('queue') - ->callback(fn ($name, $queue) => $this->action($name, $queue)); + ->callback(fn ($name, $limit, $queue) => $this->action($name, $limit, $queue)); } /** * @param string $name The name of the queue to retry jobs from + * @param int $limit * @param Connection $queue */ - public function action(string $name, Connection $queue): void + public function action(string $name, int $limit, Connection $queue): void { if (!$name) { Console::error('Missing required parameter $name'); @@ -59,6 +62,6 @@ class QueueRetry extends Action Console::log('Retrying failed jobs...'); - $queueClient->retry(); + $queueClient->retry($limit); } } From 53438acccebe3f0fd8d699f20051ac5e81f09a99 Mon Sep 17 00:00:00 2001 From: shimon Date: Wed, 28 Feb 2024 21:01:42 +0200 Subject: [PATCH 32/38] adding limit to queue retry --- src/Appwrite/Platform/Tasks/QueueRetry.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/QueueRetry.php b/src/Appwrite/Platform/Tasks/QueueRetry.php index bae5a59c4..6c7714ff0 100644 --- a/src/Appwrite/Platform/Tasks/QueueRetry.php +++ b/src/Appwrite/Platform/Tasks/QueueRetry.php @@ -9,6 +9,7 @@ use Utopia\Queue\Client; use Utopia\Queue\Connection; use Utopia\Validator\Integer; use Utopia\Validator\WhiteList; +use Utopia\Validator\Wildcard; class QueueRetry extends Action { @@ -36,18 +37,24 @@ class QueueRetry extends Action Event::MIGRATIONS_QUEUE_NAME, Event::HAMSTER_CLASS_NAME ]), 'Queue name') - ->param('limit', 0, new Integer(true), 'jobs limit', true) + ->param('limit', null, new Wildcard(), 'jobs limit', true) ->inject('queue') ->callback(fn ($name, $limit, $queue) => $this->action($name, $limit, $queue)); } /** * @param string $name The name of the queue to retry jobs from - * @param int $limit + * @param mixed $limit * @param Connection $queue */ - public function action(string $name, int $limit, Connection $queue): void + public function action(string $name, mixed $limit, Connection $queue): void { + + if (!\is_numeric($limit) && $limit !== null) { + Console::error('$limit parameter should be an integer'); + return; + } + if (!$name) { Console::error('Missing required parameter $name'); return; From 658396b4a857739bb83f50461ae6f950c83ae133 Mon Sep 17 00:00:00 2001 From: shimon Date: Wed, 28 Feb 2024 21:09:10 +0200 Subject: [PATCH 33/38] adding limit to queue retry --- src/Appwrite/Platform/Tasks/QueueRetry.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Appwrite/Platform/Tasks/QueueRetry.php b/src/Appwrite/Platform/Tasks/QueueRetry.php index 6c7714ff0..f34448d88 100644 --- a/src/Appwrite/Platform/Tasks/QueueRetry.php +++ b/src/Appwrite/Platform/Tasks/QueueRetry.php @@ -37,7 +37,7 @@ class QueueRetry extends Action Event::MIGRATIONS_QUEUE_NAME, Event::HAMSTER_CLASS_NAME ]), 'Queue name') - ->param('limit', null, new Wildcard(), 'jobs limit', true) + ->param('limit', 0, new Wildcard(), 'jobs limit', true) ->inject('queue') ->callback(fn ($name, $limit, $queue) => $this->action($name, $limit, $queue)); } @@ -50,18 +50,14 @@ class QueueRetry extends Action public function action(string $name, mixed $limit, Connection $queue): void { - if (!\is_numeric($limit) && $limit !== null) { - Console::error('$limit parameter should be an integer'); - return; - } - if (!$name) { Console::error('Missing required parameter $name'); return; } + $limit = (int)$limit; $queueClient = new Client($name, $queue); - + var_dump($limit); if ($queueClient->countFailedJobs() === 0) { Console::error('No failed jobs found.'); return; From 1a59361200e85cbea84dbda3642e1f5eb8da0570 Mon Sep 17 00:00:00 2001 From: shimon Date: Wed, 28 Feb 2024 21:15:59 +0200 Subject: [PATCH 34/38] adding limit to queue retry --- src/Appwrite/Platform/Tasks/QueueRetry.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Tasks/QueueRetry.php b/src/Appwrite/Platform/Tasks/QueueRetry.php index f34448d88..2781d9a3f 100644 --- a/src/Appwrite/Platform/Tasks/QueueRetry.php +++ b/src/Appwrite/Platform/Tasks/QueueRetry.php @@ -57,7 +57,7 @@ class QueueRetry extends Action $limit = (int)$limit; $queueClient = new Client($name, $queue); - var_dump($limit); + if ($queueClient->countFailedJobs() === 0) { Console::error('No failed jobs found.'); return; From 0b4716721d80a25e86f2aeeb910de07f2f87e493 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Fri, 1 Mar 2024 16:16:29 +0000 Subject: [PATCH 35/38] chore: update composer --- composer.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/composer.lock b/composer.lock index c83094b4d..c8fe99a70 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a8c299cb631eb98bbcf0a58a815f74ac", + "content-hash": "f7a4173bb5adeea3518f949fa883a282", "packages": [ { "name": "adhocore/jwt", @@ -156,11 +156,11 @@ }, { "name": "appwrite/php-runtimes", - "version": "0.13.2", + "version": "0.13.3", "source": { "type": "git", "url": "https://github.com/appwrite/runtimes.git", - "reference": "214a37c2c66e0f2bc9c30fdfde66955d9fd084a1" + "reference": "5d93fc578a9a543bcdc9b2c0562d80a51d56c73d" }, "require": { "php": ">=8.0", @@ -195,7 +195,7 @@ "php", "runtimes" ], - "time": "2023-11-22T15:36:00+00:00" + "time": "2024-03-01T14:47:47+00:00" }, { "name": "chillerlan/php-qrcode", @@ -3043,16 +3043,16 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.8.1", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "bc3dc91a5e9b14aa06d1d9e90647c5c5a2cc5353" + "reference": "153ae662783729388a584b4361f2545e4d841e3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/bc3dc91a5e9b14aa06d1d9e90647c5c5a2cc5353", - "reference": "bc3dc91a5e9b14aa06d1d9e90647c5c5a2cc5353", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/153ae662783729388a584b4361f2545e4d841e3c", + "reference": "153ae662783729388a584b4361f2545e4d841e3c", "shasum": "" }, "require": { @@ -3095,9 +3095,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.1" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.2" }, - "time": "2024-01-18T19:15:27+00:00" + "time": "2024-02-23T11:10:43+00:00" }, { "name": "phpspec/prophecy", From 868bba52aa65eb5d07118c167705716ddae891f4 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Fri, 1 Mar 2024 22:05:42 +0530 Subject: [PATCH 36/38] Update app/init.php Co-authored-by: Torsten Dittmann --- app/init.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/init.php b/app/init.php index 80c3670ee..9696b08f6 100644 --- a/app/init.php +++ b/app/init.php @@ -112,7 +112,7 @@ const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return const APP_KEY_ACCCESS = 24 * 60 * 60; // 24 hours const APP_USER_ACCCESS = 24 * 60 * 60; // 24 hours const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours -const APP_CACHE_BUSTER = 329; +const APP_CACHE_BUSTER = 331; const APP_VERSION_STABLE = '1.4.13'; const APP_DATABASE_ATTRIBUTE_EMAIL = 'email'; const APP_DATABASE_ATTRIBUTE_ENUM = 'enum'; From 45092a104975048b4ec5a7ddd69a777ebd06aaae Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Fri, 1 Mar 2024 22:10:25 +0530 Subject: [PATCH 37/38] Update docker-compose.yml --- docker-compose.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index a1a8c0c3a..3d4c5d57b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -935,8 +935,9 @@ services: # # MailCatcher - An SMTP server. Catches all system emails and displays them in a nice UI. # RequestCatcher - An HTTP server. Catches all system https calls and displays them using a simple HTTP API. Used to debug & tests webhooks and HTTP tasks - # RedisCommander - A nice UI for exploring Redis data - # Webgrind - A nice UI for exploring and debugging code-level stuff + # Redis Insight - A nice UI for exploring Redis data + # Adminer - A nice UI for exploring MariaDB data + # GraphQl Explorer - A nice UI for exploring GraphQL API maildev: # used mainly for dev tests image: appwrite/mailcatcher:1.0.0 From fc498fd80fff82e9103986923fdbc2c763c0ed83 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 3 Mar 2024 08:19:36 +0545 Subject: [PATCH 38/38] update cover image for SDKs --- src/Appwrite/Platform/Tasks/SDKs.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index 256b36d88..068da7c68 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -74,7 +74,7 @@ class SDKs extends Action $spec = file_get_contents(__DIR__ . '/../../../../app/config/specs/swagger2-' . $version . '-' . $language['family'] . '.json'); - $cover = 'https://appwrite.io/images/github.png'; + $cover = 'https://github.com/appwrite/appwrite/raw/main/public/images/github.png'; $result = \realpath(__DIR__ . '/../../../../app') . '/sdks/' . $key . '-' . $language['key']; $resultExamples = \realpath(__DIR__ . '/../../../..') . '/docs/examples/' . $version . '/' . $key . '-' . $language['key']; $target = \realpath(__DIR__ . '/../../../../app') . '/sdks/git/' . $language['key'] . '/';