1
0
Fork 0
mirror of synced 2024-05-17 11:12:41 +12:00

Merge branch 'main' of https://github.com/appwrite/appwrite into sync-main-1.5.x-2

This commit is contained in:
Torsten Dittmann 2024-03-05 10:31:51 +01:00
commit a53e630536
34 changed files with 608 additions and 238 deletions

2
.env
View file

@ -79,7 +79,7 @@ _APP_MAINTENANCE_RETENTION_CACHE=2592000
_APP_MAINTENANCE_RETENTION_EXECUTION=1209600 _APP_MAINTENANCE_RETENTION_EXECUTION=1209600
_APP_MAINTENANCE_RETENTION_ABUSE=86400 _APP_MAINTENANCE_RETENTION_ABUSE=86400
_APP_MAINTENANCE_RETENTION_AUDIT=1209600 _APP_MAINTENANCE_RETENTION_AUDIT=1209600
_APP_USAGE_AGGREGATION_INTERVAL=30 _APP_USAGE_AGGREGATION_INTERVAL=20
_APP_MAINTENANCE_RETENTION_USAGE_HOURLY=8640000 _APP_MAINTENANCE_RETENTION_USAGE_HOURLY=8640000
_APP_MAINTENANCE_RETENTION_SCHEDULES=86400 _APP_MAINTENANCE_RETENTION_SCHEDULES=86400
_APP_USAGE_STATS=enabled _APP_USAGE_STATS=enabled

View file

@ -17,6 +17,12 @@ jobs:
fetch-depth: 2 fetch-depth: 2
submodules: recursive submodules: recursive
- 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 - name: Login to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
@ -35,11 +41,11 @@ jobs:
uses: docker/build-push-action@v4 uses: docker/build-push-action@v4
with: with:
context: . context: .
platforms: linux/amd64 platforms: linux/amd64,linux/arm64
build-args: | build-args: |
VERSION=${{ steps.meta.outputs.version }} VERSION=${{ steps.meta.outputs.version }}
VITE_APPWRITE_GROWTH_ENDPOINT=https://growth.appwrite.io/v1 VITE_APPWRITE_GROWTH_ENDPOINT=https://growth.appwrite.io/v1
VITE_GA_PROJECT=G-L7G2B6PLDS VITE_GA_PROJECT=G-L7G2B6PLDS
VITE_CONSOLE_MODE=cloud VITE_CONSOLE_MODE=cloud
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}

View file

@ -34,28 +34,6 @@ $commonCollections = [
'array' => false, 'array' => false,
'filters' => [], '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', '$id' => 'accessedAt',
'type' => Database::VAR_DATETIME, 'type' => Database::VAR_DATETIME,

View file

@ -32,7 +32,7 @@ return [
Exception::GENERAL_SERVICE_DISABLED => [ Exception::GENERAL_SERVICE_DISABLED => [
'name' => Exception::GENERAL_SERVICE_DISABLED, 'name' => Exception::GENERAL_SERVICE_DISABLED,
'description' => 'The requested service is disabled. You can enable the service from the Appwrite console.', 'description' => 'The requested service is disabled. You can enable the service from the Appwrite console.',
'code' => 503, 'code' => 403,
], ],
Exception::GENERAL_UNAUTHORIZED_SCOPE => [ Exception::GENERAL_UNAUTHORIZED_SCOPE => [
'name' => Exception::GENERAL_UNAUTHORIZED_SCOPE, 'name' => Exception::GENERAL_UNAUTHORIZED_SCOPE,

View file

@ -2237,7 +2237,7 @@ App::get('/v1/account/logs')
} }
$response->dynamic(new Document([ $response->dynamic(new Document([
'total' => $audit->countLogsByUser($user->getInternalId()), 'total' => $audit->countLogsByUser($user->getId()),
'logs' => $output, 'logs' => $output,
]), Response::MODEL_LOG_LIST); ]), Response::MODEL_LOG_LIST);
}); });

View file

@ -76,7 +76,7 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro
} }
if (empty($gitHubSession)) { 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', ''); $provider = $gitHubSession->getAttribute('provider', '');

View file

@ -3687,7 +3687,7 @@ App::get('/v1/databases/usage')
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $metric) { foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [ $result = $dbForProject->findOne('stats_v2', [
Query::equal('metric', [$metric]), Query::equal('metric', [$metric]),
Query::equal('period', ['inf']) Query::equal('period', ['inf'])
]); ]);
@ -3695,7 +3695,7 @@ App::get('/v1/databases/usage')
$stats[$metric]['total'] = $result['value'] ?? 0; $stats[$metric]['total'] = $result['value'] ?? 0;
$limit = $days['limit']; $limit = $days['limit'];
$period = $days['period']; $period = $days['period'];
$results = $dbForProject->find('stats', [ $results = $dbForProject->find('stats_v2', [
Query::equal('metric', [$metric]), Query::equal('metric', [$metric]),
Query::equal('period', [$period]), Query::equal('period', [$period]),
Query::limit($limit), Query::limit($limit),
@ -3771,7 +3771,7 @@ App::get('/v1/databases/:databaseId/usage')
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $metric) { foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [ $result = $dbForProject->findOne('stats_v2', [
Query::equal('metric', [$metric]), Query::equal('metric', [$metric]),
Query::equal('period', ['inf']) Query::equal('period', ['inf'])
]); ]);
@ -3779,7 +3779,7 @@ App::get('/v1/databases/:databaseId/usage')
$stats[$metric]['total'] = $result['value'] ?? 0; $stats[$metric]['total'] = $result['value'] ?? 0;
$limit = $days['limit']; $limit = $days['limit'];
$period = $days['period']; $period = $days['period'];
$results = $dbForProject->find('stats', [ $results = $dbForProject->find('stats_v2', [
Query::equal('metric', [$metric]), Query::equal('metric', [$metric]),
Query::equal('period', [$period]), Query::equal('period', [$period]),
Query::limit($limit), Query::limit($limit),
@ -3857,7 +3857,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/usage')
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $metric) { foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [ $result = $dbForProject->findOne('stats_v2', [
Query::equal('metric', [$metric]), Query::equal('metric', [$metric]),
Query::equal('period', ['inf']) Query::equal('period', ['inf'])
]); ]);
@ -3865,7 +3865,7 @@ App::get('/v1/databases/:databaseId/collections/:collectionId/usage')
$stats[$metric]['total'] = $result['value'] ?? 0; $stats[$metric]['total'] = $result['value'] ?? 0;
$limit = $days['limit']; $limit = $days['limit'];
$period = $days['period']; $period = $days['period'];
$results = $dbForProject->find('stats', [ $results = $dbForProject->find('stats_v2', [
Query::equal('metric', [$metric]), Query::equal('metric', [$metric]),
Query::equal('period', [$period]), Query::equal('period', [$period]),
Query::limit($limit), Query::limit($limit),

View file

@ -501,7 +501,7 @@ App::get('/v1/functions/:functionId/usage')
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $metric) { foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [ $result = $dbForProject->findOne('stats_v2', [
Query::equal('metric', [$metric]), Query::equal('metric', [$metric]),
Query::equal('period', ['inf']) Query::equal('period', ['inf'])
]); ]);
@ -509,7 +509,7 @@ App::get('/v1/functions/:functionId/usage')
$stats[$metric]['total'] = $result['value'] ?? 0; $stats[$metric]['total'] = $result['value'] ?? 0;
$limit = $days['limit']; $limit = $days['limit'];
$period = $days['period']; $period = $days['period'];
$results = $dbForProject->find('stats', [ $results = $dbForProject->find('stats_v2', [
Query::equal('metric', [$metric]), Query::equal('metric', [$metric]),
Query::equal('period', [$period]), Query::equal('period', [$period]),
Query::limit($limit), Query::limit($limit),
@ -593,7 +593,7 @@ App::get('/v1/functions/usage')
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $metric) { foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [ $result = $dbForProject->findOne('stats_v2', [
Query::equal('metric', [$metric]), Query::equal('metric', [$metric]),
Query::equal('period', ['inf']) Query::equal('period', ['inf'])
]); ]);
@ -601,7 +601,7 @@ App::get('/v1/functions/usage')
$stats[$metric]['total'] = $result['value'] ?? 0; $stats[$metric]['total'] = $result['value'] ?? 0;
$limit = $days['limit']; $limit = $days['limit'];
$period = $days['period']; $period = $days['period'];
$results = $dbForProject->find('stats', [ $results = $dbForProject->find('stats_v2', [
Query::equal('metric', [$metric]), Query::equal('metric', [$metric]),
Query::equal('period', [$period]), Query::equal('period', [$period]),
Query::limit($limit), Query::limit($limit),

View file

@ -702,6 +702,47 @@ App::get('/v1/health/storage/local')
$response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS); $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')
->inject('deviceFunctions')
->inject('deviceBuilds')
->action(function (Response $response, Device $deviceFiles, Device $deviceFunctions, Device $deviceBuilds) {
$devices = [$deviceFiles, $deviceFunctions, $deviceBuilds];
$checkStart = \microtime(true);
foreach ($devices as $device) {
if (!$device->write($device->getPath('health.txt'), 'test', 'text/plain')) {
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());
}
}
$output = [
'status' => 'pass',
'ping' => \round((\microtime(true) - $checkStart) / 1000)
];
$response->dynamic(new Document($output), Response::MODEL_HEALTH_STATUS);
});
App::get('/v1/health/anti-virus') App::get('/v1/health/anti-virus')
->desc('Get antivirus') ->desc('Get antivirus')
->groups(['api', 'health']) ->groups(['api', 'health'])

View file

@ -73,7 +73,7 @@ App::get('/v1/project/usage')
Authorization::skip(function () use ($dbForProject, $firstDay, $lastDay, $period, $metrics, &$total, &$stats) { Authorization::skip(function () use ($dbForProject, $firstDay, $lastDay, $period, $metrics, &$total, &$stats) {
foreach ($metrics['total'] as $metric) { foreach ($metrics['total'] as $metric) {
$result = $dbForProject->findOne('stats', [ $result = $dbForProject->findOne('stats_v2', [
Query::equal('metric', [$metric]), Query::equal('metric', [$metric]),
Query::equal('period', ['inf']) Query::equal('period', ['inf'])
]); ]);
@ -81,7 +81,7 @@ App::get('/v1/project/usage')
} }
foreach ($metrics['period'] as $metric) { foreach ($metrics['period'] as $metric) {
$results = $dbForProject->find('stats', [ $results = $dbForProject->find('stats_v2', [
Query::equal('metric', [$metric]), Query::equal('metric', [$metric]),
Query::equal('period', [$period]), Query::equal('period', [$period]),
Query::greaterThanEqual('time', $firstDay), Query::greaterThanEqual('time', $firstDay),
@ -116,7 +116,7 @@ App::get('/v1/project/usage')
$id = $function->getId(); $id = $function->getId();
$name = $function->getAttribute('name'); $name = $function->getAttribute('name');
$metric = str_replace('{functionInternalId}', $function->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS); $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('metric', [$metric]),
Query::equal('period', ['inf']) Query::equal('period', ['inf'])
]); ]);
@ -132,7 +132,7 @@ App::get('/v1/project/usage')
$id = $bucket->getId(); $id = $bucket->getId();
$name = $bucket->getAttribute('name'); $name = $bucket->getAttribute('name');
$metric = str_replace('{bucketInternalId}', $bucket->getInternalId(), METRIC_BUCKET_ID_FILES_STORAGE); $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('metric', [$metric]),
Query::equal('period', ['inf']) Query::equal('period', ['inf'])
]); ]);

View file

@ -1482,7 +1482,6 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
if ($deviceDeleted) { if ($deviceDeleted) {
$queueForDeletes $queueForDeletes
->setType(DELETE_TYPE_CACHE_BY_RESOURCE) ->setType(DELETE_TYPE_CACHE_BY_RESOURCE)
->setResourceType('bucket/' . $bucket->getId())
->setResource('file/' . $fileId) ->setResource('file/' . $fileId)
; ;
@ -1540,7 +1539,7 @@ App::get('/v1/storage/usage')
$total = []; $total = [];
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) { Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) {
foreach ($metrics as $metric) { foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [ $result = $dbForProject->findOne('stats_v2', [
Query::equal('metric', [$metric]), Query::equal('metric', [$metric]),
Query::equal('period', ['inf']) Query::equal('period', ['inf'])
]); ]);
@ -1548,7 +1547,7 @@ App::get('/v1/storage/usage')
$stats[$metric]['total'] = $result['value'] ?? 0; $stats[$metric]['total'] = $result['value'] ?? 0;
$limit = $days['limit']; $limit = $days['limit'];
$period = $days['period']; $period = $days['period'];
$results = $dbForProject->find('stats', [ $results = $dbForProject->find('stats_v2', [
Query::equal('metric', [$metric]), Query::equal('metric', [$metric]),
Query::equal('period', [$period]), Query::equal('period', [$period]),
Query::limit($limit), Query::limit($limit),
@ -1625,7 +1624,7 @@ App::get('/v1/storage/:bucketId/usage')
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) { Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats, &$total) {
foreach ($metrics as $metric) { foreach ($metrics as $metric) {
$result = $dbForProject->findOne('stats', [ $result = $dbForProject->findOne('stats_v2', [
Query::equal('metric', [$metric]), Query::equal('metric', [$metric]),
Query::equal('period', ['inf']) Query::equal('period', ['inf'])
]); ]);
@ -1633,7 +1632,7 @@ App::get('/v1/storage/:bucketId/usage')
$stats[$metric]['total'] = $result['value'] ?? 0; $stats[$metric]['total'] = $result['value'] ?? 0;
$limit = $days['limit']; $limit = $days['limit'];
$period = $days['period']; $period = $days['period'];
$results = $dbForProject->find('stats', [ $results = $dbForProject->find('stats_v2', [
Query::equal('metric', [$metric]), Query::equal('metric', [$metric]),
Query::equal('period', [$period]), Query::equal('period', [$period]),
Query::limit($limit), Query::limit($limit),

View file

@ -813,9 +813,6 @@ App::get('/v1/users/:userId/logs')
$output[$i] = new Document([ $output[$i] = new Document([
'event' => $log['event'], 'event' => $log['event'],
'userId' => ID::custom($log['data']['userId']),
'userEmail' => $log['data']['userEmail'] ?? null,
'userName' => $log['data']['userName'] ?? null,
'ip' => $log['ip'], 'ip' => $log['ip'],
'time' => $log['time'], 'time' => $log['time'],
'osCode' => $os['osCode'], 'osCode' => $os['osCode'],
@ -844,7 +841,7 @@ App::get('/v1/users/:userId/logs')
} }
$response->dynamic(new Document([ $response->dynamic(new Document([
'total' => $audit->countLogsByUser($user->getInternalId()), 'total' => $audit->countLogsByUser($user->getId()),
'logs' => $output, 'logs' => $output,
]), Response::MODEL_LOG_LIST); ]), Response::MODEL_LOG_LIST);
}); });
@ -2125,7 +2122,7 @@ App::get('/v1/users/usage')
Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) { Authorization::skip(function () use ($dbForProject, $days, $metrics, &$stats) {
foreach ($metrics as $count => $metric) { foreach ($metrics as $count => $metric) {
$result = $dbForProject->findOne('stats', [ $result = $dbForProject->findOne('stats_v2', [
Query::equal('metric', [$metric]), Query::equal('metric', [$metric]),
Query::equal('period', ['inf']) Query::equal('period', ['inf'])
]); ]);
@ -2133,7 +2130,7 @@ App::get('/v1/users/usage')
$stats[$metric]['total'] = $result['value'] ?? 0; $stats[$metric]['total'] = $result['value'] ?? 0;
$limit = $days['limit']; $limit = $days['limit'];
$period = $days['period']; $period = $days['period'];
$results = $dbForProject->find('stats', [ $results = $dbForProject->find('stats_v2', [
Query::equal('metric', [$metric]), Query::equal('metric', [$metric]),
Query::equal('period', [$period]), Query::equal('period', [$period]),
Query::limit($limit), Query::limit($limit),

View file

@ -9,12 +9,15 @@ use Utopia\Logger\Log;
use Utopia\Logger\Log\User; use Utopia\Logger\Log\User;
use Swoole\Http\Request as SwooleRequest; use Swoole\Http\Request as SwooleRequest;
use Appwrite\Utopia\Request; use Appwrite\Utopia\Request;
use MaxMind\Db\Reader;
use Appwrite\Utopia\Response; use Appwrite\Utopia\Response;
use Appwrite\Utopia\View; use Appwrite\Utopia\View;
use Appwrite\Extend\Exception as AppwriteException; use Appwrite\Extend\Exception as AppwriteException;
use Utopia\Config\Config; use Utopia\Config\Config;
use Utopia\Domains\Domain; use Utopia\Domains\Domain;
use Appwrite\Event\Certificate; use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Event\Usage;
use Appwrite\Network\Validator\Origin; use Appwrite\Network\Validator\Origin;
use Appwrite\Utopia\Response\Filters\V11 as ResponseV11; use Appwrite\Utopia\Response\Filters\V11 as ResponseV11;
use Appwrite\Utopia\Response\Filters\V12 as ResponseV12; use Appwrite\Utopia\Response\Filters\V12 as ResponseV12;
@ -27,6 +30,7 @@ use Utopia\CLI\Console;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Query; use Utopia\Database\Query;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Hostname; use Utopia\Validator\Hostname;
use Appwrite\Utopia\Request\Filters\V12 as RequestV12; use Appwrite\Utopia\Request\Filters\V12 as RequestV12;
@ -35,13 +39,14 @@ use Appwrite\Utopia\Request\Filters\V14 as RequestV14;
use Appwrite\Utopia\Request\Filters\V15 as RequestV15; use Appwrite\Utopia\Request\Filters\V15 as RequestV15;
use Appwrite\Utopia\Request\Filters\V16 as RequestV16; use Appwrite\Utopia\Request\Filters\V16 as RequestV16;
use Appwrite\Utopia\Request\Filters\V17 as RequestV17; use Appwrite\Utopia\Request\Filters\V17 as RequestV17;
use Executor\Executor;
use Utopia\Validator\Text; use Utopia\Validator\Text;
Config::setParam('domainVerification', false); Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost'); Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); 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'); $utopia->getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml');
@ -113,59 +118,218 @@ function router(App $utopia, Database $dbForConsole, SwooleRequest $swooleReques
$path .= '?' . $query; $path .= '?' . $query;
} }
$body = $swooleRequest->getContent() ?? '';
$method = $swooleRequest->server['request_method'];
$requestHeaders = $request->getHeaders(); $requestHeaders = $request->getHeaders();
$body = \json_encode([ $project = Authorization::skip(fn () => $dbForConsole->getDocument('projects', $projectId));
'async' => false,
'body' => $swooleRequest->getContent() ?? '', $dbForProject = $getProjectDB($project);
'method' => $swooleRequest->server['request_method'],
'path' => $path, $function = Authorization::skip(fn () => $dbForProject->getDocument('functions', $functionId));
'headers' => $requestHeaders
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 = [ $queueForEvents
'Content-Type: application/json', ->setParam('functionId', $function->getId())
'Content-Length: ' . \strlen($body), ->setParam('executionId', $execution->getId())
'X-Appwrite-Project: ' . $projectId ->setContext('function', $function);
];
$ch = \curl_init(); $durationStart = \microtime(true);
\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);
$executionResponse = \curl_exec($ch); $vars = [];
$statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = \curl_error($ch);
$errNo = \curl_errno($ch);
\curl_close($ch); // V2 vars
if ($version === 'v2') {
if ($errNo !== 0) { $vars = \array_merge($vars, [
throw new AppwriteException(AppwriteException::GENERAL_ARGUMENT_INVALID, "Internal error: " . $error); '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) { // Shared vars
$error = \json_decode($executionResponse, true)['message']; foreach ($function->getAttribute('varsProject', []) as $var) {
throw new AppwriteException(AppwriteException::GENERAL_ARGUMENT_INVALID, "Execution error: " . $error); $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'; // Appwrite vars
foreach ($execution['responseHeaders'] as $header) { $vars = \array_merge($vars, [
if (\strtolower($header['name']) === 'content-type') { 'APPWRITE_FUNCTION_ID' => $functionId,
$contentType = $header['value']; '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'] ?? ''; $body = $execution['responseBody'] ?? '';
$encodingKey = \array_search('x-open-runtimes-encoding', \array_column($execution['responseHeaders'], 'name')); $encodingKey = \array_search('x-open-runtimes-encoding', \array_column($execution['responseHeaders'], 'name'));
@ -175,6 +339,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 $response
->setContentType($contentType) ->setContentType($contentType)
->setStatusCode($execution['responseStatusCode'] ?? 200) ->setStatusCode($execution['responseStatusCode'] ?? 200)
@ -213,7 +386,7 @@ App::init()
$mainDomain = App::getEnv('_APP_DOMAIN', ''); $mainDomain = App::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain // Only run Router when external domain
if ($host !== $mainDomain) { if ($host !== $mainDomain) {
if (router($utopia, $dbForConsole, $swooleRequest, $request, $response)) { if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) {
return; return;
} }
} }
@ -445,7 +618,11 @@ App::options()
->inject('request') ->inject('request')
->inject('response') ->inject('response')
->inject('dbForConsole') ->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 * Appwrite Router
*/ */
@ -453,7 +630,7 @@ App::options()
$mainDomain = App::getEnv('_APP_DOMAIN', ''); $mainDomain = App::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain // Only run Router when external domain
if ($host !== $mainDomain) { if ($host !== $mainDomain) {
if (router($utopia, $dbForConsole, $swooleRequest, $request, $response)) { if (router($utopia, $dbForConsole, $getProjectDB, $swooleRequest, $request, $response, $queueForEvents, $queueForUsage, $geodb)) {
return; return;
} }
} }
@ -632,10 +809,10 @@ App::error()
->setParam('development', App::isDevelopment()) ->setParam('development', App::isDevelopment())
->setParam('projectName', $project->getAttribute('name')) ->setParam('projectName', $project->getAttribute('name'))
->setParam('projectURL', $project->getAttribute('url')) ->setParam('projectURL', $project->getAttribute('url'))
->setParam('message', $error->getMessage()) ->setParam('message', $output['message'] ?? '')
->setParam('type', $type) ->setParam('type', $output['type'] ?? '')
->setParam('code', $code) ->setParam('code', $output['code'] ?? '')
->setParam('trace', $trace); ->setParam('trace', $output['trace'] ?? []);
$response->html($layout->render()); $response->html($layout->render());
} }

View file

@ -404,22 +404,24 @@ App::init()
; ;
$useCache = $route->getLabel('cache', false); $useCache = $route->getLabel('cache', false);
if ($useCache) { if ($useCache) {
$key = md5($request->getURI() . implode('*', $request->getParams()) . '*' . APP_CACHE_BUSTER); $key = md5($request->getURI() . implode('*', $request->getParams()) . '*' . APP_CACHE_BUSTER);
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key));
$cache = new Cache( $cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId()) new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
); );
$timestamp = 60 * 60 * 24 * 30; $timestamp = 60 * 60 * 24 * 30;
$data = $cache->load($key, $timestamp); $data = $cache->load($key, $timestamp);
if (!empty($data) && !$cacheLog->isEmpty()) { if (!empty($data)) {
$parts = explode('/', $cacheLog->getAttribute('resourceType')); $data = json_decode($data, true);
$parts = explode('/', $data['resourceType']);
$type = $parts[0] ?? null; $type = $parts[0] ?? null;
if ($type === 'bucket') { if ($type === 'bucket') {
$bucketId = $parts[1] ?? null; $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()); $isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles()); $isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
@ -431,12 +433,11 @@ App::init()
$fileSecurity = $bucket->getAttribute('fileSecurity', false); $fileSecurity = $bucket->getAttribute('fileSecurity', false);
$validator = new Authorization(Database::PERMISSION_READ); $validator = new Authorization(Database::PERMISSION_READ);
$valid = $validator->isValid($bucket->getRead()); $valid = $validator->isValid($bucket->getRead());
if (!$fileSecurity && !$valid) { if (!$fileSecurity && !$valid) {
throw new Exception(Exception::USER_UNAUTHORIZED); throw new Exception(Exception::USER_UNAUTHORIZED);
} }
$parts = explode('/', $cacheLog->getAttribute('resource')); $parts = explode('/', $data['resource']);
$fileId = $parts[1] ?? null; $fileId = $parts[1] ?? null;
if ($fileSecurity && !$valid) { if ($fileSecurity && !$valid) {
@ -453,8 +454,8 @@ App::init()
$response $response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $timestamp) . ' GMT') ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $timestamp) . ' GMT')
->addHeader('X-Appwrite-Cache', 'hit') ->addHeader('X-Appwrite-Cache', 'hit')
->setContentType($cacheLog->getAttribute('mimeType')) ->setContentType($data['contentType'])
->send($data) ->send(base64_decode($data['payload']))
; ;
} else { } else {
$response->addHeader('X-Appwrite-Cache', 'miss'); $response->addHeader('X-Appwrite-Cache', 'miss');
@ -650,6 +651,7 @@ App::shutdown()
if ($useCache) { if ($useCache) {
$resource = $resourceType = null; $resource = $resourceType = null;
$data = $response->getPayload(); $data = $response->getPayload();
if (!empty($data['payload'])) { if (!empty($data['payload'])) {
$pattern = $route->getLabel('cache.resource', null); $pattern = $route->getLabel('cache.resource', null);
if (!empty($pattern)) { if (!empty($pattern)) {
@ -661,17 +663,22 @@ App::shutdown()
$resourceType = $parseLabel($pattern, $responsePayload, $requestParams, $user); $resourceType = $parseLabel($pattern, $responsePayload, $requestParams, $user);
} }
$key = md5($request->getURI() . '*' . implode('*', $request->getParams())) . '*' . APP_CACHE_BUSTER; $key = md5($request->getURI() . implode('*', $request->getParams()) . '*' . APP_CACHE_BUSTER);
$signature = md5($data['payload']); $data = json_encode([
$cacheLog = Authorization::skip(fn () => $dbForProject->getDocument('cache', $key)); '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', ''); $accessedAt = $cacheLog->getAttribute('accessedAt', '');
$now = DateTime::now(); $now = DateTime::now();
if ($cacheLog->isEmpty()) { if ($cacheLog->isEmpty()) {
Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([ Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([
'$id' => $key, '$id' => $key,
'resource' => $resource, 'resource' => $resource,
'resourceType' => $resourceType,
'mimeType' => $response->getContentType(),
'accessedAt' => $now, 'accessedAt' => $now,
'signature' => $signature, 'signature' => $signature,
]))); ])));
@ -684,7 +691,7 @@ App::shutdown()
$cache = new Cache( $cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId()) new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
); );
$cache->save($key, $data['payload']); $cache->save($key, $data);
} }
} }
} }

View file

@ -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_KEY_ACCCESS = 24 * 60 * 60; // 24 hours
const APP_USER_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_UPDATE = 24 * 60 * 60; // 24 hours
const APP_CACHE_BUSTER = 330; const APP_CACHE_BUSTER = 331;
const APP_VERSION_STABLE = '1.5.0'; const APP_VERSION_STABLE = '1.5.0';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email'; const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum'; const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';

View file

@ -267,7 +267,6 @@ try {
Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine()); Console::error($e->getMessage() . ', File: ' . $e->getFile() . ', Line: ' . $e->getLine());
} }
$worker = $platform->getWorker(); $worker = $platform->getWorker();
$worker $worker
@ -283,9 +282,9 @@ $worker
->inject('logger') ->inject('logger')
->inject('log') ->inject('log')
->inject('pools') ->inject('pools')
->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools) use ($queueName) { ->inject('project')
->action(function (Throwable $error, ?Logger $logger, Log $log, Group $pools, Document $project) use ($queueName) {
$pools->reclaim(); $pools->reclaim();
$version = App::getEnv('_APP_VERSION', 'UNKNOWN'); $version = App::getEnv('_APP_VERSION', 'UNKNOWN');
if ($error instanceof PDOException) { if ($error instanceof PDOException) {
@ -301,6 +300,7 @@ $worker
$log->setAction('appwrite-queue-' . $queueName); $log->setAction('appwrite-queue-' . $queueName);
$log->addTag('verboseType', get_class($error)); $log->addTag('verboseType', get_class($error));
$log->addTag('code', $error->getCode()); $log->addTag('code', $error->getCode());
$log->addTag('projectId', $project->getId() ?? 'n/a');
$log->addExtra('file', $error->getFile()); $log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine()); $log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString()); $log->addExtra('trace', $error->getTraceAsString());

103
composer.lock generated
View file

@ -156,16 +156,16 @@
}, },
{ {
"name": "appwrite/php-runtimes", "name": "appwrite/php-runtimes",
"version": "0.13.2", "version": "0.13.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/appwrite/runtimes.git", "url": "https://github.com/appwrite/runtimes.git",
"reference": "214a37c2c66e0f2bc9c30fdfde66955d9fd084a1" "reference": "5d93fc578a9a543bcdc9b2c0562d80a51d56c73d"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/appwrite/runtimes/zipball/214a37c2c66e0f2bc9c30fdfde66955d9fd084a1", "url": "https://api.github.com/repos/appwrite/runtimes/zipball/5d93fc578a9a543bcdc9b2c0562d80a51d56c73d",
"reference": "214a37c2c66e0f2bc9c30fdfde66955d9fd084a1", "reference": "5d93fc578a9a543bcdc9b2c0562d80a51d56c73d",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -204,9 +204,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/appwrite/runtimes/issues", "issues": "https://github.com/appwrite/runtimes/issues",
"source": "https://github.com/appwrite/runtimes/tree/0.13.2" "source": "https://github.com/appwrite/runtimes/tree/0.13.3"
}, },
"time": "2023-11-22T15:36:00+00:00" "time": "2024-03-01T14:47:47+00:00"
}, },
{ {
"name": "beberlei/assert", "name": "beberlei/assert",
@ -3136,20 +3136,21 @@
}, },
{ {
"name": "phar-io/manifest", "name": "phar-io/manifest",
"version": "2.0.3", "version": "2.0.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phar-io/manifest.git", "url": "https://github.com/phar-io/manifest.git",
"reference": "97803eca37d319dfa7826cc2437fc020857acb53" "reference": "54750ef60c58e43759730615a392c31c80e23176"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176",
"reference": "97803eca37d319dfa7826cc2437fc020857acb53", "reference": "54750ef60c58e43759730615a392c31c80e23176",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-dom": "*", "ext-dom": "*",
"ext-libxml": "*",
"ext-phar": "*", "ext-phar": "*",
"ext-xmlwriter": "*", "ext-xmlwriter": "*",
"phar-io/version": "^3.0.1", "phar-io/version": "^3.0.1",
@ -3190,9 +3191,15 @@
"description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
"support": { "support": {
"issues": "https://github.com/phar-io/manifest/issues", "issues": "https://github.com/phar-io/manifest/issues",
"source": "https://github.com/phar-io/manifest/tree/2.0.3" "source": "https://github.com/phar-io/manifest/tree/2.0.4"
}, },
"time": "2021-07-20T11:28:43+00:00" "funding": [
{
"url": "https://github.com/theseer",
"type": "github"
}
],
"time": "2024-03-03T12:33:53+00:00"
}, },
{ {
"name": "phar-io/version", "name": "phar-io/version",
@ -3531,16 +3538,16 @@
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
"version": "9.2.30", "version": "9.2.31",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089" "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca2bd87d2f9215904682a9cb9bb37dda98e76089", "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965",
"reference": "ca2bd87d2f9215904682a9cb9bb37dda98e76089", "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3597,7 +3604,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.30" "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31"
}, },
"funding": [ "funding": [
{ {
@ -3605,7 +3612,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2023-12-22T06:47:57+00:00" "time": "2024-03-02T06:37:42+00:00"
}, },
{ {
"name": "phpunit/php-file-iterator", "name": "phpunit/php-file-iterator",
@ -4003,16 +4010,16 @@
}, },
{ {
"name": "sebastian/cli-parser", "name": "sebastian/cli-parser",
"version": "1.0.1", "version": "1.0.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/cli-parser.git", "url": "https://github.com/sebastianbergmann/cli-parser.git",
"reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
"reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4047,7 +4054,7 @@
"homepage": "https://github.com/sebastianbergmann/cli-parser", "homepage": "https://github.com/sebastianbergmann/cli-parser",
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/cli-parser/issues", "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
"source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2"
}, },
"funding": [ "funding": [
{ {
@ -4055,7 +4062,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2020-09-28T06:08:49+00:00" "time": "2024-03-02T06:27:43+00:00"
}, },
{ {
"name": "sebastian/code-unit", "name": "sebastian/code-unit",
@ -4301,16 +4308,16 @@
}, },
{ {
"name": "sebastian/diff", "name": "sebastian/diff",
"version": "4.0.5", "version": "4.0.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/diff.git", "url": "https://github.com/sebastianbergmann/diff.git",
"reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131" "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131", "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc",
"reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131", "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4355,7 +4362,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/diff/issues", "issues": "https://github.com/sebastianbergmann/diff/issues",
"source": "https://github.com/sebastianbergmann/diff/tree/4.0.5" "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6"
}, },
"funding": [ "funding": [
{ {
@ -4363,7 +4370,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2023-05-07T05:35:17+00:00" "time": "2024-03-02T06:30:58+00:00"
}, },
{ {
"name": "sebastian/environment", "name": "sebastian/environment",
@ -4430,16 +4437,16 @@
}, },
{ {
"name": "sebastian/exporter", "name": "sebastian/exporter",
"version": "4.0.5", "version": "4.0.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git", "url": "https://github.com/sebastianbergmann/exporter.git",
"reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72",
"reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4495,7 +4502,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues", "issues": "https://github.com/sebastianbergmann/exporter/issues",
"source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6"
}, },
"funding": [ "funding": [
{ {
@ -4503,20 +4510,20 @@
"type": "github" "type": "github"
} }
], ],
"time": "2022-09-14T06:03:37+00:00" "time": "2024-03-02T06:33:00+00:00"
}, },
{ {
"name": "sebastian/global-state", "name": "sebastian/global-state",
"version": "5.0.6", "version": "5.0.7",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/global-state.git", "url": "https://github.com/sebastianbergmann/global-state.git",
"reference": "bde739e7565280bda77be70044ac1047bc007e34" "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34", "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
"reference": "bde739e7565280bda77be70044ac1047bc007e34", "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4559,7 +4566,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/global-state/issues", "issues": "https://github.com/sebastianbergmann/global-state/issues",
"source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6" "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7"
}, },
"funding": [ "funding": [
{ {
@ -4567,7 +4574,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2023-08-02T09:26:13+00:00" "time": "2024-03-02T06:35:11+00:00"
}, },
{ {
"name": "sebastian/lines-of-code", "name": "sebastian/lines-of-code",
@ -5287,16 +5294,16 @@
}, },
{ {
"name": "theseer/tokenizer", "name": "theseer/tokenizer",
"version": "1.2.2", "version": "1.2.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/theseer/tokenizer.git", "url": "https://github.com/theseer/tokenizer.git",
"reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96" "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96", "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
"reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96", "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -5325,7 +5332,7 @@
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"support": { "support": {
"issues": "https://github.com/theseer/tokenizer/issues", "issues": "https://github.com/theseer/tokenizer/issues",
"source": "https://github.com/theseer/tokenizer/tree/1.2.2" "source": "https://github.com/theseer/tokenizer/tree/1.2.3"
}, },
"funding": [ "funding": [
{ {
@ -5333,7 +5340,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2023-11-20T00:12:19+00:00" "time": "2024-03-03T12:36:25+00:00"
}, },
{ {
"name": "twig/twig", "name": "twig/twig",

View file

@ -971,7 +971,6 @@ services:
# - appwrite # - appwrite
# volumes: # volumes:
# - appwrite-uploads:/storage/uploads # - appwrite-uploads:/storage/uploads
# Dev Tools Start ------------------------------------------------------------------------------------------ # Dev Tools Start ------------------------------------------------------------------------------------------
# #
# The Appwrite Team uses the following tools to help debug, monitor and diagnose the Appwrite stack # The Appwrite Team uses the following tools to help debug, monitor and diagnose the Appwrite stack

View file

@ -0,0 +1 @@
Check the Appwrite storage device is up and connection is successful.

View file

@ -10,7 +10,6 @@ class Delete extends Event
{ {
protected string $type = ''; protected string $type = '';
protected ?Document $document = null; protected ?Document $document = null;
protected ?string $resourceType = null;
protected ?string $resource = null; protected ?string $resource = null;
protected ?string $datetime = null; protected ?string $datetime = null;
protected ?string $hourlyUsageRetentionDatetime = null; protected ?string $hourlyUsageRetentionDatetime = null;
@ -108,19 +107,6 @@ class Delete extends Event
return $this; 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. * Returns the set document for the delete event.
* *
@ -147,7 +133,6 @@ class Delete extends Event
'type' => $this->type, 'type' => $this->type,
'document' => $this->document, 'document' => $this->document,
'resource' => $this->resource, 'resource' => $this->resource,
'resourceType' => $this->resourceType,
'datetime' => $this->datetime, 'datetime' => $this->datetime,
'hourlyUsageRetentionDatetime' => $this->hourlyUsageRetentionDatetime 'hourlyUsageRetentionDatetime' => $this->hourlyUsageRetentionDatetime
]); ]);

View file

@ -338,6 +338,14 @@ class Mail extends Event
return $this; 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') public function setAttachment(string $content, string $filename, string $encoding = 'base64', string $type = 'plain/text')
{ {
$this->attachment = [ $this->attachment = [
@ -349,11 +357,45 @@ class Mail extends Event
return $this; return $this;
} }
/**
* Get attachment
*
* @return array
*/
public function getAttachment(): array public function getAttachment(): array
{ {
return $this->attachment; return $this->attachment;
} }
/**
* Reset attachment
*
* @return self
*/
public function resetAttachment(): self
{
$this->attachment = [];
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 = [];
return $this;
}
/** /**
* Executes the event and sends it to the mails worker. * Executes the event and sends it to the mails worker.
* *
@ -365,6 +407,7 @@ class Mail extends Event
$client = new Client($this->queue, $this->connection); $client = new Client($this->queue, $this->connection);
return $client->enqueue([ return $client->enqueue([
'project' => $this->project,
'recipient' => $this->recipient, 'recipient' => $this->recipient,
'name' => $this->name, 'name' => $this->name,
'subject' => $this->subject, 'subject' => $this->subject,

View file

@ -269,7 +269,7 @@ class CalcTierStats extends Action
$limit = $periods[$range]['limit']; $limit = $periods[$range]['limit'];
$period = $periods[$range]['period']; $period = $periods[$range]['period'];
$requestDocs = $dbForProject->find('stats', [ $requestDocs = $dbForProject->find('stats_v2', [
Query::equal('metric', [$metric]), Query::equal('metric', [$metric]),
Query::equal('period', [$period]), Query::equal('period', [$period]),
Query::limit($limit), Query::limit($limit),

View file

@ -163,8 +163,8 @@ class CreateInfMetric extends Action
try { try {
$id = \md5("_inf_{$metric}"); $id = \md5("_inf_{$metric}");
$dbForProject->deleteDocument('stats', $id); $dbForProject->deleteDocument('stats_v2', $id);
$dbForProject->createDocument('stats', new Document([ $dbForProject->createDocument('stats_v2', new Document([
'$id' => $id, '$id' => $id,
'metric' => $metric, 'metric' => $metric,
'period' => 'inf', 'period' => 'inf',
@ -186,7 +186,7 @@ class CreateInfMetric extends Action
protected function getFromMetric(database $dbForProject, string $metric): int|float protected function getFromMetric(database $dbForProject, string $metric): int|float
{ {
return $dbForProject->sum('stats', 'value', [ return $dbForProject->sum('stats_v2', 'value', [
Query::equal('metric', [ Query::equal('metric', [
$metric, $metric,
]), ]),

View file

@ -7,7 +7,9 @@ use Utopia\CLI\Console;
use Utopia\Platform\Action; use Utopia\Platform\Action;
use Utopia\Queue\Client; use Utopia\Queue\Client;
use Utopia\Queue\Connection; use Utopia\Queue\Connection;
use Utopia\Validator\Integer;
use Utopia\Validator\WhiteList; use Utopia\Validator\WhiteList;
use Utopia\Validator\Wildcard;
class QueueRetry extends Action class QueueRetry extends Action
{ {
@ -35,21 +37,25 @@ class QueueRetry extends Action
Event::MIGRATIONS_QUEUE_NAME, Event::MIGRATIONS_QUEUE_NAME,
Event::HAMSTER_CLASS_NAME Event::HAMSTER_CLASS_NAME
]), 'Queue name') ]), 'Queue name')
->param('limit', 0, new Wildcard(), 'jobs limit', true)
->inject('queue') ->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 string $name The name of the queue to retry jobs from
* @param mixed $limit
* @param Connection $queue * @param Connection $queue
*/ */
public function action(string $name, Connection $queue): void public function action(string $name, mixed $limit, Connection $queue): void
{ {
if (!$name) { if (!$name) {
Console::error('Missing required parameter $name'); Console::error('Missing required parameter $name');
return; return;
} }
$limit = (int)$limit;
$queueClient = new Client($name, $queue); $queueClient = new Client($name, $queue);
if ($queueClient->countFailedJobs() === 0) { if ($queueClient->countFailedJobs() === 0) {
@ -59,6 +65,6 @@ class QueueRetry extends Action
Console::log('Retrying failed jobs...'); Console::log('Retrying failed jobs...');
$queueClient->retry(); $queueClient->retry($limit);
} }
} }

View file

@ -72,7 +72,7 @@ class SDKs extends Action
$spec = file_get_contents(__DIR__ . '/../../../../app/config/specs/swagger2-' . $version . '-' . $language['family'] . '.json'); $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']; $result = \realpath(__DIR__ . '/../../../../app') . '/sdks/' . $key . '-' . $language['key'];
$resultExamples = \realpath(__DIR__ . '/../../../..') . '/docs/examples/' . $version . '/' . $key . '-' . $language['key']; $resultExamples = \realpath(__DIR__ . '/../../../..') . '/docs/examples/' . $version . '/' . $key . '-' . $language['key'];
$target = \realpath(__DIR__ . '/../../../../app') . '/sdks/git/' . $language['key'] . '/'; $target = \realpath(__DIR__ . '/../../../../app') . '/sdks/git/' . $language['key'] . '/';

View file

@ -71,7 +71,6 @@ class Deletes extends Action
$datetime = $payload['datetime'] ?? null; $datetime = $payload['datetime'] ?? null;
$hourlyUsageRetentionDatetime = $payload['hourlyUsageRetentionDatetime'] ?? null; $hourlyUsageRetentionDatetime = $payload['hourlyUsageRetentionDatetime'] ?? null;
$resource = $payload['resource'] ?? null; $resource = $payload['resource'] ?? null;
$resourceType = $payload['resourceType'] ?? null;
$document = new Document($payload['document'] ?? []); $document = new Document($payload['document'] ?? []);
$project = new Document($payload['project'] ?? []); $project = new Document($payload['project'] ?? []);
@ -81,6 +80,12 @@ class Deletes extends Action
switch (\strval($type)) { switch (\strval($type)) {
case DELETE_TYPE_DOCUMENT: case DELETE_TYPE_DOCUMENT:
switch ($document->getCollection()) { 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: case DELETE_TYPE_PROJECTS:
$this->deleteProject($dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $document); $this->deleteProject($dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $document);
break; break;
@ -109,6 +114,10 @@ class Deletes extends Action
$this->deleteRule($dbForConsole, $document); $this->deleteRule($dbForConsole, $document);
break; break;
default: 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()); Console::error('No lazy delete operation available for document of type: ' . $document->getCollection());
break; break;
} }
@ -138,7 +147,7 @@ class Deletes extends Action
$this->deleteUsageStats($project, $getProjectDB, $hourlyUsageRetentionDatetime); $this->deleteUsageStats($project, $getProjectDB, $hourlyUsageRetentionDatetime);
break; break;
case DELETE_TYPE_CACHE_BY_RESOURCE: case DELETE_TYPE_CACHE_BY_RESOURCE:
$this->deleteCacheByResource($project, $getProjectDB, $resource, $resourceType); $this->deleteCacheByResource($project, $getProjectDB, $resource);
break; break;
case DELETE_TYPE_CACHE_BY_TIMESTAMP: case DELETE_TYPE_CACHE_BY_TIMESTAMP:
$this->deleteCacheByDate($project, $getProjectDB, $datetime); $this->deleteCacheByDate($project, $getProjectDB, $datetime);
@ -323,37 +332,32 @@ class Deletes extends Action
* @param string $resource * @param string $resource
* @return void * @return void
* @throws Authorization * @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(); $projectId = $project->getId();
$dbForProject = $getProjectDB($project); $dbForProject = $getProjectDB($project);
$document = $dbForProject->findOne('cache', [Query::equal('resource', [$resource])]);
$cache = new Cache( if ($document) {
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $projectId) $cache = new Cache(
); new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $projectId)
);
$query[] = Query::equal('resource', [$resource]); $this->deleteById(
if (!empty($resourceType)) { $document,
$query[] = Query::equal('resourceType', [$resourceType]); $dbForProject,
} function ($document) use ($cache, $projectId) {
$path = APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $projectId . DIRECTORY_SEPARATOR . $document->getId();
$this->deleteByGroup( if ($cache->purge($document->getId())) {
'cache', Console::success('Deleting cache file: ' . $path);
$query, } else {
$dbForProject, Console::error('Failed to delete cache file: ' . $path);
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);
} }
} );
); }
} }
/** /**
@ -393,6 +397,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 Database $dbForConsole
* @param callable $getProjectDB * @param callable $getProjectDB
@ -404,7 +474,7 @@ class Deletes extends Action
{ {
$dbForProject = $getProjectDB($project); $dbForProject = $getProjectDB($project);
// Delete Usage stats // Delete Usage stats
$this->deleteByGroup('stats', [ $this->deleteByGroup('stats_v2', [
Query::lessThan('time', $hourlyUsageRetentionDatetime), Query::lessThan('time', $hourlyUsageRetentionDatetime),
Query::equal('period', ['1h']), Query::equal('period', ['1h']),
], $dbForProject); ], $dbForProject);

View file

@ -286,7 +286,7 @@ class Hamster extends Action
$limit = $periodValue['limit']; $limit = $periodValue['limit'];
$period = $periodValue['period']; $period = $periodValue['period'];
$requestDocs = $dbForProject->find('stats', [ $requestDocs = $dbForProject->find('stats_v2', [
Query::equal('period', [$period]), Query::equal('period', [$period]),
Query::equal('metric', [$metric]), Query::equal('metric', [$metric]),
Query::limit($limit), Query::limit($limit),

View file

@ -389,6 +389,41 @@ class Messaging extends Action
$log->addTag('type', $host); $log->addTag('type', $host);
if (empty($payload['message'])) {
Console::error('Message arg not found');
return;
}
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'))) {
Console::error('Skipped sms processing. No Phone provider has been set.');
return;
}
$from = App::getEnv('_APP_SMS_FROM'); $from = App::getEnv('_APP_SMS_FROM');
$provider = new Document([ $provider = new Document([

View file

@ -75,6 +75,7 @@ class Usage extends Action
} }
$this->stats[$projectId]['project'] = $project; $this->stats[$projectId]['project'] = $project;
$this->stats[$projectId]['receivedAt'] = DateTime::now();
foreach ($payload['metrics'] ?? [] as $metric) { foreach ($payload['metrics'] ?? [] as $metric) {
$this->keys++; $this->keys++;
if (!isset($this->stats[$projectId]['keys'][$metric['key']])) { if (!isset($this->stats[$projectId]['keys'][$metric['key']])) {
@ -127,8 +128,8 @@ class Usage extends Action
} }
break; break;
case $document->getCollection() === 'databases': // databases case $document->getCollection() === 'databases': // databases
$collections = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getInternalId(), METRIC_DATABASE_ID_COLLECTIONS))); $collections = $dbForProject->getDocument('stats_v2', 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))); $documents = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{databaseInternalId}', $document->getInternalId(), METRIC_DATABASE_ID_DOCUMENTS)));
if (!empty($collections['value'])) { if (!empty($collections['value'])) {
$metrics[] = [ $metrics[] = [
'key' => METRIC_COLLECTIONS, 'key' => METRIC_COLLECTIONS,
@ -146,7 +147,7 @@ class Usage extends Action
case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections case str_starts_with($document->getCollection(), 'database_') && !str_contains($document->getCollection(), 'collection'): //collections
$parts = explode('_', $document->getCollection()); $parts = explode('_', $document->getCollection());
$databaseInternalId = $parts[1] ?? 0; $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'])) { if (!empty($documents['value'])) {
$metrics[] = [ $metrics[] = [
@ -161,8 +162,8 @@ class Usage extends Action
break; break;
case $document->getCollection() === 'buckets': case $document->getCollection() === 'buckets':
$files = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace('{bucketInternalId}', $document->getInternalId(), METRIC_BUCKET_ID_FILES))); $files = $dbForProject->getDocument('stats_v2', 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))); $storage = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{bucketInternalId}', $document->getInternalId(), METRIC_BUCKET_ID_FILES_STORAGE)));
if (!empty($files['value'])) { if (!empty($files['value'])) {
$metrics[] = [ $metrics[] = [
@ -180,13 +181,13 @@ class Usage extends Action
break; break;
case $document->getCollection() === 'functions': case $document->getCollection() === 'functions':
$deployments = $dbForProject->getDocument('stats', md5(self::INFINITY_PERIOD . str_replace(['{resourceType}', '{resourceInternalId}'], ['functions', $document->getInternalId()], METRIC_FUNCTION_ID_DEPLOYMENTS))); $deployments = $dbForProject->getDocument('stats_v2', 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))); $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', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_BUILDS))); $builds = $dbForProject->getDocument('stats_v2', 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))); $buildsStorage = $dbForProject->getDocument('stats_v2', 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))); $buildsCompute = $dbForProject->getDocument('stats_v2', 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))); $executions = $dbForProject->getDocument('stats_v2', 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))); $executionsCompute = $dbForProject->getDocument('stats_v2', md5(self::INFINITY_PERIOD . str_replace('{functionInternalId}', $document->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_COMPUTE)));
if (!empty($deployments['value'])) { if (!empty($deployments['value'])) {
$metrics[] = [ $metrics[] = [

View file

@ -56,12 +56,12 @@ class UsageDump extends Action
foreach ($payload['stats'] ?? [] as $stats) { foreach ($payload['stats'] ?? [] as $stats) {
$project = new Document($stats['project'] ?? []); $project = new Document($stats['project'] ?? []);
$numberOfKeys = !empty($stats['keys']) ? count($stats['keys']) : 0; $numberOfKeys = !empty($stats['keys']) ? count($stats['keys']) : 0;
$receivedAt = $stats['receivedAt'] ?? 'NONE';
if ($numberOfKeys === 0) { if ($numberOfKeys === 0) {
continue; 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 { try {
$dbForProject = $getProjectDB($project); $dbForProject = $getProjectDB($project);
@ -75,7 +75,7 @@ class UsageDump extends Action
$id = \md5("{$time}_{$period}_{$key}"); $id = \md5("{$time}_{$period}_{$key}");
try { try {
$dbForProject->createDocument('stats', new Document([ $dbForProject->createDocument('stats_v2', new Document([
'$id' => $id, '$id' => $id,
'period' => $period, 'period' => $period,
'time' => $time, 'time' => $time,
@ -86,14 +86,14 @@ class UsageDump extends Action
} catch (Duplicate $th) { } catch (Duplicate $th) {
if ($value < 0) { if ($value < 0) {
$dbForProject->decreaseDocumentAttribute( $dbForProject->decreaseDocumentAttribute(
'stats', 'stats_v2',
$id, $id,
'value', 'value',
abs($value) abs($value)
); );
} else { } else {
$dbForProject->increaseDocumentAttribute( $dbForProject->increaseDocumentAttribute(
'stats', 'stats_v2',
$id, $id,
'value', 'value',
$value $value

View file

@ -291,7 +291,7 @@ class OpenAPI3 extends Format
case 'Utopia\Database\Validator\UID': case 'Utopia\Database\Validator\UID':
case 'Utopia\Validator\Text': case 'Utopia\Validator\Text':
$node['schema']['type'] = $validator->getType(); $node['schema']['type'] = $validator->getType();
$node['schema']['x-example'] = '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; $node['schema']['x-example'] = '[' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . ']';
break; break;
case 'Utopia\Validator\Boolean': case 'Utopia\Validator\Boolean':
$node['schema']['type'] = $validator->getType(); $node['schema']['type'] = $validator->getType();
@ -302,7 +302,7 @@ class OpenAPI3 extends Format
$node['schema']['x-upload-id'] = true; $node['schema']['x-upload-id'] = true;
} }
$node['schema']['type'] = $validator->getType(); $node['schema']['type'] = $validator->getType();
$node['schema']['x-example'] = '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; $node['schema']['x-example'] = '[' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . ']';
break; break;
case 'Utopia\Database\Validator\DatetimeValidator': case 'Utopia\Database\Validator\DatetimeValidator':
$node['schema']['type'] = $validator->getType(); $node['schema']['type'] = $validator->getType();

View file

@ -290,7 +290,7 @@ class Swagger2 extends Format
case 'Utopia\Validator\Text': case 'Utopia\Validator\Text':
case 'Utopia\Database\Validator\UID': case 'Utopia\Database\Validator\UID':
$node['type'] = $validator->getType(); $node['type'] = $validator->getType();
$node['x-example'] = '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; $node['x-example'] = '[' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . ']';
break; break;
case 'Utopia\Validator\Boolean': case 'Utopia\Validator\Boolean':
$node['type'] = $validator->getType(); $node['type'] = $validator->getType();
@ -301,7 +301,7 @@ class Swagger2 extends Format
$node['x-upload-id'] = true; $node['x-upload-id'] = true;
} }
$node['type'] = $validator->getType(); $node['type'] = $validator->getType();
$node['x-example'] = '<' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . '>'; $node['x-example'] = '[' . \strtoupper(Template::fromCamelCaseToSnake($node['name'])) . ']';
break; break;
case 'Utopia\Database\Validator\DatetimeValidator': case 'Utopia\Database\Validator\DatetimeValidator':
$node['type'] = $validator->getType(); $node['type'] = $validator->getType();

View file

@ -407,6 +407,24 @@ class HealthCustomServerTest extends Scope
return []; 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 public function testStorageAntiVirusSuccess(): array
{ {
/** /**

View file

@ -2015,7 +2015,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $id, 'x-appwrite-project' => $id,
], $this->getHeaders())); ], $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([ $response = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
'content-type' => 'application/json', 'content-type' => 'application/json',
@ -2025,7 +2025,7 @@ class ProjectsConsoleClientTest extends Scope
'name' => 'Arsenal' 'name' => 'Arsenal'
]); ]);
$this->assertEquals(503, $response['headers']['status-code']); $this->assertEquals(403, $response['headers']['status-code']);
// Cleanup // Cleanup