@devicecloud.dev/dcd 4.2.0 → 4.2.2
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/commands/list.d.ts +38 -0
- package/dist/commands/list.js +143 -0
- package/dist/commands/status.d.ts +9 -0
- package/dist/commands/status.js +79 -8
- package/dist/config/flags/output.flags.js +1 -1
- package/dist/gateways/api-gateway.d.ts +27 -0
- package/dist/gateways/api-gateway.js +290 -150
- package/dist/methods.js +144 -35
- package/dist/services/execution-plan.service.js +9 -0
- package/dist/services/execution-plan.utils.js +6 -10
- package/dist/services/test-submission.service.js +5 -0
- package/dist/types/domain/device.types.d.ts +1 -0
- package/dist/types/domain/device.types.js +1 -0
- package/dist/types/generated/schema.types.d.ts +1 -1
- package/dist/types/schema.types.d.ts +154 -101
- package/oclif.manifest.json +97 -2
- package/package.json +1 -2
|
@@ -3,6 +3,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ApiGateway = void 0;
|
|
4
4
|
const path = require("node:path");
|
|
5
5
|
exports.ApiGateway = {
|
|
6
|
+
/**
|
|
7
|
+
* Enhances generic "fetch failed" errors with more specific diagnostic information
|
|
8
|
+
* @param error - The original TypeError from fetch
|
|
9
|
+
* @param url - The URL that was being fetched
|
|
10
|
+
* @returns Enhanced error with diagnostic information
|
|
11
|
+
*/
|
|
12
|
+
enhanceFetchError(error, url) {
|
|
13
|
+
const urlObj = new URL(url);
|
|
14
|
+
const { hostname, origin } = urlObj;
|
|
15
|
+
let message = `Network request failed: ${url}\n\n`;
|
|
16
|
+
message += `Possible causes:\n`;
|
|
17
|
+
message += ` 1. No internet connection - check your network connectivity\n`;
|
|
18
|
+
message += ` 2. DNS resolution failed - unable to resolve "${hostname}"\n`;
|
|
19
|
+
message += ` 3. Firewall or proxy blocking the request\n`;
|
|
20
|
+
message += ` 4. API server is down or unreachable\n`;
|
|
21
|
+
message += ` 5. SSL/TLS certificate validation failed\n\n`;
|
|
22
|
+
message += `Troubleshooting steps:\n`;
|
|
23
|
+
message += ` • Check internet connection: ping google.com\n`;
|
|
24
|
+
message += ` • Test API reachability: curl ${origin}\n`;
|
|
25
|
+
message += ` • Verify API URL is correct: ${origin}\n`;
|
|
26
|
+
message += ` • Check for proxy/VPN interference\n`;
|
|
27
|
+
message += ` • Try again in a few moments if server is temporarily down\n\n`;
|
|
28
|
+
message += `Original error: ${error.message}`;
|
|
29
|
+
const enhancedError = new Error(message);
|
|
30
|
+
enhancedError.name = 'NetworkError';
|
|
31
|
+
enhancedError.stack = error.stack;
|
|
32
|
+
return enhancedError;
|
|
33
|
+
},
|
|
6
34
|
/**
|
|
7
35
|
* Standardized error handling for API responses
|
|
8
36
|
* @param res - The fetch response object
|
|
@@ -55,117 +83,169 @@ exports.ApiGateway = {
|
|
|
55
83
|
}
|
|
56
84
|
},
|
|
57
85
|
async checkForExistingUpload(baseUrl, apiKey, sha) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
headers: {
|
|
72
|
-
'content-type': 'application/json',
|
|
73
|
-
'x-app-api-key': apiKey,
|
|
74
|
-
},
|
|
75
|
-
method: 'POST',
|
|
76
|
-
});
|
|
77
|
-
if (!res.ok) {
|
|
78
|
-
await this.handleApiError(res, 'Failed to download artifacts');
|
|
86
|
+
try {
|
|
87
|
+
const res = await fetch(`${baseUrl}/uploads/checkForExistingUpload`, {
|
|
88
|
+
body: JSON.stringify({ sha }),
|
|
89
|
+
headers: {
|
|
90
|
+
'content-type': 'application/json',
|
|
91
|
+
'x-app-api-key': apiKey,
|
|
92
|
+
},
|
|
93
|
+
method: 'POST',
|
|
94
|
+
});
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
await this.handleApiError(res, 'Failed to check for existing upload');
|
|
97
|
+
}
|
|
98
|
+
return res.json();
|
|
79
99
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
100
|
+
catch (error) {
|
|
101
|
+
// Handle network-level errors (DNS, connection refused, timeout, etc.)
|
|
102
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
103
|
+
throw this.enhanceFetchError(error, `${baseUrl}/uploads/checkForExistingUpload`);
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
85
106
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
107
|
+
},
|
|
108
|
+
async downloadArtifactsZip(baseUrl, apiKey, uploadId, results, artifactsPath = './artifacts.zip') {
|
|
109
|
+
try {
|
|
110
|
+
const res = await fetch(`${baseUrl}/results/${uploadId}/download`, {
|
|
111
|
+
body: JSON.stringify({ results }),
|
|
112
|
+
headers: {
|
|
113
|
+
'content-type': 'application/json',
|
|
114
|
+
'x-app-api-key': apiKey,
|
|
115
|
+
},
|
|
116
|
+
method: 'POST',
|
|
117
|
+
});
|
|
118
|
+
if (!res.ok) {
|
|
119
|
+
await this.handleApiError(res, 'Failed to download artifacts');
|
|
120
|
+
}
|
|
121
|
+
// Handle tilde expansion for home directory
|
|
122
|
+
if (artifactsPath.startsWith('~/') || artifactsPath === '~') {
|
|
123
|
+
artifactsPath = artifactsPath.replace(/^~(?=$|\/|\\)/,
|
|
124
|
+
// eslint-disable-next-line unicorn/prefer-module
|
|
125
|
+
require('node:os').homedir());
|
|
99
126
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
127
|
+
// Create directory structure if it doesn't exist
|
|
128
|
+
// eslint-disable-next-line unicorn/prefer-module
|
|
129
|
+
const { dirname } = require('node:path');
|
|
130
|
+
// eslint-disable-next-line unicorn/prefer-module
|
|
131
|
+
const { createWriteStream, mkdirSync } = require('node:fs');
|
|
132
|
+
// eslint-disable-next-line unicorn/prefer-module
|
|
133
|
+
const { finished } = require('node:stream/promises');
|
|
134
|
+
// eslint-disable-next-line unicorn/prefer-module
|
|
135
|
+
const { Readable } = require('node:stream');
|
|
136
|
+
const directory = dirname(artifactsPath);
|
|
137
|
+
if (directory !== '.') {
|
|
138
|
+
try {
|
|
139
|
+
mkdirSync(directory, { recursive: true });
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
// Ignore if directory already exists
|
|
143
|
+
if (error.code !== 'EEXIST') {
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
104
146
|
}
|
|
105
147
|
}
|
|
148
|
+
const fileStream = createWriteStream(artifactsPath, { flags: 'w' });
|
|
149
|
+
await finished(Readable.fromWeb(res.body).pipe(fileStream));
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
153
|
+
throw this.enhanceFetchError(error, `${baseUrl}/results/${uploadId}/download`);
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
106
156
|
}
|
|
107
|
-
const fileStream = createWriteStream(artifactsPath, { flags: 'w' });
|
|
108
|
-
await finished(Readable.fromWeb(res.body).pipe(fileStream));
|
|
109
157
|
},
|
|
110
158
|
async finaliseUpload(config) {
|
|
111
159
|
const { baseUrl, apiKey, id, metadata, path, sha, supabaseSuccess, backblazeSuccess } = config;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
160
|
+
try {
|
|
161
|
+
const res = await fetch(`${baseUrl}/uploads/finaliseUpload`, {
|
|
162
|
+
body: JSON.stringify({
|
|
163
|
+
backblazeSuccess,
|
|
164
|
+
id,
|
|
165
|
+
metadata,
|
|
166
|
+
path, // This is tempPath for TUS uploads
|
|
167
|
+
sha,
|
|
168
|
+
supabaseSuccess,
|
|
169
|
+
}),
|
|
170
|
+
headers: {
|
|
171
|
+
'content-type': 'application/json',
|
|
172
|
+
'x-app-api-key': apiKey,
|
|
173
|
+
},
|
|
174
|
+
method: 'POST',
|
|
175
|
+
});
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
await this.handleApiError(res, 'Failed to finalize upload');
|
|
178
|
+
}
|
|
179
|
+
return res.json();
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
183
|
+
throw this.enhanceFetchError(error, `${baseUrl}/uploads/finaliseUpload`);
|
|
184
|
+
}
|
|
185
|
+
throw error;
|
|
129
186
|
}
|
|
130
|
-
return res.json();
|
|
131
187
|
},
|
|
132
188
|
async getBinaryUploadUrl(baseUrl, apiKey, platform, fileSize) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
189
|
+
try {
|
|
190
|
+
const res = await fetch(`${baseUrl}/uploads/getBinaryUploadUrl`, {
|
|
191
|
+
body: JSON.stringify({ platform, fileSize }),
|
|
192
|
+
headers: {
|
|
193
|
+
'content-type': 'application/json',
|
|
194
|
+
'x-app-api-key': apiKey,
|
|
195
|
+
},
|
|
196
|
+
method: 'POST',
|
|
197
|
+
});
|
|
198
|
+
if (!res.ok) {
|
|
199
|
+
await this.handleApiError(res, 'Failed to get upload URL');
|
|
200
|
+
}
|
|
201
|
+
return res.json();
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
205
|
+
throw this.enhanceFetchError(error, `${baseUrl}/uploads/getBinaryUploadUrl`);
|
|
206
|
+
}
|
|
207
|
+
throw error;
|
|
143
208
|
}
|
|
144
|
-
return res.json();
|
|
145
209
|
},
|
|
146
210
|
async finishLargeFile(baseUrl, apiKey, fileId, partSha1Array) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
211
|
+
try {
|
|
212
|
+
const res = await fetch(`${baseUrl}/uploads/finishLargeFile`, {
|
|
213
|
+
body: JSON.stringify({ fileId, partSha1Array }),
|
|
214
|
+
headers: {
|
|
215
|
+
'content-type': 'application/json',
|
|
216
|
+
'x-app-api-key': apiKey,
|
|
217
|
+
},
|
|
218
|
+
method: 'POST',
|
|
219
|
+
});
|
|
220
|
+
if (!res.ok) {
|
|
221
|
+
await this.handleApiError(res, 'Failed to finish large file');
|
|
222
|
+
}
|
|
223
|
+
return res.json();
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
227
|
+
throw this.enhanceFetchError(error, `${baseUrl}/uploads/finishLargeFile`);
|
|
228
|
+
}
|
|
229
|
+
throw error;
|
|
157
230
|
}
|
|
158
|
-
return res.json();
|
|
159
231
|
},
|
|
160
232
|
async getResultsForUpload(baseUrl, apiKey, uploadId) {
|
|
161
233
|
// TODO: merge with getUploadStatus
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
234
|
+
try {
|
|
235
|
+
const res = await fetch(`${baseUrl}/results/${uploadId}`, {
|
|
236
|
+
headers: { 'x-app-api-key': apiKey },
|
|
237
|
+
});
|
|
238
|
+
if (!res.ok) {
|
|
239
|
+
await this.handleApiError(res, 'Failed to get results');
|
|
240
|
+
}
|
|
241
|
+
return res.json();
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
245
|
+
throw this.enhanceFetchError(error, `${baseUrl}/results/${uploadId}`);
|
|
246
|
+
}
|
|
247
|
+
throw error;
|
|
167
248
|
}
|
|
168
|
-
return res.json();
|
|
169
249
|
},
|
|
170
250
|
async getUploadStatus(baseUrl, apiKey, options) {
|
|
171
251
|
const queryParams = new URLSearchParams();
|
|
@@ -175,28 +255,80 @@ exports.ApiGateway = {
|
|
|
175
255
|
if (options.name) {
|
|
176
256
|
queryParams.append('name', options.name);
|
|
177
257
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
258
|
+
try {
|
|
259
|
+
const response = await fetch(`${baseUrl}/uploads/status?${queryParams}`, {
|
|
260
|
+
headers: {
|
|
261
|
+
'x-app-api-key': apiKey,
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
if (!response.ok) {
|
|
265
|
+
await this.handleApiError(response, 'Failed to get upload status');
|
|
266
|
+
}
|
|
267
|
+
return response.json();
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
271
|
+
throw this.enhanceFetchError(error, `${baseUrl}/uploads/status?${queryParams}`);
|
|
272
|
+
}
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
async listUploads(baseUrl, apiKey, options = {}) {
|
|
277
|
+
const queryParams = new URLSearchParams();
|
|
278
|
+
if (options.name) {
|
|
279
|
+
queryParams.append('name', options.name);
|
|
280
|
+
}
|
|
281
|
+
if (options.from) {
|
|
282
|
+
queryParams.append('from', options.from);
|
|
283
|
+
}
|
|
284
|
+
if (options.to) {
|
|
285
|
+
queryParams.append('to', options.to);
|
|
286
|
+
}
|
|
287
|
+
if (options.limit !== undefined) {
|
|
288
|
+
queryParams.append('limit', String(options.limit));
|
|
289
|
+
}
|
|
290
|
+
if (options.offset !== undefined) {
|
|
291
|
+
queryParams.append('offset', String(options.offset));
|
|
292
|
+
}
|
|
293
|
+
const url = `${baseUrl}/uploads/list?${queryParams}`;
|
|
294
|
+
try {
|
|
295
|
+
const response = await fetch(url, {
|
|
296
|
+
headers: {
|
|
297
|
+
'x-app-api-key': apiKey,
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
if (!response.ok) {
|
|
301
|
+
await this.handleApiError(response, 'Failed to list uploads');
|
|
302
|
+
}
|
|
303
|
+
return response.json();
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
307
|
+
throw this.enhanceFetchError(error, url);
|
|
308
|
+
}
|
|
309
|
+
throw error;
|
|
185
310
|
}
|
|
186
|
-
return response.json();
|
|
187
311
|
},
|
|
188
312
|
async uploadFlow(baseUrl, apiKey, testFormData) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
313
|
+
try {
|
|
314
|
+
const res = await fetch(`${baseUrl}/uploads/flow`, {
|
|
315
|
+
body: testFormData,
|
|
316
|
+
headers: {
|
|
317
|
+
'x-app-api-key': apiKey,
|
|
318
|
+
},
|
|
319
|
+
method: 'POST',
|
|
320
|
+
});
|
|
321
|
+
if (!res.ok) {
|
|
322
|
+
await this.handleApiError(res, 'Failed to upload test flows');
|
|
323
|
+
}
|
|
324
|
+
return res.json();
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
328
|
+
throw this.enhanceFetchError(error, `${baseUrl}/uploads/flow`);
|
|
329
|
+
}
|
|
330
|
+
throw error;
|
|
198
331
|
}
|
|
199
|
-
return res.json();
|
|
200
332
|
},
|
|
201
333
|
/**
|
|
202
334
|
* Generic report download method that handles both junit and allure reports
|
|
@@ -232,51 +364,59 @@ exports.ApiGateway = {
|
|
|
232
364
|
const { endpoint, defaultFilename, notFoundMessage, errorPrefix } = config[reportType];
|
|
233
365
|
const finalReportPath = reportPath || path.resolve(process.cwd(), defaultFilename);
|
|
234
366
|
const url = `${baseUrl}${endpoint}`;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
367
|
+
try {
|
|
368
|
+
// Make the download request
|
|
369
|
+
const res = await fetch(url, {
|
|
370
|
+
headers: {
|
|
371
|
+
'x-app-api-key': apiKey,
|
|
372
|
+
},
|
|
373
|
+
method: 'GET',
|
|
374
|
+
});
|
|
375
|
+
if (!res.ok) {
|
|
376
|
+
const errorText = await res.text();
|
|
377
|
+
if (res.status === 404) {
|
|
378
|
+
throw new Error(notFoundMessage);
|
|
379
|
+
}
|
|
380
|
+
throw new Error(`${errorPrefix}: ${res.status} ${errorText}`);
|
|
246
381
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
// eslint-disable-next-line unicorn/prefer-module
|
|
254
|
-
require('node:os').homedir());
|
|
255
|
-
}
|
|
256
|
-
// Create directory structure if it doesn't exist
|
|
257
|
-
// eslint-disable-next-line unicorn/prefer-module
|
|
258
|
-
const { dirname } = require('node:path');
|
|
259
|
-
// eslint-disable-next-line unicorn/prefer-module
|
|
260
|
-
const { createWriteStream, mkdirSync } = require('node:fs');
|
|
261
|
-
// eslint-disable-next-line unicorn/prefer-module
|
|
262
|
-
const { finished } = require('node:stream/promises');
|
|
263
|
-
// eslint-disable-next-line unicorn/prefer-module
|
|
264
|
-
const { Readable } = require('node:stream');
|
|
265
|
-
const directory = dirname(expandedPath);
|
|
266
|
-
if (directory !== '.') {
|
|
267
|
-
try {
|
|
268
|
-
mkdirSync(directory, { recursive: true });
|
|
382
|
+
// Handle tilde expansion for home directory (applies to all report types)
|
|
383
|
+
let expandedPath = finalReportPath;
|
|
384
|
+
if (finalReportPath.startsWith('~/') || finalReportPath === '~') {
|
|
385
|
+
expandedPath = finalReportPath.replace(/^~/,
|
|
386
|
+
// eslint-disable-next-line unicorn/prefer-module
|
|
387
|
+
require('node:os').homedir());
|
|
269
388
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
389
|
+
// Create directory structure if it doesn't exist
|
|
390
|
+
// eslint-disable-next-line unicorn/prefer-module
|
|
391
|
+
const { dirname } = require('node:path');
|
|
392
|
+
// eslint-disable-next-line unicorn/prefer-module
|
|
393
|
+
const { createWriteStream, mkdirSync } = require('node:fs');
|
|
394
|
+
// eslint-disable-next-line unicorn/prefer-module
|
|
395
|
+
const { finished } = require('node:stream/promises');
|
|
396
|
+
// eslint-disable-next-line unicorn/prefer-module
|
|
397
|
+
const { Readable } = require('node:stream');
|
|
398
|
+
const directory = dirname(expandedPath);
|
|
399
|
+
if (directory !== '.') {
|
|
400
|
+
try {
|
|
401
|
+
mkdirSync(directory, { recursive: true });
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
// Ignore EEXIST errors (directory already exists)
|
|
405
|
+
if (error.code !== 'EEXIST') {
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
274
408
|
}
|
|
275
409
|
}
|
|
410
|
+
// Write the file using streaming for better memory efficiency
|
|
411
|
+
// Use 'w' flag to overwrite existing files instead of failing
|
|
412
|
+
const fileStream = createWriteStream(expandedPath, { flags: 'w' });
|
|
413
|
+
await finished(Readable.fromWeb(res.body).pipe(fileStream));
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
417
|
+
throw this.enhanceFetchError(error, url);
|
|
418
|
+
}
|
|
419
|
+
throw error;
|
|
276
420
|
}
|
|
277
|
-
// Write the file using streaming for better memory efficiency
|
|
278
|
-
// Use 'w' flag to overwrite existing files instead of failing
|
|
279
|
-
const fileStream = createWriteStream(expandedPath, { flags: 'w' });
|
|
280
|
-
await finished(Readable.fromWeb(res.body).pipe(fileStream));
|
|
281
421
|
},
|
|
282
422
|
};
|