diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8256ddc7a..b02d021f1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -210,7 +210,7 @@ jobs: with: script: | const allDatabases = ['MariaDB', 'PostgreSQL', 'MongoDB']; - const allModes = ['dedicated', 'shared_v1', 'shared_v2']; + const allModes = ['dedicated', 'shared']; const defaultDatabases = ['MongoDB']; const defaultModes = ['dedicated']; @@ -479,11 +479,8 @@ jobs: env: _APP_BROWSER_HOST: http://invalid-browser/v1 _APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }} - _APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }} _APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }} - _APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }} _APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }} - _APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }} run: | docker load --input /tmp/${{ env.IMAGE }}.tar docker compose pull --quiet --ignore-buildable @@ -557,11 +554,8 @@ jobs: env: _APP_OPTIONS_ABUSE: enabled _APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }} - _APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }} _APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }} - _APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }} _APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }} - _APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }} run: | docker load --input /tmp/${{ env.IMAGE }}.tar docker compose pull --quiet --ignore-buildable @@ -618,11 +612,8 @@ jobs: timeout-minutes: 5 env: _APP_DATABASE_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'database_db_main' || '' }} - _APP_DATABASE_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'database_db_main' || '' }} _APP_DATABASE_DOCUMENTSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'documentsdb_db_main' || '' }} - _APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'documentsdb_db_main' || '' }} _APP_DATABASE_VECTORSDB_SHARED_TABLES: ${{ matrix.mode != 'dedicated' && 'vectorsdb_db_main' || '' }} - _APP_DATABASE_VECTORSDB_SHARED_TABLES_V1: ${{ matrix.mode == 'shared_v1' && 'vectorsdb_db_main' || '' }} run: | docker load --input /tmp/${{ env.IMAGE }}.tar docker compose pull --quiet --ignore-buildable diff --git a/CHANGES.md b/CHANGES.md index 548c0d72b0..6894322043 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -892,7 +892,7 @@ * Unset index length by @fogelito in https://github.com/appwrite/appwrite/pull/8978 * Update base to 0.9.5 by @basert in https://github.com/appwrite/appwrite/pull/9005 * Sync main into 1.6.x by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/9011 -* Improved shared tables V2 by @abnegate in https://github.com/appwrite/appwrite/pull/9013 +* Improved shared tables by @abnegate in https://github.com/appwrite/appwrite/pull/9013 * Ensure backwards compatibility for 1.6.x by @christyjacob4 in https://github.com/appwrite/appwrite/pull/9018 # Version 1.6.0 diff --git a/app/config/sdks.php b/app/config/sdks.php index 47dc8845b6..e89265b05e 100644 --- a/app/config/sdks.php +++ b/app/config/sdks.php @@ -300,6 +300,26 @@ return [ 'repoBranch' => 'main', 'changelog' => \realpath(__DIR__ . '/../../docs/sdks/cursor-plugin/CHANGELOG.md'), ], + [ + 'key' => 'claude-plugin', + 'name' => 'ClaudePlugin', + 'version' => '0.1.0', + 'url' => 'https://github.com/appwrite/claude-plugin.git', + 'enabled' => true, + 'beta' => false, + 'dev' => false, + 'hidden' => false, + 'spec' => 'static', + 'family' => APP_SDK_PLATFORM_STATIC, + 'prism' => 'claude-plugin', + 'source' => \realpath(__DIR__ . '/../sdks/static-claude-plugin'), + 'gitUrl' => 'git@github.com:appwrite/claude-plugin.git', + 'gitRepoName' => 'claude-plugin', + 'gitUserName' => 'appwrite', + 'gitBranch' => 'dev', + 'repoBranch' => 'main', + 'changelog' => \realpath(__DIR__ . '/../../docs/sdks/claude-plugin/CHANGELOG.md'), + ], ], ], diff --git a/app/config/templates/site.php b/app/config/templates/site.php index 26f8e39817..b26d31f475 100644 --- a/app/config/templates/site.php +++ b/app/config/templates/site.php @@ -1487,13 +1487,13 @@ return [ ] ], [ - 'key' => 'crm-dashboard-react-admin', - 'name' => 'CRM dashboard with React Admin', - 'tagline' => 'A React-based admin dashboard template with CRM features.', + 'key' => 'dashboard-react-admin', + 'name' => 'E-commerce dashboard with React Admin', + 'tagline' => 'A React-based admin dashboard template with e-commerce features.', 'score' => 4, // 0 to 10 based on looks of screenshot (avoid 1,2,3,8,9,10 if possible) - 'useCases' => [SiteUseCases::DASHBOARD], - 'screenshotDark' => $url . '/images/sites/templates/crm-dashboard-react-admin-dark.png', - 'screenshotLight' => $url . '/images/sites/templates/crm-dashboard-react-admin-light.png', + 'useCases' => [SiteUseCases::DASHBOARD, SiteUseCases::ECOMMERCE], + 'screenshotDark' => $url . '/images/sites/templates/dashboard-react-admin-dark.png', + 'screenshotLight' => $url . '/images/sites/templates/dashboard-react-admin-light.png', 'frameworks' => [ getFramework('REACT', [ 'providerRootDirectory' => './react/react-admin', diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 5567281e67..bba00bede1 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -971,7 +971,8 @@ Http::shutdown() if ($useCache) { $resource = $resourceType = null; $data = $response->getPayload(); - if (! empty($data['payload'])) { + $statusCode = $response->getStatusCode(); + if (! empty($data['payload']) && $statusCode >= 200 && $statusCode < 300) { $pattern = $route->getLabel('cache.resource', null); if (! empty($pattern)) { $resource = $parseLabel($pattern, $responsePayload, $requestParams, $user); diff --git a/app/http.php b/app/http.php index afcc2d2d0f..b72f3b7f34 100644 --- a/app/http.php +++ b/app/http.php @@ -413,27 +413,19 @@ $http->on(Constant::EVENT_START, function ($http) use ($payloadSize, $totalWorke $projectCollections = $collections['projects']; $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); - $sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', '')); - $sharedTablesV2 = \array_diff($sharedTables, $sharedTablesV1); - $documentsSharedTables = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', '')); - $documentsSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', '')); - $documentsSharedTablesV2 = \array_diff($documentsSharedTables, $documentsSharedTablesV1); - $vectorSharedTables = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', '')); - $vectorSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', '')); - $vectorSharedTablesV2 = \array_diff($vectorSharedTables, $vectorSharedTablesV1); $cache = $app->getResource('cache'); - // All shared tables V2 pools that need project metadata collections - $sharedTablesV2All = \array_values(\array_unique(\array_filter([ - ...$sharedTablesV2, - ...$documentsSharedTablesV2, - ...$vectorSharedTablesV2, + // All shared tables pools that need project metadata collections + $allSharedTables = \array_values(\array_unique(\array_filter([ + ...$sharedTables, + ...$documentsSharedTables, + ...$vectorSharedTables, ]))); - foreach ($sharedTablesV2All as $hostname) { + foreach ($allSharedTables as $hostname) { Span::init('database.setup'); Span::add('database.hostname', $hostname); diff --git a/docker-compose.yml b/docker-compose.yml index 391d71fb48..2e53b67901 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -242,7 +242,6 @@ services: - _APP_EXPERIMENT_LOGGING_PROVIDER - _APP_EXPERIMENT_LOGGING_CONFIG - _APP_DATABASE_SHARED_TABLES - - _APP_DATABASE_SHARED_TABLES_V1 - _APP_DATABASE_SHARED_NAMESPACE - _APP_FUNCTIONS_CREATION_ABUSE_LIMIT - _APP_CUSTOM_DOMAIN_DENY_LIST @@ -462,7 +461,6 @@ services: - _APP_EXECUTOR_SECRET - _APP_EXECUTOR_HOST - _APP_DATABASE_SHARED_TABLES - - _APP_DATABASE_SHARED_TABLES_V1 - _APP_EMAIL_CERTIFICATES - _APP_MAINTENANCE_RETENTION_AUDIT - _APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE diff --git a/public/images/sites/templates/crm-dashboard-react-admin-dark.png b/public/images/sites/templates/dashboard-react-admin-dark.png similarity index 100% rename from public/images/sites/templates/crm-dashboard-react-admin-dark.png rename to public/images/sites/templates/dashboard-react-admin-dark.png diff --git a/public/images/sites/templates/crm-dashboard-react-admin-light.png b/public/images/sites/templates/dashboard-react-admin-light.png similarity index 100% rename from public/images/sites/templates/crm-dashboard-react-admin-light.png rename to public/images/sites/templates/dashboard-react-admin-light.png diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Create.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Create.php index 3d07c65250..294a6712a9 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Create.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Create.php @@ -49,11 +49,6 @@ class Create extends Action $databaseOverride = ''; $dbScheme = ''; $databaseSharedTables = []; - $databaseSharedTablesV1 = []; - $databaseSharedTablesV2 = []; - $projectSharedTables = []; - $projectSharedTablesV1 = []; - $projectSharedTablesV2 = []; switch ($databasetype) { case DOCUMENTSDB: @@ -62,7 +57,6 @@ class Create extends Action $databaseOverride = System::getEnv('_APP_DATABASE_DOCUMENTSDB_OVERRIDE'); $dbScheme = System::getEnv('_APP_DB_HOST_DOCUMENTSDB', 'mongodb'); $databaseSharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES', ''))); - $databaseSharedTablesV1 = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_DOCUMENTSDB_SHARED_TABLES_V1', ''))); break; case VECTORSDB: $databases = Config::getParam('pools-vectorsdb', []); @@ -70,7 +64,6 @@ class Create extends Action $databaseOverride = System::getEnv('_APP_DATABASE_VECTORSDB_OVERRIDE'); $dbScheme = System::getEnv('_APP_DB_HOST_VECTORSDB', 'postgresql'); $databaseSharedTables = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES', ''))); - $databaseSharedTablesV1 = \array_filter(\explode(',', System::getEnv('_APP_DATABASE_VECTORSDB_SHARED_TABLES_V1', ''))); break; default: // legacy/tablesdb @@ -78,8 +71,7 @@ class Create extends Action return $dsn; } - $isSharedTablesV1 = false; - $isSharedTablesV2 = false; + $isSharedTables = false; if (!empty($dsn)) { try { @@ -90,10 +82,7 @@ class Create extends Action } $projectSharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); - $projectSharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', '')); - $projectSharedTablesV2 = \array_diff($projectSharedTables, $projectSharedTablesV1); - $isSharedTablesV1 = \in_array($dsnHost, $projectSharedTablesV1); - $isSharedTablesV2 = \in_array($dsnHost, $projectSharedTablesV2); + $isSharedTables = \in_array($dsnHost, $projectSharedTables); } if ($region !== 'default') { @@ -102,18 +91,14 @@ class Create extends Action return str_contains($value, $region); }); } - $databaseSharedTablesV2 = \array_diff($databaseSharedTables, $databaseSharedTablesV1); $index = \array_search($databaseOverride, $databases); if ($index !== false) { $selectedDsn = $databases[$index]; } else { if (!empty($dsn) && !empty($databaseSharedTables)) { - $beforeFilter = \array_values($databases); - if ($isSharedTablesV1) { - $databases = array_filter($databases, fn ($value) => \in_array($value, $databaseSharedTablesV1)); - } elseif ($isSharedTablesV2) { - $databases = array_filter($databases, fn ($value) => \in_array($value, $databaseSharedTablesV2)); + if ($isSharedTables) { + $databases = array_filter($databases, fn ($value) => \in_array($value, $databaseSharedTables)); } else { $databases = array_filter($databases, fn ($value) => !\in_array($value, $databaseSharedTables)); } diff --git a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php index 0668e5ceb0..a9f4a6139b 100644 --- a/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php +++ b/src/Appwrite/Platform/Modules/Functions/Workers/Builds.php @@ -145,7 +145,8 @@ class Builds extends Action $log, $executor, $plan, - $platform + $platform, + (int) ($payload['timeout'] ?? System::getEnv('_APP_COMPUTE_BUILD_TIMEOUT', 900)) ); break; @@ -180,7 +181,8 @@ class Builds extends Action Log $log, Executor $executor, array $plan, - array $platform + array $platform, + int $timeout ): void { Console::info('Deployment action started'); @@ -689,10 +691,7 @@ class Builds extends Action $cpus = $spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT; $memory = max($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT, $minMemory); - $timeout = (int) System::getEnv('_APP_COMPUTE_BUILD_TIMEOUT', 900); - - $jwtExpiry = (int) System::getEnv('_APP_COMPUTE_BUILD_TIMEOUT', 900); - $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0); + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $timeout, 0); $apiKey = $jwtObj->encode([ 'projectId' => $project->getId(), diff --git a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php index 9070962e7d..c509a565cd 100644 --- a/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php +++ b/src/Appwrite/Platform/Modules/Projects/Http/Projects/Create.php @@ -21,8 +21,6 @@ use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Exception\Duplicate; use Utopia\Database\Helpers\ID; -use Utopia\Database\Helpers\Permission; -use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\UID; use Utopia\DSN\DSN; use Utopia\Platform\Scope\HTTP; @@ -209,32 +207,16 @@ class Create extends Action } $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); - $sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', '')); $projectTables = !\in_array($dsn->getHost(), $sharedTables); - $sharedTablesV1 = \in_array($dsn->getHost(), $sharedTablesV1); - $sharedTablesV2 = !$projectTables && !$sharedTablesV1; - $sharedTables = $sharedTablesV1 || $sharedTablesV2; - if (!$sharedTablesV2) { + if ($projectTables) { $adapter = new DatabasePool($pools->get($dsn->getHost())); $dbForProject = new Database($adapter, $cache); - $dbForProject->setDatabase(APP_DATABASE); - - if ($sharedTables) { - $tenant = null; - if ($sharedTablesV1) { - $tenant = $project->getSequence(); - } - $dbForProject - ->setSharedTables(true) - ->setTenant($tenant) - ->setNamespace($dsn->getParam('namespace')); - } else { - $dbForProject - ->setSharedTables(false) - ->setTenant(null) - ->setNamespace('_' . $project->getSequence()); - } + $dbForProject + ->setDatabase(APP_DATABASE) + ->setSharedTables(false) + ->setTenant(null) + ->setNamespace('_' . $project->getSequence()); $create = true; @@ -244,27 +226,11 @@ class Create extends Action $create = false; } - if ($create || $projectTables) { - $adapter = new AdapterDatabase($dbForProject); - $audit = new Audit($adapter); - $audit->setup(); - } + $adapter = new AdapterDatabase($dbForProject); + $audit = new Audit($adapter); + $audit->setup(); - if (!$create && $sharedTablesV1) { - $adapter = new AdapterDatabase($dbForProject); - $attributes = $adapter->getAttributeDocuments(); - $indexes = $adapter->getIndexDocuments(); - $dbForProject->createDocument(Database::METADATA, new Document([ - '$id' => ID::custom('audit'), - '$permissions' => [Permission::create(Role::any())], - 'name' => 'audit', - 'attributes' => $attributes, - 'indexes' => $indexes, - 'documentSecurity' => true - ])); - } - - if ($create || $sharedTablesV1) { + if ($create) { /** @var array $collections */ $collections = Config::getParam('collections', [])['projects'] ?? []; @@ -279,37 +245,7 @@ class Create extends Action try { $dbForProject->createCollection($key, $attributes, $indexes); } catch (Duplicate) { - try { - $dbForProject->createDocument(Database::METADATA, new Document([ - '$id' => ID::custom($key), - '$permissions' => [Permission::create(Role::any())], - 'name' => $key, - 'attributes' => $attributes, - 'indexes' => $indexes, - 'documentSecurity' => true - ])); - } catch (Duplicate) { - // Metadata already exists from concurrent creation - } - } catch (\Throwable $e) { - // PostgreSQL adapter may throw a non-Duplicate exception when - // a table or index already exists during concurrent project - // creation in shared mode. Treat as duplicate if metadata - // can be created successfully. - try { - $dbForProject->createDocument(Database::METADATA, new Document([ - '$id' => ID::custom($key), - '$permissions' => [Permission::create(Role::any())], - 'name' => $key, - 'attributes' => $attributes, - 'indexes' => $indexes, - 'documentSecurity' => true - ])); - } catch (Duplicate) { - // Metadata already exists from concurrent creation - } catch (\Throwable) { - throw $e; // Rethrow original if metadata creation also fails - } + // Collection already exists } } } diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php index f0ee045214..f6b6eb25da 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Files/Preview/Get.php @@ -54,7 +54,7 @@ class Get extends Action ->label('cache', true) ->label('cache.resourceType', 'bucket/{request.bucketId}') ->label('cache.resource', 'file/{request.fileId}') - ->label('cache.params', ['width', 'height', 'gravity', 'quality', 'borderWidth', 'borderColor', 'borderRadius', 'opacity', 'rotation', 'background', 'output']) + ->label('cache.params', ['width', 'height', 'gravity', 'quality', 'borderWidth', 'borderColor', 'borderRadius', 'opacity', 'rotation', 'background', 'output', 'project']) ->label('sdk', new Method( namespace: 'storage', group: 'files', diff --git a/src/Appwrite/Platform/Tasks/SDKs.php b/src/Appwrite/Platform/Tasks/SDKs.php index 526ea304de..aac738915d 100644 --- a/src/Appwrite/Platform/Tasks/SDKs.php +++ b/src/Appwrite/Platform/Tasks/SDKs.php @@ -5,6 +5,7 @@ namespace Appwrite\Platform\Tasks; use Appwrite\SDK\Language\AgentSkills; use Appwrite\SDK\Language\Android; use Appwrite\SDK\Language\Apple; +use Appwrite\SDK\Language\ClaudePlugin; use Appwrite\SDK\Language\CLI; use Appwrite\SDK\Language\CursorPlugin; use Appwrite\SDK\Language\Dart; @@ -451,6 +452,9 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND case 'cursor-plugin': $config = new CursorPlugin(); break; + case 'claude-plugin': + $config = new ClaudePlugin(); + break; default: throw new \Exception('Language "' . $language['key'] . '" not supported'); } diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index f4978780a1..6801d12b77 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -651,11 +651,8 @@ class Deletes extends Action ]; $sharedTables = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES', '')); - $sharedTablesV1 = \explode(',', System::getEnv('_APP_DATABASE_SHARED_TABLES_V1', '')); $projectTables = !\in_array($dsn->getHost(), $sharedTables); - $sharedTablesV1 = \in_array($dsn->getHost(), $sharedTablesV1); - $sharedTablesV2 = !$projectTables && !$sharedTablesV1; $allDatabases = [ new Document([ @@ -758,23 +755,7 @@ class Deletes extends Action ), $databasesToClean )); - } elseif ($sharedTablesV1) { - /** - * Temporary disabling deletes for internal collections - */ - $queries = \array_map( - fn ($id) => Query::notEqual('$id', $id), - $projectCollectionIds - ); - - $queries[] = Query::orderAsc(); - - $this->deleteByGroup( - Database::METADATA, - $queries, - $dbForProject - ); - } elseif ($sharedTablesV2) { + } else { $queries = \array_map( fn ($id) => Query::notEqual('$id', $id), $projectCollectionIds diff --git a/src/Appwrite/Utopia/Request.php b/src/Appwrite/Utopia/Request.php index 3f1ea794ab..32f0fa89a9 100644 --- a/src/Appwrite/Utopia/Request.php +++ b/src/Appwrite/Utopia/Request.php @@ -238,6 +238,9 @@ class Request extends UtopiaRequest if ($allowedParams !== null) { $params = array_intersect_key($params, array_flip($allowedParams)); } + if (!isset($params['project'])) { + $params['project'] = $this->getHeader('x-appwrite-project', ''); + } ksort($params); return md5($this->getURI() . '*' . serialize($params) . '*' . APP_CACHE_BUSTER); } diff --git a/tests/e2e/Services/Storage/StorageBase.php b/tests/e2e/Services/Storage/StorageBase.php index d1cb548016..60a4aefc85 100644 --- a/tests/e2e/Services/Storage/StorageBase.php +++ b/tests/e2e/Services/Storage/StorageBase.php @@ -1050,6 +1050,28 @@ trait StorageBase $this->assertEquals(404, $file['headers']['status-code']); } + public function testFilePreviewAvifPublic(): void + { + $data = $this->setupBucketFile(); + $bucketId = $data['bucketId']; + $fileId = $data['fileId']; + $projectId = $this->getProject()['$id']; + + // Matches the customer's URL pattern: no headers, project + output in query string only + $preview = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId . '/preview', [ + 'content-type' => 'application/json', + ], [ + 'project' => $projectId, + 'width' => 1080, + 'quality' => 40, + 'output' => 'avif', + ]); + + $this->assertEquals(200, $preview['headers']['status-code']); + $this->assertEquals('image/avif', $preview['headers']['content-type']); + $this->assertNotEmpty($preview['body']); + } + public function testFilePreview(): void { $data = $this->setupBucketFile(); @@ -1069,6 +1091,49 @@ trait StorageBase $this->assertEquals(200, $preview['headers']['status-code']); $this->assertEquals('image/webp', $preview['headers']['content-type']); $this->assertNotEmpty($preview['body']); + + // Preview PNG as avif + $avifPreview = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId . '/preview', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'width' => 1080, + 'quality' => 40, + 'output' => 'avif', + ]); + + $this->assertEquals(200, $avifPreview['headers']['status-code']); + $this->assertEquals('image/avif', $avifPreview['headers']['content-type']); + $this->assertNotEmpty($avifPreview['body']); + + // Preview JPEG as avif + $jpegFile = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'fileId' => ID::unique(), + 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/disk-a/kitten-1.jpg'), 'image/jpeg', 'kitten-1.jpg'), + 'permissions' => [ + Permission::read(Role::any()), + Permission::update(Role::any()), + Permission::delete(Role::any()), + ], + ]); + + $this->assertEquals(201, $jpegFile['headers']['status-code']); + + $avifFromJpeg = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $jpegFile['body']['$id'] . '/preview', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'width' => 1080, + 'quality' => 40, + 'output' => 'avif', + ]); + + $this->assertEquals(200, $avifFromJpeg['headers']['status-code']); + $this->assertEquals('image/avif', $avifFromJpeg['headers']['content-type']); + $this->assertNotEmpty($avifFromJpeg['body']); } public function testDeletePartiallyUploadedFile(): void