appwrite-utils-cli 0.0.46 → 0.0.48

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,69 @@ 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
+ oldId = item[importDef.updateMapping.originalIdField];
1211
+ if (oldId) {
1212
+ itemDataToUpdate = currentData?.data.find(
1213
+ ({ context, rawData, finalData }) => {
1214
+ const targetField =
1215
+ importDef.updateMapping!.targetField ??
1216
+ importDef.updateMapping!.originalIdField;
1217
+ return (
1218
+ `${context[targetField]}` === `${oldId}` ||
1219
+ `${rawData[targetField]}` === `${oldId}` ||
1220
+ `${finalData[targetField]}` === `${oldId}`
1221
+ );
1220
1222
  }
1223
+ );
1224
+
1225
+ if (itemDataToUpdate) {
1226
+ newId =
1227
+ itemDataToUpdate.finalData.docId ||
1228
+ itemDataToUpdate.context.docId;
1221
1229
  }
1222
1230
  }
1223
- } else {
1231
+ }
1232
+
1233
+ // If updateMapping is not defined or did not find the item, use primaryKeyField
1234
+ if (!itemDataToUpdate && importDef.primaryKeyField) {
1235
+ oldId = item[importDef.primaryKeyField];
1236
+ if (oldId) {
1237
+ newId = oldIdToNewIdMap?.get(`${oldId}`);
1238
+ if (
1239
+ !newId &&
1240
+ this.getCollectionKey(this.config.usersCollectionName) ===
1241
+ this.getCollectionKey(collection.name)
1242
+ ) {
1243
+ for (const [key, value] of this.mergedUserMap.entries()) {
1244
+ if (value.includes(`${oldId}`)) {
1245
+ newId = key;
1246
+ break;
1247
+ }
1248
+ }
1249
+ }
1250
+ }
1251
+
1252
+ if (oldId && !itemDataToUpdate) {
1253
+ itemDataToUpdate = currentData?.data.find(
1254
+ (data) =>
1255
+ `${data.context[importDef.primaryKeyField]}` === `${oldId}`
1256
+ );
1257
+ }
1258
+ }
1259
+
1260
+ if (!oldId) {
1224
1261
  logger.error(
1225
1262
  `No old ID found (to update another document with) in prepareUpdateData for ${
1226
1263
  collection.name
@@ -1228,12 +1265,7 @@ export class DataLoader {
1228
1265
  );
1229
1266
  continue;
1230
1267
  }
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
1268
+
1237
1269
  if (!newId && !itemDataToUpdate) {
1238
1270
  logger.error(
1239
1271
  `No new id found for collection ${
@@ -1260,6 +1292,7 @@ export class DataLoader {
1260
1292
  continue;
1261
1293
  }
1262
1294
  }
1295
+
1263
1296
  if (!itemDataToUpdate || !newId) {
1264
1297
  logger.error(
1265
1298
  `No data or ID (docId) found for collection ${
@@ -1272,49 +1305,51 @@ export class DataLoader {
1272
1305
  );
1273
1306
  continue;
1274
1307
  }
1308
+
1275
1309
  transformedData = this.mergeObjects(
1276
1310
  itemDataToUpdate.finalData,
1277
1311
  transformedData
1278
1312
  );
1313
+
1279
1314
  // Create a context object for the item, including the new ID and transformed data
1280
1315
  let context = this.createContext(db, collection, item, newId);
1281
- context = { ...context, ...transformedData };
1316
+ context = this.mergeObjects(context, transformedData);
1317
+
1282
1318
  // Validate the item before proceeding
1283
- const isValid = this.importDataActions.validateItem(
1319
+ const isValid = await this.importDataActions.validateItem(
1284
1320
  item,
1285
1321
  importDef.attributeMappings,
1286
1322
  context
1287
1323
  );
1288
- // Log info and continue to the next item if it's invalid
1324
+
1289
1325
  if (!isValid) {
1290
1326
  logger.info(
1291
1327
  `Skipping item: ${JSON.stringify(item, null, 2)} because it's invalid`
1292
1328
  );
1293
1329
  continue;
1294
1330
  }
1331
+
1295
1332
  // Update the attribute mappings with any actions that need to be performed post-import
1296
1333
  const mappingsWithActions = this.getAttributeMappingsWithActions(
1297
1334
  importDef.attributeMappings,
1298
1335
  context,
1299
1336
  transformedData
1300
1337
  );
1338
+
1301
1339
  // Update the import definition with the new attribute mappings
1302
1340
  const newImportDef = {
1303
1341
  ...importDef,
1304
1342
  attributeMappings: mappingsWithActions,
1305
1343
  };
1306
- // Add the item with its context and final data to the current collection data
1344
+
1307
1345
  if (itemDataToUpdate) {
1308
- // Update the existing item's finalData and context in place
1309
1346
  itemDataToUpdate.finalData = this.mergeObjects(
1310
1347
  itemDataToUpdate.finalData,
1311
1348
  transformedData
1312
1349
  );
1313
1350
  itemDataToUpdate.context = context;
1314
1351
  itemDataToUpdate.importDef = newImportDef;
1315
- currentData!.data.push(itemDataToUpdate);
1316
1352
  } else {
1317
- // If no existing item matches, then add the new item
1318
1353
  currentData!.data.push({
1319
1354
  rawData: item,
1320
1355
  context: context,
@@ -1322,6 +1357,7 @@ export class DataLoader {
1322
1357
  finalData: transformedData,
1323
1358
  });
1324
1359
  }
1360
+
1325
1361
  // Since we're modifying currentData in place, we ensure no duplicates are added
1326
1362
  this.importMap.set(this.getCollectionKey(collection.name), currentData!);
1327
1363
  }
@@ -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,7 +96,8 @@ 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
- if (!dataLoader) {
99
+ if (!databaseRan) {
100
+ databaseRan = db;
96
101
  dataLoader = new DataLoader(
97
102
  this.appwriteFolderPath,
98
103
  this.importDataActions,
@@ -101,18 +106,38 @@ export class ImportController {
101
106
  this.setupOptions.shouldWriteFile
102
107
  );
103
108
  await dataLoader.start(db.$id);
104
- } else {
105
- console.log(`Using data from previous import run`);
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);
106
114
  }
107
- await this.importCollections(db, dataLoader);
108
- await resolveAndUpdateRelationships(db.$id, this.database, this.config);
109
- await this.executePostImportActions(db.$id, dataLoader);
110
115
  console.log(`---------------------------------`);
111
116
  console.log(`Finished import data for database: ${db.name}`);
112
117
  console.log(`---------------------------------`);
113
118
  }
114
119
  }
115
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
+
116
141
  async importCollections(db: ConfigDatabase, dataLoader: DataLoader) {
117
142
  if (!this.config.collections) {
118
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
+ };