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