diff --git a/composer.json b/composer.json index 5585f2a..4c9cdc4 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "php": ">=8.1", "ext-curl": "*", "ext-openssl": "*", - "appwrite/appwrite": "19.*", + "appwrite/appwrite": "20.*", "utopia-php/database": "5.*", "utopia-php/storage": "1.0.*", "utopia-php/dsn": "0.2.*", diff --git a/composer.lock b/composer.lock index 17d1422..9444b08 100644 --- a/composer.lock +++ b/composer.lock @@ -4,29 +4,29 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "37980b9001fbbd4f213f3102c1332727", + "content-hash": "cce689808f9fd30a82c0b7d1819af517", "packages": [ { "name": "appwrite/appwrite", - "version": "19.1.0", + "version": "20.1.0", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-for-php.git", - "reference": "8738e812062f899c85b2598eef43d6a247f08a56" + "reference": "e8c76cbe6b09f964694a3d1b9096cb91737e48e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/8738e812062f899c85b2598eef43d6a247f08a56", - "reference": "8738e812062f899c85b2598eef43d6a247f08a56", + "url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/e8c76cbe6b09f964694a3d1b9096cb91737e48e2", + "reference": "e8c76cbe6b09f964694a3d1b9096cb91737e48e2", "shasum": "" }, "require": { "ext-curl": "*", "ext-json": "*", - "php": ">=7.1.0" + "php": ">=8.0.0" }, "require-dev": { - "mockery/mockery": "^1.6.12", + "mockery/mockery": "1.6.12", "phpunit/phpunit": "^10" }, "type": "library", @@ -39,14 +39,14 @@ "license": [ "BSD-3-Clause" ], - "description": "Appwrite is an open-source self-hosted backend server that abstract and simplify complex and repetitive development tasks behind a very simple REST API", + "description": "Appwrite is an open-source self-hosted backend server that abstracts and simplifies complex and repetitive development tasks behind a very simple REST API", "support": { "email": "team@appwrite.io", "issues": "https://github.com/appwrite/sdk-for-php/issues", - "source": "https://github.com/appwrite/sdk-for-php/tree/19.1.0", + "source": "https://github.com/appwrite/sdk-for-php/tree/20.1.0", "url": "https://appwrite.io/support" }, - "time": "2025-12-18T08:07:43+00:00" + "time": "2026-02-12T11:55:40+00:00" }, { "name": "brick/math", @@ -4997,7 +4997,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index 6b730a1..c786fc7 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -11,6 +11,7 @@ use Appwrite\Enums\PasswordHash; use Appwrite\Enums\Runtime; use Appwrite\InputFile; +use Appwrite\Services\Backups; use Appwrite\Services\Functions; use Appwrite\Services\Sites; use Appwrite\Services\Storage; @@ -38,6 +39,7 @@ use Utopia\Migration\Resources\Auth\Membership; use Utopia\Migration\Resources\Auth\Team; use Utopia\Migration\Resources\Auth\User; +use Utopia\Migration\Resources\Backups\Policy; use Utopia\Migration\Resources\Database\Column; use Utopia\Migration\Resources\Database\Database; use Utopia\Migration\Resources\Database\Index; @@ -60,6 +62,7 @@ class Appwrite extends Destination protected string $key; + private Backups $backups; private Functions $functions; private Sites $sites; private Storage $storage; @@ -94,6 +97,7 @@ public function __construct( ->setProject($project) ->setKey($key); + $this->backups = new Backups($this->client); $this->functions = new Functions($this->client); $this->sites = new Sites($this->client); $this->storage = new Storage($this->client); @@ -138,6 +142,9 @@ public static function getSupportedResources(): array Resource::TYPE_DEPLOYMENT, Resource::TYPE_ENVIRONMENT_VARIABLE, + // Backups + Resource::TYPE_BACKUP_POLICY, + // Sites Resource::TYPE_SITE, Resource::TYPE_SITE_DEPLOYMENT, @@ -222,6 +229,15 @@ public function report(array $resources = [], array $resourceIds = []): array $this->sites->create('', '', Framework::OTHER(), BuildRuntime::STATIC1()); } + // Backups + if (\in_array(Resource::TYPE_BACKUP_POLICY, $resources)) { + $scope = 'policies.read'; + $this->backups->listPolicies(); + + $scope = 'policies.write'; + $this->backups->createPolicy('', [], 0, ''); + } + } catch (AppwriteException $e) { if ($e->getCode() === 403) { throw new \Exception('Missing scope: ' . $scope, previous: $e); @@ -259,6 +275,7 @@ protected function import(array $resources, callable $callback): void Transfer::GROUP_STORAGE => $this->importFileResource($resource), Transfer::GROUP_AUTH => $this->importAuthResource($resource), Transfer::GROUP_FUNCTIONS => $this->importFunctionResource($resource), + Transfer::GROUP_BACKUPS => $this->importBackupResource($resource), Transfer::GROUP_SITES => $this->importSiteResource($resource), default => throw new \Exception('Invalid resource group'), }; @@ -1406,9 +1423,6 @@ public function importFunctionResource(Resource $resource): Resource 'dart-2.17' => Runtime::DART217(), 'dart-2.18' => Runtime::DART218(), 'dart-2.19' => Runtime::DART219(), - 'deno-1.21' => Runtime::DENO121(), - 'deno-1.24' => Runtime::DENO124(), - 'deno-1.35' => Runtime::DENO135(), 'deno-1.40' => Runtime::DENO140(), 'deno-1.46' => Runtime::DENO146(), 'deno-2.0' => Runtime::DENO20(), @@ -1467,6 +1481,31 @@ public function importFunctionResource(Resource $resource): Resource return $resource; } + /** + * @throws \Exception + */ + public function importBackupResource(Resource $resource): Resource + { + switch ($resource->getName()) { + case Resource::TYPE_BACKUP_POLICY: + /** @var Policy $resource */ + $this->backups->createPolicy( + policyId: $resource->getId(), + services: $resource->getServices(), + retention: $resource->getRetention(), + schedule: $resource->getSchedule(), + name: $resource->getPolicyName() ?: null, + resourceId: $resource->getResourceId() ?: null, + enabled: $resource->getEnabled(), + ); + break; + } + + $resource->setStatus(Resource::STATUS_SUCCESS); + + return $resource; + } + /** * @throws AppwriteException * @throws \Exception @@ -1573,9 +1612,6 @@ public function importSiteResource(Resource $resource): Resource 'dart-2.17' => BuildRuntime::DART217(), 'dart-2.18' => BuildRuntime::DART218(), 'dart-2.19' => BuildRuntime::DART219(), - 'deno-1.21' => BuildRuntime::DENO121(), - 'deno-1.24' => BuildRuntime::DENO124(), - 'deno-1.35' => BuildRuntime::DENO135(), 'deno-1.40' => BuildRuntime::DENO140(), 'deno-1.46' => BuildRuntime::DENO146(), 'deno-2.0' => BuildRuntime::DENO20(), diff --git a/src/Migration/Resource.php b/src/Migration/Resource.php index e338385..0b2cbef 100644 --- a/src/Migration/Resource.php +++ b/src/Migration/Resource.php @@ -60,6 +60,9 @@ abstract class Resource implements \JsonSerializable public const TYPE_ENVIRONMENT_VARIABLE = 'environment-variable'; + // Backups + public const TYPE_BACKUP_POLICY = 'backup-policy'; + // legacy terminologies public const TYPE_DOCUMENT = 'document'; public const TYPE_ATTRIBUTE = 'attribute'; @@ -89,6 +92,7 @@ abstract class Resource implements \JsonSerializable self::TYPE_ENVIRONMENT_VARIABLE, self::TYPE_TEAM, self::TYPE_MEMBERSHIP, + self::TYPE_BACKUP_POLICY, // legacy self::TYPE_DOCUMENT, diff --git a/src/Migration/Resources/Backups/Policy.php b/src/Migration/Resources/Backups/Policy.php new file mode 100644 index 0000000..d5358b4 --- /dev/null +++ b/src/Migration/Resources/Backups/Policy.php @@ -0,0 +1,115 @@ + $services + * @param int $retention + * @param string $schedule + * @param bool $enabled + * @param string $resourceId + * @param string $resourceType + */ + public function __construct( + string $id = '', + private readonly string $name = '', + private readonly array $services = [], + private readonly int $retention = 0, + private readonly string $schedule = '', + private readonly bool $enabled = true, + private readonly string $resourceId = '', + private readonly string $resourceType = '', + ) { + $this->id = $id; + } + + /** + * @param array $array + * @return self + */ + public static function fromArray(array $array): self + { + return new self( + $array['id'], + $array['name'] ?? '', + $array['services'] ?? [], + $array['retention'] ?? 0, + $array['schedule'] ?? '', + $array['enabled'] ?? true, + $array['resourceId'] ?? '', + $array['resourceType'] ?? '', + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'services' => $this->services, + 'retention' => $this->retention, + 'schedule' => $this->schedule, + 'enabled' => $this->enabled, + 'resourceId' => $this->resourceId, + 'resourceType' => $this->resourceType, + ]; + } + + public static function getName(): string + { + return Resource::TYPE_BACKUP_POLICY; + } + + public function getGroup(): string + { + return Transfer::GROUP_BACKUPS; + } + + public function getPolicyName(): string + { + return $this->name; + } + + /** + * @return array + */ + public function getServices(): array + { + return $this->services; + } + + public function getRetention(): int + { + return $this->retention; + } + + public function getSchedule(): string + { + return $this->schedule; + } + + public function getEnabled(): bool + { + return $this->enabled; + } + + public function getResourceId(): string + { + return $this->resourceId; + } + + public function getResourceType(): string + { + return $this->resourceType; + } +} diff --git a/src/Migration/Source.php b/src/Migration/Source.php index a6b154a..9d64b18 100644 --- a/src/Migration/Source.php +++ b/src/Migration/Source.php @@ -36,6 +36,11 @@ public function getFunctionsBatchSize(): int return static::$defaultBatchSize; } + public function getBackupsBatchSize(): int + { + return static::$defaultBatchSize; + } + public function getSitesBatchSize(): int { return static::$defaultBatchSize; @@ -94,6 +99,7 @@ public function exportResources(array $resources): void Transfer::GROUP_DATABASES => Transfer::GROUP_DATABASES_RESOURCES, Transfer::GROUP_STORAGE => Transfer::GROUP_STORAGE_RESOURCES, Transfer::GROUP_FUNCTIONS => Transfer::GROUP_FUNCTIONS_RESOURCES, + Transfer::GROUP_BACKUPS => Transfer::GROUP_BACKUPS_RESOURCES, Transfer::GROUP_SITES => Transfer::GROUP_SITES_RESOURCES, ]; @@ -123,6 +129,9 @@ public function exportResources(array $resources): void case Transfer::GROUP_FUNCTIONS: $this->exportGroupFunctions($this->getFunctionsBatchSize(), $resources); break; + case Transfer::GROUP_BACKUPS: + $this->exportGroupBackups($this->getBackupsBatchSize(), $resources); + break; case Transfer::GROUP_SITES: $this->exportGroupSites($this->getSitesBatchSize(), $resources); break; @@ -162,6 +171,14 @@ abstract protected function exportGroupStorage(int $batchSize, array $resources) */ abstract protected function exportGroupFunctions(int $batchSize, array $resources): void; + /** + * Export Backups Group + * + * @param int $batchSize + * @param array $resources Resources to export + */ + abstract protected function exportGroupBackups(int $batchSize, array $resources): void; + /** * Export Sites Group * diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index c99b667..da105bd 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -5,6 +5,7 @@ use Appwrite\AppwriteException; use Appwrite\Client; use Appwrite\Query; +use Appwrite\Services\Backups; use Appwrite\Services\Databases; use Appwrite\Services\Functions; use Appwrite\Services\Sites; @@ -19,6 +20,7 @@ use Utopia\Migration\Resources\Auth\Membership; use Utopia\Migration\Resources\Auth\Team; use Utopia\Migration\Resources\Auth\User; +use Utopia\Migration\Resources\Backups\Policy; use Utopia\Migration\Resources\Database\Column; use Utopia\Migration\Resources\Database\Columns\Boolean; use Utopia\Migration\Resources\Database\Columns\DateTime; @@ -74,6 +76,8 @@ class Appwrite extends Source private Sites $sites; + private Backups $backups; + private Reader $database; /** @@ -97,6 +101,7 @@ public function __construct( $this->storage = new Storage($this->client); $this->functions = new Functions($this->client); $this->sites = new Sites($this->client); + $this->backups = new Backups($this->client); $this->headers['x-appwrite-project'] = $this->project; $this->headers['x-appwrite-key'] = $this->key; @@ -153,6 +158,9 @@ public static function getSupportedResources(): array Resource::TYPE_DEPLOYMENT, Resource::TYPE_ENVIRONMENT_VARIABLE, + // Backups + Resource::TYPE_BACKUP_POLICY, + // Sites Resource::TYPE_SITE, Resource::TYPE_SITE_DEPLOYMENT, @@ -195,6 +203,7 @@ public function report(array $resources = [], array $resourceIds = []): array $this->reportDatabases($resources, $report, $resourceIds); $this->reportStorage($resources, $report, $resourceIds); $this->reportFunctions($resources, $report, $resourceIds); + $this->reportBackups($resources, $report, $resourceIds); $this->reportSites($resources, $report, $resourceIds); $report['version'] = $this->call( @@ -1778,6 +1787,89 @@ private function exportDeploymentData(Func $func, array $deployment): void } } + protected function exportGroupBackups(int $batchSize, array $resources): void + { + try { + if (\in_array(Resource::TYPE_BACKUP_POLICY, $resources)) { + $this->exportBackupPolicies($batchSize); + } + } catch (\Throwable $e) { + $this->addError(new Exception( + Resource::TYPE_BACKUP_POLICY, + Transfer::GROUP_BACKUPS, + message: $e->getMessage(), + code: $e->getCode(), + previous: $e + )); + } + } + + /** + * @param array $resources + * @param array $report + * @param array> $resourceIds + */ + private function reportBackups(array $resources, array &$report, array $resourceIds = []): void + { + if (!\in_array(Resource::TYPE_BACKUP_POLICY, $resources)) { + return; + } + + try { + $queries = $this->buildQueries( + resourceType: Resource::TYPE_BACKUP_POLICY, + resourceIds: $resourceIds, + limit: 1 + ); + $report[Resource::TYPE_BACKUP_POLICY] = $this->backups->listPolicies($queries)['total']; + } catch (AppwriteException $e) { + // Re-throw permission errors (401/403) as they indicate configuration issues + if ($e->getCode() === 401 || $e->getCode() === 403) { + throw new \Exception('Missing permission to access backup policies: ' . $e->getMessage(), previous: $e); + } + + // For other errors (404/501), treat as feature not available - skip gracefully + // Backup policies are Cloud-only, may not be available on self-hosted + $report[Resource::TYPE_BACKUP_POLICY] = 0; + } + } + + /** + * @throws AppwriteException + */ + private function exportBackupPolicies(int $batchSize): void + { + $queries = []; + + if ($this->rootResourceId !== '' && $this->rootResourceType === Resource::TYPE_BACKUP_POLICY) { + $queries[] = Query::equal('$id', $this->rootResourceId); + $queries[] = Query::limit(1); + } + + $response = $this->backups->listPolicies($queries); + + if (empty($response['policies'])) { + return; + } + + $policies = []; + + foreach ($response['policies'] as $policy) { + $policies[] = new Policy( + $policy['$id'], + $policy['name'] ?? '', + $policy['services'] ?? [], + $policy['retention'] ?? 0, + $policy['schedule'] ?? '', + $policy['enabled'] ?? true, + $policy['resourceId'] ?? '', + $policy['resourceType'] ?? '', + ); + } + + $this->callback($policies); + } + /** * @throws AppwriteException */ diff --git a/src/Migration/Sources/CSV.php b/src/Migration/Sources/CSV.php index 3cd227c..3a58afb 100644 --- a/src/Migration/Sources/CSV.php +++ b/src/Migration/Sources/CSV.php @@ -375,6 +375,11 @@ protected function exportGroupFunctions(int $batchSize, array $resources): void /** * @throws \Exception */ + protected function exportGroupBackups(int $batchSize, array $resources): void + { + throw new \Exception('Not Implemented'); + } + protected function exportGroupSites(int $batchSize, array $resources): void { throw new \Exception('Not Implemented'); diff --git a/src/Migration/Sources/Firebase.php b/src/Migration/Sources/Firebase.php index b1ef5b7..23e9601 100644 --- a/src/Migration/Sources/Firebase.php +++ b/src/Migration/Sources/Firebase.php @@ -809,6 +809,11 @@ protected function exportGroupFunctions(int $batchSize, array $resources): void throw new \Exception('Not implemented'); } + protected function exportGroupBackups(int $batchSize, array $resources): void + { + throw new \Exception('Not implemented'); + } + protected function exportGroupSites(int $batchSize, array $resources): void { throw new \Exception('Not implemented'); diff --git a/src/Migration/Sources/JSON.php b/src/Migration/Sources/JSON.php index 8abf63d..80fbc4c 100644 --- a/src/Migration/Sources/JSON.php +++ b/src/Migration/Sources/JSON.php @@ -204,6 +204,11 @@ protected function exportGroupFunctions(int $batchSize, array $resources): void /** * @throws \Exception */ + protected function exportGroupBackups(int $batchSize, array $resources): void + { + throw new \Exception('Not Implemented'); + } + protected function exportGroupSites(int $batchSize, array $resources): void { throw new \Exception('Not Implemented'); diff --git a/src/Migration/Sources/NHost.php b/src/Migration/Sources/NHost.php index 5c56324..56251ee 100644 --- a/src/Migration/Sources/NHost.php +++ b/src/Migration/Sources/NHost.php @@ -849,6 +849,11 @@ protected function exportGroupFunctions(int $batchSize, array $resources): void throw new \Exception('Not Implemented'); } + protected function exportGroupBackups(int $batchSize, array $resources): void + { + throw new \Exception('Not Implemented'); + } + protected function exportGroupSites(int $batchSize, array $resources): void { throw new \Exception('Not Implemented'); diff --git a/src/Migration/Transfer.php b/src/Migration/Transfer.php index b4da500..198692d 100644 --- a/src/Migration/Transfer.php +++ b/src/Migration/Transfer.php @@ -18,6 +18,8 @@ class Transfer public const GROUP_SETTINGS = 'settings'; + public const GROUP_BACKUPS = 'backups'; + public const GROUP_AUTH_RESOURCES = [ Resource::TYPE_USER, Resource::TYPE_TEAM, @@ -52,6 +54,10 @@ class Transfer public const GROUP_SETTINGS_RESOURCES = []; + public const GROUP_BACKUPS_RESOURCES = [ + Resource::TYPE_BACKUP_POLICY, + ]; + public const ALL_PUBLIC_RESOURCES = [ Resource::TYPE_USER, Resource::TYPE_TEAM, @@ -69,6 +75,7 @@ class Transfer Resource::TYPE_INDEX, Resource::TYPE_COLUMN, Resource::TYPE_ROW, + Resource::TYPE_BACKUP_POLICY, // legacy Resource::TYPE_DOCUMENT, @@ -343,6 +350,7 @@ public static function extractServices(array $services): array self::GROUP_AUTH => array_merge($resources, self::GROUP_AUTH_RESOURCES), self::GROUP_DATABASES => array_merge($resources, self::GROUP_DATABASES_RESOURCES), self::GROUP_SETTINGS => array_merge($resources, self::GROUP_SETTINGS_RESOURCES), + self::GROUP_BACKUPS => array_merge($resources, self::GROUP_BACKUPS_RESOURCES), default => throw new \Exception('No service group found'), }; } diff --git a/tests/Migration/Unit/Adapters/MockDestination.php b/tests/Migration/Unit/Adapters/MockDestination.php index d8258b3..dc3baf0 100644 --- a/tests/Migration/Unit/Adapters/MockDestination.php +++ b/tests/Migration/Unit/Adapters/MockDestination.php @@ -51,6 +51,7 @@ public static function getSupportedResources(): array Resource::TYPE_ENVIRONMENT_VARIABLE, Resource::TYPE_TEAM, Resource::TYPE_MEMBERSHIP, + Resource::TYPE_BACKUP_POLICY, ]; } diff --git a/tests/Migration/Unit/Adapters/MockSource.php b/tests/Migration/Unit/Adapters/MockSource.php index 64e5640..c86b969 100644 --- a/tests/Migration/Unit/Adapters/MockSource.php +++ b/tests/Migration/Unit/Adapters/MockSource.php @@ -81,6 +81,9 @@ public static function getSupportedResources(): array Resource::TYPE_TEAM, Resource::TYPE_MEMBERSHIP, + // Backups + Resource::TYPE_BACKUP_POLICY, + // legacy Resource::TYPE_DOCUMENT, Resource::TYPE_ATTRIBUTE, @@ -161,6 +164,23 @@ protected function exportGroupFunctions(int $batchSize, array $resources): void } } + /** + * Export Backups Group + * + * @param int $batchSize Max 100 + * @param string[] $resources Resources to export + */ + protected function exportGroupBackups(int $batchSize, array $resources): void + { + foreach (Transfer::GROUP_BACKUPS_RESOURCES as $resource) { + if (!\in_array($resource, $resources)) { + continue; + } + + $this->handleResourceTransfer(Transfer::GROUP_BACKUPS, $resource); + } + } + /** * Export Sites Group *