Skip to content

Commit e0377b4

Browse files
committed
feat: add destination scope validation and improve backup policy migration
1 parent 9a87401 commit e0377b4

10 files changed

Lines changed: 141 additions & 34 deletions

File tree

src/Migration/Destinations/Appwrite.php

Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use Utopia\Migration\Resources\Auth\Membership;
3535
use Utopia\Migration\Resources\Auth\Team;
3636
use Utopia\Migration\Resources\Auth\User;
37+
use Utopia\Migration\Resources\Backups\Policy;
3738
use Utopia\Migration\Resources\Database\Column;
3839
use Utopia\Migration\Resources\Database\Database;
3940
use Utopia\Migration\Resources\Database\Index;
@@ -42,7 +43,6 @@
4243
use Utopia\Migration\Resources\Functions\Deployment;
4344
use Utopia\Migration\Resources\Functions\EnvVar;
4445
use Utopia\Migration\Resources\Functions\Func;
45-
use Utopia\Migration\Resources\Backups\Policy;
4646
use Utopia\Migration\Resources\Storage\Bucket;
4747
use Utopia\Migration\Resources\Storage\File;
4848
use Utopia\Migration\Transfer;
@@ -210,6 +210,35 @@ public function report(array $resources = [], array $resourceIds = []): array
210210
throw $e;
211211
}
212212

213+
// Backups (uses call() instead of SDK, so needs separate error handling)
214+
if (\in_array(Resource::TYPE_BACKUP_POLICY, $resources)) {
215+
try {
216+
$scope = 'policies.read';
217+
$this->call('GET', '/backups/policies', [
218+
'Content-Type' => 'application/json',
219+
'X-Appwrite-Project' => $this->project,
220+
'X-Appwrite-Key' => $this->key,
221+
]);
222+
223+
$scope = 'policies.write';
224+
$this->call('POST', '/backups/policies', [
225+
'Content-Type' => 'application/json',
226+
'X-Appwrite-Project' => $this->project,
227+
'X-Appwrite-Key' => $this->key,
228+
], []);
229+
} catch (\Throwable $e) {
230+
$body = \json_decode($e->getMessage(), true);
231+
if (\is_array($body) && ($body['code'] ?? 0) === 401) {
232+
$type = $body['type'] ?? '';
233+
if ($type === 'additional_resource_not_allowed') {
234+
throw new \Exception('Backups are not available on the destination project\'s plan', previous: $e);
235+
}
236+
throw new \Exception('Missing scope: ' . $scope, previous: $e);
237+
}
238+
throw $e;
239+
}
240+
}
241+
213242
return [];
214243
}
215244

@@ -1448,28 +1477,54 @@ public function importFunctionResource(Resource $resource): Resource
14481477
return $resource;
14491478
}
14501479

1480+
/**
1481+
* @throws \Exception
1482+
*/
14511483
public function importBackupResource(Resource $resource): Resource
14521484
{
1453-
/** @var Policy $resource */
1454-
$params = [
1455-
'policyId' => $resource->getId(),
1456-
'name' => $resource->getPolicyName(),
1457-
'services' => $resource->getServices(),
1458-
'enabled' => $resource->getEnabled(),
1459-
'retention' => $resource->getRetention(),
1460-
'schedule' => $resource->getSchedule(),
1461-
];
1485+
switch ($resource->getName()) {
1486+
case Resource::TYPE_BACKUP_POLICY:
1487+
/** @var Policy $resource */
1488+
$params = [
1489+
'policyId' => $resource->getId(),
1490+
'name' => $resource->getPolicyName(),
1491+
'services' => $resource->getServices(),
1492+
'enabled' => $resource->getEnabled(),
1493+
'retention' => $resource->getRetention(),
1494+
'schedule' => $resource->getSchedule(),
1495+
];
1496+
1497+
if ($resource->getResourceId()) {
1498+
$collection = match ($resource->getResourceType()) {
1499+
Resource::TYPE_DATABASE => 'databases',
1500+
Resource::TYPE_BUCKET => 'buckets',
1501+
Resource::TYPE_FUNCTION => null, // Functions don't support per-resource backup policies
1502+
default => null,
1503+
};
1504+
1505+
if ($collection !== null) {
1506+
$doc = $this->database->getDocument($collection, $resource->getResourceId());
1507+
if ($doc->isEmpty()) {
1508+
throw new Exception(
1509+
resourceName: $resource->getName(),
1510+
resourceGroup: $resource->getGroup(),
1511+
resourceId: $resource->getId(),
1512+
message: 'Referenced ' . $resource->getResourceType() . ' "' . $resource->getResourceId() . '" not found on destination',
1513+
);
1514+
}
1515+
}
14621516

1463-
if ($resource->getResourceId()) {
1464-
$params['resourceId'] = $resource->getResourceId();
1465-
$params['resourceType'] = $resource->getResourceType();
1466-
}
1517+
$params['resourceId'] = $resource->getResourceId();
1518+
$params['resourceType'] = $resource->getResourceType();
1519+
}
14671520

1468-
$this->call('POST', '/backups/policies', [
1469-
'Content-Type' => 'application/json',
1470-
'X-Appwrite-Project' => $this->project,
1471-
'X-Appwrite-Key' => $this->key,
1472-
], $params);
1521+
$this->call('POST', '/backups/policies', [
1522+
'Content-Type' => 'application/json',
1523+
'X-Appwrite-Project' => $this->project,
1524+
'X-Appwrite-Key' => $this->key,
1525+
], $params);
1526+
break;
1527+
}
14731528

14741529
$resource->setStatus(Resource::STATUS_SUCCESS);
14751530

src/Migration/Resource.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ abstract class Resource implements \JsonSerializable
5454

5555
public const TYPE_ENVIRONMENT_VARIABLE = 'environment-variable';
5656

57+
// Backups
5758
public const TYPE_BACKUP_POLICY = 'backup-policy';
5859

5960
// legacy terminologies

src/Migration/Source.php

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ public function getFunctionsBatchSize(): int
3636
return static::$defaultBatchSize;
3737
}
3838

39+
public function getBackupsBatchSize(): int
40+
{
41+
return static::$defaultBatchSize;
42+
}
43+
3944
/**
4045
* @param array<Resource> $resources
4146
* @return void
@@ -125,11 +130,6 @@ public function exportResources(array $resources): void
125130
}
126131
}
127132

128-
public function getBackupsBatchSize(): int
129-
{
130-
return static::$defaultBatchSize;
131-
}
132-
133133
/**
134134
* Export Auth Group
135135
*
@@ -168,8 +168,5 @@ abstract protected function exportGroupFunctions(int $batchSize, array $resource
168168
* @param int $batchSize
169169
* @param array<string> $resources Resources to export
170170
*/
171-
protected function exportGroupBackups(int $batchSize, array $resources): void
172-
{
173-
// Override in subclasses to support backup policy migration
174-
}
171+
abstract protected function exportGroupBackups(int $batchSize, array $resources): void;
175172
}

src/Migration/Sources/Appwrite.php

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Utopia\Migration\Resources\Auth\Membership;
1919
use Utopia\Migration\Resources\Auth\Team;
2020
use Utopia\Migration\Resources\Auth\User;
21+
use Utopia\Migration\Resources\Backups\Policy;
2122
use Utopia\Migration\Resources\Database\Column;
2223
use Utopia\Migration\Resources\Database\Columns\Boolean;
2324
use Utopia\Migration\Resources\Database\Columns\DateTime;
@@ -39,7 +40,6 @@
3940
use Utopia\Migration\Resources\Functions\Deployment;
4041
use Utopia\Migration\Resources\Functions\EnvVar;
4142
use Utopia\Migration\Resources\Functions\Func;
42-
use Utopia\Migration\Resources\Backups\Policy;
4343
use Utopia\Migration\Resources\Storage\Bucket;
4444
use Utopia\Migration\Resources\Storage\File;
4545
use Utopia\Migration\Source;
@@ -143,8 +143,6 @@ public static function getSupportedResources(): array
143143
Resource::TYPE_DEPLOYMENT,
144144
Resource::TYPE_ENVIRONMENT_VARIABLE,
145145

146-
// Settings
147-
148146
// Backups
149147
Resource::TYPE_BACKUP_POLICY,
150148
];
@@ -183,7 +181,7 @@ public function report(array $resources = [], array $resourceIds = []): array
183181
$this->reportDatabases($resources, $report, $resourceIds);
184182
$this->reportStorage($resources, $report, $resourceIds);
185183
$this->reportFunctions($resources, $report, $resourceIds);
186-
$this->reportBackups($resources, $report);
184+
$this->reportBackups($resources, $report, $resourceIds);
187185

188186
$report['version'] = $this->call(
189187
'GET',
@@ -1636,7 +1634,12 @@ protected function exportGroupBackups(int $batchSize, array $resources): void
16361634
}
16371635
}
16381636

1639-
private function reportBackups(array $resources, array &$report): void
1637+
/**
1638+
* @param array<string> $resources
1639+
* @param array<string, mixed> $report
1640+
* @param array<string, array<string>> $resourceIds
1641+
*/
1642+
private function reportBackups(array $resources, array &$report, array $resourceIds = []): void
16401643
{
16411644
if (!\in_array(Resource::TYPE_BACKUP_POLICY, $resources)) {
16421645
return;
@@ -1648,12 +1651,16 @@ private function reportBackups(array $resources, array &$report): void
16481651
]);
16491652

16501653
$report[Resource::TYPE_BACKUP_POLICY] = $response['total'] ?? 0;
1651-
} catch (\Throwable $e) {
1654+
} catch (\Throwable) {
16521655
// Backup policies are Cloud-only, skip gracefully for self-hosted
16531656
$report[Resource::TYPE_BACKUP_POLICY] = 0;
16541657
}
16551658
}
16561659

1660+
/**
1661+
* @param int $batchSize
1662+
* @throws \Exception
1663+
*/
16571664
private function exportBackupPolicies(int $batchSize): void
16581665
{
16591666
$response = $this->call('GET', '/backups/policies', [

src/Migration/Sources/CSV.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,14 @@ protected function exportGroupFunctions(int $batchSize, array $resources): void
372372
throw new \Exception('Not Implemented');
373373
}
374374

375+
/**
376+
* @throws \Exception
377+
*/
378+
protected function exportGroupBackups(int $batchSize, array $resources): void
379+
{
380+
throw new \Exception('Not Implemented');
381+
}
382+
375383
/**
376384
* @param callable(resource $stream, string $delimiter): void $callback
377385
* @return void

src/Migration/Sources/Firebase.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,4 +808,9 @@ protected function exportGroupFunctions(int $batchSize, array $resources): void
808808
{
809809
throw new \Exception('Not implemented');
810810
}
811+
812+
protected function exportGroupBackups(int $batchSize, array $resources): void
813+
{
814+
throw new \Exception('Not implemented');
815+
}
811816
}

src/Migration/Sources/JSON.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,14 @@ protected function exportGroupFunctions(int $batchSize, array $resources): void
201201
throw new \Exception('Not Implemented');
202202
}
203203

204+
/**
205+
* @throws \Exception
206+
*/
207+
protected function exportGroupBackups(int $batchSize, array $resources): void
208+
{
209+
throw new \Exception('Not Implemented');
210+
}
211+
204212
/**
205213
* @param callable(Items): void $callback
206214
* @throws \Exception|JsonMachineException

src/Migration/Sources/NHost.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,4 +848,9 @@ protected function exportGroupFunctions(int $batchSize, array $resources): void
848848
{
849849
throw new \Exception('Not Implemented');
850850
}
851+
852+
protected function exportGroupBackups(int $batchSize, array $resources): void
853+
{
854+
throw new \Exception('Not Implemented');
855+
}
851856
}

tests/Migration/Unit/Adapters/MockDestination.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public static function getSupportedResources(): array
4848
Resource::TYPE_ENVIRONMENT_VARIABLE,
4949
Resource::TYPE_TEAM,
5050
Resource::TYPE_MEMBERSHIP,
51+
Resource::TYPE_BACKUP_POLICY,
5152
];
5253
}
5354

tests/Migration/Unit/Adapters/MockSource.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ public static function getSupportedResources(): array
7878
Resource::TYPE_TEAM,
7979
Resource::TYPE_MEMBERSHIP,
8080

81+
// Backups
82+
Resource::TYPE_BACKUP_POLICY,
83+
8184
// legacy
8285
Resource::TYPE_DOCUMENT,
8386
Resource::TYPE_ATTRIBUTE,
@@ -157,4 +160,21 @@ protected function exportGroupFunctions(int $batchSize, array $resources): void
157160
$this->handleResourceTransfer(Transfer::GROUP_FUNCTIONS, $resource);
158161
}
159162
}
163+
164+
/**
165+
* Export Backups Group
166+
*
167+
* @param int $batchSize Max 100
168+
* @param string[] $resources Resources to export
169+
*/
170+
protected function exportGroupBackups(int $batchSize, array $resources): void
171+
{
172+
foreach (Transfer::GROUP_BACKUPS_RESOURCES as $resource) {
173+
if (!\in_array($resource, $resources)) {
174+
continue;
175+
}
176+
177+
$this->handleResourceTransfer(Transfer::GROUP_BACKUPS, $resource);
178+
}
179+
}
160180
}

0 commit comments

Comments
 (0)