@eluvio/elv-client-js 4.2.13 → 4.2.15

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 (52) hide show
  1. package/dist/ElvClient-min.js +1 -1
  2. package/dist/ElvClient-node-min.js +1 -1
  3. package/dist/ElvFrameClient-min.js +1 -1
  4. package/dist/ElvPermissionsClient-min.js +1 -1
  5. package/dist/ElvWalletClient-min.js +1 -1
  6. package/dist/ElvWalletClient-node-min.js +1 -1
  7. package/dist/src/AuthorizationClient.js +723 -710
  8. package/dist/src/ContentObjectAudit.js +56 -56
  9. package/dist/src/Crypto.js +85 -85
  10. package/dist/src/ElvClient.js +502 -530
  11. package/dist/src/ElvWallet.js +28 -30
  12. package/dist/src/EthClient.js +311 -311
  13. package/dist/src/FrameClient.js +65 -64
  14. package/dist/src/HttpClient.js +60 -60
  15. package/dist/src/Id.js +2 -1
  16. package/dist/src/PermissionsClient.js +487 -499
  17. package/dist/src/RemoteSigner.js +178 -123
  18. package/dist/src/UserProfileClient.js +374 -392
  19. package/dist/src/Utils.js +66 -69
  20. package/dist/src/Validation.js +10 -10
  21. package/dist/src/client/ABRPublishing.js +239 -239
  22. package/dist/src/client/AccessGroups.js +474 -477
  23. package/dist/src/client/ContentAccess.js +1713 -1708
  24. package/dist/src/client/ContentManagement.js +871 -871
  25. package/dist/src/client/Contracts.js +736 -582
  26. package/dist/src/client/Files.js +684 -700
  27. package/dist/src/client/LiveConf.js +6 -1
  28. package/dist/src/client/LiveStream.js +686 -722
  29. package/dist/src/client/NFT.js +14 -14
  30. package/dist/src/client/NTP.js +84 -84
  31. package/dist/src/client/Shares.js +60 -53
  32. package/dist/src/walletClient/ClientMethods.js +951 -977
  33. package/dist/src/walletClient/Notifications.js +14 -14
  34. package/dist/src/walletClient/Profile.js +66 -66
  35. package/dist/src/walletClient/Utils.js +15 -15
  36. package/dist/src/walletClient/index.js +584 -581
  37. package/package.json +3 -2
  38. package/src/ElvClient.js +2 -1
  39. package/src/FrameClient.js +3 -0
  40. package/src/LogMessage.js +1 -1
  41. package/src/RemoteSigner.js +17 -3
  42. package/src/client/ABRPublishing.js +1 -1
  43. package/src/client/ContentAccess.js +17 -13
  44. package/src/client/Contracts.js +88 -7
  45. package/src/walletClient/index.js +15 -4
  46. package/utilities/ChannelCreate.js +1 -1
  47. package/utilities/CompositionCreate.js +517 -0
  48. package/utilities/LibraryDownloadMp4.js +371 -0
  49. package/utilities/MezDownloadMp4.js +177 -0
  50. package/utilities/lib/DownloadFile.js +88 -0
  51. package/utilities/lib/FrameAccurateVideo.js +431 -0
  52. package/utilities/lib/concerns/Metadata.js +2 -1
@@ -0,0 +1,371 @@
1
+ // Utility that accepts a library ID and generates a download URL for each object inside of it
2
+ const R = require("ramda");
3
+ const { ModOpt, NewOpt } = require("./lib/options");
4
+ const Utility = require("./lib/Utility");
5
+ const { PublicMetadataPathArrayModel } = require("./lib/models/PublicMetadataPath");
6
+
7
+ const JSONConcern = require("./lib/concerns/JSON");
8
+ const ArgLibraryId = require("./lib/concerns/ArgLibraryId");
9
+ const Metadata = require("./lib/concerns/Metadata");
10
+ const FabricObject = require("./lib/concerns/FabricObject");
11
+
12
+ const path = require("path");
13
+ const fs = require("fs");
14
+ const https = require("https");
15
+
16
+ class LibraryDownloadMp4 extends Utility {
17
+ blueprint() {
18
+ return {
19
+ concerns: [JSONConcern, ArgLibraryId, Metadata, FabricObject],
20
+ options: [
21
+ ModOpt("libraryId", { demand: true }),
22
+ NewOpt("filter", {
23
+ descTemplate:
24
+ "JSON expression (or path to JSON file if starting with '@') to filter objects by (public) metadata",
25
+ type: "string",
26
+ }),
27
+ NewOpt("date", {
28
+ descTemplate: "include latest commit date/time if available",
29
+ type: "boolean",
30
+ }),
31
+ NewOpt("fields", {
32
+ coerce: PublicMetadataPathArrayModel,
33
+ descTemplate:
34
+ "Path(s) for additional metadata values to include (each must start with /public/)",
35
+ string: true,
36
+ type: "array",
37
+ }),
38
+ NewOpt("hash", {
39
+ descTemplate: "include latest version hash",
40
+ type: "boolean",
41
+ }),
42
+ NewOpt("name", {
43
+ descTemplate: "include object name if available",
44
+ type: "boolean",
45
+ }),
46
+ NewOpt("size", {
47
+ descTemplate: "include object total size",
48
+ type: "boolean",
49
+ }),
50
+ NewOpt("offering", {
51
+ descTemplate: "Offering name for download URL",
52
+ type: "string",
53
+ }),
54
+ NewOpt("format", {
55
+ descTemplate: "Format for download URL (default mp4)",
56
+ type: "string",
57
+ }),
58
+ NewOpt("downloadDir", {
59
+ descTemplate: "Directory to save files",
60
+ type: "string",
61
+ }),
62
+ NewOpt("failLog", {
63
+ descTemplate: "Write failures to a JSON file",
64
+ type: "string",
65
+ })
66
+ ],
67
+ };
68
+ }
69
+
70
+ header() {
71
+ return `List and download objects for library ${this.args.libraryId}`;
72
+ }
73
+
74
+ sleep(ms) {
75
+ return new Promise((resolve) => setTimeout(resolve, ms));
76
+ }
77
+
78
+ async retry(fn, opts = {}) {
79
+ const {
80
+ retries = 3,
81
+ delay = 1000,
82
+ onRetry = null
83
+ } = opts;
84
+
85
+ let attempt = 0;
86
+ while (attempt < retries) {
87
+ try {
88
+ return await fn();
89
+ } catch (err) {
90
+ attempt++;
91
+ if (attempt >= retries) throw err;
92
+
93
+ if (onRetry) onRetry(err, attempt);
94
+ else this.logger.warn(`Retry ${attempt}/${retries} after error: ${err.message}`);
95
+
96
+ await this.sleep(delay * Math.pow(2, attempt));
97
+ }
98
+ }
99
+ }
100
+
101
+
102
+ async downloadFile(url, filepath) {
103
+ return this.retry(() => {
104
+ return new Promise((resolve, reject) => {
105
+ const startDownload = (currentUrl, redirCount = 0) => {
106
+ if (redirCount > 5) {
107
+ return reject(new Error("Too many redirects"));
108
+ }
109
+
110
+ const req = https.get(currentUrl, (res) => {
111
+ // Handle redirects
112
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
113
+ const nextUrl = res.headers.location.startsWith("http")
114
+ ? res.headers.location
115
+ : new URL(res.headers.location, currentUrl).href;
116
+
117
+ this.logger.log(`Redirected → ${nextUrl}`);
118
+ return startDownload(nextUrl, redirCount + 1);
119
+ }
120
+
121
+ if (res.statusCode !== 200) {
122
+ return reject(new Error(`Download failed (HTTP ${res.statusCode})`));
123
+ }
124
+
125
+ const totalSize = parseInt(res.headers["content-length"] || "0", 10);
126
+ let downloaded = 0;
127
+
128
+ const writeStream = fs.createWriteStream(filepath);
129
+
130
+ // Progress bar
131
+ res.on("data", (chunk) => {
132
+ downloaded += chunk.length;
133
+
134
+ if (totalSize > 0) {
135
+ const pct = (downloaded / totalSize) * 100;
136
+ const barLength = 30;
137
+ const filled = Math.round((pct / 100) * barLength);
138
+ const bar = "█".repeat(filled) + "░".repeat(barLength - filled);
139
+
140
+ process.stdout.write(
141
+ `\r${bar} ${pct.toFixed(1)}% (${(downloaded / 1e6).toFixed(2)}MB/${(totalSize / 1e6).toFixed(2)}MB)`
142
+ );
143
+ } else {
144
+ process.stdout.write(`\rDownloaded ${(downloaded / 1e6).toFixed(2)}MB`);
145
+ }
146
+ });
147
+
148
+ res.on("end", () => process.stdout.write("\n"));
149
+
150
+ res.pipe(writeStream);
151
+
152
+ writeStream.on("finish", () => writeStream.close(resolve));
153
+
154
+ writeStream.on("error", (err) => {
155
+ fs.unlink(filepath, () => reject(err));
156
+ });
157
+ });
158
+
159
+ // -----------------------------
160
+ // Add timeout here
161
+ // -----------------------------
162
+ const timeoutMs = 5000; // 5 seconds
163
+ req.setTimeout(timeoutMs, () => {
164
+ req.abort();
165
+ // Throw a special error for retry logic
166
+ reject(new Error("DOWNLOAD_TIMEOUT"));
167
+ });
168
+
169
+ // Handle request errors
170
+ req.on("error", (err) => {
171
+ if (err.code === "ECONNRESET") {
172
+ reject(new Error("DOWNLOAD_TIMEOUT"));
173
+ } else {
174
+ reject(err);
175
+ }
176
+ });
177
+ };
178
+
179
+ // begin download
180
+ startDownload(url);
181
+ });
182
+ }, {
183
+ retries: 3, // retry up to 3 times
184
+ onRetry: (err, attempt) => {
185
+ if (err.message === "DOWNLOAD_TIMEOUT") {
186
+ this.logger.warn(`Download timed out. Retrying attempt ${attempt}...`);
187
+ } else {
188
+ this.logger.warn(`Download failed: ${err.message}. Retrying attempt ${attempt}...`);
189
+ }
190
+ }
191
+ });
192
+ }
193
+
194
+
195
+ sanitizeFilename(name, fallback) {
196
+ if (!name) return fallback;
197
+ return name
198
+ .replace(/[^a-zA-Z0-9._-]+/g, "_")
199
+ .replace(/_+/g, "_")
200
+ .substring(0, 180);
201
+ }
202
+
203
+ async processObject(e, client, libraryId, format, offering, targetDir, failedDownloads) {
204
+ const objectId = e.objectId;
205
+ const objectName = R.path(["metadata", "public", "name"], e) || objectId;
206
+
207
+ this.logger.log(`\n--- Processing ${objectName} (${objectId}) ---`);
208
+
209
+ const formattedObj = { object_id: objectId, name: objectName };
210
+
211
+ // Skip existing
212
+ const existing = fs.readdirSync(targetDir).find((f) => f.includes(objectId));
213
+ if (existing) {
214
+ this.logger.log(`Skipping ${objectName}: already exists (${existing})`);
215
+ formattedObj.download_url = "SKIPPED_ALREADY_EXISTS";
216
+ return formattedObj;
217
+ }
218
+
219
+ try {
220
+ // Version hash
221
+ const versionHash = await this.retry(() =>
222
+ this.concerns.FabricObject.latestVersionHash({ libraryId, objectId })
223
+ );
224
+
225
+ // Start job
226
+ const response = await this.retry(() =>
227
+ client.MakeFileServiceRequest({
228
+ versionHash,
229
+ path: "/call/media/files",
230
+ method: "POST",
231
+ body: { format, offering },
232
+ })
233
+ );
234
+
235
+ const jobId = response.job_id;
236
+ this.logger.log(`Started job ${jobId}`);
237
+
238
+ // Poll job
239
+ let status;
240
+ let lastProgress = -1;
241
+
242
+ do {
243
+ await this.sleep(2000);
244
+
245
+ status = await this.retry(() =>
246
+ client.MakeFileServiceRequest({
247
+ versionHash,
248
+ path: `/call/media/files/${jobId}`,
249
+ })
250
+ );
251
+
252
+ const progress = status?.progress || 0;
253
+ if (progress !== lastProgress) {
254
+ process.stdout.write(`Progress: ${progress.toFixed(1)}%\r`);
255
+ lastProgress = progress;
256
+ }
257
+ } while (status?.status !== "completed");
258
+
259
+ process.stdout.write("\n");
260
+
261
+ const filename = this.sanitizeFilename(
262
+ status.filename,
263
+ `${objectId}.mp4`
264
+ );
265
+
266
+ const outputFile = path.join(targetDir, filename);
267
+
268
+ const downloadUrl = await this.retry(() =>
269
+ client.FabricUrl({
270
+ versionHash,
271
+ call: `/media/files/${jobId}/download`,
272
+ service: "files",
273
+ queryParams: {
274
+ "header-x_set_content_disposition": `attachment; filename=${filename}`,
275
+ },
276
+ })
277
+ );
278
+
279
+ formattedObj.download_url = downloadUrl;
280
+
281
+ // NOW using HTTPS downloader
282
+ this.logger.log(`Downloading → ${outputFile}`);
283
+ await this.downloadFile(downloadUrl, outputFile);
284
+
285
+ this.logger.log(`✔ Completed: ${outputFile}`);
286
+ return formattedObj;
287
+
288
+ } catch (err) {
289
+ this.logger.error(`FAILED: ${objectId} - ${err.message}`);
290
+
291
+ failedDownloads.push({
292
+ object_id: objectId,
293
+ name: objectName,
294
+ error: err.message,
295
+ timestamp: new Date().toISOString(),
296
+ });
297
+
298
+ return formattedObj;
299
+ }
300
+ }
301
+
302
+ async body() {
303
+ const libraryId = this.args.libraryId;
304
+ const format = this.args.format || "mp4";
305
+ const offering = this.args.offering || "default";
306
+
307
+ const filter =
308
+ this.args.filter &&
309
+ this.concerns.JSON.parseStringOrFile({ strOrPath: this.args.filter });
310
+
311
+ if (!this.args.fields) this.args.fields = [];
312
+
313
+ const select = ["/public/name", ...this.args.fields];
314
+
315
+ let objectList = await this.concerns.ArgLibraryId.libObjectList({
316
+ filterOptions: { select, filter },
317
+ });
318
+
319
+ this.logger.log(`Found ${objectList.length} object(s)\n`);
320
+
321
+ const client = await this.concerns.Client.get();
322
+ const failedDownloads = [];
323
+
324
+ const targetDir = this.args.downloadDir
325
+ ? path.resolve(this.args.downloadDir)
326
+ : process.cwd();
327
+
328
+ if (!fs.existsSync(targetDir))
329
+ fs.mkdirSync(targetDir, { recursive: true });
330
+
331
+ // Sequential downloads
332
+ const results = [];
333
+ for (const obj of objectList) {
334
+ const r = await this.processObject(
335
+ obj,
336
+ client,
337
+ libraryId,
338
+ format,
339
+ offering,
340
+ targetDir,
341
+ failedDownloads
342
+ );
343
+ results.push(r);
344
+ }
345
+
346
+ // Summary
347
+ this.logger.log("\n=== SUMMARY ===");
348
+ this.logger.log(`Processed: ${objectList.length}`);
349
+ this.logger.log(`Successful: ${results.length - failedDownloads.length}`);
350
+ this.logger.log(`Failed: ${failedDownloads.length}`);
351
+
352
+ if (failedDownloads.length > 0) {
353
+ this.logger.warn("\n=== FAILED DOWNLOADS ===");
354
+ this.logger.logTable({ list: failedDownloads });
355
+
356
+ if (this.args.failLog) {
357
+ const failPath = path.resolve(this.args.failLog);
358
+ fs.writeFileSync(failPath, JSON.stringify(failedDownloads, null, 2));
359
+ this.logger.warn(`Failures written to: ${failPath}`);
360
+ }
361
+ }
362
+
363
+ return { results, failedDownloads };
364
+ }
365
+ }
366
+
367
+ if (require.main === module) {
368
+ Utility.cmdLineInvoke(LibraryDownloadMp4);
369
+ } else {
370
+ module.exports = LibraryDownloadMp4;
371
+ }
@@ -0,0 +1,177 @@
1
+ // Utility that accepts an object ID and generates a download URL
2
+
3
+ const { NewOpt } = require("./lib/options");
4
+ const Utility = require("./lib/Utility");
5
+ const path = require("path");
6
+ const fs = require("fs");
7
+ const https = require("https");
8
+
9
+ const ArgOutfile = require("./lib/concerns/ArgOutfile");
10
+ const ExistObj = require("./lib/concerns/ExistObj");
11
+ const FabricObject = require("./lib/concerns/FabricObject");
12
+ const DownloadFile = require("./lib/downloadFile");
13
+
14
+ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
15
+ const sanitizeFilename = (name, fallback) => {
16
+ if (!name) return fallback;
17
+ return name
18
+ .replace(/\s+/g, "_")
19
+ .replace(/\//g, "_")
20
+ .replace(/ - /g, "-")
21
+ };
22
+
23
+ class MezDownloadMp4 extends Utility {
24
+ blueprint() {
25
+ return {
26
+ concerns: [ArgOutfile, ExistObj, FabricObject],
27
+ options: [
28
+ NewOpt("versionHash", {
29
+ descTemplate: "specific versionHash to download (optional)",
30
+ type: "string"
31
+ }),
32
+ NewOpt("offering", {
33
+ descTemplate: "Offering name to use, defaults to 'default'",
34
+ type: "string"
35
+ }),
36
+ NewOpt("format", {
37
+ descTemplate: "Format to request (default mp4)",
38
+ type: "string"
39
+ }),
40
+ NewOpt("downloadDir", {
41
+ descTemplate: "Directory to save downloaded file",
42
+ type: "string"
43
+ })
44
+ ]
45
+ };
46
+ }
47
+
48
+ header() {
49
+ return `Download file for object ${this.args.objectId}`;
50
+ }
51
+
52
+ async body() {
53
+ const { libraryId, objectId } = await this.concerns.ExistObj.argsProc();
54
+ const client = await this.concerns.Client.get();
55
+
56
+ // -----------------------
57
+ // Determine versionHash
58
+ // -----------------------
59
+ const versionHash = await this.concerns.FabricObject.latestVersionHash({
60
+ libraryId,
61
+ objectId
62
+ });
63
+
64
+ const offeringName = this.args.offering || "default";
65
+ const format = this.args.format || "mp4";
66
+
67
+ try {
68
+ // Start media download job
69
+ const response = await client.MakeFileServiceRequest({
70
+ versionHash,
71
+ path: "/call/media/files",
72
+ method: "POST",
73
+ body: {
74
+ format,
75
+ offering: offeringName
76
+ }
77
+ });
78
+
79
+ const jobId = response.job_id;
80
+
81
+ // Poll job progress
82
+ let status;
83
+ do {
84
+ await sleep(2000);
85
+ status = await client.MakeFileServiceRequest({
86
+ versionHash,
87
+ path: `/call/media/files/${jobId}`
88
+ });
89
+
90
+ this.logger.log(`${(status?.progress || 0).toFixed(1)} / 100`);
91
+ } while (status?.status !== "completed");
92
+
93
+ // Clean filename
94
+ const filename = sanitizeFilename(status.filename, "file_download");
95
+
96
+ // Build download URL
97
+ const downloadUrl = await client.FabricUrl({
98
+ versionHash,
99
+ call: `/media/files/${jobId}/download`,
100
+ service: "files",
101
+ queryParams: {
102
+ "header-x_set_content_disposition": `attachment; filename=${filename}`
103
+ }
104
+ });
105
+
106
+ const output = {
107
+ libraryId,
108
+ objectId,
109
+ versionHash,
110
+ jobId,
111
+ filename,
112
+ downloadUrl
113
+ };
114
+
115
+ // Display
116
+ this.logger.log("\n=== Download Info ===\n");
117
+ this.logger.logTable([{
118
+ "Version Hash": output.versionHash,
119
+ "Filename": output.filename
120
+ }]);
121
+ this.logger.log("Download URL:\n", output.downloadUrl, "\n");
122
+
123
+ // -----------------------
124
+ // HTTPS download with progress bar
125
+ // -----------------------
126
+ if (downloadUrl) {
127
+
128
+ const targetDir = this.args.downloadDir
129
+ ? path.resolve(this.args.downloadDir)
130
+ : process.cwd();
131
+
132
+ if (!fs.existsSync(targetDir)) {
133
+ fs.mkdirSync(targetDir, { recursive: true });
134
+ }
135
+
136
+ const outputFile = path.join(targetDir, output.filename || "download.mp4");
137
+
138
+ this.logger.log(`Downloading via https → ${outputFile}\n`);
139
+
140
+ try {
141
+ await DownloadFile({
142
+ url: downloadUrl,
143
+ dest: outputFile,
144
+ logger: this.logger,
145
+ maxRedirects: 5,
146
+ });
147
+
148
+ this.logger.log(`\nDownload complete: ${outputFile}`);
149
+ } catch (err) {
150
+ this.logger.error("\nHTTPS download failed:", err.message);
151
+ }
152
+ }
153
+
154
+ // -----------------------
155
+ // Outfile support
156
+ // -----------------------
157
+ if (this.args.outfile) {
158
+ if (this.args.json) {
159
+ this.concerns.ArgOutfile.writeJson({ obj: output });
160
+ } else {
161
+ this.concerns.ArgOutfile.writeTable({ list: [output] });
162
+ }
163
+ }
164
+
165
+ } catch (error) {
166
+ this.logger.error(error);
167
+ this.logger.error(JSON.stringify(error, null, 2));
168
+ throw error;
169
+ }
170
+ }
171
+ }
172
+
173
+ if (require.main === module) {
174
+ Utility.cmdLineInvoke(MezDownloadMp4);
175
+ } else {
176
+ module.exports = MezDownloadMp4;
177
+ }
@@ -0,0 +1,88 @@
1
+ const fs = require("fs");
2
+ const https = require("https");
3
+
4
+ /**
5
+ * Download a file over HTTPS with redirect handling and progress reporting.
6
+ *
7
+ * @param {Object} params
8
+ * @param {string} params.url - Initial download URL
9
+ * @param {string} params.dest - Destination file path
10
+ * @param {Object} [params.logger=console] - Logger with log/error methods
11
+ * @param {number} [params.maxRedirects=5] - Maximum redirect depth
12
+ * @returns {Promise<void>}
13
+ */
14
+ const DownloadFile = ({
15
+ url,
16
+ dest,
17
+ logger = console,
18
+ maxRedirects = 5
19
+ }) => {
20
+ const download = (currentUrl, redirects = 0) =>
21
+ new Promise((resolve, reject) => {
22
+ if (redirects > maxRedirects) {
23
+ return reject(new Error("Too many redirects"));
24
+ }
25
+
26
+ https.get(currentUrl, res => {
27
+
28
+ // Handle redirects
29
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
30
+ const nextUrl = res.headers.location.startsWith("http")
31
+ ? res.headers.location
32
+ : new URL(res.headers.location, currentUrl).href;
33
+
34
+ logger.log(`Redirected → ${nextUrl}`);
35
+ return resolve(download(nextUrl, redirects + 1));
36
+ }
37
+
38
+ if (res.statusCode !== 200) {
39
+ return reject(new Error(`Download failed (HTTP ${res.statusCode})`));
40
+ }
41
+
42
+ const totalSize = parseInt(res.headers["content-length"] || "0", 10);
43
+ let downloaded = 0;
44
+
45
+ const writeStream = fs.createWriteStream(dest);
46
+
47
+ // Progress reporting
48
+ res.on("data", chunk => {
49
+ downloaded += chunk.length;
50
+
51
+ if (totalSize > 0) {
52
+ const percent = (downloaded / totalSize) * 100;
53
+ const mbDownloaded = (downloaded / (1024 * 1024)).toFixed(2);
54
+ const mbTotal = (totalSize / (1024 * 1024)).toFixed(2);
55
+
56
+ const barLength = 30;
57
+ const filled = Math.round((percent / 100) * barLength);
58
+ const bar = "█".repeat(filled) + "░".repeat(barLength - filled);
59
+
60
+ process.stdout.write(
61
+ `\r${bar} ${percent.toFixed(1)}% (${mbDownloaded} MB / ${mbTotal} MB)`
62
+ );
63
+ } else {
64
+ process.stdout.write(
65
+ `\rDownloaded ${(downloaded / (1024 * 1024)).toFixed(2)} MB`
66
+ );
67
+ }
68
+ });
69
+
70
+ res.on("end", () => process.stdout.write("\n"));
71
+
72
+ res.pipe(writeStream);
73
+
74
+ writeStream.on("finish", () => {
75
+ writeStream.close(resolve);
76
+ });
77
+
78
+ writeStream.on("error", err => {
79
+ fs.unlink(dest, () => reject(err));
80
+ });
81
+
82
+ }).on("error", reject);
83
+ });
84
+
85
+ return download(url);
86
+ };
87
+
88
+ module.exports = DownloadFile;