@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.
- package/dist/drivers/AWSFileStorage.d.ts +360 -4
- package/dist/drivers/AWSFileStorage.d.ts.map +1 -1
- package/dist/drivers/AWSFileStorage.js +357 -4
- package/dist/drivers/AWSFileStorage.js.map +1 -1
- package/dist/drivers/AzureFileStorage.d.ts +362 -2
- package/dist/drivers/AzureFileStorage.d.ts.map +1 -1
- package/dist/drivers/AzureFileStorage.js +357 -2
- package/dist/drivers/AzureFileStorage.js.map +1 -1
- package/dist/drivers/BoxFileStorage.d.ts +648 -20
- package/dist/drivers/BoxFileStorage.d.ts.map +1 -1
- package/dist/drivers/BoxFileStorage.js +951 -126
- package/dist/drivers/BoxFileStorage.js.map +1 -1
- package/dist/drivers/DropboxFileStorage.d.ts +437 -15
- package/dist/drivers/DropboxFileStorage.d.ts.map +1 -1
- package/dist/drivers/DropboxFileStorage.js +431 -15
- package/dist/drivers/DropboxFileStorage.js.map +1 -1
- package/dist/drivers/GoogleDriveFileStorage.d.ts +342 -16
- package/dist/drivers/GoogleDriveFileStorage.d.ts.map +1 -1
- package/dist/drivers/GoogleDriveFileStorage.js +340 -16
- package/dist/drivers/GoogleDriveFileStorage.js.map +1 -1
- package/dist/drivers/GoogleFileStorage.d.ts +358 -2
- package/dist/drivers/GoogleFileStorage.d.ts.map +1 -1
- package/dist/drivers/GoogleFileStorage.js +356 -2
- package/dist/drivers/GoogleFileStorage.js.map +1 -1
- package/dist/drivers/SharePointFileStorage.d.ts +434 -20
- package/dist/drivers/SharePointFileStorage.d.ts.map +1 -1
- package/dist/drivers/SharePointFileStorage.js +453 -22
- package/dist/drivers/SharePointFileStorage.js.map +1 -1
- package/dist/generic/FileStorageBase.d.ts +326 -108
- package/dist/generic/FileStorageBase.d.ts.map +1 -1
- package/dist/generic/FileStorageBase.js +54 -6
- package/dist/generic/FileStorageBase.js.map +1 -1
- package/dist/util.d.ts +125 -18
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +125 -18
- package/dist/util.js.map +1 -1
- package/package.json +8 -7
- 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
|
-
*
|
|
61
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
//
|
|
828
|
+
// First check if directory already exists
|
|
430
829
|
try {
|
|
431
|
-
await this.
|
|
432
|
-
|
|
830
|
+
if (await this.DirectoryExists(normalizedPath)) {
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
433
833
|
}
|
|
434
834
|
catch (error) {
|
|
435
|
-
//
|
|
835
|
+
// Ignore error, we'll try to create it anyway
|
|
436
836
|
}
|
|
437
|
-
//
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
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
|
-
|
|
446
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
557
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
1258
|
+
const fileId = await this._getIdFromPath(objectName);
|
|
1259
|
+
if (fileId) {
|
|
1260
|
+
console.log(`File already exists with ID: ${fileId}`);
|
|
565
1261
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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([
|