|
10 | 10 | use Appwrite\Enums\Framework; |
11 | 11 | use Appwrite\Enums\PasswordHash; |
12 | 12 | use Appwrite\Enums\ProjectProtocolId; |
| 13 | +use Appwrite\Enums\ProxyResourceType; |
13 | 14 | use Appwrite\Enums\Runtime; |
14 | 15 | use Appwrite\Enums\SmtpEncryption; |
| 16 | +use Appwrite\Enums\StatusCode; |
15 | 17 | use Appwrite\InputFile; |
16 | 18 | use Appwrite\Services\Functions; |
17 | 19 | use Appwrite\Services\Messaging; |
18 | 20 | use Appwrite\Services\Project; |
| 21 | +use Appwrite\Services\Proxy; |
19 | 22 | use Appwrite\Services\Sites; |
20 | 23 | use Appwrite\Services\Storage; |
21 | 24 | use Appwrite\Services\Teams; |
|
51 | 54 | use Utopia\Migration\Resources\Database\Index; |
52 | 55 | use Utopia\Migration\Resources\Database\Row; |
53 | 56 | use Utopia\Migration\Resources\Database\Table; |
| 57 | +use Utopia\Migration\Resources\Domains\Rule; |
54 | 58 | use Utopia\Migration\Resources\Functions\Deployment; |
55 | 59 | use Utopia\Migration\Resources\Functions\EnvVar; |
56 | 60 | use Utopia\Migration\Resources\Functions\Func; |
@@ -106,6 +110,7 @@ class Appwrite extends Destination |
106 | 110 | private Functions $functions; |
107 | 111 | private Messaging $messaging; |
108 | 112 | private Project $project; |
| 113 | + private Proxy $proxy; |
109 | 114 | private Sites $sites; |
110 | 115 | private Storage $storage; |
111 | 116 | private Teams $teams; |
@@ -191,6 +196,7 @@ public function __construct( |
191 | 196 | $this->functions = new Functions($this->client); |
192 | 197 | $this->messaging = new Messaging($this->client); |
193 | 198 | $this->project = new Project($this->client); |
| 199 | + $this->proxy = new Proxy($this->client); |
194 | 200 | $this->sites = new Sites($this->client); |
195 | 201 | $this->storage = new Storage($this->client); |
196 | 202 | $this->teams = new Teams($this->client); |
@@ -302,6 +308,9 @@ public static function getSupportedResources(): array |
302 | 308 |
|
303 | 309 | // Backups |
304 | 310 | Resource::TYPE_BACKUP_POLICY, |
| 311 | + |
| 312 | + // Domains |
| 313 | + Resource::TYPE_RULE, |
305 | 314 | ]; |
306 | 315 | } |
307 | 316 |
|
@@ -461,6 +470,7 @@ protected function import(array $resources, callable $callback): void |
461 | 470 | Transfer::GROUP_INTEGRATIONS => $this->importIntegrationsResource($resource), |
462 | 471 | Transfer::GROUP_BACKUPS => $this->importBackupResource($resource), |
463 | 472 | Transfer::GROUP_PROJECTS => $this->importProjectsResource($resource), |
| 473 | + Transfer::GROUP_DOMAINS => $this->importDomainsResource($resource), |
464 | 474 | default => throw new \Exception('Invalid resource group', Exception::CODE_VALIDATION), |
465 | 475 | }; |
466 | 476 | } catch (\Throwable $e) { |
@@ -3156,6 +3166,25 @@ public function importProjectsResource(Resource $resource): Resource |
3156 | 3166 | return $resource; |
3157 | 3167 | } |
3158 | 3168 |
|
| 3169 | + public function importDomainsResource(Resource $resource): Resource |
| 3170 | + { |
| 3171 | + switch ($resource->getName()) { |
| 3172 | + case Resource::TYPE_RULE: |
| 3173 | + /** @var Rule $resource */ |
| 3174 | + $success = $this->createRule($resource); |
| 3175 | + if (!$success) { |
| 3176 | + return $resource; |
| 3177 | + } |
| 3178 | + break; |
| 3179 | + } |
| 3180 | + |
| 3181 | + if ($resource->getStatus() !== Resource::STATUS_SKIPPED) { |
| 3182 | + $resource->setStatus(Resource::STATUS_SUCCESS); |
| 3183 | + } |
| 3184 | + |
| 3185 | + return $resource; |
| 3186 | + } |
| 3187 | + |
3159 | 3188 | protected function createProjectVariable(ProjectVariable $resource): bool |
3160 | 3189 | { |
3161 | 3190 | $existing = $this->dbForProject->findOne('variables', [ |
@@ -3280,6 +3309,91 @@ protected function createSMTP(SMTP $resource): bool |
3280 | 3309 | return true; |
3281 | 3310 | } |
3282 | 3311 |
|
| 3312 | + /** |
| 3313 | + * Auto-generated rules (default `.appwrite.network` domains for functions/sites) |
| 3314 | + * are recreated automatically on the destination when the parent Function/Site |
| 3315 | + * is migrated, so only manual rules need to be imported. |
| 3316 | + * |
| 3317 | + * Function/site IDs are preserved across migration, so the source |
| 3318 | + * `deploymentResourceId` is passed through directly. |
| 3319 | + */ |
| 3320 | + protected function createRule(Rule $resource): bool |
| 3321 | + { |
| 3322 | + if ($resource->getTrigger() !== 'manual') { |
| 3323 | + $resource->setStatus(Resource::STATUS_SKIPPED, 'Auto-generated rule, recreated by parent resource migration'); |
| 3324 | + return false; |
| 3325 | + } |
| 3326 | + |
| 3327 | + $type = $resource->getType(); |
| 3328 | + $deploymentResourceType = $resource->getDeploymentResourceType(); |
| 3329 | + $branch = $resource->getDeploymentVcsProviderBranch(); |
| 3330 | + |
| 3331 | + try { |
| 3332 | + switch ($type) { |
| 3333 | + case 'api': |
| 3334 | + $this->proxy->createAPIRule($resource->getDomain()); |
| 3335 | + break; |
| 3336 | + |
| 3337 | + case 'redirect': |
| 3338 | + $statusCode = match ($resource->getRedirectStatusCode()) { |
| 3339 | + 301 => StatusCode::MOVEDPERMANENTLY301(), |
| 3340 | + 302 => StatusCode::FOUND302(), |
| 3341 | + 307 => StatusCode::TEMPORARYREDIRECT307(), |
| 3342 | + 308 => StatusCode::PERMANENTREDIRECT308(), |
| 3343 | + default => StatusCode::MOVEDPERMANENTLY301(), |
| 3344 | + }; |
| 3345 | + |
| 3346 | + $resourceType = $deploymentResourceType === 'site' |
| 3347 | + ? ProxyResourceType::SITE() |
| 3348 | + : ProxyResourceType::FUNCTIONMODEL(); |
| 3349 | + |
| 3350 | + $this->proxy->createRedirectRule( |
| 3351 | + $resource->getDomain(), |
| 3352 | + $resource->getRedirectUrl(), |
| 3353 | + $statusCode, |
| 3354 | + $resource->getDeploymentResourceId(), |
| 3355 | + $resourceType, |
| 3356 | + ); |
| 3357 | + break; |
| 3358 | + |
| 3359 | + case 'deployment': |
| 3360 | + if ($deploymentResourceType === 'function') { |
| 3361 | + $this->proxy->createFunctionRule( |
| 3362 | + $resource->getDomain(), |
| 3363 | + $resource->getDeploymentResourceId(), |
| 3364 | + $branch !== '' ? $branch : null, |
| 3365 | + ); |
| 3366 | + } elseif ($deploymentResourceType === 'site') { |
| 3367 | + $this->proxy->createSiteRule( |
| 3368 | + $resource->getDomain(), |
| 3369 | + $resource->getDeploymentResourceId(), |
| 3370 | + $branch !== '' ? $branch : null, |
| 3371 | + ); |
| 3372 | + } else { |
| 3373 | + $resource->setStatus(Resource::STATUS_SKIPPED, 'Unsupported deployment resource type "' . $deploymentResourceType . '"'); |
| 3374 | + return false; |
| 3375 | + } |
| 3376 | + break; |
| 3377 | + |
| 3378 | + default: |
| 3379 | + $resource->setStatus(Resource::STATUS_SKIPPED, 'Unsupported rule type "' . $type . '"'); |
| 3380 | + return false; |
| 3381 | + } |
| 3382 | + } catch (AppwriteException $e) { |
| 3383 | + // 409 means the domain is owned by another project/organization — the |
| 3384 | + // user has to release it there before re-running. Surface as a warning, |
| 3385 | + // not an error, so the rest of the migration continues. |
| 3386 | + if ($e->getCode() === 409) { |
| 3387 | + $resource->setStatus(Resource::STATUS_WARNING, 'Domain "' . $resource->getDomain() . '" is owned by another project. Remove it there and re-run the migration.'); |
| 3388 | + return false; |
| 3389 | + } |
| 3390 | + |
| 3391 | + throw $e; |
| 3392 | + } |
| 3393 | + |
| 3394 | + return true; |
| 3395 | + } |
| 3396 | + |
3283 | 3397 | protected function createWebhook(Webhook $resource): bool |
3284 | 3398 | { |
3285 | 3399 | $existing = $this->dbForPlatform->findOne('webhooks', [ |
|
0 commit comments