dalux-build-api 2.3.0 → 2.4.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 (48) hide show
  1. package/package.json +31 -11
  2. package/src/api/CompaniesApi.js +0 -67
  3. package/src/api/CompanyCatalogApi.js +0 -129
  4. package/src/api/FileAreasApi.js +0 -57
  5. package/src/api/FileRevisionsApi.js +0 -33
  6. package/src/api/FileUploadApi.js +0 -64
  7. package/src/api/FilesApi.js +0 -721
  8. package/src/api/FoldersApi.js +0 -283
  9. package/src/api/FormsApi.js +0 -53
  10. package/src/api/InspectionPlansApi.js +0 -140
  11. package/src/api/ProjectTemplatesApi.js +0 -25
  12. package/src/api/ProjectsApi.js +0 -127
  13. package/src/api/TasksApi.js +0 -193
  14. package/src/api/TestPlansApi.js +0 -137
  15. package/src/api/UsersApi.js +0 -52
  16. package/src/api/VersionSetsApi.js +0 -75
  17. package/src/api/WorkPackagesApi.js +0 -30
  18. package/src/apiClient.js +0 -143
  19. package/src/browser.js +0 -84
  20. package/src/configuration.js +0 -39
  21. package/src/index.js +0 -133
  22. package/src/models/common.js +0 -22
  23. package/src/models/companies/index.js +0 -14
  24. package/src/models/companyCatalog/index.js +0 -19
  25. package/src/models/convert.js +0 -44
  26. package/src/models/fileAreas/index.js +0 -20
  27. package/src/models/fileRevisions/index.js +0 -12
  28. package/src/models/fileUpload/index.js +0 -12
  29. package/src/models/files/index.js +0 -97
  30. package/src/models/folders/index.js +0 -20
  31. package/src/models/forms/index.js +0 -19
  32. package/src/models/helpers.js +0 -62
  33. package/src/models/index.js +0 -178
  34. package/src/models/inspectionPlans/index.js +0 -61
  35. package/src/models/projectTemplates/index.js +0 -11
  36. package/src/models/projects/index.js +0 -57
  37. package/src/models/tasks/index.js +0 -105
  38. package/src/models/testPlans/index.js +0 -61
  39. package/src/models/users/index.js +0 -29
  40. package/src/models/versionSets/index.js +0 -22
  41. package/src/models/workPackages/index.js +0 -19
  42. package/src/next.js +0 -89
  43. package/src/utils/errors.js +0 -45
  44. package/src/utils/index.js +0 -26
  45. package/src/utils/pagination.js +0 -82
  46. package/src/utils/pathResolver.js +0 -124
  47. package/src/utils/search.js +0 -61
  48. package/src/utils/validation.js +0 -36
@@ -1,721 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const readline = require('readline');
6
- const axios = require('axios');
7
- const { findByField, findAllByField } = require('../utils/search');
8
- const { resolveFolderIdFromNamedPath } = require('../utils/pathResolver');
9
- const { validateProjectId, validateFileAreaId, validateFolderId } = require('../utils/validation');
10
- const { convertToModel, convertToModelList } = require('../models/convert');
11
- const { FileSchema, FileResponseSchema, FilesListResponseSchema } = require('../models/files');
12
-
13
- /**
14
- * API methods for files within a file area.
15
- */
16
- class FilesApi {
17
- /**
18
- * @param {import('../apiClient')} apiClient
19
- */
20
- constructor(apiClient) {
21
- this._client = apiClient;
22
- }
23
-
24
- /**
25
- * Browse all files on the given project and file area.
26
- * GET /6.1/projects/{projectId}/file_areas/{fileAreaId}/files
27
- * @param {string} projectId
28
- * @param {string} fileAreaId
29
- * @param {object} [params] - Optional documented params such as includeProperties. The files endpoint does not support OData $filter.
30
- * @returns {Promise<object>}
31
- */
32
- async listFiles(projectId, fileAreaId, params = {}) {
33
- const response = await this._client.get(
34
- `/6.1/projects/${projectId}/file_areas/${fileAreaId}/files`,
35
- params,
36
- );
37
- return convertToModel(response, FilesListResponseSchema, 'FilesListResponse');
38
- }
39
-
40
- /**
41
- * Retrieve all files by following bookmark pagination (metadata.totalRemainingItems).
42
- * @param {string} projectId
43
- * @param {string} fileAreaId
44
- * @param {object} [params]
45
- * @param {boolean} [verbose=false]
46
- * @returns {Promise<object[]>}
47
- */
48
- async getAllFiles(projectId, fileAreaId, params = {}, verbose = false) {
49
- validateProjectId(projectId);
50
- validateFileAreaId(fileAreaId);
51
- const allItems = [];
52
- let currentParams = { ...params };
53
- let hasNextPage = true;
54
- const urlPath = `/6.1/projects/${projectId}/file_areas/${fileAreaId}/files`;
55
-
56
- while (hasNextPage) {
57
- const response = await this._client.get(urlPath, currentParams);
58
- const items = response && response.items;
59
- if (items && items.length) {
60
- allItems.push(...items);
61
- }
62
- const remaining = ((response && response.metadata) || {}).totalRemainingItems ?? 0;
63
- if (verbose) {
64
- console.log(`Retrieved ${allItems.length} files so far, ${remaining} remaining...`);
65
- }
66
- if (!items || !items.length || remaining === 0) {
67
- hasNextPage = false;
68
- } else {
69
- const nextLink = (response.links || []).find((l) => l.rel === 'nextPage');
70
- if (nextLink) {
71
- const bookmark = new URL(nextLink.href).searchParams.get('bookmark');
72
- currentParams = { ...params, bookmark };
73
- } else {
74
- hasNextPage = false;
75
- }
76
- }
77
- }
78
- if (verbose) {
79
- console.log(`Done. Total files retrieved: ${allItems.length}`);
80
- }
81
- return convertToModelList(allItems, FileSchema, 'File');
82
- }
83
-
84
- /**
85
- * All files in a folder.
86
- *
87
- * Supports two call styles matching the Python client:
88
- * - `getAllFilesInFolder(projectId, fileAreaId, folderId, params?, verbose?)` — explicit IDs
89
- * - `getAllFilesInFolder(projectId, fileAreaIdOrPath, null, params?, verbose?)` — full path
90
- * (e.g. ``"Files/4_Design/C07_Geometry"``)
91
- *
92
- * @param {string} projectId
93
- * @param {string} fileAreaIdOrPath - File area ID, OR a full path starting with the file area name.
94
- * @param {string|null} [folderId=null] - Folder ID. When null, fileAreaIdOrPath is treated as a path.
95
- * @param {object} [params]
96
- * @param {boolean} [verbose=false]
97
- * @returns {Promise<object[]>}
98
- */
99
- async getAllFilesInFolder(projectId, fileAreaIdOrPath, folderId = null, params = {}, verbose = false) {
100
- validateProjectId(projectId);
101
-
102
- let fileAreaId;
103
- let resolvedFolderId;
104
-
105
- if (folderId == null) {
106
- const resolved = await resolveFolderIdFromNamedPath(
107
- this._client, projectId, fileAreaIdOrPath, { verbose },
108
- );
109
- if (!resolved.fileAreaId || !resolved.folderId) {
110
- if (verbose) console.log(`Could not resolve folder path: ${fileAreaIdOrPath}`);
111
- return [];
112
- }
113
- fileAreaId = resolved.fileAreaId;
114
- resolvedFolderId = resolved.folderId;
115
- } else {
116
- fileAreaId = fileAreaIdOrPath;
117
- resolvedFolderId = folderId;
118
- validateFileAreaId(fileAreaId);
119
- validateFolderId(resolvedFolderId);
120
- }
121
-
122
- const allFiles = await this.getAllFiles(projectId, fileAreaId, params, verbose);
123
- const filtered = allFiles.filter((f) => {
124
- const data = f.data || f;
125
- return data.folderId === resolvedFolderId;
126
- });
127
- if (verbose) {
128
- console.log(`Files matching folder '${resolvedFolderId}': ${filtered.length}`);
129
- }
130
- return filtered;
131
- }
132
-
133
- /**
134
- * Download a file from a direct download URL using X-API-KEY (same as Python client).
135
- * @param {string} downloadLink
136
- * @param {string} fileName
137
- * @param {string} [savePath] - Directory to save into (default: current working directory)
138
- * @returns {Promise<string>} Absolute path to saved file
139
- */
140
- async downloadFileFromLink(downloadLink, fileName, savePath) {
141
- const apiKey = this._client.configuration.apiKey;
142
- const dir = savePath || '.';
143
- await fs.promises.mkdir(dir, { recursive: true });
144
- const filePath = path.join(dir, fileName);
145
-
146
- const response = await axios.get(downloadLink, {
147
- headers: { 'X-API-KEY': apiKey },
148
- responseType: 'stream',
149
- validateStatus: () => true,
150
- });
151
- if (response.status !== 200) {
152
- throw new Error(`Failed to download file. Status code: ${response.status}`);
153
- }
154
- await new Promise((resolve, reject) => {
155
- const ws = fs.createWriteStream(filePath);
156
- response.data.pipe(ws);
157
- response.data.on('error', reject);
158
- ws.on('finish', () => resolve());
159
- ws.on('error', reject);
160
- });
161
- return path.resolve(filePath);
162
- }
163
-
164
- /**
165
- * Fetch a file's raw bytes from a direct download URL using X-API-KEY, without
166
- * writing to disk. Useful when the caller needs to relay the content onward
167
- * (e.g. as an HTTP response body) rather than save it to a local filesystem.
168
- * @param {string} downloadLink
169
- * @returns {Promise<{ buffer: Buffer, contentType: string|undefined }>}
170
- */
171
- async downloadFileBuffer(downloadLink) {
172
- const apiKey = this._client.configuration.apiKey;
173
- const response = await axios.get(downloadLink, {
174
- headers: { 'X-API-KEY': apiKey },
175
- responseType: 'arraybuffer',
176
- validateStatus: () => true,
177
- });
178
- if (response.status !== 200) {
179
- throw new Error(`Failed to download file. Status code: ${response.status}`);
180
- }
181
- return { buffer: Buffer.from(response.data), contentType: response.headers['content-type'] };
182
- }
183
-
184
- /**
185
- * Get a file by IDs or by full path.
186
- *
187
- * Supports two call styles matching the Python client:
188
- * - `getFile(projectId, fileAreaId, fileId, options?)` — explicit IDs
189
- * - `getFile(projectId, fullPath, null, options?)` — full path including file name
190
- * (e.g. ``"Files/folder/.../file.ifc"``)
191
- *
192
- * @param {string} projectId
193
- * @param {string} fileAreaIdOrPath - File area ID, OR a full path.
194
- * @param {string|null} [fileId=null] - File ID. When null, fileAreaIdOrPath is treated as a path.
195
- * @param {{ download?: boolean, savePath?: string, verbose?: boolean, params?: object }} [options]
196
- * @returns {Promise<object|string>}
197
- */
198
- async getFile(projectId, fileAreaIdOrPath, fileId = null, options = {}) {
199
- validateProjectId(projectId);
200
-
201
- if (fileId == null) {
202
- // Path-based lookup
203
- const { download = false, savePath, verbose = false, params } = options;
204
- const pathStr = fileAreaIdOrPath;
205
- const notFound = `File does not exist: ${pathStr}`;
206
- const parts = pathStr.split('/').map((p) => p.trim()).filter(Boolean);
207
- if (parts.length < 3) {
208
- throw new Error('path must include file area name, folder path, and file name');
209
- }
210
- const resolved = await resolveFolderIdFromNamedPath(
211
- this._client, projectId, parts.slice(0, -1).join('/'), { verbose },
212
- );
213
- if (!resolved.fileAreaId || !resolved.folderId) return notFound;
214
- const files = await this.getAllFilesInFolder(
215
- projectId, resolved.fileAreaId, resolved.folderId, params, verbose,
216
- );
217
- const candidateFileName = parts[parts.length - 1];
218
- const fileMatch = findByField(files, 'fileName', candidateFileName, (x) => x.data || x)
219
- || findByField(files, 'file_name', candidateFileName, (x) => x.data || x);
220
- if (!fileMatch) return notFound;
221
-
222
- if (download) {
223
- const data = fileMatch.data || fileMatch;
224
- if (data.downloadLink) {
225
- const name = data.fileName || data.file_name || candidateFileName;
226
- const downloadedFilePath = await this.downloadFileFromLink(data.downloadLink, name, savePath);
227
- return { ...fileMatch, downloadedFilePath };
228
- }
229
- }
230
- return fileMatch;
231
- }
232
-
233
- // ID-based lookup (original behaviour)
234
- const rawResponse = await this._client.get(
235
- `/5.0/projects/${projectId}/file_areas/${fileAreaIdOrPath}/files/${fileId}`,
236
- );
237
- const fileInfo = convertToModel(rawResponse, FileResponseSchema, 'FileResponse');
238
- if (options.download && fileInfo && fileInfo.data && fileInfo.data.downloadLink) {
239
- const name = fileInfo.data.fileName || fileId;
240
- const downloadedPath = await this.downloadFileFromLink(
241
- fileInfo.data.downloadLink,
242
- name,
243
- options.savePath,
244
- );
245
- fileInfo.downloadedFilePath = downloadedPath;
246
- }
247
- return fileInfo;
248
- }
249
-
250
- /**
251
- * Apply file name filters (contains, startsWith, endsWith, extensions).
252
- * @param {object[]} files
253
- * @param {object} filters
254
- * @returns {object[]}
255
- */
256
- _applyFileNameFilters(files, filters = {}) {
257
- const {
258
- contains = null,
259
- containsMatch = 'any',
260
- notContains = null,
261
- startsWith = null,
262
- notStartsWith = null,
263
- endsWith = null,
264
- notEndsWith = null,
265
- extensions = null,
266
- notExtensions = null,
267
- verbose = false,
268
- } = filters;
269
-
270
- function getName(f) {
271
- return ((f.data || f).fileName || (f.data || f).file_name || '').toLowerCase();
272
- }
273
- function norm(vals) {
274
- return (vals || []).map((v) => String(v).toLowerCase()).filter(Boolean);
275
- }
276
- function normExts(vals) {
277
- return (vals || []).map((v) => (v.startsWith('.') ? v : `.${v}`).toLowerCase()).filter(Boolean);
278
- }
279
-
280
- let filtered = files;
281
-
282
- const containsVals = norm(contains);
283
- if (containsVals.length) {
284
- const before = filtered.length;
285
- const matchFn = containsMatch === 'all'
286
- ? (name) => containsVals.every((v) => name.includes(v))
287
- : (name) => containsVals.some((v) => name.includes(v));
288
- filtered = filtered.filter((f) => matchFn(getName(f)));
289
- if (verbose) console.log(`Files matching contains ${JSON.stringify(containsVals)} (${containsMatch}): ${filtered.length} / ${before}`);
290
- }
291
-
292
- const notContainsVals = norm(notContains);
293
- if (notContainsVals.length) {
294
- const before = filtered.length;
295
- filtered = filtered.filter((f) => !notContainsVals.some((v) => getName(f).includes(v)));
296
- if (verbose) console.log(`Files excluding contains ${JSON.stringify(notContainsVals)}: ${filtered.length} / ${before}`);
297
- }
298
-
299
- const startsWithVals = norm(startsWith);
300
- if (startsWithVals.length) {
301
- const before = filtered.length;
302
- filtered = filtered.filter((f) => startsWithVals.some((v) => getName(f).startsWith(v)));
303
- if (verbose) console.log(`Files matching startsWith ${JSON.stringify(startsWithVals)}: ${filtered.length} / ${before}`);
304
- }
305
-
306
- const notStartsWithVals = norm(notStartsWith);
307
- if (notStartsWithVals.length) {
308
- const before = filtered.length;
309
- filtered = filtered.filter((f) => !notStartsWithVals.some((v) => getName(f).startsWith(v)));
310
- if (verbose) console.log(`Files excluding startsWith ${JSON.stringify(notStartsWithVals)}: ${filtered.length} / ${before}`);
311
- }
312
-
313
- const endsWithVals = norm(endsWith);
314
- if (endsWithVals.length) {
315
- const before = filtered.length;
316
- filtered = filtered.filter((f) => endsWithVals.some((v) => getName(f).endsWith(v)));
317
- if (verbose) console.log(`Files matching endsWith ${JSON.stringify(endsWithVals)}: ${filtered.length} / ${before}`);
318
- }
319
-
320
- const notEndsWithVals = norm(notEndsWith);
321
- if (notEndsWithVals.length) {
322
- const before = filtered.length;
323
- filtered = filtered.filter((f) => !notEndsWithVals.some((v) => getName(f).endsWith(v)));
324
- if (verbose) console.log(`Files excluding endsWith ${JSON.stringify(notEndsWithVals)}: ${filtered.length} / ${before}`);
325
- }
326
-
327
- const extVals = normExts(extensions);
328
- if (extVals.length) {
329
- const before = filtered.length;
330
- filtered = filtered.filter((f) => extVals.some((v) => getName(f).endsWith(v)));
331
- if (verbose) console.log(`Files matching extensions ${JSON.stringify(extVals)}: ${filtered.length} / ${before}`);
332
- }
333
-
334
- const notExtVals = normExts(notExtensions);
335
- if (notExtVals.length) {
336
- const before = filtered.length;
337
- filtered = filtered.filter((f) => !notExtVals.some((v) => getName(f).endsWith(v)));
338
- if (verbose) console.log(`Files excluding extensions ${JSON.stringify(notExtVals)}: ${filtered.length} / ${before}`);
339
- }
340
-
341
- return filtered;
342
- }
343
-
344
- /**
345
- * Download all files in a folder with optional file name filters.
346
- *
347
- * Supports two call styles matching the Python client:
348
- * - `bulkDownloadFolder(projectId, fileAreaId, folderId, savePath?, opts?)` — explicit IDs
349
- * - `bulkDownloadFolder(projectId, fullPath, null, savePath?, opts?)` — full path
350
- *
351
- * @param {string} projectId
352
- * @param {string} fileAreaIdOrPath - File area ID or full path starting with file area name.
353
- * @param {string|null} [folderId=null] - Folder ID; when null, fileAreaIdOrPath is a full path.
354
- * @param {string} [savePath]
355
- * @param {object} [opts]
356
- * @param {string[]} [opts.contains]
357
- * @param {string} [opts.containsMatch='any']
358
- * @param {string[]} [opts.notContains]
359
- * @param {string[]} [opts.startsWith]
360
- * @param {string[]} [opts.notStartsWith]
361
- * @param {string[]} [opts.endsWith]
362
- * @param {string[]} [opts.notEndsWith]
363
- * @param {string[]} [opts.extensions]
364
- * @param {string[]} [opts.notExtensions]
365
- * @param {object} [opts.params]
366
- * @param {boolean} [opts.verbose=false]
367
- * @returns {Promise<Array<{ fileName: string, downloadedFilePath: string }>>}
368
- */
369
- async bulkDownloadFolder(projectId, fileAreaIdOrPath, folderId = null, savePath, opts = {}) {
370
- if (typeof savePath === 'object' && savePath !== null && !Array.isArray(savePath)) {
371
- opts = savePath;
372
- savePath = undefined;
373
- }
374
- const { params = {}, verbose = false, ...filterOpts } = opts;
375
-
376
- let files = await this.getAllFilesInFolder(
377
- projectId, fileAreaIdOrPath, folderId, params, verbose,
378
- );
379
-
380
- // Legacy keyword/extension filter aliases for backward compatibility
381
- if (opts.filenameKeywords && opts.filenameKeywords.length) {
382
- filterOpts.contains = opts.filenameKeywords;
383
- filterOpts.containsMatch = opts.filenameKeywordsMatch || 'any';
384
- }
385
- if (opts.filenameExtensions && opts.filenameExtensions.length) {
386
- filterOpts.extensions = opts.filenameExtensions;
387
- }
388
-
389
- files = this._applyFileNameFilters(files, { ...filterOpts, verbose });
390
-
391
- const results = [];
392
- for (let i = 0; i < files.length; i += 1) {
393
- const f = files[i];
394
- const data = f.data || f;
395
- const fileName = data.fileName || data.file_name || data.fileId || `file_${i + 1}`;
396
- const downloadLink = data.downloadLink;
397
- if (!downloadLink) {
398
- if (verbose) console.log(` [${i + 1}/${files.length}] Skipping '${fileName}' (no downloadLink)`);
399
- continue;
400
- }
401
- if (verbose) console.log(` [${i + 1}/${files.length}] Downloading '${fileName}'...`);
402
- const downloadedFilePath = await this.downloadFileFromLink(downloadLink, fileName, savePath);
403
- results.push({ fileName, downloadedFilePath });
404
- }
405
- if (verbose) console.log(`Bulk download complete. ${results.length} file(s) downloaded.`);
406
- return results;
407
- }
408
-
409
- /**
410
- * Download a list of files by IDs or full paths.
411
- *
412
- * Matches Python's ``bulk_download_files``:
413
- * - When *fileAreaId* is ``null``, each item in *files* is treated as a full path
414
- * (e.g. ``"Files/folder/.../file.ext"``).
415
- * - When *fileAreaId* is provided, items are treated as file IDs.
416
- *
417
- * @param {string} projectId
418
- * @param {string[]} files - List of file IDs or full paths.
419
- * @param {string|null} [fileAreaId=null] - Required when items are file IDs; null for paths.
420
- * @param {string} [savePath]
421
- * @param {object} [opts]
422
- * @param {object} [opts.params]
423
- * @param {boolean} [opts.verbose=false]
424
- * @returns {Promise<Array<{ fileName: string, downloadedFilePath: string }>>}
425
- */
426
- async bulkDownloadFiles(projectId, files, fileAreaId = null, savePath, opts = {}) {
427
- if (typeof savePath === 'object' && savePath !== null) {
428
- opts = savePath;
429
- savePath = undefined;
430
- }
431
- const { params = {}, verbose = false } = opts;
432
-
433
- const results = [];
434
- const total = files.length;
435
- const fileAreasCache = {};
436
- const foldersCache = {};
437
- const resolvedPathsCache = {};
438
- const allFilesCache = {};
439
-
440
- for (let i = 0; i < files.length; i += 1) {
441
- const item = files[i];
442
- let fileData = null;
443
-
444
- if (fileAreaId == null) {
445
- // Path-based: "FileArea/Folder/.../file.ext"
446
- const parts = item.split('/').map((p) => p.trim()).filter(Boolean);
447
- if (parts.length < 3) {
448
- if (verbose) console.log(` [${i + 1}/${total}] Skipping '${item}' (invalid path)`);
449
- continue;
450
- }
451
- const { fileAreaId: resolvedFaId, folderId } = await resolveFolderIdFromNamedPath(
452
- this._client, projectId, parts.slice(0, -1).join('/'),
453
- { verbose, fileAreasCache, foldersCache, resolvedPathsCache },
454
- );
455
- if (!resolvedFaId || !folderId) {
456
- if (verbose) console.log(` [${i + 1}/${total}] Skipping '${item}' (File does not exist: ${item})`);
457
- continue;
458
- }
459
- if (!allFilesCache[resolvedFaId]) {
460
- allFilesCache[resolvedFaId] = await this.getAllFiles(projectId, resolvedFaId, params, verbose);
461
- }
462
- const folderFiles = (allFilesCache[resolvedFaId] || []).filter((f) => {
463
- const d = f.data || f;
464
- return d.folderId === folderId;
465
- });
466
- const candidateName = parts[parts.length - 1];
467
- const match = findByField(folderFiles, 'fileName', candidateName, (x) => x.data || x)
468
- || findByField(folderFiles, 'file_name', candidateName, (x) => x.data || x);
469
- if (!match) {
470
- if (verbose) console.log(` [${i + 1}/${total}] Skipping '${item}' (File does not exist: ${item})`);
471
- continue;
472
- }
473
- fileData = match.data || match;
474
- } else {
475
- // ID-based
476
- const fileInfo = await this.getFile(projectId, fileAreaId, item);
477
- if (!fileInfo || typeof fileInfo === 'string') {
478
- if (verbose) console.log(` [${i + 1}/${total}] Skipping '${item}' (${fileInfo || 'not found'})`);
479
- continue;
480
- }
481
- fileData = fileInfo.data || fileInfo;
482
- }
483
-
484
- const fileName = fileData.fileName || fileData.file_name || fileData.fileId || item;
485
- const downloadLink = fileData.downloadLink;
486
- if (!downloadLink) {
487
- if (verbose) console.log(` [${i + 1}/${total}] Skipping '${fileName}' (no downloadLink)`);
488
- continue;
489
- }
490
- if (verbose) console.log(` [${i + 1}/${total}] Downloading '${fileName}'...`);
491
- const downloadedFilePath = await this.downloadFileFromLink(downloadLink, fileName, savePath);
492
- results.push({ fileName, downloadedFilePath });
493
- }
494
- if (verbose) console.log(`Done. ${results.length}/${total} file(s) downloaded.`);
495
- return results;
496
- }
497
-
498
- /**
499
- * Download files by id (fetches metadata per id then streams from downloadLink).
500
- * @param {string} projectId
501
- * @param {string} fileAreaId
502
- * @param {string[]} fileIds
503
- * @param {string} [savePath]
504
- * @param {{ verbose?: boolean }} [options]
505
- * @returns {Promise<Array<{ fileId: string, fileName: string, downloadedFilePath: string }>>}
506
- */
507
- async bulkDownloadByIds(projectId, fileAreaId, fileIds, savePath, options = {}) {
508
- if (typeof savePath === 'object' && savePath !== null) {
509
- options = savePath;
510
- savePath = undefined;
511
- }
512
- const { verbose = false } = options;
513
- const results = [];
514
- const total = fileIds.length;
515
- for (let i = 0; i < fileIds.length; i += 1) {
516
- const fileId = fileIds[i];
517
- const fileInfo = await this.getFile(projectId, fileAreaId, fileId);
518
- const data = (fileInfo && fileInfo.data) || {};
519
- const fileName = data.fileName || fileId;
520
- const downloadLink = data.downloadLink;
521
- if (!downloadLink) {
522
- if (verbose) {
523
- console.log(` [${i + 1}/${total}] Skipping '${fileName}' (no downloadLink)`);
524
- }
525
- continue;
526
- }
527
- if (verbose) {
528
- console.log(` [${i + 1}/${total}] Downloading '${fileName}'...`);
529
- }
530
- const downloadedFilePath = await this.downloadFileFromLink(downloadLink, fileName, savePath);
531
- results.push({ fileId, fileName, downloadedFilePath });
532
- }
533
- if (verbose) {
534
- console.log(`Done. ${results.length}/${total} file(s) downloaded.`);
535
- }
536
- return results;
537
- }
538
-
539
- /**
540
- * Interactively browse the folder tree level-by-level and select files.
541
- *
542
- * Navigation (type into stdin):
543
- * - A **folder number** (single token) → enter that folder.
544
- * - ``b`` / ``back`` → go up one level.
545
- * - **File numbers / ranges** (e.g. ``1``, ``3-5``) → toggle selection.
546
- * - ``d`` / ``done`` → finish and return selected file IDs.
547
- *
548
- * @param {string} projectId
549
- * @param {string} fileAreaId
550
- * @param {object|null} [tree] - Pre-built tree from FoldersApi.getFileAreaTree.
551
- * If not provided, *foldersApi* must be supplied.
552
- * @param {object|null} [foldersApi] - FoldersApi instance to build the tree automatically.
553
- * @returns {Promise<string[]>} Ordered list of selected file IDs.
554
- */
555
- async selectFilesInteractive(projectId, fileAreaId, tree = null, foldersApi = null) {
556
- if (!tree) {
557
- if (!foldersApi) throw new Error("Either 'tree' or 'foldersApi' must be provided.");
558
- tree = await foldersApi.getFileAreaTree(projectId, fileAreaId, this);
559
- }
560
-
561
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
562
- const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
563
-
564
- function getFid(f) {
565
- const d = f.data || f || {};
566
- return d.id || d.fileId || null;
567
- }
568
- function getFname(f) {
569
- return (f.data || f || {}).fileName || '<unknown>';
570
- }
571
- function hasContent(node) {
572
- if (node.files.length) return true;
573
- return node.children.some(hasContent);
574
- }
575
- function countFiles(node) {
576
- return node.files.length + node.children.reduce((s, c) => s + countFiles(c), 0);
577
- }
578
- function parseTokens(raw, maxIdx) {
579
- const chosen = new Set();
580
- for (const token of raw.split(/\s+/)) {
581
- if (token.includes('-')) {
582
- const [lo, hi] = token.split('-').map(Number);
583
- if (!isNaN(lo) && !isNaN(hi)) {
584
- for (let n = lo; n <= hi; n++) chosen.add(n);
585
- }
586
- } else {
587
- const n = parseInt(token, 10);
588
- if (!isNaN(n)) chosen.add(n);
589
- }
590
- }
591
- return new Set([...chosen].filter((n) => n >= 1 && n <= maxIdx));
592
- }
593
-
594
- const selectedIds = [];
595
- const selectedSet = new Set();
596
- const stack = [tree];
597
-
598
- try {
599
- while (true) {
600
- const node = stack[stack.length - 1];
601
- const folders = node.children
602
- .filter(hasContent)
603
- .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
604
- const files = [...node.files].sort((a, b) =>
605
- getFname(a).toLowerCase().localeCompare(getFname(b).toLowerCase()),
606
- );
607
-
608
- console.log();
609
- const header = node.path || `[root] ${node.name}`;
610
- console.log(` ${'='.repeat(60)}`);
611
- console.log(` ${header}`);
612
- console.log(` ${'='.repeat(60)}`);
613
- const navHints = [];
614
- if (stack.length > 1) navHints.push('[b] back');
615
- navHints.push(`[d] done (${selectedIds.length} selected)`);
616
- console.log(` ${navHints.join(' | ')}`);
617
-
618
- if (folders.length) {
619
- console.log('\n Folders:');
620
- folders.forEach((child, i) => {
621
- const total = countFiles(child);
622
- console.log(` [${String(i + 1).padStart(3)}] ${child.name}/ (${total} file(s))`);
623
- });
624
- } else {
625
- console.log('\n Folders: (none)');
626
- }
627
-
628
- const fileOffset = folders.length;
629
- if (files.length) {
630
- console.log('\n Files:');
631
- files.forEach((f, j) => {
632
- const idx = fileOffset + j + 1;
633
- const mark = selectedSet.has(getFid(f)) ? '✓ ' : ' ';
634
- console.log(` [${String(idx).padStart(3)}] ${mark}${getFname(f)}`);
635
- });
636
- } else {
637
- console.log('\n Files: (none)');
638
- }
639
-
640
- console.log();
641
- const raw = (await question(' > ')).trim();
642
- const cmd = raw.toLowerCase();
643
-
644
- if (cmd === 'd' || cmd === 'done') break;
645
- if (cmd === '') continue;
646
- if (cmd === 'b' || cmd === 'back') {
647
- if (stack.length > 1) stack.pop();
648
- continue;
649
- }
650
-
651
- // Single folder navigation
652
- const tokens = raw.split(/\s+/);
653
- if (tokens.length === 1 && /^\d+$/.test(tokens[0])) {
654
- const idx = parseInt(tokens[0], 10);
655
- if (idx >= 1 && idx <= folders.length) {
656
- stack.push(folders[idx - 1]);
657
- continue;
658
- }
659
- }
660
-
661
- // File toggles
662
- const chosen = parseTokens(raw, fileOffset + files.length);
663
- let toggled = 0;
664
- for (const idx of [...chosen].sort((a, b) => a - b)) {
665
- if (idx <= folders.length) continue;
666
- const fileIdx = idx - fileOffset - 1;
667
- if (fileIdx >= 0 && fileIdx < files.length) {
668
- const f = files[fileIdx];
669
- const fid = getFid(f);
670
- if (!fid) continue;
671
- if (selectedSet.has(fid)) {
672
- selectedSet.delete(fid);
673
- const pos = selectedIds.indexOf(fid);
674
- if (pos !== -1) selectedIds.splice(pos, 1);
675
- } else {
676
- selectedSet.add(fid);
677
- selectedIds.push(fid);
678
- }
679
- toggled++;
680
- }
681
- }
682
- if (toggled) console.log(` → ${selectedIds.length} file(s) selected total.`);
683
- }
684
- } finally {
685
- rl.close();
686
- }
687
-
688
- console.log(`\n Done. ${selectedIds.length} file(s) selected.`);
689
- return selectedIds;
690
- }
691
-
692
- /**
693
- * Retrieve properties mapping for a specific file.
694
- * GET /1.0/projects/{projectId}/file_areas/{fileAreaId}/files/{fileId}/properties/1.0/mappings
695
- * @param {string} projectId
696
- * @param {string} fileAreaId
697
- * @param {string} fileId
698
- * @returns {Promise<object>}
699
- */
700
- getFilePropertiesMapping(projectId, fileAreaId, fileId) {
701
- return this._client.get(
702
- `/1.0/projects/${projectId}/file_areas/${fileAreaId}/files/${fileId}/properties/1.0/mappings`,
703
- );
704
- }
705
-
706
- /**
707
- * Retrieve valid property values for a specific file property mapping.
708
- * GET /1.0/projects/{projectId}/file_areas/{fileAreaId}/files/properties/1.0/mappings/{filePropertyId}/values
709
- * @param {string} projectId
710
- * @param {string} fileAreaId
711
- * @param {string} filePropertyId
712
- * @returns {Promise<object>}
713
- */
714
- getFilePropertyMappingValues(projectId, fileAreaId, filePropertyId) {
715
- return this._client.get(
716
- `/1.0/projects/${projectId}/file_areas/${fileAreaId}/files/properties/1.0/mappings/${filePropertyId}/values`,
717
- );
718
- }
719
- }
720
-
721
- module.exports = FilesApi;