appwrite-utils-cli 1.5.2 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +199 -0
- package/README.md +251 -29
- package/dist/adapters/AdapterFactory.d.ts +10 -3
- package/dist/adapters/AdapterFactory.js +213 -17
- package/dist/adapters/TablesDBAdapter.js +60 -17
- package/dist/backups/operations/bucketBackup.d.ts +19 -0
- package/dist/backups/operations/bucketBackup.js +197 -0
- package/dist/backups/operations/collectionBackup.d.ts +30 -0
- package/dist/backups/operations/collectionBackup.js +201 -0
- package/dist/backups/operations/comprehensiveBackup.d.ts +25 -0
- package/dist/backups/operations/comprehensiveBackup.js +238 -0
- package/dist/backups/schemas/bucketManifest.d.ts +93 -0
- package/dist/backups/schemas/bucketManifest.js +33 -0
- package/dist/backups/schemas/comprehensiveManifest.d.ts +108 -0
- package/dist/backups/schemas/comprehensiveManifest.js +32 -0
- package/dist/backups/tracking/centralizedTracking.d.ts +34 -0
- package/dist/backups/tracking/centralizedTracking.js +274 -0
- package/dist/cli/commands/configCommands.d.ts +8 -0
- package/dist/cli/commands/configCommands.js +160 -0
- package/dist/cli/commands/databaseCommands.d.ts +13 -0
- package/dist/cli/commands/databaseCommands.js +478 -0
- package/dist/cli/commands/functionCommands.d.ts +7 -0
- package/dist/cli/commands/functionCommands.js +289 -0
- package/dist/cli/commands/schemaCommands.d.ts +7 -0
- package/dist/cli/commands/schemaCommands.js +134 -0
- package/dist/cli/commands/transferCommands.d.ts +5 -0
- package/dist/cli/commands/transferCommands.js +384 -0
- package/dist/collections/attributes.d.ts +5 -4
- package/dist/collections/attributes.js +539 -246
- package/dist/collections/indexes.js +39 -37
- package/dist/collections/methods.d.ts +2 -16
- package/dist/collections/methods.js +90 -538
- package/dist/collections/transferOperations.d.ts +7 -0
- package/dist/collections/transferOperations.js +331 -0
- package/dist/collections/wipeOperations.d.ts +16 -0
- package/dist/collections/wipeOperations.js +328 -0
- package/dist/config/configMigration.d.ts +87 -0
- package/dist/config/configMigration.js +390 -0
- package/dist/config/configValidation.d.ts +66 -0
- package/dist/config/configValidation.js +358 -0
- package/dist/config/yamlConfig.d.ts +455 -1
- package/dist/config/yamlConfig.js +145 -52
- package/dist/databases/methods.js +3 -2
- package/dist/databases/setup.d.ts +1 -2
- package/dist/databases/setup.js +9 -87
- package/dist/examples/yamlTerminologyExample.d.ts +42 -0
- package/dist/examples/yamlTerminologyExample.js +269 -0
- package/dist/functions/deployments.js +11 -10
- package/dist/functions/methods.d.ts +1 -1
- package/dist/functions/methods.js +5 -4
- package/dist/init.js +9 -9
- package/dist/interactiveCLI.d.ts +8 -17
- package/dist/interactiveCLI.js +181 -1172
- package/dist/main.js +364 -21
- package/dist/migrations/afterImportActions.js +22 -30
- package/dist/migrations/appwriteToX.js +71 -25
- package/dist/migrations/dataLoader.js +35 -26
- package/dist/migrations/importController.js +29 -30
- package/dist/migrations/relationships.js +13 -12
- package/dist/migrations/services/ImportOrchestrator.js +16 -19
- package/dist/migrations/transfer.js +46 -46
- package/dist/migrations/yaml/YamlImportConfigLoader.d.ts +3 -1
- package/dist/migrations/yaml/YamlImportConfigLoader.js +6 -3
- package/dist/migrations/yaml/YamlImportIntegration.d.ts +9 -3
- package/dist/migrations/yaml/YamlImportIntegration.js +22 -11
- package/dist/migrations/yaml/generateImportSchemas.d.ts +14 -1
- package/dist/migrations/yaml/generateImportSchemas.js +736 -7
- package/dist/schemas/authUser.d.ts +1 -1
- package/dist/setupController.js +3 -2
- package/dist/shared/backupMetadataSchema.d.ts +94 -0
- package/dist/shared/backupMetadataSchema.js +38 -0
- package/dist/shared/backupTracking.d.ts +18 -0
- package/dist/shared/backupTracking.js +176 -0
- package/dist/shared/confirmationDialogs.js +15 -15
- package/dist/shared/errorUtils.d.ts +54 -0
- package/dist/shared/errorUtils.js +95 -0
- package/dist/shared/functionManager.js +20 -19
- package/dist/shared/indexManager.js +12 -11
- package/dist/shared/jsonSchemaGenerator.js +10 -26
- package/dist/shared/logging.d.ts +51 -0
- package/dist/shared/logging.js +70 -0
- package/dist/shared/messageFormatter.d.ts +2 -0
- package/dist/shared/messageFormatter.js +10 -0
- package/dist/shared/migrationHelpers.d.ts +6 -16
- package/dist/shared/migrationHelpers.js +24 -21
- package/dist/shared/operationLogger.d.ts +8 -1
- package/dist/shared/operationLogger.js +11 -24
- package/dist/shared/operationQueue.d.ts +28 -1
- package/dist/shared/operationQueue.js +268 -66
- package/dist/shared/operationsTable.d.ts +26 -0
- package/dist/shared/operationsTable.js +286 -0
- package/dist/shared/operationsTableSchema.d.ts +48 -0
- package/dist/shared/operationsTableSchema.js +35 -0
- package/dist/shared/relationshipExtractor.d.ts +56 -0
- package/dist/shared/relationshipExtractor.js +138 -0
- package/dist/shared/schemaGenerator.d.ts +19 -1
- package/dist/shared/schemaGenerator.js +56 -75
- package/dist/storage/backupCompression.d.ts +20 -0
- package/dist/storage/backupCompression.js +67 -0
- package/dist/storage/methods.d.ts +16 -2
- package/dist/storage/methods.js +98 -14
- package/dist/users/methods.js +9 -8
- package/dist/utils/configDiscovery.d.ts +78 -0
- package/dist/utils/configDiscovery.js +430 -0
- package/dist/utils/directoryUtils.d.ts +22 -0
- package/dist/utils/directoryUtils.js +59 -0
- package/dist/utils/getClientFromConfig.d.ts +17 -8
- package/dist/utils/getClientFromConfig.js +162 -17
- package/dist/utils/helperFunctions.d.ts +16 -2
- package/dist/utils/helperFunctions.js +19 -5
- package/dist/utils/loadConfigs.d.ts +34 -9
- package/dist/utils/loadConfigs.js +236 -316
- package/dist/utils/pathResolvers.d.ts +53 -0
- package/dist/utils/pathResolvers.js +72 -0
- package/dist/utils/projectConfig.d.ts +119 -0
- package/dist/utils/projectConfig.js +171 -0
- package/dist/utils/retryFailedPromises.js +4 -2
- package/dist/utils/sessionAuth.d.ts +48 -0
- package/dist/utils/sessionAuth.js +164 -0
- package/dist/utils/sessionPreservationExample.d.ts +1666 -0
- package/dist/utils/sessionPreservationExample.js +101 -0
- package/dist/utils/setupFiles.js +301 -41
- package/dist/utils/typeGuards.d.ts +35 -0
- package/dist/utils/typeGuards.js +57 -0
- package/dist/utils/versionDetection.js +145 -9
- package/dist/utils/yamlConverter.d.ts +53 -3
- package/dist/utils/yamlConverter.js +232 -13
- package/dist/utils/yamlLoader.d.ts +70 -0
- package/dist/utils/yamlLoader.js +263 -0
- package/dist/utilsController.d.ts +36 -3
- package/dist/utilsController.js +186 -56
- package/package.json +12 -2
- package/src/adapters/AdapterFactory.ts +263 -35
- package/src/adapters/TablesDBAdapter.ts +225 -36
- package/src/backups/operations/bucketBackup.ts +277 -0
- package/src/backups/operations/collectionBackup.ts +310 -0
- package/src/backups/operations/comprehensiveBackup.ts +342 -0
- package/src/backups/schemas/bucketManifest.ts +78 -0
- package/src/backups/schemas/comprehensiveManifest.ts +76 -0
- package/src/backups/tracking/centralizedTracking.ts +352 -0
- package/src/cli/commands/configCommands.ts +194 -0
- package/src/cli/commands/databaseCommands.ts +635 -0
- package/src/cli/commands/functionCommands.ts +379 -0
- package/src/cli/commands/schemaCommands.ts +163 -0
- package/src/cli/commands/transferCommands.ts +457 -0
- package/src/collections/attributes.ts +900 -621
- package/src/collections/attributes.ts.backup +1555 -0
- package/src/collections/indexes.ts +116 -114
- package/src/collections/methods.ts +295 -968
- package/src/collections/transferOperations.ts +516 -0
- package/src/collections/wipeOperations.ts +501 -0
- package/src/config/README.md +274 -0
- package/src/config/configMigration.ts +575 -0
- package/src/config/configValidation.ts +445 -0
- package/src/config/yamlConfig.ts +168 -55
- package/src/databases/methods.ts +3 -2
- package/src/databases/setup.ts +11 -138
- package/src/examples/yamlTerminologyExample.ts +341 -0
- package/src/functions/deployments.ts +14 -12
- package/src/functions/methods.ts +11 -11
- package/src/functions/templates/hono-typescript/README.md +286 -0
- package/src/functions/templates/hono-typescript/package.json +26 -0
- package/src/functions/templates/hono-typescript/src/adapters/request.ts +74 -0
- package/src/functions/templates/hono-typescript/src/adapters/response.ts +106 -0
- package/src/functions/templates/hono-typescript/src/app.ts +180 -0
- package/src/functions/templates/hono-typescript/src/context.ts +103 -0
- package/src/functions/templates/hono-typescript/src/index.ts +54 -0
- package/src/functions/templates/hono-typescript/src/middleware/appwrite.ts +119 -0
- package/src/functions/templates/hono-typescript/tsconfig.json +20 -0
- package/src/functions/templates/typescript-node/package.json +2 -1
- package/src/functions/templates/typescript-node/src/context.ts +103 -0
- package/src/functions/templates/typescript-node/src/index.ts +18 -12
- package/src/functions/templates/uv/pyproject.toml +1 -0
- package/src/functions/templates/uv/src/context.py +125 -0
- package/src/functions/templates/uv/src/index.py +35 -5
- package/src/init.ts +9 -11
- package/src/interactiveCLI.ts +278 -1596
- package/src/main.ts +418 -24
- package/src/migrations/afterImportActions.ts +71 -44
- package/src/migrations/appwriteToX.ts +100 -34
- package/src/migrations/dataLoader.ts +48 -34
- package/src/migrations/importController.ts +44 -39
- package/src/migrations/relationships.ts +28 -18
- package/src/migrations/services/ImportOrchestrator.ts +24 -27
- package/src/migrations/transfer.ts +159 -121
- package/src/migrations/yaml/YamlImportConfigLoader.ts +11 -4
- package/src/migrations/yaml/YamlImportIntegration.ts +47 -20
- package/src/migrations/yaml/generateImportSchemas.ts +751 -12
- package/src/setupController.ts +3 -2
- package/src/shared/backupMetadataSchema.ts +93 -0
- package/src/shared/backupTracking.ts +211 -0
- package/src/shared/confirmationDialogs.ts +19 -19
- package/src/shared/errorUtils.ts +110 -0
- package/src/shared/functionManager.ts +21 -20
- package/src/shared/indexManager.ts +12 -11
- package/src/shared/jsonSchemaGenerator.ts +38 -52
- package/src/shared/logging.ts +75 -0
- package/src/shared/messageFormatter.ts +14 -1
- package/src/shared/migrationHelpers.ts +45 -38
- package/src/shared/operationLogger.ts +11 -36
- package/src/shared/operationQueue.ts +322 -93
- package/src/shared/operationsTable.ts +338 -0
- package/src/shared/operationsTableSchema.ts +60 -0
- package/src/shared/relationshipExtractor.ts +214 -0
- package/src/shared/schemaGenerator.ts +179 -219
- package/src/storage/backupCompression.ts +88 -0
- package/src/storage/methods.ts +131 -34
- package/src/users/methods.ts +11 -9
- package/src/utils/configDiscovery.ts +502 -0
- package/src/utils/directoryUtils.ts +61 -0
- package/src/utils/getClientFromConfig.ts +205 -22
- package/src/utils/helperFunctions.ts +23 -5
- package/src/utils/loadConfigs.ts +313 -345
- package/src/utils/pathResolvers.ts +81 -0
- package/src/utils/projectConfig.ts +299 -0
- package/src/utils/retryFailedPromises.ts +4 -2
- package/src/utils/sessionAuth.ts +230 -0
- package/src/utils/setupFiles.ts +322 -54
- package/src/utils/typeGuards.ts +65 -0
- package/src/utils/versionDetection.ts +218 -64
- package/src/utils/yamlConverter.ts +296 -13
- package/src/utils/yamlLoader.ts +364 -0
- package/src/utilsController.ts +314 -110
- package/tests/README.md +497 -0
- package/tests/adapters/AdapterFactory.test.ts +277 -0
- package/tests/integration/syncOperations.test.ts +463 -0
- package/tests/jest.config.js +25 -0
- package/tests/migration/configMigration.test.ts +546 -0
- package/tests/setup.ts +62 -0
- package/tests/testUtils.ts +340 -0
- package/tests/utils/loadConfigs.test.ts +350 -0
- package/tests/validation/configValidation.test.ts +412 -0
- package/src/utils/schemaStrings.ts +0 -517
@@ -0,0 +1,197 @@
|
|
1
|
+
import JSZip from "jszip";
|
2
|
+
import { ID, Query } from "node-appwrite";
|
3
|
+
import { InputFile } from "node-appwrite/file";
|
4
|
+
import pLimit from "p-limit";
|
5
|
+
import { MessageFormatter } from "../../shared/messageFormatter.js";
|
6
|
+
import { logger } from "../../shared/logging.js";
|
7
|
+
import { ulid } from "ulidx";
|
8
|
+
/**
|
9
|
+
* Downloads all files from a bucket in parallel and creates a ZIP backup
|
10
|
+
*/
|
11
|
+
export async function backupBucket(storage, bucketId, backupBucketId, options = {}) {
|
12
|
+
const { compressionLevel = 6, parallelDownloads = 10, onProgress } = options;
|
13
|
+
const errors = [];
|
14
|
+
let totalSizeBytes = 0;
|
15
|
+
try {
|
16
|
+
// Step 1: Get bucket metadata
|
17
|
+
MessageFormatter.info(`Fetching bucket metadata for ${bucketId}`, { prefix: "Backup" });
|
18
|
+
const bucket = await storage.getBucket(bucketId);
|
19
|
+
// Step 2: List all files in bucket with pagination
|
20
|
+
MessageFormatter.info(`Listing all files in bucket ${bucketId}`, { prefix: "Backup" });
|
21
|
+
const allFiles = [];
|
22
|
+
let lastFileId;
|
23
|
+
while (true) {
|
24
|
+
const queries = [Query.limit(100)];
|
25
|
+
if (lastFileId) {
|
26
|
+
queries.push(Query.cursorAfter(lastFileId));
|
27
|
+
}
|
28
|
+
const filesPage = await storage.listFiles(bucketId, queries);
|
29
|
+
allFiles.push(...filesPage.files);
|
30
|
+
if (filesPage.files.length < 100)
|
31
|
+
break;
|
32
|
+
lastFileId = filesPage.files[filesPage.files.length - 1].$id;
|
33
|
+
}
|
34
|
+
MessageFormatter.info(`Found ${allFiles.length} files to backup`, { prefix: "Backup" });
|
35
|
+
if (allFiles.length === 0) {
|
36
|
+
// Empty bucket - create minimal backup
|
37
|
+
const manifest = {
|
38
|
+
version: "1.0",
|
39
|
+
bucketId: bucket.$id,
|
40
|
+
bucketName: bucket.name,
|
41
|
+
createdAt: new Date().toISOString(),
|
42
|
+
fileCount: 0,
|
43
|
+
totalSizeBytes: 0,
|
44
|
+
compression: bucket.compression === 'gzip' ? 'gzip' : 'none',
|
45
|
+
files: [],
|
46
|
+
bucketConfiguration: {
|
47
|
+
$permissions: bucket.$permissions,
|
48
|
+
fileSecurity: bucket.fileSecurity,
|
49
|
+
enabled: bucket.enabled,
|
50
|
+
maximumFileSize: bucket.maximumFileSize,
|
51
|
+
allowedFileExtensions: bucket.allowedFileExtensions,
|
52
|
+
compression: bucket.compression,
|
53
|
+
encryption: bucket.encryption,
|
54
|
+
antivirus: bucket.antivirus
|
55
|
+
}
|
56
|
+
};
|
57
|
+
const manifestFileId = await uploadManifest(storage, backupBucketId, bucketId, manifest);
|
58
|
+
return {
|
59
|
+
backupFileId: '',
|
60
|
+
manifestFileId,
|
61
|
+
fileCount: 0,
|
62
|
+
totalSizeBytes: 0,
|
63
|
+
zipSizeBytes: 0,
|
64
|
+
status: 'completed'
|
65
|
+
};
|
66
|
+
}
|
67
|
+
// Step 3: Download all files in parallel with concurrency limit
|
68
|
+
MessageFormatter.info(`Downloading ${allFiles.length} files in parallel (max ${parallelDownloads} concurrent)`, { prefix: "Backup" });
|
69
|
+
const limit = pLimit(parallelDownloads);
|
70
|
+
const downloadedFiles = new Map();
|
71
|
+
let successCount = 0;
|
72
|
+
let errorCount = 0;
|
73
|
+
const downloadTasks = allFiles.map((file, index) => limit(async () => {
|
74
|
+
try {
|
75
|
+
const fileBuffer = await storage.getFileDownload(bucketId, file.$id);
|
76
|
+
const buffer = Buffer.from(fileBuffer);
|
77
|
+
downloadedFiles.set(file.$id, { buffer, file });
|
78
|
+
successCount++;
|
79
|
+
totalSizeBytes += file.sizeOriginal || buffer.length;
|
80
|
+
if (onProgress) {
|
81
|
+
onProgress(successCount + errorCount, allFiles.length, file.name);
|
82
|
+
}
|
83
|
+
logger.debug(`Downloaded file ${file.name}`, {
|
84
|
+
fileId: file.$id,
|
85
|
+
size: buffer.length
|
86
|
+
});
|
87
|
+
}
|
88
|
+
catch (error) {
|
89
|
+
errorCount++;
|
90
|
+
const errorMsg = `Failed to download file ${file.name} (${file.$id}): ${error instanceof Error ? error.message : String(error)}`;
|
91
|
+
errors.push(errorMsg);
|
92
|
+
logger.error(errorMsg);
|
93
|
+
if (onProgress) {
|
94
|
+
onProgress(successCount + errorCount, allFiles.length, file.name);
|
95
|
+
}
|
96
|
+
}
|
97
|
+
}));
|
98
|
+
await Promise.all(downloadTasks);
|
99
|
+
if (successCount === 0) {
|
100
|
+
throw new Error(`Failed to download any files from bucket ${bucketId}`);
|
101
|
+
}
|
102
|
+
MessageFormatter.info(`Successfully downloaded ${successCount}/${allFiles.length} files`, { prefix: "Backup" });
|
103
|
+
// Step 4: Create ZIP archive with all files
|
104
|
+
MessageFormatter.info(`Creating ZIP archive for bucket ${bucketId}`, { prefix: "Backup" });
|
105
|
+
const zip = new JSZip();
|
106
|
+
for (const [fileId, { buffer, file }] of downloadedFiles.entries()) {
|
107
|
+
// Preserve file structure by using file name
|
108
|
+
const fileName = file.name || `file_${fileId}`;
|
109
|
+
zip.file(fileName, new Uint8Array(buffer));
|
110
|
+
}
|
111
|
+
// Generate ZIP buffer
|
112
|
+
const zipBuffer = await zip.generateAsync({
|
113
|
+
type: "nodebuffer",
|
114
|
+
compression: "DEFLATE",
|
115
|
+
compressionOptions: { level: compressionLevel }
|
116
|
+
});
|
117
|
+
const zipSizeBytes = zipBuffer.length;
|
118
|
+
MessageFormatter.info(`ZIP archive created: ${MessageFormatter.formatBytes(zipSizeBytes)}`, { prefix: "Backup" });
|
119
|
+
// Step 5: Create manifest
|
120
|
+
const fileMetadata = Array.from(downloadedFiles.values()).map(({ file }) => ({
|
121
|
+
$id: file.$id,
|
122
|
+
name: file.name,
|
123
|
+
size: file.sizeOriginal,
|
124
|
+
mimeType: file.mimeType,
|
125
|
+
$permissions: file.$permissions,
|
126
|
+
chunksCount: file.chunksTotal,
|
127
|
+
signature: file.signature,
|
128
|
+
$createdAt: file.$createdAt,
|
129
|
+
$updatedAt: file.$updatedAt
|
130
|
+
}));
|
131
|
+
const manifest = {
|
132
|
+
version: "1.0",
|
133
|
+
bucketId: bucket.$id,
|
134
|
+
bucketName: bucket.name,
|
135
|
+
createdAt: new Date().toISOString(),
|
136
|
+
fileCount: successCount,
|
137
|
+
totalSizeBytes,
|
138
|
+
compression: bucket.compression === 'gzip' ? 'gzip' : 'none',
|
139
|
+
files: fileMetadata,
|
140
|
+
bucketConfiguration: {
|
141
|
+
$permissions: bucket.$permissions,
|
142
|
+
fileSecurity: bucket.fileSecurity,
|
143
|
+
enabled: bucket.enabled,
|
144
|
+
maximumFileSize: bucket.maximumFileSize,
|
145
|
+
allowedFileExtensions: bucket.allowedFileExtensions,
|
146
|
+
compression: bucket.compression,
|
147
|
+
encryption: bucket.encryption,
|
148
|
+
antivirus: bucket.antivirus
|
149
|
+
}
|
150
|
+
};
|
151
|
+
// Step 6: Upload backup ZIP to backup bucket
|
152
|
+
MessageFormatter.info(`Uploading backup ZIP to bucket ${backupBucketId}`, { prefix: "Backup" });
|
153
|
+
const backupFileName = `${bucketId}.zip`;
|
154
|
+
const backupFile = await storage.createFile(backupBucketId, ID.unique(), InputFile.fromBuffer(new Uint8Array(zipBuffer), backupFileName));
|
155
|
+
// Step 7: Upload manifest JSON
|
156
|
+
const manifestFileId = await uploadManifest(storage, backupBucketId, bucketId, manifest);
|
157
|
+
const status = errorCount === 0 ? 'completed' :
|
158
|
+
successCount > 0 ? 'partial' :
|
159
|
+
'failed';
|
160
|
+
MessageFormatter.success(`Bucket backup ${status}: ${successCount}/${allFiles.length} files backed up`, { prefix: "Backup" });
|
161
|
+
return {
|
162
|
+
backupFileId: backupFile.$id,
|
163
|
+
manifestFileId,
|
164
|
+
fileCount: successCount,
|
165
|
+
totalSizeBytes,
|
166
|
+
zipSizeBytes,
|
167
|
+
status,
|
168
|
+
errors: errors.length > 0 ? errors : undefined
|
169
|
+
};
|
170
|
+
}
|
171
|
+
catch (error) {
|
172
|
+
const errorMsg = `Bucket backup failed: ${error instanceof Error ? error.message : String(error)}`;
|
173
|
+
MessageFormatter.error(errorMsg, error instanceof Error ? error : new Error(errorMsg), { prefix: "Backup" });
|
174
|
+
return {
|
175
|
+
backupFileId: '',
|
176
|
+
manifestFileId: '',
|
177
|
+
fileCount: 0,
|
178
|
+
totalSizeBytes: 0,
|
179
|
+
zipSizeBytes: 0,
|
180
|
+
status: 'failed',
|
181
|
+
errors: [errorMsg, ...errors]
|
182
|
+
};
|
183
|
+
}
|
184
|
+
}
|
185
|
+
/**
|
186
|
+
* Uploads bucket manifest JSON to backup bucket
|
187
|
+
*/
|
188
|
+
async function uploadManifest(storage, backupBucketId, bucketId, manifest) {
|
189
|
+
const manifestFileName = `${bucketId}.json`;
|
190
|
+
const manifestBuffer = Buffer.from(JSON.stringify(manifest, null, 2), 'utf-8');
|
191
|
+
const manifestFile = await storage.createFile(backupBucketId, ID.unique(), InputFile.fromBuffer(new Uint8Array(manifestBuffer), manifestFileName));
|
192
|
+
logger.info("Uploaded bucket manifest", {
|
193
|
+
manifestFileId: manifestFile.$id,
|
194
|
+
bucketId
|
195
|
+
});
|
196
|
+
return manifestFile.$id;
|
197
|
+
}
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import type { Storage, Databases } from "node-appwrite";
|
2
|
+
import type { DatabaseAdapter } from "../../adapters/DatabaseAdapter.js";
|
3
|
+
import type { AppwriteConfig } from "appwrite-utils";
|
4
|
+
export interface CollectionBackupOptions {
|
5
|
+
trackingDatabaseId: string;
|
6
|
+
databaseId: string;
|
7
|
+
collectionIds: string[];
|
8
|
+
backupFormat?: 'json' | 'zip';
|
9
|
+
onProgress?: (message: string) => void;
|
10
|
+
}
|
11
|
+
export interface CollectionBackupResult {
|
12
|
+
backupId: string;
|
13
|
+
manifestFileId: string;
|
14
|
+
databaseId: string;
|
15
|
+
collections: Array<{
|
16
|
+
collectionId: string;
|
17
|
+
collectionName: string;
|
18
|
+
documentCount: number;
|
19
|
+
status: 'completed' | 'failed';
|
20
|
+
error?: string;
|
21
|
+
}>;
|
22
|
+
totalDocuments: number;
|
23
|
+
sizeBytes: number;
|
24
|
+
status: 'completed' | 'partial' | 'failed';
|
25
|
+
errors: string[];
|
26
|
+
}
|
27
|
+
/**
|
28
|
+
* Backup specific collections from a database
|
29
|
+
*/
|
30
|
+
export declare function backupCollections(config: AppwriteConfig, databases: Databases, storage: Storage, adapter: DatabaseAdapter, options: CollectionBackupOptions): Promise<CollectionBackupResult>;
|
@@ -0,0 +1,201 @@
|
|
1
|
+
import { ID, Query } from "node-appwrite";
|
2
|
+
import { InputFile } from "node-appwrite/file";
|
3
|
+
import { ulid } from "ulidx";
|
4
|
+
import { MessageFormatter } from "../../shared/messageFormatter.js";
|
5
|
+
import { logger } from "../../shared/logging.js";
|
6
|
+
import { tryAwaitWithRetry } from "appwrite-utils";
|
7
|
+
import { splitIntoBatches } from "../../shared/migrationHelpers.js";
|
8
|
+
import { retryFailedPromises } from "../../utils/retryFailedPromises.js";
|
9
|
+
import { ProgressManager } from "../../shared/progressManager.js";
|
10
|
+
import { createBackupZip } from "../../storage/backupCompression.js";
|
11
|
+
import { recordCentralizedBackup, createCentralizedBackupTrackingTable } from "../tracking/centralizedTracking.js";
|
12
|
+
/**
|
13
|
+
* Backup specific collections from a database
|
14
|
+
*/
|
15
|
+
export async function backupCollections(config, databases, storage, adapter, options) {
|
16
|
+
const startTime = Date.now();
|
17
|
+
const backupId = ulid();
|
18
|
+
const errors = [];
|
19
|
+
const collections = [];
|
20
|
+
let totalDocuments = 0;
|
21
|
+
let totalSizeBytes = 0;
|
22
|
+
try {
|
23
|
+
// Ensure tracking table exists
|
24
|
+
await createCentralizedBackupTrackingTable(adapter, options.trackingDatabaseId);
|
25
|
+
const backupBucketId = "appwrite-backups";
|
26
|
+
MessageFormatter.info(`Starting collection backup ${backupId}`, { prefix: "Backup" });
|
27
|
+
MessageFormatter.info(`Database: ${options.databaseId}, Collections: ${options.collectionIds.length}`, { prefix: "Backup" });
|
28
|
+
// Get database info
|
29
|
+
const db = await tryAwaitWithRetry(async () => await databases.get(options.databaseId));
|
30
|
+
const backupData = {
|
31
|
+
database: JSON.stringify(db),
|
32
|
+
collections: [],
|
33
|
+
documents: []
|
34
|
+
};
|
35
|
+
// Phase 1: Count documents for progress tracking
|
36
|
+
MessageFormatter.step(1, 3, "Analyzing collections");
|
37
|
+
let totalItems = options.collectionIds.length; // Start with collection count
|
38
|
+
for (const collectionId of options.collectionIds) {
|
39
|
+
try {
|
40
|
+
const documentCount = await tryAwaitWithRetry(async () => (await databases.listDocuments(options.databaseId, collectionId, [Query.limit(1)])).total);
|
41
|
+
totalDocuments += documentCount;
|
42
|
+
totalItems += documentCount;
|
43
|
+
}
|
44
|
+
catch (error) {
|
45
|
+
MessageFormatter.warning(`Could not count documents in collection ${collectionId}`);
|
46
|
+
}
|
47
|
+
}
|
48
|
+
const progress = ProgressManager.create(`backup-collections-${backupId}`, totalItems, {
|
49
|
+
title: `Backing up ${options.collectionIds.length} collections`,
|
50
|
+
});
|
51
|
+
// Phase 2: Backup selected collections
|
52
|
+
MessageFormatter.step(2, 3, `Processing ${options.collectionIds.length} collections and ${totalDocuments} documents`);
|
53
|
+
let processedDocuments = 0;
|
54
|
+
for (const collectionId of options.collectionIds) {
|
55
|
+
try {
|
56
|
+
if (options.onProgress) {
|
57
|
+
options.onProgress(`Backing up collection: ${collectionId}`);
|
58
|
+
}
|
59
|
+
// Get collection metadata
|
60
|
+
const collection = await tryAwaitWithRetry(async () => await databases.getCollection(options.databaseId, collectionId));
|
61
|
+
backupData.collections.push(JSON.stringify(collection));
|
62
|
+
progress.increment(1, `Processing collection: ${collection.name}`);
|
63
|
+
// Backup all documents in this collection
|
64
|
+
let lastDocumentId = "";
|
65
|
+
let moreDocuments = true;
|
66
|
+
let collectionDocumentCount = 0;
|
67
|
+
while (moreDocuments) {
|
68
|
+
const documentResponse = await tryAwaitWithRetry(async () => await databases.listDocuments(options.databaseId, collectionId, [
|
69
|
+
Query.limit(500),
|
70
|
+
...(lastDocumentId ? [Query.cursorAfter(lastDocumentId)] : []),
|
71
|
+
]));
|
72
|
+
collectionDocumentCount += documentResponse.documents.length;
|
73
|
+
const documentPromises = documentResponse.documents.map(({ $id: documentId }) => databases.getDocument(options.databaseId, collectionId, documentId));
|
74
|
+
const promiseBatches = splitIntoBatches(documentPromises);
|
75
|
+
const documentsPulled = [];
|
76
|
+
for (const batch of promiseBatches) {
|
77
|
+
const successfulDocuments = await retryFailedPromises(batch);
|
78
|
+
documentsPulled.push(...successfulDocuments);
|
79
|
+
// Update progress for each batch
|
80
|
+
progress.increment(successfulDocuments.length, `Processing ${collection.name}: ${processedDocuments + successfulDocuments.length}/${totalDocuments} documents`);
|
81
|
+
processedDocuments += successfulDocuments.length;
|
82
|
+
}
|
83
|
+
backupData.documents.push({
|
84
|
+
collectionId: collectionId,
|
85
|
+
data: JSON.stringify(documentsPulled),
|
86
|
+
});
|
87
|
+
moreDocuments = documentResponse.documents.length === 500;
|
88
|
+
if (moreDocuments) {
|
89
|
+
lastDocumentId = documentResponse.documents[documentResponse.documents.length - 1].$id;
|
90
|
+
}
|
91
|
+
}
|
92
|
+
collections.push({
|
93
|
+
collectionId,
|
94
|
+
collectionName: collection.name,
|
95
|
+
documentCount: collectionDocumentCount,
|
96
|
+
status: 'completed'
|
97
|
+
});
|
98
|
+
MessageFormatter.success(`Collection ${collection.name} backed up with ${MessageFormatter.formatNumber(collectionDocumentCount)} documents`);
|
99
|
+
}
|
100
|
+
catch (error) {
|
101
|
+
const errorMsg = `Failed to backup collection ${collectionId}: ${error instanceof Error ? error.message : String(error)}`;
|
102
|
+
errors.push(errorMsg);
|
103
|
+
logger.error(errorMsg);
|
104
|
+
collections.push({
|
105
|
+
collectionId,
|
106
|
+
collectionName: collectionId,
|
107
|
+
documentCount: 0,
|
108
|
+
status: 'failed',
|
109
|
+
error: errorMsg
|
110
|
+
});
|
111
|
+
}
|
112
|
+
}
|
113
|
+
// Phase 3: Create backup file
|
114
|
+
MessageFormatter.step(3, 3, "Creating backup file");
|
115
|
+
let inputFile;
|
116
|
+
let fileName;
|
117
|
+
let backupSize;
|
118
|
+
if (options.backupFormat === 'zip') {
|
119
|
+
// Create compressed backup
|
120
|
+
const zipBuffer = await createBackupZip(backupData);
|
121
|
+
fileName = `${new Date().toISOString()}-${options.databaseId}-collections.zip`;
|
122
|
+
backupSize = zipBuffer.length;
|
123
|
+
inputFile = InputFile.fromBuffer(new Uint8Array(zipBuffer), fileName);
|
124
|
+
}
|
125
|
+
else {
|
126
|
+
// Use JSON format
|
127
|
+
const backupDataString = JSON.stringify(backupData, null, 2);
|
128
|
+
fileName = `${new Date().toISOString()}-${options.databaseId}-collections.json`;
|
129
|
+
backupSize = Buffer.byteLength(backupDataString, 'utf8');
|
130
|
+
inputFile = InputFile.fromPlainText(backupDataString, fileName);
|
131
|
+
}
|
132
|
+
const fileCreated = await storage.createFile(backupBucketId, ulid(), inputFile);
|
133
|
+
totalSizeBytes = backupSize;
|
134
|
+
// Create manifest
|
135
|
+
const manifestData = {
|
136
|
+
version: "1.0",
|
137
|
+
backupId,
|
138
|
+
databaseId: options.databaseId,
|
139
|
+
collectionIds: options.collectionIds,
|
140
|
+
collections: collections,
|
141
|
+
format: options.backupFormat || 'json',
|
142
|
+
createdAt: new Date().toISOString(),
|
143
|
+
totalDocuments: processedDocuments,
|
144
|
+
totalSizeBytes: backupSize
|
145
|
+
};
|
146
|
+
const manifestBuffer = Buffer.from(JSON.stringify(manifestData, null, 2), 'utf-8');
|
147
|
+
const manifestFile = await storage.createFile(backupBucketId, ID.unique(), InputFile.fromBuffer(new Uint8Array(manifestBuffer), `${backupId}-manifest.json`));
|
148
|
+
// Record in centralized tracking
|
149
|
+
await recordCentralizedBackup(adapter, options.trackingDatabaseId, {
|
150
|
+
backupType: 'database',
|
151
|
+
backupId: fileCreated.$id,
|
152
|
+
manifestFileId: manifestFile.$id,
|
153
|
+
format: options.backupFormat || 'json',
|
154
|
+
sizeBytes: backupSize,
|
155
|
+
databaseId: options.databaseId,
|
156
|
+
collections: backupData.collections.length,
|
157
|
+
documents: processedDocuments,
|
158
|
+
status: errors.length === 0 ? 'completed' : 'partial',
|
159
|
+
error: errors.length > 0 ? errors.join('; ') : undefined,
|
160
|
+
restorationStatus: 'not_restored'
|
161
|
+
});
|
162
|
+
progress.stop();
|
163
|
+
const duration = Date.now() - startTime;
|
164
|
+
const status = errors.length === 0 ? 'completed' :
|
165
|
+
collections.some(c => c.status === 'completed') ? 'partial' :
|
166
|
+
'failed';
|
167
|
+
MessageFormatter.success(`Collection backup ${status} in ${(duration / 1000).toFixed(2)}s`, { prefix: "Backup" });
|
168
|
+
MessageFormatter.operationSummary("Collection Backup", {
|
169
|
+
database: options.databaseId,
|
170
|
+
collections: backupData.collections.length,
|
171
|
+
documents: processedDocuments,
|
172
|
+
fileSize: MessageFormatter.formatBytes(backupSize),
|
173
|
+
backupFile: fileName,
|
174
|
+
bucket: backupBucketId,
|
175
|
+
}, duration);
|
176
|
+
return {
|
177
|
+
backupId,
|
178
|
+
manifestFileId: manifestFile.$id,
|
179
|
+
databaseId: options.databaseId,
|
180
|
+
collections,
|
181
|
+
totalDocuments: processedDocuments,
|
182
|
+
sizeBytes: totalSizeBytes,
|
183
|
+
status,
|
184
|
+
errors
|
185
|
+
};
|
186
|
+
}
|
187
|
+
catch (error) {
|
188
|
+
const errorMsg = `Collection backup failed: ${error instanceof Error ? error.message : String(error)}`;
|
189
|
+
MessageFormatter.error(errorMsg, error instanceof Error ? error : new Error(errorMsg), { prefix: "Backup" });
|
190
|
+
return {
|
191
|
+
backupId,
|
192
|
+
manifestFileId: '',
|
193
|
+
databaseId: options.databaseId,
|
194
|
+
collections: [],
|
195
|
+
totalDocuments: 0,
|
196
|
+
sizeBytes: 0,
|
197
|
+
status: 'failed',
|
198
|
+
errors: [errorMsg, ...errors]
|
199
|
+
};
|
200
|
+
}
|
201
|
+
}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import type { Storage, Databases } from "node-appwrite";
|
2
|
+
import type { DatabaseAdapter } from "../../adapters/DatabaseAdapter.js";
|
3
|
+
import type { DatabaseBackupReference, BucketBackupReference } from "../schemas/comprehensiveManifest.js";
|
4
|
+
import type { AppwriteConfig } from "appwrite-utils";
|
5
|
+
export interface ComprehensiveBackupOptions {
|
6
|
+
trackingDatabaseId: string;
|
7
|
+
backupFormat?: 'json' | 'zip';
|
8
|
+
skipDatabases?: boolean;
|
9
|
+
skipBuckets?: boolean;
|
10
|
+
parallelDownloads?: number;
|
11
|
+
onProgress?: (message: string) => void;
|
12
|
+
}
|
13
|
+
export interface ComprehensiveBackupResult {
|
14
|
+
backupId: string;
|
15
|
+
manifestFileId: string;
|
16
|
+
databaseBackups: DatabaseBackupReference[];
|
17
|
+
bucketBackups: BucketBackupReference[];
|
18
|
+
totalSizeBytes: number;
|
19
|
+
status: 'completed' | 'partial' | 'failed';
|
20
|
+
errors: string[];
|
21
|
+
}
|
22
|
+
/**
|
23
|
+
* Orchestrates comprehensive backup of ALL databases and ALL storage buckets
|
24
|
+
*/
|
25
|
+
export declare function comprehensiveBackup(config: AppwriteConfig, databases: Databases, storage: Storage, adapter: DatabaseAdapter, options: ComprehensiveBackupOptions): Promise<ComprehensiveBackupResult>;
|