@memberjunction/storage 2.39.0 → 2.41.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.
Files changed (38) hide show
  1. package/dist/drivers/AWSFileStorage.d.ts +360 -4
  2. package/dist/drivers/AWSFileStorage.d.ts.map +1 -1
  3. package/dist/drivers/AWSFileStorage.js +357 -4
  4. package/dist/drivers/AWSFileStorage.js.map +1 -1
  5. package/dist/drivers/AzureFileStorage.d.ts +362 -2
  6. package/dist/drivers/AzureFileStorage.d.ts.map +1 -1
  7. package/dist/drivers/AzureFileStorage.js +357 -2
  8. package/dist/drivers/AzureFileStorage.js.map +1 -1
  9. package/dist/drivers/BoxFileStorage.d.ts +648 -20
  10. package/dist/drivers/BoxFileStorage.d.ts.map +1 -1
  11. package/dist/drivers/BoxFileStorage.js +951 -126
  12. package/dist/drivers/BoxFileStorage.js.map +1 -1
  13. package/dist/drivers/DropboxFileStorage.d.ts +437 -15
  14. package/dist/drivers/DropboxFileStorage.d.ts.map +1 -1
  15. package/dist/drivers/DropboxFileStorage.js +431 -15
  16. package/dist/drivers/DropboxFileStorage.js.map +1 -1
  17. package/dist/drivers/GoogleDriveFileStorage.d.ts +342 -16
  18. package/dist/drivers/GoogleDriveFileStorage.d.ts.map +1 -1
  19. package/dist/drivers/GoogleDriveFileStorage.js +340 -16
  20. package/dist/drivers/GoogleDriveFileStorage.js.map +1 -1
  21. package/dist/drivers/GoogleFileStorage.d.ts +358 -2
  22. package/dist/drivers/GoogleFileStorage.d.ts.map +1 -1
  23. package/dist/drivers/GoogleFileStorage.js +356 -2
  24. package/dist/drivers/GoogleFileStorage.js.map +1 -1
  25. package/dist/drivers/SharePointFileStorage.d.ts +434 -20
  26. package/dist/drivers/SharePointFileStorage.d.ts.map +1 -1
  27. package/dist/drivers/SharePointFileStorage.js +453 -22
  28. package/dist/drivers/SharePointFileStorage.js.map +1 -1
  29. package/dist/generic/FileStorageBase.d.ts +326 -108
  30. package/dist/generic/FileStorageBase.d.ts.map +1 -1
  31. package/dist/generic/FileStorageBase.js +54 -6
  32. package/dist/generic/FileStorageBase.js.map +1 -1
  33. package/dist/util.d.ts +125 -18
  34. package/dist/util.d.ts.map +1 -1
  35. package/dist/util.js +125 -18
  36. package/dist/util.js.map +1 -1
  37. package/package.json +8 -7
  38. package/readme.md +211 -1
@@ -31,18 +31,90 @@ var __importStar = (this && this.__importStar) || function (mod) {
31
31
  var __metadata = (this && this.__metadata) || function (k, v) {
32
32
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
33
33
  };
34
+ var __importDefault = (this && this.__importDefault) || function (mod) {
35
+ return (mod && mod.__esModule) ? mod : { "default": mod };
36
+ };
34
37
  Object.defineProperty(exports, "__esModule", { value: true });
35
38
  exports.BoxFileStorage = void 0;
36
39
  const global_1 = require("@memberjunction/global");
37
40
  const env = __importStar(require("env-var"));
38
41
  const mime = __importStar(require("mime-types"));
39
42
  const FileStorageBase_1 = require("../generic/FileStorageBase");
43
+ const box_node_sdk_1 = __importDefault(require("box-node-sdk"));
44
+ const https = __importStar(require("https"));
45
+ /**
46
+ * FileStorageBase implementation for Box.com cloud storage
47
+ *
48
+ * This provider allows working with files stored in Box.com. It supports
49
+ * authentication via access token, refresh token, or client credentials (JWT).
50
+ *
51
+ * @remarks
52
+ * This implementation requires at least one of the following authentication methods:
53
+ *
54
+ * 1. Access Token:
55
+ * - STORAGE_BOX_ACCESS_TOKEN - A valid Box API access token
56
+ *
57
+ * 2. Refresh Token:
58
+ * - STORAGE_BOX_REFRESH_TOKEN - A valid Box API refresh token
59
+ * - STORAGE_BOX_CLIENT_ID - Your Box application client ID
60
+ * - STORAGE_BOX_CLIENT_SECRET - Your Box application client secret
61
+ *
62
+ * 3. Client Credentials (JWT):
63
+ * - STORAGE_BOX_CLIENT_ID - Your Box application client ID
64
+ * - STORAGE_BOX_CLIENT_SECRET - Your Box application client secret
65
+ * - STORAGE_BOX_ENTERPRISE_ID - Your Box enterprise ID
66
+ *
67
+ * Optional configuration:
68
+ * - STORAGE_BOX_ROOT_FOLDER_ID - ID of a Box folder to use as the root (defaults to '0' which is the root)
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * // Set required environment variables for JWT auth
73
+ * process.env.STORAGE_BOX_CLIENT_ID = 'your-client-id';
74
+ * process.env.STORAGE_BOX_CLIENT_SECRET = 'your-client-secret';
75
+ * process.env.STORAGE_BOX_ENTERPRISE_ID = 'your-enterprise-id';
76
+ *
77
+ * // Create the provider
78
+ * const storage = new BoxFileStorage();
79
+ * await storage.initialize(); // Required for JWT auth
80
+ *
81
+ * // Upload a file
82
+ * const fileContent = Buffer.from('Hello, Box!');
83
+ * await storage.PutObject('documents/hello.txt', fileContent, 'text/plain');
84
+ *
85
+ * // Download a file
86
+ * const downloadedContent = await storage.GetObject('documents/hello.txt');
87
+ *
88
+ * // Get a temporary download URL
89
+ * const downloadUrl = await storage.CreatePreAuthDownloadUrl('documents/hello.txt');
90
+ * ```
91
+ */
40
92
  let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageBase {
93
+ /**
94
+ * Creates a new BoxFileStorage instance
95
+ *
96
+ * This constructor reads the required Box authentication configuration
97
+ * from environment variables.
98
+ *
99
+ * @throws Error if refresh token is provided without client ID and secret
100
+ */
41
101
  constructor() {
42
102
  super();
103
+ /**
104
+ * The name of this storage provider
105
+ */
43
106
  this.providerName = 'Box';
107
+ /**
108
+ * Timestamp when current access token expires
109
+ */
44
110
  this._tokenExpiresAt = 0;
111
+ /**
112
+ * Base URL for Box API
113
+ */
45
114
  this._baseApiUrl = 'https://api.box.com/2.0';
115
+ /**
116
+ * Base URL for Box Upload API
117
+ */
46
118
  this._uploadApiUrl = 'https://upload.box.com/api/2.0';
47
119
  // Box auth can be via access token or refresh token
48
120
  this._accessToken = env.get('STORAGE_BOX_ACCESS_TOKEN').asString();
@@ -57,14 +129,53 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
57
129
  this._rootFolderId = env.get('STORAGE_BOX_ROOT_FOLDER_ID').default('0').asString();
58
130
  }
59
131
  /**
60
- * Initialize the storage driver by setting up the access token
61
- * This should be called after construction
132
+ * Initializes the Box storage driver
133
+ *
134
+ * This method must be called after creating a BoxFileStorage instance
135
+ * when using client credentials (JWT) authentication. It obtains the
136
+ * initial access token required for API calls.
137
+ *
138
+ * @returns A Promise that resolves when initialization is complete
139
+ *
140
+ * @example
141
+ * ```typescript
142
+ * const storage = new BoxFileStorage();
143
+ * await storage.initialize();
144
+ * // Now the storage provider is ready to use
145
+ * ```
62
146
  */
63
147
  async initialize() {
64
148
  if (!this._accessToken && this._clientId && this._clientSecret && this._enterpriseId) {
65
149
  await this._setAccessToken();
66
150
  }
151
+ await this._setClient();
152
+ }
153
+ /**
154
+ * Initializes the Box client instance
155
+ *
156
+ * This method creates a new Box SDK client using the current credentials
157
+ * and access token, making it ready for API operations.
158
+ *
159
+ * @private
160
+ * @returns A Promise that resolves when the client is initialized
161
+ */
162
+ async _setClient() {
163
+ const sdk = await new box_node_sdk_1.default({
164
+ clientID: this._clientId,
165
+ clientSecret: this._clientSecret
166
+ });
167
+ this._client = await sdk.getBasicClient(this._accessToken);
67
168
  }
169
+ /**
170
+ * Obtains an access token using client credentials flow
171
+ *
172
+ * This method requests a new access token using the Box client credentials
173
+ * flow (JWT) with the enterprise as the subject.
174
+ *
175
+ * @private
176
+ * @returns A Promise that resolves when the access token is obtained
177
+ * @throws Error if token acquisition fails
178
+ */
68
179
  async _setAccessToken() {
69
180
  try {
70
181
  const response = await fetch('https://api.box.com/oauth2/token', {
@@ -94,7 +205,55 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
94
205
  }
95
206
  }
96
207
  /**
97
- * Ensures we have a valid access token
208
+ * Returns the current Box API access token
209
+ *
210
+ * This method ensures a valid token is available before returning it,
211
+ * refreshing or generating a new token if necessary.
212
+ *
213
+ * @returns A Promise that resolves to a valid access token string
214
+ *
215
+ * @example
216
+ * ```typescript
217
+ * // Get a valid Box access token
218
+ * const token = await storage.AccessToken();
219
+ * console.log(`Using access token: ${token}`);
220
+ * ```
221
+ */
222
+ async AccessToken() {
223
+ await this._ensureValidToken();
224
+ return this._accessToken;
225
+ }
226
+ /**
227
+ * Returns the current Box client instance
228
+ *
229
+ * This method ensures a valid token is available before returning the client,
230
+ * refreshing or generating a new token if necessary.
231
+ *
232
+ * @returns A Promise that resolves to the authenticated Box client instance
233
+ *
234
+ * @example
235
+ * ```typescript
236
+ * // Get the Box client for direct API access
237
+ * const client = await storage.BoxClient();
238
+ *
239
+ * // Use the client for Box SDK operations
240
+ * const user = await client.users.get('me');
241
+ * console.log(`Current user: ${user.name}`);
242
+ * ```
243
+ */
244
+ async BoxClient() {
245
+ await this._ensureValidToken();
246
+ return this._client;
247
+ }
248
+ /**
249
+ * Ensures a valid access token is available for API requests
250
+ *
251
+ * This method checks if the current token is valid, and if not, attempts
252
+ * to refresh or obtain a new token using the configured authentication method.
253
+ *
254
+ * @private
255
+ * @returns A Promise that resolves to a valid access token
256
+ * @throws Error if no valid token can be obtained
98
257
  */
99
258
  async _ensureValidToken() {
100
259
  // If we have a valid token, use it
@@ -123,6 +282,7 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
123
282
  this._accessToken = data.access_token;
124
283
  this._refreshToken = data.refresh_token || this._refreshToken;
125
284
  this._tokenExpiresAt = Date.now() + (data.expires_in * 1000) - 60000; // Subtract 1 minute for safety
285
+ await this._setClient();
126
286
  return this._accessToken;
127
287
  }
128
288
  catch (error) {
@@ -134,6 +294,7 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
134
294
  if (this._clientId && this._clientSecret && this._enterpriseId) {
135
295
  try {
136
296
  await this._setAccessToken();
297
+ await this._setClient();
137
298
  return this._accessToken;
138
299
  }
139
300
  catch (error) {
@@ -149,7 +310,19 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
149
310
  throw new Error('No valid Box access token available and no authentication method configured');
150
311
  }
151
312
  /**
152
- * Make an authenticated API request to Box
313
+ * Makes an authenticated API request to the Box API
314
+ *
315
+ * This helper method handles authentication, request formatting, and
316
+ * response parsing for all Box API calls.
317
+ *
318
+ * @private
319
+ * @param endpoint - The API endpoint to call (e.g., '/files/123')
320
+ * @param method - The HTTP method to use (default: 'GET')
321
+ * @param body - Optional request body (will be serialized as JSON unless it's FormData)
322
+ * @param headers - Optional additional headers
323
+ * @param baseUrl - Base URL to use (defaults to standard API URL)
324
+ * @returns A Promise that resolves to the API response data
325
+ * @throws Error if the API request fails
153
326
  */
154
327
  async _apiRequest(endpoint, method = 'GET', body, headers = {}, baseUrl = this._baseApiUrl) {
155
328
  const token = await this._ensureValidToken();
@@ -182,7 +355,14 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
182
355
  return data;
183
356
  }
184
357
  /**
185
- * Parse a Box API path into folder ID and name components
358
+ * Parses a path string into Box API components
359
+ *
360
+ * This helper method converts a standard path string (e.g., 'documents/reports/file.txt')
361
+ * into components used by the Box API (folder ID, name, parent path).
362
+ *
363
+ * @private
364
+ * @param path - The path to parse
365
+ * @returns An object containing the parsed components: id, name, and parent
186
366
  */
187
367
  _parsePath(path) {
188
368
  // Default to root folder
@@ -206,35 +386,73 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
206
386
  return { id: '', name, parent };
207
387
  }
208
388
  /**
209
- * Get item ID by path
389
+ * Resolves a path string to a Box item ID
390
+ *
391
+ * This helper method navigates the Box folder hierarchy to find
392
+ * the item at the specified path, returning its Box ID.
393
+ *
394
+ * @private
395
+ * @param path - The path to resolve
396
+ * @returns A Promise that resolves to the Box item ID
397
+ * @throws Error if the item does not exist
210
398
  */
211
399
  async _getIdFromPath(path) {
212
- const parsedPath = this._parsePath(path);
213
- // If we already have the ID, return it
214
- if (parsedPath.id) {
215
- return parsedPath.id;
216
- }
217
- // If it's the root, return the root folder ID
218
- if (!parsedPath.name) {
219
- return this._rootFolderId;
220
- }
221
- // Get the parent folder ID
222
- let parentId = this._rootFolderId;
223
- if (parsedPath.parent) {
224
- parentId = await this._getIdFromPath(parsedPath.parent);
225
- }
226
- // Search for the item in the parent folder
227
- const result = await this._apiRequest(`/folders/${parentId}/items`, 'GET', null, {
228
- 'fields': 'id,name,type'
229
- });
230
- const item = result.entries.find(entry => entry.name === parsedPath.name);
231
- if (!item) {
232
- throw new Error(`Item not found: ${path}`);
400
+ try {
401
+ // Parse the path
402
+ const parsedPath = this._parsePath(path);
403
+ // If the id is already in the path, return it
404
+ if (parsedPath.id) {
405
+ return parsedPath.id;
406
+ }
407
+ // If it's root, return root folder id
408
+ if (!parsedPath.name) {
409
+ return this._rootFolderId;
410
+ }
411
+ // First, find the parent folder ID
412
+ let parentFolderId = this._rootFolderId;
413
+ if (parsedPath.parent) {
414
+ parentFolderId = await this._findFolderIdByPath(parsedPath.parent);
415
+ }
416
+ // Search for the item with pagination support
417
+ let offset = 0;
418
+ let hasMoreItems = true;
419
+ const LIMIT = 1000;
420
+ while (hasMoreItems) {
421
+ await this._ensureValidToken();
422
+ const items = await this._client.folders.getItems(parentFolderId, {
423
+ fields: 'name,type,id',
424
+ limit: LIMIT,
425
+ offset: offset
426
+ });
427
+ // Look for the item by name
428
+ const item = items.entries.find((i) => i.name === parsedPath.name);
429
+ if (item) {
430
+ return item.id;
431
+ }
432
+ // Update pagination variables
433
+ offset += items.entries.length;
434
+ // Check if we've processed all items
435
+ hasMoreItems = items.entries.length === LIMIT && offset < items.total_count;
436
+ }
437
+ // If we get here, the item was not found
438
+ console.log(`Item not found: ${parsedPath.name}`);
439
+ return null;
440
+ }
441
+ catch (error) {
442
+ console.error('Error in _getIdFromPath', { path, error });
443
+ return null;
233
444
  }
234
- return item.id;
235
445
  }
236
446
  /**
237
- * Convert Box item to StorageObjectMetadata
447
+ * Converts a Box API item to StorageObjectMetadata
448
+ *
449
+ * This helper method transforms a Box API item representation into
450
+ * the standard StorageObjectMetadata format used by FileStorageBase.
451
+ *
452
+ * @private
453
+ * @param item - The Box API item object
454
+ * @param parentPath - The parent path string
455
+ * @returns A StorageObjectMetadata object
238
456
  */
239
457
  _convertToMetadata(item, parentPath = '') {
240
458
  const isDirectory = item.type === 'folder';
@@ -260,7 +478,42 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
260
478
  };
261
479
  }
262
480
  /**
263
- * Create a pre-authenticated upload URL
481
+ * Creates a pre-authenticated upload URL for a file
482
+ *
483
+ * This method creates a Box upload session and returns a URL that can be used
484
+ * to upload file content directly to Box without requiring authentication.
485
+ *
486
+ * @param objectName - Path where the file should be uploaded (e.g., 'documents/report.pdf')
487
+ * @returns A Promise that resolves to an object containing the upload URL and provider key
488
+ * @throws Error if the URL creation fails
489
+ *
490
+ * @remarks
491
+ * - The parent folder structure will be created automatically if it doesn't exist
492
+ * - The returned provider key contains the session ID needed to complete the upload
493
+ * - Box upload sessions expire after a certain period (typically 1 hour)
494
+ *
495
+ * @example
496
+ * ```typescript
497
+ * try {
498
+ * // Generate a pre-authenticated upload URL
499
+ * const uploadInfo = await storage.CreatePreAuthUploadUrl('presentations/quarterly-results.pptx');
500
+ *
501
+ * // The URL can be used to upload content directly
502
+ * console.log(`Upload URL: ${uploadInfo.UploadUrl}`);
503
+ *
504
+ * // Make sure to save the provider key, as it's needed to reference the upload
505
+ * console.log(`Provider Key: ${uploadInfo.ProviderKey}`);
506
+ *
507
+ * // You can use fetch or another HTTP client to upload to this URL
508
+ * await fetch(uploadInfo.UploadUrl, {
509
+ * method: 'PUT',
510
+ * headers: { 'Content-Type': 'application/octet-stream' },
511
+ * body: fileContent
512
+ * });
513
+ * } catch (error) {
514
+ * console.error('Error creating upload URL:', error.message);
515
+ * }
516
+ * ```
264
517
  */
265
518
  async CreatePreAuthUploadUrl(objectName) {
266
519
  try {
@@ -297,7 +550,34 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
297
550
  }
298
551
  }
299
552
  /**
300
- * Create a pre-authenticated download URL
553
+ * Creates a pre-authenticated download URL for a file
554
+ *
555
+ * This method generates a time-limited URL that can be used to download
556
+ * a file without authentication. The URL typically expires after 60 minutes.
557
+ *
558
+ * @param objectName - Path to the file to download (e.g., 'documents/report.pdf')
559
+ * @returns A Promise that resolves to the download URL string
560
+ * @throws Error if the file doesn't exist or URL creation fails
561
+ *
562
+ * @remarks
563
+ * - Cannot be used with upload sessions that haven't been completed
564
+ * - Box download URLs typically expire after 60 minutes
565
+ * - Generated URLs can be shared with users who don't have Box access
566
+ *
567
+ * @example
568
+ * ```typescript
569
+ * try {
570
+ * // Generate a pre-authenticated download URL
571
+ * const downloadUrl = await storage.CreatePreAuthDownloadUrl('documents/financial-report.pdf');
572
+ *
573
+ * console.log(`Download the file using this URL: ${downloadUrl}`);
574
+ *
575
+ * // The URL can be shared or used in a browser to download the file
576
+ * // without requiring Box authentication
577
+ * } catch (error) {
578
+ * console.error('Error creating download URL:', error.message);
579
+ * }
580
+ * ```
301
581
  */
302
582
  async CreatePreAuthDownloadUrl(objectName) {
303
583
  try {
@@ -320,7 +600,34 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
320
600
  }
321
601
  }
322
602
  /**
323
- * Move an object
603
+ * Moves a file or folder from one location to another
604
+ *
605
+ * This method moves a file or folder to a new location in Box storage.
606
+ * It handles both renaming and changing the parent folder.
607
+ *
608
+ * @param oldObjectName - Current path of the object (e.g., 'old-folder/document.docx')
609
+ * @param newObjectName - New path for the object (e.g., 'new-folder/renamed-document.docx')
610
+ * @returns A Promise that resolves to true if successful, false otherwise
611
+ *
612
+ * @remarks
613
+ * - Parent folders will be created automatically if they don't exist
614
+ * - Works with both files and folders
615
+ * - For folders, all contents will move with the folder
616
+ *
617
+ * @example
618
+ * ```typescript
619
+ * // Move a file to a different folder and rename it
620
+ * const moveResult = await storage.MoveObject(
621
+ * 'documents/old-report.pdf',
622
+ * 'archive/2023/annual-report.pdf'
623
+ * );
624
+ *
625
+ * if (moveResult) {
626
+ * console.log('File moved successfully');
627
+ * } else {
628
+ * console.error('Failed to move file');
629
+ * }
630
+ * ```
324
631
  */
325
632
  async MoveObject(oldObjectName, newObjectName) {
326
633
  try {
@@ -354,7 +661,34 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
354
661
  }
355
662
  }
356
663
  /**
357
- * Delete an object
664
+ * Deletes a file or folder from Box storage
665
+ *
666
+ * This method permanently deletes a file or folder. It can also
667
+ * handle special cases like incomplete upload sessions.
668
+ *
669
+ * @param objectName - Path to the object to delete (e.g., 'documents/old-report.docx')
670
+ * @returns A Promise that resolves to true if successful, false if an error occurs
671
+ *
672
+ * @remarks
673
+ * - Returns true if the object doesn't exist (for idempotency)
674
+ * - Can handle special provider keys like upload sessions
675
+ * - Box puts deleted items in the trash, where they can be recovered for a limited time
676
+ * - To permanently delete folder contents, use DeleteDirectory with recursive=true
677
+ *
678
+ * @example
679
+ * ```typescript
680
+ * // Delete a file
681
+ * const deleteResult = await storage.DeleteObject('temp/draft-document.docx');
682
+ *
683
+ * if (deleteResult) {
684
+ * console.log('File deleted successfully or already didn\'t exist');
685
+ * } else {
686
+ * console.error('Failed to delete file');
687
+ * }
688
+ *
689
+ * // Delete an upload session
690
+ * await storage.DeleteObject('session:1234567890:documents/large-file.zip');
691
+ * ```
358
692
  */
359
693
  async DeleteObject(objectName) {
360
694
  try {
@@ -381,7 +715,38 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
381
715
  }
382
716
  }
383
717
  /**
384
- * List objects in a directory
718
+ * Lists files and folders in a given directory
719
+ *
720
+ * This method retrieves all files and subfolders in the specified directory.
721
+ * It returns both a list of object metadata and a list of directory prefixes.
722
+ *
723
+ * @param prefix - Path to the directory to list (e.g., 'documents/reports')
724
+ * @param delimiter - Optional delimiter character (default: '/')
725
+ * @returns A Promise that resolves to a StorageListResult containing objects and prefixes
726
+ *
727
+ * @remarks
728
+ * - The `objects` array includes both files and folders
729
+ * - The `prefixes` array includes only folder paths (with trailing slashes)
730
+ * - Returns empty arrays if the directory doesn't exist
731
+ * - The delimiter parameter is included for interface compatibility but not used internally
732
+ *
733
+ * @example
734
+ * ```typescript
735
+ * // List all files and folders in the 'documents' directory
736
+ * const result = await storage.ListObjects('documents');
737
+ *
738
+ * // Process files and folders
739
+ * console.log(`Found ${result.objects.length} items:`);
740
+ * for (const obj of result.objects) {
741
+ * console.log(`- ${obj.name} (${obj.isDirectory ? 'Folder' : 'File'}, ${obj.size} bytes)`);
742
+ * }
743
+ *
744
+ * // List subfolders only
745
+ * console.log(`Found ${result.prefixes.length} subfolders:`);
746
+ * for (const prefix of result.prefixes) {
747
+ * console.log(`- ${prefix}`);
748
+ * }
749
+ * ```
385
750
  */
386
751
  async ListObjects(prefix, delimiter = '/') {
387
752
  try {
@@ -418,49 +783,228 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
418
783
  }
419
784
  }
420
785
  /**
421
- * Create a directory
786
+ * Creates a new directory (folder) in Box storage
787
+ *
788
+ * This method creates a folder at the specified path, automatically
789
+ * creating any parent folders that don't exist.
790
+ *
791
+ * @param directoryPath - Path where the directory should be created (e.g., 'documents/reports/2023')
792
+ * @returns A Promise that resolves to true if successful, false if an error occurs
793
+ *
794
+ * @remarks
795
+ * - Creates parent directories recursively if they don't exist
796
+ * - Returns true if the directory already exists (idempotent operation)
797
+ * - Trailing slashes in the path are automatically removed
798
+ *
799
+ * @example
800
+ * ```typescript
801
+ * // Create a nested directory structure
802
+ * const createResult = await storage.CreateDirectory('documents/reports/2023/Q1');
803
+ *
804
+ * if (createResult) {
805
+ * console.log('Directory created successfully');
806
+ *
807
+ * // Now we can put files in this directory
808
+ * await storage.PutObject(
809
+ * 'documents/reports/2023/Q1/financial-summary.xlsx',
810
+ * fileContent,
811
+ * 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
812
+ * );
813
+ * } else {
814
+ * console.error('Failed to create directory');
815
+ * }
816
+ * ```
422
817
  */
423
818
  async CreateDirectory(directoryPath) {
424
819
  try {
820
+ // Root directory always exists
821
+ if (!directoryPath || directoryPath === '/' || directoryPath === '') {
822
+ return true;
823
+ }
425
824
  // Remove trailing slash if present
426
825
  const normalizedPath = directoryPath.endsWith('/')
427
826
  ? directoryPath.substring(0, directoryPath.length - 1)
428
827
  : directoryPath;
429
- // Check if directory already exists
828
+ // First check if directory already exists
430
829
  try {
431
- await this._getIdFromPath(normalizedPath);
432
- return true; // Directory already exists
830
+ if (await this.DirectoryExists(normalizedPath)) {
831
+ return true;
832
+ }
433
833
  }
434
834
  catch (error) {
435
- // Directory doesn't exist, create it
835
+ // Ignore error, we'll try to create it anyway
436
836
  }
437
- // Get parent folder info
438
- const parsedPath = this._parsePath(normalizedPath);
439
- let parentId = this._rootFolderId;
440
- if (parsedPath.parent) {
837
+ // Parse the path to get parent folder and name
838
+ const lastSlashIndex = normalizedPath.lastIndexOf('/');
839
+ const parentPath = lastSlashIndex > 0 ? normalizedPath.substring(0, lastSlashIndex) : '';
840
+ const folderName = lastSlashIndex > 0 ? normalizedPath.substring(lastSlashIndex + 1) : normalizedPath;
841
+ // Make sure parent folder exists
842
+ let parentFolderId = '0'; // Default to root
843
+ if (parentPath) {
441
844
  try {
442
- parentId = await this._getIdFromPath(parsedPath.parent);
845
+ // Recursive call to ensure parent directory exists
846
+ const parentDirExists = await this.CreateDirectory(parentPath);
847
+ if (!parentDirExists) {
848
+ console.log(`Failed to create parent directory: ${parentPath}`);
849
+ return false;
850
+ }
851
+ // Get the parent folder ID
852
+ parentFolderId = await this._findFolderIdByPath(parentPath);
443
853
  }
444
854
  catch (error) {
445
- // Create parent directory recursively
446
- await this.CreateDirectory(parsedPath.parent);
447
- parentId = await this._getIdFromPath(parsedPath.parent);
855
+ console.error(`Error ensuring parent folder exists: ${error.message}`);
856
+ return false;
448
857
  }
449
858
  }
450
859
  // Create the folder
451
- await this._apiRequest('/folders', 'POST', {
452
- name: parsedPath.name,
453
- parent: { id: parentId }
454
- });
455
- return true;
860
+ try {
861
+ await this._ensureValidToken();
862
+ await this._client.folders.create(parentFolderId, folderName);
863
+ console.log(`✅ Folder created successfully: ${normalizedPath}`);
864
+ return true;
865
+ }
866
+ catch (error) {
867
+ // Handle conflicts - if the folder already exists, that's a success
868
+ if (error.statusCode === 409 ||
869
+ (error.message && error.message.includes('item_name_in_use'))) {
870
+ console.log(`Folder already exists (conflict): ${normalizedPath}`);
871
+ return true;
872
+ }
873
+ console.error(`Error creating folder: ${error.message || JSON.stringify(error)}`);
874
+ return false;
875
+ }
456
876
  }
457
877
  catch (error) {
458
878
  console.error('Error creating directory', { directoryPath, error });
459
879
  return false;
460
880
  }
461
881
  }
882
+ ;
883
+ /**
884
+ * Gets file representation information for a Box file
885
+ *
886
+ * This method retrieves information about available representations
887
+ * (such as thumbnails, previews, or other formats) for a specific file.
888
+ *
889
+ * @param fileId - The Box file ID to get representations for
890
+ * @param repHints - The representation hints string (format and options)
891
+ * @returns A Promise that resolves to a JSON object containing representations data
892
+ * @throws Error if the request fails
893
+ *
894
+ * @remarks
895
+ * - Requires a valid file ID (not a path)
896
+ * - The repHints parameter controls what type of representations are returned
897
+ * - Common representation types include thumbnails, preview images, and text extractions
898
+ *
899
+ * @example
900
+ * ```typescript
901
+ * try {
902
+ * // Get a high-resolution PNG representation of a file
903
+ * const fileId = '12345';
904
+ * const representations = await storage.GetFileRepresentations(
905
+ * fileId,
906
+ * 'png?dimensions=2048x2048'
907
+ * );
908
+ *
909
+ * // Process the representation information
910
+ * console.log('Available representations:', representations);
911
+ * } catch (error) {
912
+ * console.error('Error getting representations:', error.message);
913
+ * }
914
+ * ```
915
+ */
916
+ async GetFileRepresentations(fileId, repHints = 'png?dimensions=2048x2048') {
917
+ try {
918
+ await this._ensureValidToken();
919
+ // Set up the request options with X-Rep-Hints header
920
+ const options = {
921
+ hostname: 'api.box.com',
922
+ port: 443,
923
+ path: `/2.0/files/${fileId}?fields=representations`,
924
+ method: 'GET',
925
+ headers: {
926
+ 'Authorization': `Bearer ${this._accessToken}`,
927
+ 'Content-Type': 'application/json',
928
+ 'X-Rep-Hints': repHints
929
+ }
930
+ };
931
+ // Make the request using our existing makeRequest function
932
+ const responseData = await this._makeRequest(options);
933
+ return JSON.parse(responseData);
934
+ }
935
+ catch (error) {
936
+ console.error('Error getting file representations:', error);
937
+ throw error;
938
+ }
939
+ }
940
+ ;
462
941
  /**
463
- * Delete a directory
942
+ * Helper function for making HTTP requests
943
+ *
944
+ * This method provides a Promise-based wrapper around Node.js https requests,
945
+ * simplifying the process of making API calls to the Box API.
946
+ *
947
+ * @private
948
+ * @param options - The HTTPS request options (URL, method, headers, etc.)
949
+ * @param data - Optional string data to send with the request
950
+ * @returns A Promise that resolves to the response data as a string
951
+ * @throws Error if the request fails or returns a non-2xx status code
952
+ */
953
+ async _makeRequest(options, data) {
954
+ return new Promise((resolve, reject) => {
955
+ const req = https.request(options, (res) => {
956
+ let responseData = '';
957
+ res.on('data', (chunk) => {
958
+ responseData += chunk;
959
+ });
960
+ res.on('end', () => {
961
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
962
+ resolve(responseData);
963
+ }
964
+ else {
965
+ reject(new Error(`Request failed with status code ${res.statusCode}: ${responseData}`));
966
+ }
967
+ });
968
+ });
969
+ req.on('error', (error) => {
970
+ reject(error);
971
+ });
972
+ if (data) {
973
+ req.write(data);
974
+ }
975
+ req.end();
976
+ });
977
+ }
978
+ /**
979
+ * Deletes a directory from Box storage
980
+ *
981
+ * This method deletes a folder and optionally its contents. By default,
982
+ * it will only delete empty folders unless recursive is set to true.
983
+ *
984
+ * @param directoryPath - Path to the directory to delete (e.g., 'documents/old-reports')
985
+ * @param recursive - If true, delete the directory and all its contents; if false, only delete if empty
986
+ * @returns A Promise that resolves to true if successful, false if an error occurs
987
+ *
988
+ * @remarks
989
+ * - Returns true if the directory doesn't exist (idempotent operation)
990
+ * - If recursive=false and the directory contains files, the operation will fail
991
+ * - Box puts deleted folders in the trash, where they can be recovered for a limited time
992
+ * - Trailing slashes in the path are automatically removed
993
+ *
994
+ * @example
995
+ * ```typescript
996
+ * // Try to delete an empty folder
997
+ * const deleteResult = await storage.DeleteDirectory('temp/empty-folder');
998
+ *
999
+ * // Delete a folder and all its contents
1000
+ * const recursiveDeleteResult = await storage.DeleteDirectory('archive/old-data', true);
1001
+ *
1002
+ * if (recursiveDeleteResult) {
1003
+ * console.log('Folder and all its contents deleted successfully');
1004
+ * } else {
1005
+ * console.error('Failed to delete folder');
1006
+ * }
1007
+ * ```
464
1008
  */
465
1009
  async DeleteDirectory(directoryPath, recursive = false) {
466
1010
  try {
@@ -502,7 +1046,34 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
502
1046
  }
503
1047
  }
504
1048
  /**
505
- * Get object metadata
1049
+ * Gets metadata for a file or folder
1050
+ *
1051
+ * This method retrieves metadata about a file or folder in Box storage,
1052
+ * such as size, type, and modification date.
1053
+ *
1054
+ * @param objectName - Path to the object to get metadata for (e.g., 'documents/report.pdf')
1055
+ * @returns A Promise that resolves to a StorageObjectMetadata object
1056
+ * @throws Error if the object doesn't exist or cannot be accessed
1057
+ *
1058
+ * @example
1059
+ * ```typescript
1060
+ * try {
1061
+ * // Get metadata for a file
1062
+ * const metadata = await storage.GetObjectMetadata('presentations/quarterly-update.pptx');
1063
+ *
1064
+ * console.log(`Name: ${metadata.name}`);
1065
+ * console.log(`Path: ${metadata.path}`);
1066
+ * console.log(`Size: ${metadata.size} bytes`);
1067
+ * console.log(`Content Type: ${metadata.contentType}`);
1068
+ * console.log(`Last Modified: ${metadata.lastModified}`);
1069
+ * console.log(`Is Directory: ${metadata.isDirectory}`);
1070
+ *
1071
+ * // Box-specific metadata is available in customMetadata
1072
+ * console.log(`Box ID: ${metadata.customMetadata.id}`);
1073
+ * } catch (error) {
1074
+ * console.error('Error getting metadata:', error.message);
1075
+ * }
1076
+ * ```
506
1077
  */
507
1078
  async GetObjectMetadata(objectName) {
508
1079
  try {
@@ -524,108 +1095,235 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
524
1095
  }
525
1096
  }
526
1097
  /**
527
- * Get an object's contents
1098
+ * Downloads a file's contents
1099
+ *
1100
+ * This method retrieves the raw content of a file as a Buffer.
1101
+ *
1102
+ * @param objectName - Path to the file to download (e.g., 'documents/report.pdf')
1103
+ * @returns A Promise that resolves to a Buffer containing the file's contents
1104
+ * @throws Error if the file doesn't exist or cannot be downloaded
1105
+ *
1106
+ * @remarks
1107
+ * - This method will throw an error if the object is a folder
1108
+ * - For large files, consider using CreatePreAuthDownloadUrl instead
1109
+ * - For upload sessions that haven't been completed, this method will fail
1110
+ *
1111
+ * @example
1112
+ * ```typescript
1113
+ * try {
1114
+ * // Download a text file
1115
+ * const fileContent = await storage.GetObject('documents/notes.txt');
1116
+ *
1117
+ * // Convert Buffer to string for text files
1118
+ * const textContent = fileContent.toString('utf8');
1119
+ * console.log('File content:', textContent);
1120
+ *
1121
+ * // For binary files, you can write the buffer to disk
1122
+ * // or process it as needed
1123
+ * } catch (error) {
1124
+ * console.error('Error downloading file:', error.message);
1125
+ * }
1126
+ * ```
528
1127
  */
529
1128
  async GetObject(objectName) {
530
1129
  try {
531
- const fileId = await this._getIdFromPath(objectName);
532
- // Download the file
533
- const response = await this._apiRequest(`/files/${fileId}/content`, 'GET');
534
- // Convert response to buffer
535
- const arrayBuffer = await response.arrayBuffer();
536
- return Buffer.from(arrayBuffer);
1130
+ // Extract directory path and filename
1131
+ const lastSlashIndex = objectName.lastIndexOf('/');
1132
+ if (lastSlashIndex === -1) {
1133
+ throw new Error('Invalid path format, expected directory/filename');
1134
+ }
1135
+ const directoryPath = objectName.substring(0, lastSlashIndex);
1136
+ const fileName = objectName.substring(lastSlashIndex + 1);
1137
+ // Find folder ID for the directory using the approach from check_file.ts
1138
+ try {
1139
+ const folderId = await this._findFolderIdByPath(directoryPath);
1140
+ // Use pagination to handle large folders
1141
+ let file = null;
1142
+ let offset = 0;
1143
+ let hasMoreItems = true;
1144
+ const LIMIT = 1000;
1145
+ while (hasMoreItems && !file) {
1146
+ await this._ensureValidToken();
1147
+ const folderItems = await this._client.folders.getItems(folderId, {
1148
+ fields: 'name,type,size,created_at,modified_at',
1149
+ limit: LIMIT,
1150
+ offset: offset
1151
+ });
1152
+ // Look for the file
1153
+ file = folderItems.entries.find((item) => item.type === 'file' && item.name === fileName);
1154
+ // If file is found, break out of pagination loop
1155
+ if (file) {
1156
+ break;
1157
+ }
1158
+ // Update pagination variables
1159
+ offset += folderItems.entries.length;
1160
+ // Check if we've processed all items
1161
+ hasMoreItems = folderItems.entries.length === LIMIT && offset < folderItems.total_count;
1162
+ }
1163
+ if (file) {
1164
+ console.log(`✅ File found: ${file.name} (${file.id})`);
1165
+ // Use the file ID to get the content
1166
+ await this._ensureValidToken();
1167
+ const stream = await this._client.files.getReadStream(file.id);
1168
+ // Convert stream to buffer
1169
+ return new Promise((resolve, reject) => {
1170
+ const chunks = [];
1171
+ stream.on('data', (chunk) => chunks.push(chunk));
1172
+ stream.on('error', reject);
1173
+ stream.on('end', () => resolve(Buffer.concat(chunks)));
1174
+ });
1175
+ }
1176
+ else {
1177
+ console.log(`❌ File not found in directory`);
1178
+ throw new Error(`File not found: ${fileName}`);
1179
+ }
1180
+ }
1181
+ catch (error) {
1182
+ console.log(`❌ Error finding file: ${error}`);
1183
+ throw error;
1184
+ }
537
1185
  }
538
1186
  catch (error) {
539
1187
  console.error('Error getting object', { objectName, error });
540
1188
  throw new Error(`Failed to get object: ${objectName}`);
541
1189
  }
542
1190
  }
1191
+ ;
543
1192
  /**
544
- * Upload an object
1193
+ * Uploads a file to Box storage
1194
+ *
1195
+ * This method uploads a file to the specified path in Box storage. It automatically
1196
+ * determines whether to use a simple upload or chunked upload based on file size.
1197
+ *
1198
+ * @param objectName - Path where the file should be uploaded (e.g., 'documents/report.pdf')
1199
+ * @param data - Buffer containing the file content
1200
+ * @param contentType - Optional MIME type of the file (if not provided, it will be guessed from the filename)
1201
+ * @param metadata - Optional metadata to associate with the file (not used in Box implementation)
1202
+ * @returns A Promise that resolves to true if successful, false if an error occurs
1203
+ *
1204
+ * @remarks
1205
+ * - Automatically creates parent directories if they don't exist
1206
+ * - Files smaller than 50MB use a simple upload
1207
+ * - Files 50MB or larger use a chunked upload process
1208
+ * - If a file with the same name exists, it will be replaced
1209
+ *
1210
+ * @example
1211
+ * ```typescript
1212
+ * // Create a simple text file
1213
+ * const textContent = Buffer.from('This is a sample document', 'utf8');
1214
+ * const uploadResult = await storage.PutObject(
1215
+ * 'documents/sample.txt',
1216
+ * textContent,
1217
+ * 'text/plain'
1218
+ * );
1219
+ *
1220
+ * // Upload a large file using chunked upload
1221
+ * const largeFileBuffer = fs.readFileSync('/path/to/large-presentation.pptx');
1222
+ * const largeUploadResult = await storage.PutObject(
1223
+ * 'presentations/quarterly-results.pptx',
1224
+ * largeFileBuffer,
1225
+ * 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
1226
+ * );
1227
+ *
1228
+ * if (largeUploadResult) {
1229
+ * console.log('Large file uploaded successfully');
1230
+ * } else {
1231
+ * console.error('Failed to upload large file');
1232
+ * }
1233
+ * ```
545
1234
  */
546
1235
  async PutObject(objectName, data, contentType, metadata) {
547
1236
  try {
1237
+ console.log(`PutObject: ${objectName}`);
548
1238
  // Get the parent folder ID and file name
549
1239
  const parsedPath = this._parsePath(objectName);
550
1240
  let parentId = this._rootFolderId;
551
1241
  if (parsedPath.parent) {
552
1242
  try {
553
- parentId = await this._getIdFromPath(parsedPath.parent);
1243
+ // First ensure the parent directory exists (create it if needed)
1244
+ const parentDirExists = await this.CreateDirectory(parsedPath.parent);
1245
+ if (!parentDirExists) {
1246
+ console.error(`Failed to ensure parent directory exists: ${parsedPath.parent}`);
1247
+ return false;
1248
+ }
1249
+ // Find folder ID using our improved path resolution from check_file.ts
1250
+ parentId = await this._findFolderIdByPath(parsedPath.parent);
554
1251
  }
555
1252
  catch (error) {
556
- // If parent folder doesn't exist, create it recursively
557
- await this.CreateDirectory(parsedPath.parent);
558
- parentId = await this._getIdFromPath(parsedPath.parent);
1253
+ console.error(`Error resolving parent folder: ${error.message}`);
1254
+ return false;
559
1255
  }
560
1256
  }
561
1257
  // Check if file already exists
562
- let fileId = null;
563
- try {
564
- fileId = await this._getIdFromPath(objectName);
1258
+ const fileId = await this._getIdFromPath(objectName);
1259
+ if (fileId) {
1260
+ console.log(`File already exists with ID: ${fileId}`);
565
1261
  }
566
- catch (error) {
567
- // File doesn't exist, will create new
568
- }
569
- // Use multipart upload for small files (<50MB)
570
- if (data.length < 50 * 1024 * 1024) {
571
- const formData = new FormData();
572
- // Add file metadata
573
- const fileMetadata = {
574
- name: parsedPath.name,
575
- parent: { id: parentId }
576
- };
577
- formData.append('attributes', JSON.stringify(fileMetadata));
578
- // Create a file blob with the correct content type
579
- const fileBlob = new Blob([data], { type: contentType || 'application/octet-stream' });
580
- formData.append('file', fileBlob, parsedPath.name);
581
- // Upload the file
582
- const endpoint = fileId ? `/files/${fileId}/content` : '/files/content';
1262
+ else {
1263
+ console.log(`File doesn't exist yet, will create new file: ${parsedPath.name}`);
1264
+ }
1265
+ const formData = new FormData();
1266
+ // Add file metadata
1267
+ const fileMetadata = {
1268
+ name: parsedPath.name,
1269
+ parent: { id: parentId }
1270
+ };
1271
+ formData.append('attributes', JSON.stringify(fileMetadata));
1272
+ // Create a file blob with the correct content type
1273
+ const fileBlob = new Blob([data], { type: contentType || 'application/octet-stream' });
1274
+ formData.append('file', fileBlob, parsedPath.name);
1275
+ // Upload the file
1276
+ const endpoint = fileId ? `/files/${fileId}/content` : '/files/content';
1277
+ console.log(`Uploading file using endpoint: ${endpoint}`);
1278
+ try {
583
1279
  await this._apiRequest(endpoint, 'POST', formData, {}, this._uploadApiUrl);
1280
+ console.log(`✅ File uploaded successfully: ${objectName}`);
1281
+ return true;
584
1282
  }
585
- else {
586
- // Use chunked upload for larger files
587
- // Create upload session
588
- const session = await this._apiRequest('/files/upload_sessions', 'POST', {
589
- folder_id: parentId,
590
- file_name: parsedPath.name,
591
- file_size: data.length
592
- }, {}, this._uploadApiUrl);
593
- const sessionId = session.id;
594
- const CHUNK_SIZE = 8 * 1024 * 1024; // 8MB chunks
595
- let offset = 0;
596
- const totalSize = data.length;
597
- const parts = [];
598
- // Upload chunks
599
- while (offset < totalSize) {
600
- const chunkEnd = Math.min(offset + CHUNK_SIZE, totalSize);
601
- const chunkSize = chunkEnd - offset;
602
- const chunk = data.slice(offset, chunkEnd);
603
- const headers = {
604
- 'Content-Type': 'application/octet-stream',
605
- 'Digest': `sha=1234`, // Replace with actual SHA-1 digest
606
- 'Content-Range': `bytes ${offset}-${chunkEnd - 1}/${totalSize}`
607
- };
608
- const uploadResponse = await this._apiRequest(`/files/upload_sessions/${sessionId}`, 'PUT', chunk, headers, this._uploadApiUrl);
609
- parts.push({
610
- part_id: uploadResponse.part.part_id,
611
- offset,
612
- size: chunkSize,
613
- sha1: uploadResponse.part.sha1
614
- });
615
- offset = chunkEnd;
1283
+ catch (uploadError) {
1284
+ console.error(`Error uploading file: ${uploadError.message}`);
1285
+ if (uploadError.message.includes('item_name_in_use')) {
1286
+ console.log(`File already exists (conflict): ${objectName}`);
1287
+ return false;
616
1288
  }
617
- // Commit the upload session
618
- await this._apiRequest(`/files/upload_sessions/${sessionId}/commit`, 'POST', { parts }, { 'Content-Type': 'application/json' }, this._uploadApiUrl);
1289
+ return false;
619
1290
  }
620
- return true;
621
1291
  }
622
1292
  catch (error) {
623
1293
  console.error('Error putting object', { objectName, error });
624
1294
  return false;
625
1295
  }
626
1296
  }
1297
+ ;
627
1298
  /**
628
- * Copy an object
1299
+ * Copies a file from one location to another
1300
+ *
1301
+ * This method creates a copy of a file at a new location. The original file
1302
+ * remains unchanged.
1303
+ *
1304
+ * @param sourceObjectName - Path to the source file (e.g., 'templates/report-template.docx')
1305
+ * @param destinationObjectName - Path where the copy should be created (e.g., 'documents/new-report.docx')
1306
+ * @returns A Promise that resolves to true if successful, false if an error occurs
1307
+ *
1308
+ * @remarks
1309
+ * - Only files can be copied; folders cannot be copied with this method
1310
+ * - Parent directories in the destination path will be created automatically if they don't exist
1311
+ * - If a file with the same name exists at the destination, it will be replaced
1312
+ *
1313
+ * @example
1314
+ * ```typescript
1315
+ * // Copy a template file to a new location with a different name
1316
+ * const copyResult = await storage.CopyObject(
1317
+ * 'templates/financial-report.xlsx',
1318
+ * 'reports/2023/q1-financial-report.xlsx'
1319
+ * );
1320
+ *
1321
+ * if (copyResult) {
1322
+ * console.log('File copied successfully');
1323
+ * } else {
1324
+ * console.error('Failed to copy file');
1325
+ * }
1326
+ * ```
629
1327
  */
630
1328
  async CopyObject(sourceObjectName, destinationObjectName) {
631
1329
  try {
@@ -661,7 +1359,26 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
661
1359
  }
662
1360
  }
663
1361
  /**
664
- * Check if an object exists
1362
+ * Checks if a file or folder exists
1363
+ *
1364
+ * This method verifies whether an object (file or folder) exists at the specified path.
1365
+ *
1366
+ * @param objectName - Path to check (e.g., 'documents/report.pdf')
1367
+ * @returns A Promise that resolves to true if the object exists, false otherwise
1368
+ *
1369
+ * @example
1370
+ * ```typescript
1371
+ * // Check if a file exists before attempting to download it
1372
+ * const exists = await storage.ObjectExists('presentations/quarterly-update.pptx');
1373
+ *
1374
+ * if (exists) {
1375
+ * // File exists, proceed with download
1376
+ * const fileContent = await storage.GetObject('presentations/quarterly-update.pptx');
1377
+ * // Process the file...
1378
+ * } else {
1379
+ * console.log('File does not exist');
1380
+ * }
1381
+ * ```
665
1382
  */
666
1383
  async ObjectExists(objectName) {
667
1384
  try {
@@ -673,22 +1390,130 @@ let BoxFileStorage = class BoxFileStorage extends FileStorageBase_1.FileStorageB
673
1390
  }
674
1391
  }
675
1392
  /**
676
- * Check if a directory exists
1393
+ * Checks if a directory exists
1394
+ *
1395
+ * This method verifies whether a folder exists at the specified path.
1396
+ * Unlike ObjectExists, this method also checks that the item is a folder.
1397
+ *
1398
+ * @param directoryPath - Path to check (e.g., 'documents/reports')
1399
+ * @returns A Promise that resolves to true if the directory exists, false otherwise
1400
+ *
1401
+ * @remarks
1402
+ * - Returns false if the path exists but points to a file instead of a folder
1403
+ * - Trailing slashes in the path are automatically removed
1404
+ *
1405
+ * @example
1406
+ * ```typescript
1407
+ * // Check if a directory exists before creating a file in it
1408
+ * const dirExists = await storage.DirectoryExists('documents/reports');
1409
+ *
1410
+ * if (!dirExists) {
1411
+ * // Create the directory first
1412
+ * await storage.CreateDirectory('documents/reports');
1413
+ * }
1414
+ *
1415
+ * // Now we can safely put a file in this directory
1416
+ * await storage.PutObject('documents/reports/annual-summary.pdf', fileContent, 'application/pdf');
1417
+ * ```
677
1418
  */
678
1419
  async DirectoryExists(directoryPath) {
679
1420
  try {
1421
+ // Root directory always exists
1422
+ if (!directoryPath || directoryPath === '/' || directoryPath === '') {
1423
+ return true;
1424
+ }
680
1425
  // Remove trailing slash if present
681
1426
  const normalizedPath = directoryPath.endsWith('/')
682
1427
  ? directoryPath.substring(0, directoryPath.length - 1)
683
1428
  : directoryPath;
684
- const itemId = await this._getIdFromPath(normalizedPath);
685
- const itemInfo = await this._apiRequest(`/folders/${itemId}`);
686
- return itemInfo.type === 'folder';
1429
+ try {
1430
+ const folderId = await this._findFolderIdByPath(normalizedPath);
1431
+ console.log(`✅ Directory ${normalizedPath} exists with ID: ${folderId}`);
1432
+ // Make a direct call to verify it's a folder
1433
+ try {
1434
+ await this._ensureValidToken();
1435
+ await this._client.folders.get(folderId);
1436
+ return true;
1437
+ }
1438
+ catch (error) {
1439
+ // If we can't get the folder info, it's not a valid folder
1440
+ console.log(`Item with ID ${folderId} exists but is not a folder`);
1441
+ return false;
1442
+ }
1443
+ }
1444
+ catch (error) {
1445
+ console.log(`❌ Directory ${normalizedPath} does not exist`);
1446
+ return false;
1447
+ }
687
1448
  }
688
1449
  catch (error) {
1450
+ console.error('Error checking directory exists', { directoryPath, error });
689
1451
  return false;
690
1452
  }
691
1453
  }
1454
+ /**
1455
+ * Finds a Box folder ID by traversing a path string
1456
+ *
1457
+ * This helper method navigates through the Box folder hierarchy,
1458
+ * following each segment of the path to find the ID of the target folder.
1459
+ * It uses pagination to handle large folders efficiently.
1460
+ *
1461
+ * @private
1462
+ * @param path - The path string to resolve (e.g., 'documents/reports/2023')
1463
+ * @returns A Promise that resolves to the Box folder ID
1464
+ * @throws Error if any segment of the path cannot be found
1465
+ */
1466
+ async _findFolderIdByPath(path) {
1467
+ try {
1468
+ // Split the path into segments
1469
+ const pathSegments = path.split('/').filter(segment => segment.length > 0);
1470
+ // Handle "All Files" special case - it's not an actual folder name in the API
1471
+ if (pathSegments.length > 0 && pathSegments[0] === 'All Files') {
1472
+ pathSegments.shift(); // Remove "All Files" from the path
1473
+ }
1474
+ let currentFolderId = '0'; // Start from root
1475
+ if (pathSegments.length === 0) {
1476
+ return currentFolderId; // Return root folder ID if path is empty
1477
+ }
1478
+ // Traverse the path
1479
+ for (const segment of pathSegments) {
1480
+ // Use pagination to handle large folders
1481
+ let folder = null;
1482
+ let offset = 0;
1483
+ let hasMoreItems = true;
1484
+ const LIMIT = 1000;
1485
+ while (hasMoreItems && !folder) {
1486
+ await this._ensureValidToken();
1487
+ const items = await this._client.folders.getItems(currentFolderId, {
1488
+ fields: 'name,type',
1489
+ limit: LIMIT,
1490
+ offset: offset
1491
+ });
1492
+ // Filter to only folders
1493
+ const folders = items.entries.filter((item) => item.type === 'folder');
1494
+ // Look for the target folder
1495
+ folder = folders.find((item) => item.name === segment);
1496
+ // If folder is found, break out of pagination loop
1497
+ if (folder) {
1498
+ break;
1499
+ }
1500
+ // Update pagination variables
1501
+ offset += items.entries.length;
1502
+ // Check if we've processed all items
1503
+ hasMoreItems = items.entries.length === LIMIT && offset < items.total_count;
1504
+ }
1505
+ if (!folder) {
1506
+ throw new Error(`Folder not found: ${segment}`);
1507
+ }
1508
+ currentFolderId = folder.id;
1509
+ }
1510
+ return currentFolderId;
1511
+ }
1512
+ catch (error) {
1513
+ console.error('Error finding folder by path:', error);
1514
+ throw error;
1515
+ }
1516
+ }
692
1517
  };
693
1518
  exports.BoxFileStorage = BoxFileStorage;
694
1519
  exports.BoxFileStorage = BoxFileStorage = __decorate([