diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index 12ab2e3..f4fd87e 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -4,11 +4,15 @@ use Appwrite\AppwriteException; use Appwrite\Client; +use Appwrite\Enums\Adapter; +use Appwrite\Enums\BuildRuntime; use Appwrite\Enums\Compression; +use Appwrite\Enums\Framework; use Appwrite\Enums\PasswordHash; use Appwrite\Enums\Runtime; use Appwrite\InputFile; use Appwrite\Services\Functions; +use Appwrite\Services\Sites; use Appwrite\Services\Storage; use Appwrite\Services\Teams; use Appwrite\Services\Users; @@ -42,6 +46,9 @@ use Utopia\Migration\Resources\Functions\Deployment; use Utopia\Migration\Resources\Functions\EnvVar; use Utopia\Migration\Resources\Functions\Func; +use Utopia\Migration\Resources\Sites\Deployment as SiteDeployment; +use Utopia\Migration\Resources\Sites\EnvVar as SiteEnvVar; +use Utopia\Migration\Resources\Sites\Site; use Utopia\Migration\Resources\Storage\Bucket; use Utopia\Migration\Resources\Storage\File; use Utopia\Migration\Transfer; @@ -54,6 +61,7 @@ class Appwrite extends Destination protected string $key; private Functions $functions; + private Sites $sites; private Storage $storage; private Teams $teams; private Users $users; @@ -87,6 +95,7 @@ public function __construct( ->setKey($key); $this->functions = new Functions($this->client); + $this->sites = new Sites($this->client); $this->storage = new Storage($this->client); $this->teams = new Teams($this->client); $this->users = new Users($this->client); @@ -128,6 +137,11 @@ public static function getSupportedResources(): array Resource::TYPE_FUNCTION, Resource::TYPE_DEPLOYMENT, Resource::TYPE_ENVIRONMENT_VARIABLE, + + // Sites + Resource::TYPE_SITE, + Resource::TYPE_SITE_DEPLOYMENT, + Resource::TYPE_SITE_VARIABLE, ]; } @@ -199,6 +213,15 @@ public function report(array $resources = [], array $resourceIds = []): array $this->functions->create('', '', Runtime::NODE180()); } + // Sites + if (\in_array(Resource::TYPE_SITE, $resources)) { + $scope = 'sites.read'; + $this->sites->list(); + + $scope = 'sites.write'; + $this->sites->create('', '', Framework::OTHER(), BuildRuntime::STATIC1()); + } + } catch (AppwriteException $e) { if ($e->getCode() === 403) { throw new \Exception('Missing scope: ' . $scope, previous: $e); @@ -236,6 +259,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_SITES => $this->importSiteResource($resource), default => throw new \Exception('Invalid resource group'), }; } catch (\Throwable $e) { @@ -1505,4 +1529,206 @@ private function importDeployment(Deployment $deployment): Resource return $deployment; } + + /** + * @throws AppwriteException + */ + public function importSiteResource(Resource $resource): Resource + { + switch ($resource->getName()) { + case Resource::TYPE_SITE: + /** @var Site $resource */ + + $buildRuntime = match ($resource->getBuildRuntime()) { + 'node-14.5' => BuildRuntime::NODE145(), + 'node-16.0' => BuildRuntime::NODE160(), + 'node-18.0' => BuildRuntime::NODE180(), + 'node-19.0' => BuildRuntime::NODE190(), + 'node-20.0' => BuildRuntime::NODE200(), + 'node-21.0' => BuildRuntime::NODE210(), + 'node-22' => BuildRuntime::NODE22(), + 'php-8.0' => BuildRuntime::PHP80(), + 'php-8.1' => BuildRuntime::PHP81(), + 'php-8.2' => BuildRuntime::PHP82(), + 'php-8.3' => BuildRuntime::PHP83(), + 'ruby-3.0' => BuildRuntime::RUBY30(), + 'ruby-3.1' => BuildRuntime::RUBY31(), + 'ruby-3.2' => BuildRuntime::RUBY32(), + 'ruby-3.3' => BuildRuntime::RUBY33(), + 'python-3.8' => BuildRuntime::PYTHON38(), + 'python-3.9' => BuildRuntime::PYTHON39(), + 'python-3.10' => BuildRuntime::PYTHON310(), + 'python-3.11' => BuildRuntime::PYTHON311(), + 'python-3.12' => BuildRuntime::PYTHON312(), + 'python-ml-3.11' => BuildRuntime::PYTHONML311(), + 'python-ml-3.12' => BuildRuntime::PYTHONML312(), + 'dart-3.0' => BuildRuntime::DART30(), + 'dart-3.1' => BuildRuntime::DART31(), + 'dart-3.3' => BuildRuntime::DART33(), + 'dart-3.5' => BuildRuntime::DART35(), + 'dart-3.8' => BuildRuntime::DART38(), + 'dart-3.9' => BuildRuntime::DART39(), + 'dart-2.15' => BuildRuntime::DART215(), + 'dart-2.16' => BuildRuntime::DART216(), + '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(), + 'dotnet-6.0' => BuildRuntime::DOTNET60(), + 'dotnet-7.0' => BuildRuntime::DOTNET70(), + 'dotnet-8.0' => BuildRuntime::DOTNET80(), + 'java-8.0' => BuildRuntime::JAVA80(), + 'java-11.0' => BuildRuntime::JAVA110(), + 'java-17.0' => BuildRuntime::JAVA170(), + 'java-18.0' => BuildRuntime::JAVA180(), + 'java-21.0' => BuildRuntime::JAVA210(), + 'java-22' => BuildRuntime::JAVA22(), + 'swift-5.5' => BuildRuntime::SWIFT55(), + 'swift-5.8' => BuildRuntime::SWIFT58(), + 'swift-5.9' => BuildRuntime::SWIFT59(), + 'swift-5.10' => BuildRuntime::SWIFT510(), + 'kotlin-1.6' => BuildRuntime::KOTLIN16(), + 'kotlin-1.8' => BuildRuntime::KOTLIN18(), + 'kotlin-1.9' => BuildRuntime::KOTLIN19(), + 'kotlin-2.0' => BuildRuntime::KOTLIN20(), + 'cpp-17' => BuildRuntime::CPP17(), + 'cpp-20' => BuildRuntime::CPP20(), + 'bun-1.0' => BuildRuntime::BUN10(), + 'bun-1.1' => BuildRuntime::BUN11(), + 'go-1.23' => BuildRuntime::GO123(), + 'static-1' => BuildRuntime::STATIC1(), + 'flutter-3.24' => BuildRuntime::FLUTTER324(), + 'flutter-3.27' => BuildRuntime::FLUTTER327(), + 'flutter-3.29' => BuildRuntime::FLUTTER329(), + 'flutter-3.32' => BuildRuntime::FLUTTER332(), + 'flutter-3.35' => BuildRuntime::FLUTTER335(), + default => throw new \Exception('Invalid Build Runtime: ' . $resource->getBuildRuntime()), + }; + + $framework = match ($resource->getFramework()) { + 'analog' => Framework::ANALOG(), + 'angular' => Framework::ANGULAR(), + 'astro' => Framework::ASTRO(), + 'flutter', 'flutter-web' => Framework::FLUTTER(), + 'lynx' => Framework::LYNX(), + 'nextjs' => Framework::NEXTJS(), + 'nuxt' => Framework::NUXT(), + 'react' => Framework::REACT(), + 'react-native' => Framework::REACTNATIVE(), + 'remix' => Framework::REMIX(), + 'svelte-kit' => Framework::SVELTEKIT(), + 'tanstack-start' => Framework::TANSTACKSTART(), + 'vite' => Framework::VITE(), + 'vue' => Framework::VUE(), + default => Framework::OTHER(), + }; + + $adapter = match ($resource->getAdapter()) { + 'static' => Adapter::STATIC(), + 'ssr' => Adapter::SSR(), + default => null, + }; + + $this->sites->create( + $resource->getId(), + $resource->getSiteName(), + $framework, + $buildRuntime, + $resource->getEnabled(), + $resource->getLogging(), + $resource->getTimeout(), + $resource->getInstallCommand(), + $resource->getBuildCommand(), + $resource->getOutputDirectory(), + $adapter, + fallbackFile: $resource->getFallbackFile(), + specification: $resource->getSpecification(), + ); + break; + case Resource::TYPE_SITE_VARIABLE: + /** @var SiteEnvVar $resource */ + $this->sites->createVariable( + $resource->getSite()->getId(), + $resource->getKey(), + $resource->getValue(), + $resource->getSecret() + ); + break; + case Resource::TYPE_SITE_DEPLOYMENT: + /** @var SiteDeployment $resource */ + return $this->importSiteDeployment($resource); + } + + $resource->setStatus(Resource::STATUS_SUCCESS); + + return $resource; + } + + /** + * @throws AppwriteException + * @throws \Exception + */ + private function importSiteDeployment(SiteDeployment $deployment): Resource + { + $siteId = $deployment->getSite()->getId(); + + if ($deployment->getSize() <= Transfer::STORAGE_MAX_CHUNK_SIZE) { + $response = $this->client->call( + 'POST', + "/sites/{$siteId}/deployments", + [ + 'content-type' => 'multipart/form-data', + ], + [ + 'siteId' => $siteId, + 'code' => new \CURLFile('data://application/gzip;base64,' . base64_encode($deployment->getData()), 'application/gzip', 'deployment.tar.gz'), + 'activate' => $deployment->getActivated(), + ] + ); + + if (!\is_array($response) || !isset($response['$id'])) { + throw new \Exception('Site deployment creation failed'); + } + + $deployment->setStatus(Resource::STATUS_SUCCESS); + + return $deployment; + } + + $response = $this->client->call( + 'POST', + "/sites/{$siteId}/deployments", + [ + 'content-type' => 'multipart/form-data', + 'content-range' => 'bytes ' . ($deployment->getStart()) . '-' . ($deployment->getEnd() == ($deployment->getSize() - 1) ? $deployment->getSize() : $deployment->getEnd()) . '/' . $deployment->getSize(), + 'x-appwrite-id' => $deployment->getId(), + ], + [ + 'siteId' => $siteId, + 'code' => new \CURLFile('data://application/gzip;base64,' . base64_encode($deployment->getData()), 'application/gzip', 'deployment.tar.gz'), + 'activate' => $deployment->getActivated(), + ] + ); + + if (!\is_array($response) || !isset($response['$id'])) { + throw new \Exception('Site deployment creation failed'); + } + + if ($deployment->getStart() === 0) { + $deployment->setId($response['$id']); + } + + if ($deployment->getEnd() == ($deployment->getSize() - 1)) { + $deployment->setStatus(Resource::STATUS_SUCCESS); + } else { + $deployment->setStatus(Resource::STATUS_PENDING); + } + + return $deployment; + } } diff --git a/src/Migration/Resource.php b/src/Migration/Resource.php index 9645cc6..e338385 100644 --- a/src/Migration/Resource.php +++ b/src/Migration/Resource.php @@ -42,6 +42,8 @@ abstract class Resource implements \JsonSerializable public const TYPE_FUNCTION = 'function'; + public const TYPE_SITE = 'site'; + public const TYPE_INDEX = 'index'; // Children (Resources that are created by other resources) @@ -50,6 +52,10 @@ abstract class Resource implements \JsonSerializable public const TYPE_DEPLOYMENT = 'deployment'; + public const TYPE_SITE_DEPLOYMENT = 'site-deployment'; + + public const TYPE_SITE_VARIABLE = 'site-variable'; + public const TYPE_HASH = 'hash'; public const TYPE_ENVIRONMENT_VARIABLE = 'environment-variable'; @@ -74,6 +80,9 @@ abstract class Resource implements \JsonSerializable self::TYPE_FILE, self::TYPE_FUNCTION, self::TYPE_DEPLOYMENT, + self::TYPE_SITE, + self::TYPE_SITE_DEPLOYMENT, + self::TYPE_SITE_VARIABLE, self::TYPE_HASH, self::TYPE_INDEX, self::TYPE_USER, diff --git a/src/Migration/Resources/Sites/Deployment.php b/src/Migration/Resources/Sites/Deployment.php new file mode 100644 index 0000000..d042916 --- /dev/null +++ b/src/Migration/Resources/Sites/Deployment.php @@ -0,0 +1,115 @@ +id = $id; + } + + /** + * @param array $array + * @return self + */ + public static function fromArray(array $array): self + { + return new self( + $array['id'], + Site::fromArray($array['site']), + $array['size'], + $array['start'] ?? 0, + $array['end'] ?? 0, + $array['data'] ?? '', + $array['activated'] ?? false + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'site' => $this->site->jsonSerialize(), + 'size' => $this->size, + 'start' => $this->start, + 'end' => $this->end, + 'data' => $this->data, + 'activated' => $this->activated, + ]; + } + + public static function getName(): string + { + return Resource::TYPE_SITE_DEPLOYMENT; + } + + public function getGroup(): string + { + return Transfer::GROUP_SITES; + } + + public function getSite(): Site + { + return $this->site; + } + + public function getSize(): int + { + return $this->size; + } + + public function setStart(int $start): self + { + $this->start = $start; + + return $this; + } + + public function getStart(): int + { + return $this->start; + } + + public function setEnd(int $end): self + { + $this->end = $end; + + return $this; + } + + public function getEnd(): int + { + return $this->end; + } + + public function setData(string $data): self + { + $this->data = $data; + + return $this; + } + + public function getData(): string + { + return $this->data; + } + + public function getActivated(): bool + { + return $this->activated; + } +} diff --git a/src/Migration/Resources/Sites/EnvVar.php b/src/Migration/Resources/Sites/EnvVar.php new file mode 100644 index 0000000..4adf98c --- /dev/null +++ b/src/Migration/Resources/Sites/EnvVar.php @@ -0,0 +1,78 @@ +id = $id; + } + + /** + * @param array $array + * @return self + */ + public static function fromArray(array $array): self + { + return new self( + $array['id'], + Site::fromArray($array['site']), + $array['key'], + $array['value'], + $array['secret'] ?? false + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'site' => $this->site->jsonSerialize(), + 'key' => $this->key, + 'value' => $this->value, + 'secret' => $this->secret, + ]; + } + + public static function getName(): string + { + return Resource::TYPE_SITE_VARIABLE; + } + + public function getGroup(): string + { + return Transfer::GROUP_SITES; + } + + public function getSite(): Site + { + return $this->site; + } + + public function getKey(): string + { + return $this->key; + } + + public function getValue(): string + { + return $this->value; + } + + public function getSecret(): bool + { + return $this->secret; + } +} diff --git a/src/Migration/Resources/Sites/Site.php b/src/Migration/Resources/Sites/Site.php new file mode 100644 index 0000000..b04d06b --- /dev/null +++ b/src/Migration/Resources/Sites/Site.php @@ -0,0 +1,166 @@ +id = $id; + } + + /** + * @param array $array + * @return self + */ + public static function fromArray(array $array): self + { + return new self( + $array['id'], + $array['name'], + $array['framework'], + $array['buildRuntime'], + $array['enabled'] ?? true, + $array['logging'] ?? true, + $array['timeout'] ?? 600, + $array['installCommand'] ?? '', + $array['buildCommand'] ?? '', + $array['outputDirectory'] ?? '', + $array['adapter'] ?? 'static', + $array['fallbackFile'] ?? '', + $array['specification'] ?? '', + $array['activeDeployment'] ?? '' + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'framework' => $this->framework, + 'buildRuntime' => $this->buildRuntime, + 'enabled' => $this->enabled, + 'logging' => $this->logging, + 'timeout' => $this->timeout, + 'installCommand' => $this->installCommand, + 'buildCommand' => $this->buildCommand, + 'outputDirectory' => $this->outputDirectory, + 'adapter' => $this->adapter, + 'fallbackFile' => $this->fallbackFile, + 'specification' => $this->specification, + 'activeDeployment' => $this->activeDeployment, + ]; + } + + public static function getName(): string + { + return Resource::TYPE_SITE; + } + + public function getGroup(): string + { + return Transfer::GROUP_SITES; + } + + public function getSiteName(): string + { + return $this->name; + } + + public function getFramework(): string + { + return $this->framework; + } + + public function getBuildRuntime(): string + { + return $this->buildRuntime; + } + + public function getEnabled(): bool + { + return $this->enabled; + } + + public function getLogging(): bool + { + return $this->logging; + } + + public function getTimeout(): int + { + return $this->timeout; + } + + public function getInstallCommand(): string + { + return $this->installCommand; + } + + public function getBuildCommand(): string + { + return $this->buildCommand; + } + + public function getOutputDirectory(): string + { + return $this->outputDirectory; + } + + public function getAdapter(): string + { + return $this->adapter; + } + + public function getFallbackFile(): string + { + return $this->fallbackFile; + } + + public function getSpecification(): string + { + return $this->specification; + } + + public function getActiveDeployment(): string + { + return $this->activeDeployment; + } +} diff --git a/src/Migration/Source.php b/src/Migration/Source.php index fb4a146..a6b154a 100644 --- a/src/Migration/Source.php +++ b/src/Migration/Source.php @@ -36,6 +36,11 @@ public function getFunctionsBatchSize(): int return static::$defaultBatchSize; } + public function getSitesBatchSize(): int + { + return static::$defaultBatchSize; + } + /** * @param array $resources * @return void @@ -89,6 +94,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_SITES => Transfer::GROUP_SITES_RESOURCES, ]; foreach ($mapping as $group => $resources) { @@ -117,6 +123,9 @@ public function exportResources(array $resources): void case Transfer::GROUP_FUNCTIONS: $this->exportGroupFunctions($this->getFunctionsBatchSize(), $resources); break; + case Transfer::GROUP_SITES: + $this->exportGroupSites($this->getSitesBatchSize(), $resources); + break; } } } @@ -152,4 +161,12 @@ abstract protected function exportGroupStorage(int $batchSize, array $resources) * @param array $resources Resources to export */ abstract protected function exportGroupFunctions(int $batchSize, array $resources): void; + + /** + * Export Sites Group + * + * @param int $batchSize + * @param array $resources Resources to export + */ + abstract protected function exportGroupSites(int $batchSize, array $resources): void; } diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index d4b16d9..852330d 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -7,6 +7,7 @@ use Appwrite\Query; use Appwrite\Services\Databases; use Appwrite\Services\Functions; +use Appwrite\Services\Sites; use Appwrite\Services\Storage; use Appwrite\Services\Teams; use Appwrite\Services\Users; @@ -39,6 +40,9 @@ use Utopia\Migration\Resources\Functions\Deployment; use Utopia\Migration\Resources\Functions\EnvVar; use Utopia\Migration\Resources\Functions\Func; +use Utopia\Migration\Resources\Sites\Deployment as SiteDeployment; +use Utopia\Migration\Resources\Sites\EnvVar as SiteEnvVar; +use Utopia\Migration\Resources\Sites\Site; use Utopia\Migration\Resources\Storage\Bucket; use Utopia\Migration\Resources\Storage\File; use Utopia\Migration\Source; @@ -64,6 +68,8 @@ class Appwrite extends Source private Functions $functions; + private Sites $sites; + private Reader $database; /** @@ -86,6 +92,7 @@ public function __construct( $this->teams = new Teams($this->client); $this->storage = new Storage($this->client); $this->functions = new Functions($this->client); + $this->sites = new Sites($this->client); $this->headers['X-Appwrite-Project'] = $this->project; $this->headers['X-Appwrite-Key'] = $this->key; @@ -142,6 +149,11 @@ public static function getSupportedResources(): array Resource::TYPE_DEPLOYMENT, Resource::TYPE_ENVIRONMENT_VARIABLE, + // Sites + Resource::TYPE_SITE, + Resource::TYPE_SITE_DEPLOYMENT, + Resource::TYPE_SITE_VARIABLE, + // Settings ]; } @@ -179,6 +191,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->reportSites($resources, $report, $resourceIds); $report['version'] = $this->call( 'GET', @@ -403,6 +416,70 @@ private function reportFunctions(array $resources, array &$report, array $resour } } + private function reportSites(array $resources, array &$report, array $resourceIds = []): void + { + $needVarsOrDeployments = ( + \in_array(Resource::TYPE_SITE_DEPLOYMENT, $resources) || + \in_array(Resource::TYPE_SITE_VARIABLE, $resources) + ); + + $sites = []; + $totalSites = 0; + + if (!$needVarsOrDeployments && \in_array(Resource::TYPE_SITE, $resources)) { + $siteQueries = $this->buildQueries( + resourceType: Resource::TYPE_SITE, + resourceIds: $resourceIds, + limit: 1 + ); + $report[Resource::TYPE_SITE] = $this->sites->list($siteQueries)['total']; + return; + } + + if ($needVarsOrDeployments) { + $lastSite = null; + while (true) { + $params = $this->buildQueries( + resourceType: Resource::TYPE_SITE, + resourceIds: $resourceIds, + cursor: $lastSite, + ); + $siteList = $this->sites->list($params); + + $totalSites = $siteList['total']; + $currentSites = $siteList['sites']; + $sites = array_merge($sites, $currentSites); + + if (count($currentSites) === 0 || count($currentSites) < self::DEFAULT_PAGE_LIMIT) { + break; + } + + $lastSite = $currentSites[count($currentSites) - 1]['$id']; + } + } + + if (\in_array(Resource::TYPE_SITE, $resources)) { + $report[Resource::TYPE_SITE] = $totalSites; + } + + if (\in_array(Resource::TYPE_SITE_DEPLOYMENT, $resources)) { + $report[Resource::TYPE_SITE_DEPLOYMENT] = 0; + foreach ($sites as $site) { + if (!empty($site['deploymentId'])) { + $report[Resource::TYPE_SITE_DEPLOYMENT] += 1; + } + } + } + + if (\in_array(Resource::TYPE_SITE_VARIABLE, $resources)) { + $report[Resource::TYPE_SITE_VARIABLE] = 0; + foreach ($sites as $site) { + $variables = $this->sites->listVariables($site['$id']); + $report[Resource::TYPE_SITE_VARIABLE] += $variables['total'] ?? 0; + } + } + } + /** * Export Auth Resources * @@ -1399,6 +1476,37 @@ protected function exportGroupFunctions(int $batchSize, array $resources): void } } + protected function exportGroupSites(int $batchSize, array $resources): void + { + try { + if (\in_array(Resource::TYPE_SITE, $resources)) { + $this->exportSites($batchSize); + } + } catch (\Throwable $e) { + $this->addError(new Exception( + Resource::TYPE_SITE, + Transfer::GROUP_SITES, + message: $e->getMessage(), + code: $e->getCode(), + previous: $e + )); + } + + try { + if (\in_array(Resource::TYPE_SITE_DEPLOYMENT, $resources)) { + $this->exportSiteDeployments($batchSize, true); + } + } catch (\Throwable $e) { + $this->addError(new Exception( + Resource::TYPE_SITE_DEPLOYMENT, + Transfer::GROUP_SITES, + message: $e->getMessage(), + code: $e->getCode(), + previous: $e + )); + } + } + /** * @throws AppwriteException */ @@ -1614,6 +1722,217 @@ private function exportDeploymentData(Func $func, array $deployment): void } } + /** + * @throws AppwriteException + */ + private function exportSites(int $batchSize): void + { + $this->sites = new Sites($this->client); + /** @var Site|null $lastSite */ + $lastSite = null; + + while (true) { + $queries = [Query::limit($batchSize)]; + + if ($this->rootResourceId !== '' && $this->rootResourceType === Resource::TYPE_SITE) { + $queries[] = Query::equal('$id', $this->rootResourceId); + $queries[] = Query::limit(1); + } + + if ($lastSite) { + $queries[] = Query::cursorAfter($lastSite->getId()); + } + + $response = $this->sites->list($queries); + + if ($response['total'] === 0) { + return; + } + + $sites = []; + $convertedResources = []; + + foreach ($response['sites'] as $site) { + $convertedSite = new Site( + $site['$id'], + $site['name'], + $site['framework'], + $site['buildRuntime'], + $site['enabled'], + $site['logging'], + $site['timeout'], + $site['installCommand'] ?? '', + $site['buildCommand'] ?? '', + $site['outputDirectory'] ?? '', + $site['adapter'] ?? 'static', + $site['fallbackFile'] ?? '', + $site['specification'] ?? '', + $site['deploymentId'] ?? '' + ); + $sites[] = $convertedSite; + $convertedResources[] = $convertedSite; + + $variables = $this->sites->listVariables($site['$id']); + foreach ($variables['variables'] ?? [] as $var) { + $convertedResources[] = new SiteEnvVar( + $var['$id'], + $convertedSite, + $var['key'], + $var['value'], + $var['secret'] ?? false + ); + } + } + + $lastSite = $sites[count($sites) - 1]; + + $this->callback($convertedResources); + + if (count($sites) < $batchSize) { + return; + } + } + } + + /** + * @throws AppwriteException + */ + private function exportSiteDeployments(int $batchSize, bool $exportOnlyActive = false): void + { + $this->sites = new Sites($this->client); + $sites = $this->cache->get(Site::getName()); + + foreach ($sites as $site) { + /** @var Site $site */ + $lastDocument = null; + + if ($exportOnlyActive && $site->getActiveDeployment()) { + $deployment = $this->sites->getDeployment($site->getId(), $site->getActiveDeployment()); + + try { + $this->exportSiteDeploymentData($site, $deployment); + } catch (\Throwable $e) { + $site->setStatus(Resource::STATUS_ERROR, $e->getMessage()); + } + + continue; + } + + while (true) { + $queries = [Query::limit($batchSize)]; + + if ($lastDocument) { + $queries[] = Query::cursorAfter($lastDocument); + } + + $response = $this->sites->listDeployments( + $site->getId(), + $queries + ); + + foreach ($response['deployments'] as $deployment) { + try { + $this->exportSiteDeploymentData($site, $deployment); + } catch (\Throwable $e) { + $site->setStatus(Resource::STATUS_ERROR, $e->getMessage()); + } + + $lastDocument = $deployment['$id']; + } + + if (count($response['deployments']) < $batchSize) { + break; + } + } + } + } + + /** + * @throws \Exception + */ + private function exportSiteDeploymentData(Site $site, array $deployment): void + { + $start = 0; + $end = Transfer::STORAGE_MAX_CHUNK_SIZE - 1; + + $responseHeaders = []; + + $this->call( + 'HEAD', + "/sites/{$site->getId()}/deployments/{$deployment['$id']}/download", + [], + [], + $responseHeaders + ); + + if (!\array_key_exists('Content-Length', $responseHeaders)) { + $file = $this->call( + 'GET', + "/sites/{$site->getId()}/deployments/{$deployment['$id']}/download", + [], + [], + $responseHeaders + ); + + $size = mb_strlen($file); + + if ($end > $size) { + $end = $size - 1; + } + + $siteDeployment = new SiteDeployment( + $deployment['$id'], + $site, + $size, + $start, + $end, + $file, + $deployment['$id'] === $site->getActiveDeployment() + ); + $siteDeployment->setSequence($siteDeployment->getId()); + + $this->callback([$siteDeployment]); + + return; + } + + $fileSize = $responseHeaders['Content-Length']; + + $siteDeployment = new SiteDeployment( + $deployment['$id'], + $site, + $fileSize, + $start, + $end, + '', + $deployment['$id'] === $site->getActiveDeployment() + ); + + $siteDeployment->setSequence($siteDeployment->getId()); + + while ($start < $fileSize) { + $chunkData = $this->call( + 'GET', + "/sites/{$site->getId()}/deployments/{$siteDeployment->getSequence()}/download", + ['range' => "bytes=$start-$end"] + ); + + $siteDeployment + ->setData($chunkData) + ->setStart($start) + ->setEnd($end); + + $this->callback([$siteDeployment]); + + $start += Transfer::STORAGE_MAX_CHUNK_SIZE; + $end += Transfer::STORAGE_MAX_CHUNK_SIZE; + + if ($end > $fileSize) { + $end = $fileSize - 1; + } + } + } + /** * Build queries with optional filtering by resource IDs */ diff --git a/src/Migration/Sources/CSV.php b/src/Migration/Sources/CSV.php index 7aaeaa3..8f1027c 100644 --- a/src/Migration/Sources/CSV.php +++ b/src/Migration/Sources/CSV.php @@ -372,6 +372,14 @@ protected function exportGroupFunctions(int $batchSize, array $resources): void throw new \Exception('Not Implemented'); } + /** + * @throws \Exception + */ + protected function exportGroupSites(int $batchSize, array $resources): void + { + throw new \Exception('Not Implemented'); + } + /** * @param callable(resource $stream, string $delimiter): void $callback * @return void diff --git a/src/Migration/Sources/Firebase.php b/src/Migration/Sources/Firebase.php index 12117d6..b1ef5b7 100644 --- a/src/Migration/Sources/Firebase.php +++ b/src/Migration/Sources/Firebase.php @@ -808,4 +808,9 @@ protected function exportGroupFunctions(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 779e267..8abf63d 100644 --- a/src/Migration/Sources/JSON.php +++ b/src/Migration/Sources/JSON.php @@ -201,6 +201,14 @@ protected function exportGroupFunctions(int $batchSize, array $resources): void throw new \Exception('Not Implemented'); } + /** + * @throws \Exception + */ + protected function exportGroupSites(int $batchSize, array $resources): void + { + throw new \Exception('Not Implemented'); + } + /** * @param callable(Items): void $callback * @throws \Exception|JsonMachineException diff --git a/src/Migration/Sources/NHost.php b/src/Migration/Sources/NHost.php index f65e700..5c56324 100644 --- a/src/Migration/Sources/NHost.php +++ b/src/Migration/Sources/NHost.php @@ -848,4 +848,9 @@ protected function exportGroupFunctions(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 1633092..b4da500 100644 --- a/src/Migration/Transfer.php +++ b/src/Migration/Transfer.php @@ -12,6 +12,8 @@ class Transfer public const GROUP_FUNCTIONS = 'functions'; + public const GROUP_SITES = 'sites'; + public const GROUP_DATABASES = 'databases'; public const GROUP_SETTINGS = 'settings'; @@ -34,6 +36,12 @@ class Transfer Resource::TYPE_DEPLOYMENT ]; + public const GROUP_SITES_RESOURCES = [ + Resource::TYPE_SITE, + Resource::TYPE_SITE_VARIABLE, + Resource::TYPE_SITE_DEPLOYMENT + ]; + public const GROUP_DATABASES_RESOURCES = [ Resource::TYPE_DATABASE, Resource::TYPE_TABLE, @@ -53,6 +61,9 @@ class Transfer Resource::TYPE_FUNCTION, Resource::TYPE_ENVIRONMENT_VARIABLE, Resource::TYPE_DEPLOYMENT, + Resource::TYPE_SITE, + Resource::TYPE_SITE_VARIABLE, + Resource::TYPE_SITE_DEPLOYMENT, Resource::TYPE_DATABASE, Resource::TYPE_TABLE, Resource::TYPE_INDEX, @@ -69,6 +80,7 @@ class Transfer Resource::TYPE_BUCKET, Resource::TYPE_DATABASE, Resource::TYPE_FUNCTION, + Resource::TYPE_SITE, Resource::TYPE_USER, Resource::TYPE_TEAM, ]; @@ -325,6 +337,7 @@ public static function extractServices(array $services): array foreach ($services as $service) { $resources = match ($service) { self::GROUP_FUNCTIONS => array_merge($resources, self::GROUP_FUNCTIONS_RESOURCES), + self::GROUP_SITES => array_merge($resources, self::GROUP_SITES_RESOURCES), self::GROUP_STORAGE => array_merge($resources, self::GROUP_STORAGE_RESOURCES), self::GROUP_GENERAL => array_merge($resources, []), self::GROUP_AUTH => array_merge($resources, self::GROUP_AUTH_RESOURCES), diff --git a/tests/Migration/Unit/Adapters/MockDestination.php b/tests/Migration/Unit/Adapters/MockDestination.php index 7c9806c..d8258b3 100644 --- a/tests/Migration/Unit/Adapters/MockDestination.php +++ b/tests/Migration/Unit/Adapters/MockDestination.php @@ -42,6 +42,9 @@ public static function getSupportedResources(): array Resource::TYPE_FILE, Resource::TYPE_FUNCTION, Resource::TYPE_DEPLOYMENT, + Resource::TYPE_SITE, + Resource::TYPE_SITE_DEPLOYMENT, + Resource::TYPE_SITE_VARIABLE, Resource::TYPE_HASH, Resource::TYPE_INDEX, Resource::TYPE_USER, diff --git a/tests/Migration/Unit/Adapters/MockSource.php b/tests/Migration/Unit/Adapters/MockSource.php index 41d352e..64e5640 100644 --- a/tests/Migration/Unit/Adapters/MockSource.php +++ b/tests/Migration/Unit/Adapters/MockSource.php @@ -71,6 +71,9 @@ public static function getSupportedResources(): array Resource::TYPE_FILE, Resource::TYPE_FUNCTION, Resource::TYPE_DEPLOYMENT, + Resource::TYPE_SITE, + Resource::TYPE_SITE_DEPLOYMENT, + Resource::TYPE_SITE_VARIABLE, Resource::TYPE_HASH, Resource::TYPE_INDEX, Resource::TYPE_USER, @@ -157,4 +160,21 @@ protected function exportGroupFunctions(int $batchSize, array $resources): void $this->handleResourceTransfer(Transfer::GROUP_FUNCTIONS, $resource); } } + + /** + * Export Sites Group + * + * @param int $batchSize Max 100 + * @param string[] $resources Resources to export + */ + protected function exportGroupSites(int $batchSize, array $resources): void + { + foreach (Transfer::GROUP_SITES_RESOURCES as $resource) { + if (!\in_array($resource, $resources)) { + continue; + } + + $this->handleResourceTransfer(Transfer::GROUP_SITES, $resource); + } + } }