appwrite-utils-cli 0.0.47 → 0.0.49

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.
@@ -393,7 +393,7 @@ export class DataLoader {
393
393
  }
394
394
  }
395
395
  console.log("Running update references");
396
- // this.dealWithMergedUsers();
396
+ this.dealWithMergedUsers();
397
397
  this.updateOldReferencesForNew();
398
398
  console.log("Done running update references");
399
399
  }
@@ -1164,19 +1164,18 @@ export class DataLoader {
1164
1164
  * @param collection - The collection configuration.
1165
1165
  * @param importDef - The import definition containing the attribute mappings and other relevant info.
1166
1166
  */
1167
- prepareUpdateData(
1167
+ async prepareUpdateData(
1168
1168
  db: ConfigDatabase,
1169
1169
  collection: CollectionCreate,
1170
1170
  importDef: ImportDef
1171
1171
  ) {
1172
- // Retrieve the current collection data and old-to-new ID map from the import map
1173
1172
  const currentData = this.importMap.get(
1174
1173
  this.getCollectionKey(collection.name)
1175
1174
  );
1176
1175
  const oldIdToNewIdMap = this.oldIdToNewIdPerCollectionMap.get(
1177
1176
  this.getCollectionKey(collection.name)
1178
1177
  );
1179
- // Log an error and return if no current data is found for the collection
1178
+
1180
1179
  if (
1181
1180
  !(currentData?.data && currentData?.data.length > 0) &&
1182
1181
  !oldIdToNewIdMap
@@ -1186,7 +1185,7 @@ export class DataLoader {
1186
1185
  );
1187
1186
  return;
1188
1187
  }
1189
- // Load the raw data based on the import definition
1188
+
1190
1189
  const rawData = this.loadData(importDef);
1191
1190
  const operationId = this.collectionImportOperations.get(
1192
1191
  this.getCollectionKey(collection.name)
@@ -1196,31 +1195,75 @@ export class DataLoader {
1196
1195
  `No import operation found for collection ${collection.name}`
1197
1196
  );
1198
1197
  }
1198
+
1199
1199
  for (const item of rawData) {
1200
- // Transform the item data based on the attribute mappings
1201
1200
  let transformedData = this.transformData(
1202
1201
  item,
1203
1202
  importDef.attributeMappings
1204
1203
  );
1205
1204
  let newId: string | undefined;
1206
1205
  let oldId: string | undefined;
1207
- // Determine the new ID for the item based on the primary key field or update mapping
1208
- oldId = item[importDef.primaryKeyField];
1209
- if (oldId) {
1210
- newId = oldIdToNewIdMap?.get(`${oldId}`);
1211
- if (
1212
- !newId &&
1213
- this.getCollectionKey(this.config.usersCollectionName) ===
1214
- this.getCollectionKey(collection.name)
1215
- ) {
1216
- for (const [key, value] of this.mergedUserMap.entries()) {
1217
- if (value.includes(`${oldId}`)) {
1218
- newId = key;
1219
- break;
1206
+ let itemDataToUpdate: CollectionImportData["data"][number] | undefined;
1207
+
1208
+ // Try to find itemDataToUpdate using updateMapping
1209
+ if (importDef.updateMapping) {
1210
+ console.log(importDef.updateMapping);
1211
+ oldId =
1212
+ item[importDef.updateMapping.originalIdField] ||
1213
+ transformedData[importDef.updateMapping.originalIdField];
1214
+ if (oldId) {
1215
+ itemDataToUpdate = currentData?.data.find(
1216
+ ({ context, rawData, finalData }) => {
1217
+ const targetField =
1218
+ importDef.updateMapping!.targetField ??
1219
+ importDef.updateMapping!.originalIdField;
1220
+
1221
+ return (
1222
+ `${context[targetField]}` === `${oldId}` ||
1223
+ `${rawData[targetField]}` === `${oldId}` ||
1224
+ `${finalData[targetField]}` === `${oldId}`
1225
+ );
1226
+ }
1227
+ );
1228
+
1229
+ if (itemDataToUpdate) {
1230
+ newId =
1231
+ itemDataToUpdate.finalData.docId ||
1232
+ itemDataToUpdate.context.docId;
1233
+ }
1234
+ }
1235
+ }
1236
+
1237
+ // If updateMapping is not defined or did not find the item, use primaryKeyField
1238
+ if (!itemDataToUpdate && importDef.primaryKeyField) {
1239
+ oldId =
1240
+ item[importDef.primaryKeyField] ||
1241
+ transformedData[importDef.primaryKeyField];
1242
+ if (oldId) {
1243
+ newId = oldIdToNewIdMap?.get(`${oldId}`);
1244
+ if (
1245
+ !newId &&
1246
+ this.getCollectionKey(this.config.usersCollectionName) ===
1247
+ this.getCollectionKey(collection.name)
1248
+ ) {
1249
+ for (const [key, value] of this.mergedUserMap.entries()) {
1250
+ if (value.includes(`${oldId}`)) {
1251
+ newId = key;
1252
+ break;
1253
+ }
1220
1254
  }
1221
1255
  }
1222
1256
  }
1223
- } else {
1257
+
1258
+ if (oldId && !itemDataToUpdate) {
1259
+ itemDataToUpdate = currentData?.data.find(
1260
+ (data) =>
1261
+ `${data.context[importDef.primaryKeyField]}` === `${oldId}`
1262
+ );
1263
+ }
1264
+ }
1265
+
1266
+ if (!oldId) {
1224
1267
  logger.error(
1225
1268
  `No old ID found (to update another document with) in prepareUpdateData for ${
1226
1269
  collection.name
@@ -1228,15 +1271,10 @@ export class DataLoader {
1228
1271
  );
1229
1272
  continue;
1230
1273
  }
1231
- const itemDataToUpdate = this.importMap
1232
- .get(this.getCollectionKey(collection.name))
1233
- ?.data.find(
1234
- (data) => `${data.context[importDef.primaryKeyField]}` === `${oldId}`
1235
- );
1236
- // Log an error and continue to the next item if no new ID is found
1274
+
1237
1275
  if (!newId && !itemDataToUpdate) {
1238
1276
  logger.error(
1239
- `No new id found for collection ${
1277
+ `No new id && no data found for collection ${
1240
1278
  collection.name
1241
1279
  } for updateDef ${JSON.stringify(
1242
1280
  item,
@@ -1246,7 +1284,8 @@ export class DataLoader {
1246
1284
  );
1247
1285
  continue;
1248
1286
  } else if (itemDataToUpdate) {
1249
- newId = itemDataToUpdate.finalData.docId;
1287
+ newId =
1288
+ itemDataToUpdate.finalData.docId || itemDataToUpdate.context.docId;
1250
1289
  if (!newId) {
1251
1290
  logger.error(
1252
1291
  `No new id found for collection ${
@@ -1255,11 +1294,16 @@ export class DataLoader {
1255
1294
  item,
1256
1295
  null,
1257
1296
  2
1297
+ )} but has itemDataToUpdate ${JSON.stringify(
1298
+ itemDataToUpdate,
1299
+ null,
1300
+ 2
1258
1301
  )} but it says it's supposed to have one...`
1259
1302
  );
1260
1303
  continue;
1261
1304
  }
1262
1305
  }
1306
+
1263
1307
  if (!itemDataToUpdate || !newId) {
1264
1308
  logger.error(
1265
1309
  `No data or ID (docId) found for collection ${
@@ -1272,49 +1316,51 @@ export class DataLoader {
1272
1316
  );
1273
1317
  continue;
1274
1318
  }
1319
+
1275
1320
  transformedData = this.mergeObjects(
1276
1321
  itemDataToUpdate.finalData,
1277
1322
  transformedData
1278
1323
  );
1324
+
1279
1325
  // Create a context object for the item, including the new ID and transformed data
1280
1326
  let context = this.createContext(db, collection, item, newId);
1281
- context = { ...context, ...transformedData };
1327
+ context = this.mergeObjects(context, transformedData);
1328
+
1282
1329
  // Validate the item before proceeding
1283
- const isValid = this.importDataActions.validateItem(
1330
+ const isValid = await this.importDataActions.validateItem(
1284
1331
  item,
1285
1332
  importDef.attributeMappings,
1286
1333
  context
1287
1334
  );
1288
- // Log info and continue to the next item if it's invalid
1335
+
1289
1336
  if (!isValid) {
1290
1337
  logger.info(
1291
1338
  `Skipping item: ${JSON.stringify(item, null, 2)} because it's invalid`
1292
1339
  );
1293
1340
  continue;
1294
1341
  }
1342
+
1295
1343
  // Update the attribute mappings with any actions that need to be performed post-import
1296
1344
  const mappingsWithActions = this.getAttributeMappingsWithActions(
1297
1345
  importDef.attributeMappings,
1298
1346
  context,
1299
1347
  transformedData
1300
1348
  );
1349
+
1301
1350
  // Update the import definition with the new attribute mappings
1302
1351
  const newImportDef = {
1303
1352
  ...importDef,
1304
1353
  attributeMappings: mappingsWithActions,
1305
1354
  };
1306
- // Add the item with its context and final data to the current collection data
1355
+
1307
1356
  if (itemDataToUpdate) {
1308
- // Update the existing item's finalData and context in place
1309
1357
  itemDataToUpdate.finalData = this.mergeObjects(
1310
1358
  itemDataToUpdate.finalData,
1311
1359
  transformedData
1312
1360
  );
1313
1361
  itemDataToUpdate.context = context;
1314
1362
  itemDataToUpdate.importDef = newImportDef;
1315
- currentData!.data.push(itemDataToUpdate);
1316
1363
  } else {
1317
- // If no existing item matches, then add the new item
1318
1364
  currentData!.data.push({
1319
1365
  rawData: item,
1320
1366
  context: context,
@@ -1322,6 +1368,7 @@ export class DataLoader {
1322
1368
  finalData: transformedData,
1323
1369
  });
1324
1370
  }
1371
+
1325
1372
  // Since we're modifying currentData in place, we ensure no duplicates are added
1326
1373
  this.importMap.set(this.getCollectionKey(collection.name), currentData!);
1327
1374
  }
@@ -1,5 +1,14 @@
1
- import { Databases, Query, type Models } from "node-appwrite";
2
- import { tryAwaitWithRetry } from "../utils/helperFunctions.js";
1
+ import { Client, Databases, Query, type Models } from "node-appwrite";
2
+ import {
3
+ getAppwriteClient,
4
+ tryAwaitWithRetry,
5
+ } from "../utils/helperFunctions.js";
6
+ import {
7
+ transferDocumentsBetweenDbsLocalToLocal,
8
+ transferDocumentsBetweenDbsLocalToRemote,
9
+ } from "./collections.js";
10
+ import { createOrUpdateAttribute } from "./attributes.js";
11
+ import { parseAttribute } from "appwrite-utils";
3
12
 
4
13
  export const fetchAllDatabases = async (
5
14
  database: Databases
@@ -26,3 +35,213 @@ export const fetchAllDatabases = async (
26
35
  }
27
36
  return allDatabases;
28
37
  };
38
+
39
+ /**
40
+ * Transfers all collections and documents from one local database to another local database.
41
+ *
42
+ * @param {Databases} localDb - The local database instance.
43
+ * @param {string} fromDbId - The ID of the source database.
44
+ * @param {string} targetDbId - The ID of the target database.
45
+ * @return {Promise<void>} A promise that resolves when the transfer is complete.
46
+ */
47
+ export const transferDatabaseLocalToLocal = async (
48
+ localDb: Databases,
49
+ fromDbId: string,
50
+ targetDbId: string
51
+ ) => {
52
+ let lastCollectionId: string | undefined;
53
+ let fromCollections = await tryAwaitWithRetry(
54
+ async () => await localDb.listCollections(fromDbId, [Query.limit(50)])
55
+ );
56
+ const allFromCollections = fromCollections.collections;
57
+ if (fromCollections.collections.length < 50) {
58
+ lastCollectionId = undefined;
59
+ } else {
60
+ lastCollectionId =
61
+ fromCollections.collections[fromCollections.collections.length - 1].$id;
62
+ while (lastCollectionId) {
63
+ const collections = await localDb.listCollections(fromDbId, [
64
+ Query.limit(50),
65
+ Query.cursorAfter(lastCollectionId),
66
+ ]);
67
+ allFromCollections.push(...collections.collections);
68
+ if (collections.collections.length < 50) {
69
+ break;
70
+ }
71
+ lastCollectionId =
72
+ collections.collections[collections.collections.length - 1].$id;
73
+ }
74
+ }
75
+ lastCollectionId = undefined;
76
+ let toCollections = await tryAwaitWithRetry(
77
+ async () => await localDb.listCollections(targetDbId, [Query.limit(50)])
78
+ );
79
+ const allToCollections = toCollections.collections;
80
+ if (toCollections.collections.length < 50) {
81
+ } else {
82
+ lastCollectionId =
83
+ toCollections.collections[toCollections.collections.length - 1].$id;
84
+ while (lastCollectionId) {
85
+ const collections = await localDb.listCollections(targetDbId, [
86
+ Query.limit(50),
87
+ Query.cursorAfter(lastCollectionId),
88
+ ]);
89
+ allToCollections.push(...collections.collections);
90
+ if (collections.collections.length < 50) {
91
+ lastCollectionId = undefined;
92
+ } else {
93
+ lastCollectionId =
94
+ collections.collections[collections.collections.length - 1].$id;
95
+ }
96
+ }
97
+ }
98
+ for (const collection of allFromCollections) {
99
+ const toCollection = allToCollections.find((c) => c.$id === collection.$id);
100
+ if (toCollection) {
101
+ await transferDocumentsBetweenDbsLocalToLocal(
102
+ localDb,
103
+ fromDbId,
104
+ targetDbId,
105
+ collection.$id,
106
+ toCollection.$id
107
+ );
108
+ } else {
109
+ console.log(
110
+ `Collection ${collection.name} not found in destination database, creating...`
111
+ );
112
+ const newCollection = await tryAwaitWithRetry(
113
+ async () =>
114
+ await localDb.createCollection(
115
+ targetDbId,
116
+ collection.$id,
117
+ collection.name,
118
+ collection.$permissions,
119
+ collection.documentSecurity,
120
+ collection.enabled
121
+ )
122
+ );
123
+ console.log(`Collection ${newCollection.name} created`);
124
+ for (const attribute of collection.attributes) {
125
+ await tryAwaitWithRetry(
126
+ async () =>
127
+ await createOrUpdateAttribute(
128
+ localDb,
129
+ targetDbId,
130
+ newCollection,
131
+ parseAttribute(attribute as any)
132
+ )
133
+ );
134
+ }
135
+ for (const index of collection.indexes) {
136
+ await tryAwaitWithRetry(
137
+ async () =>
138
+ await localDb.createIndex(
139
+ targetDbId,
140
+ newCollection.$id,
141
+ index.key,
142
+ index.type,
143
+ index.attributes,
144
+ index.orders
145
+ )
146
+ );
147
+ }
148
+ await transferDocumentsBetweenDbsLocalToLocal(
149
+ localDb,
150
+ fromDbId,
151
+ targetDbId,
152
+ collection.$id,
153
+ newCollection.$id
154
+ );
155
+ }
156
+ }
157
+ };
158
+
159
+ export const transferDatabaseLocalToRemote = async (
160
+ localDb: Databases,
161
+ endpoint: string,
162
+ projectId: string,
163
+ apiKey: string,
164
+ fromDbId: string,
165
+ toDbId: string
166
+ ) => {
167
+ const client = getAppwriteClient(endpoint, projectId, apiKey);
168
+ const remoteDb = new Databases(client);
169
+
170
+ let lastCollectionId: string | undefined;
171
+ let fromCollections = await tryAwaitWithRetry(
172
+ async () => await localDb.listCollections(fromDbId, [Query.limit(50)])
173
+ );
174
+ const allFromCollections = fromCollections.collections;
175
+ if (fromCollections.collections.length < 50) {
176
+ } else {
177
+ lastCollectionId =
178
+ fromCollections.collections[fromCollections.collections.length - 1].$id;
179
+ while (lastCollectionId) {
180
+ const collections = await tryAwaitWithRetry(
181
+ async () =>
182
+ await localDb.listCollections(fromDbId, [
183
+ Query.limit(50),
184
+ Query.cursorAfter(lastCollectionId!),
185
+ ])
186
+ );
187
+ allFromCollections.push(...collections.collections);
188
+ if (collections.collections.length < 50) {
189
+ break;
190
+ }
191
+ lastCollectionId =
192
+ collections.collections[collections.collections.length - 1].$id;
193
+ }
194
+ }
195
+
196
+ for (const collection of allFromCollections) {
197
+ const toCollection = await tryAwaitWithRetry(
198
+ async () =>
199
+ await remoteDb.createCollection(
200
+ toDbId,
201
+ collection.$id,
202
+ collection.name,
203
+ collection.$permissions,
204
+ collection.documentSecurity,
205
+ collection.enabled
206
+ )
207
+ );
208
+ console.log(`Collection ${toCollection.name} created`);
209
+
210
+ for (const attribute of collection.attributes) {
211
+ await tryAwaitWithRetry(
212
+ async () =>
213
+ await createOrUpdateAttribute(
214
+ remoteDb,
215
+ toDbId,
216
+ toCollection,
217
+ parseAttribute(attribute as any)
218
+ )
219
+ );
220
+ }
221
+
222
+ for (const index of collection.indexes) {
223
+ await tryAwaitWithRetry(
224
+ async () =>
225
+ await remoteDb.createIndex(
226
+ toDbId,
227
+ toCollection.$id,
228
+ index.key,
229
+ index.type,
230
+ index.attributes,
231
+ index.orders
232
+ )
233
+ );
234
+ }
235
+
236
+ await transferDocumentsBetweenDbsLocalToRemote(
237
+ localDb,
238
+ endpoint,
239
+ projectId,
240
+ apiKey,
241
+ fromDbId,
242
+ toDbId,
243
+ collection.$id,
244
+ toCollection.$id
245
+ );
246
+ }
247
+ };
@@ -26,6 +26,9 @@ import {
26
26
  OperationSchema,
27
27
  } from "./backup.js";
28
28
  import { DataLoader, type CollectionImportData } from "./dataLoader.js";
29
+ import { transferDocumentsBetweenDbsLocalToLocal } from "./collections.js";
30
+ import { transferDatabaseLocalToLocal } from "./databases.js";
31
+ import { transferStorageLocalToLocal } from "./storage.js";
29
32
 
30
33
  export class ImportController {
31
34
  private config: AppwriteConfig;
@@ -73,6 +76,7 @@ export class ImportController {
73
76
  )
74
77
  .map((db) => db.name);
75
78
  let dataLoader: DataLoader | undefined;
79
+ let databaseRan: ConfigDatabase | undefined;
76
80
  for (let db of this.config.databases) {
77
81
  if (
78
82
  db.name.toLowerCase().trim().replace(" ", "") === "migrations" ||
@@ -92,23 +96,48 @@ export class ImportController {
92
96
  console.log(`Starting import data for database: ${db.name}`);
93
97
  console.log(`---------------------------------`);
94
98
  // await this.importCollections(db);
95
- dataLoader = new DataLoader(
96
- this.appwriteFolderPath,
97
- this.importDataActions,
98
- this.database,
99
- this.config,
100
- this.setupOptions.shouldWriteFile
101
- );
102
- await dataLoader.start(db.$id);
103
- await this.importCollections(db, dataLoader);
104
- await resolveAndUpdateRelationships(db.$id, this.database, this.config);
105
- await this.executePostImportActions(db.$id, dataLoader);
99
+ if (!databaseRan) {
100
+ databaseRan = db;
101
+ dataLoader = new DataLoader(
102
+ this.appwriteFolderPath,
103
+ this.importDataActions,
104
+ this.database,
105
+ this.config,
106
+ this.setupOptions.shouldWriteFile
107
+ );
108
+ await dataLoader.start(db.$id);
109
+ await this.importCollections(db, dataLoader);
110
+ await resolveAndUpdateRelationships(db.$id, this.database, this.config);
111
+ await this.executePostImportActions(db.$id, dataLoader);
112
+ } else if (databaseRan.$id !== db.$id) {
113
+ await this.updateOthersToFinalData(databaseRan, db);
114
+ }
106
115
  console.log(`---------------------------------`);
107
116
  console.log(`Finished import data for database: ${db.name}`);
108
117
  console.log(`---------------------------------`);
109
118
  }
110
119
  }
111
120
 
121
+ async updateOthersToFinalData(
122
+ updatedDb: ConfigDatabase,
123
+ targetDb: ConfigDatabase
124
+ ) {
125
+ await transferDatabaseLocalToLocal(
126
+ this.database,
127
+ updatedDb.$id,
128
+ targetDb.$id
129
+ );
130
+ await transferStorageLocalToLocal(
131
+ this.storage,
132
+ `${this.config.documentBucketId}_${updatedDb.name
133
+ .toLowerCase()
134
+ .replace(" ", "")}`,
135
+ `${this.config.documentBucketId}_${targetDb.name
136
+ .toLowerCase()
137
+ .replace(" ", "")}`
138
+ );
139
+ }
140
+
112
141
  async importCollections(db: ConfigDatabase, dataLoader: DataLoader) {
113
142
  if (!this.config.collections) {
114
143
  return;
@@ -10,7 +10,10 @@ import {
10
10
  import { type OperationCreate, type BackupCreate } from "./backup.js";
11
11
  import { splitIntoBatches } from "./migrationHelper.js";
12
12
  import type { AppwriteConfig } from "appwrite-utils";
13
- import { tryAwaitWithRetry } from "../utils/helperFunctions.js";
13
+ import {
14
+ getAppwriteClient,
15
+ tryAwaitWithRetry,
16
+ } from "../utils/helperFunctions.js";
14
17
 
15
18
  export const logOperation = async (
16
19
  db: Databases,
@@ -372,3 +375,123 @@ export const backupDatabase = async (
372
375
  console.log("Database Backup Complete");
373
376
  console.log("---------------------------------");
374
377
  };
378
+
379
+ export const transferStorageLocalToLocal = async (
380
+ storage: Storage,
381
+ fromBucketId: string,
382
+ toBucketId: string
383
+ ) => {
384
+ console.log(`Transferring files from ${fromBucketId} to ${toBucketId}`);
385
+ let lastFileId: string | undefined;
386
+ let fromFiles = await tryAwaitWithRetry(
387
+ async () => await storage.listFiles(fromBucketId, [Query.limit(100)])
388
+ );
389
+ const allFromFiles = fromFiles.files;
390
+ let numberOfFiles = 0;
391
+ if (fromFiles.files.length < 100) {
392
+ for (const file of allFromFiles) {
393
+ const fileData = await storage.getFileDownload(file.bucketId, file.$id);
394
+ const fileToCreate = InputFile.fromBuffer(
395
+ Buffer.from(fileData),
396
+ file.name
397
+ );
398
+ console.log(`Creating file: ${file.name}`);
399
+ tryAwaitWithRetry(
400
+ async () =>
401
+ await storage.createFile(
402
+ toBucketId,
403
+ file.$id,
404
+ fileToCreate,
405
+ file.$permissions
406
+ )
407
+ );
408
+ numberOfFiles++;
409
+ }
410
+ } else {
411
+ lastFileId = fromFiles.files[fromFiles.files.length - 1].$id;
412
+ while (lastFileId) {
413
+ const files = await storage.listFiles(fromBucketId, [
414
+ Query.limit(100),
415
+ Query.cursorAfter(lastFileId),
416
+ ]);
417
+ allFromFiles.push(...files.files);
418
+ if (files.files.length < 100) {
419
+ lastFileId = undefined;
420
+ } else {
421
+ lastFileId = files.files[files.files.length - 1].$id;
422
+ }
423
+ }
424
+ for (const file of allFromFiles) {
425
+ const fileData = await storage.getFileDownload(file.bucketId, file.$id);
426
+ const fileToCreate = InputFile.fromBuffer(
427
+ Buffer.from(fileData),
428
+ file.name
429
+ );
430
+ await tryAwaitWithRetry(
431
+ async () =>
432
+ await storage.createFile(
433
+ toBucketId,
434
+ file.$id,
435
+ fileToCreate,
436
+ file.$permissions
437
+ )
438
+ );
439
+ numberOfFiles++;
440
+ }
441
+ }
442
+
443
+ console.log(
444
+ `Transferred ${numberOfFiles} files from ${fromBucketId} to ${toBucketId}`
445
+ );
446
+ };
447
+
448
+ export const transferStorageLocalToRemote = async (
449
+ localStorage: Storage,
450
+ endpoint: string,
451
+ projectId: string,
452
+ apiKey: string,
453
+ fromBucketId: string,
454
+ toBucketId: string
455
+ ) => {
456
+ console.log(
457
+ `Transferring files from current storage ${fromBucketId} to ${endpoint} bucket ${toBucketId}`
458
+ );
459
+ const client = getAppwriteClient(endpoint, apiKey, projectId);
460
+ const remoteStorage = new Storage(client);
461
+ let numberOfFiles = 0;
462
+ let lastFileId: string | undefined;
463
+ let fromFiles = await tryAwaitWithRetry(
464
+ async () => await localStorage.listFiles(fromBucketId, [Query.limit(100)])
465
+ );
466
+ const allFromFiles = fromFiles.files;
467
+ if (fromFiles.files.length === 100) {
468
+ lastFileId = fromFiles.files[fromFiles.files.length - 1].$id;
469
+ while (lastFileId) {
470
+ const files = await localStorage.listFiles(fromBucketId, [
471
+ Query.limit(100),
472
+ Query.cursorAfter(lastFileId),
473
+ ]);
474
+ allFromFiles.push(...files.files);
475
+ if (files.files.length < 100) {
476
+ break;
477
+ }
478
+ lastFileId = files.files[files.files.length - 1].$id;
479
+ }
480
+ }
481
+
482
+ for (const file of allFromFiles) {
483
+ await tryAwaitWithRetry(
484
+ async () =>
485
+ await remoteStorage.createFile(
486
+ toBucketId,
487
+ file.$id,
488
+ file,
489
+ file.$permissions
490
+ )
491
+ );
492
+ numberOfFiles++;
493
+ }
494
+ console.log(
495
+ `Transferred ${numberOfFiles} files from ${fromBucketId} to ${toBucketId}`
496
+ );
497
+ };