@lynker-desktop/electron-sdk 0.0.9-alpha.59 → 0.0.9-alpha.60

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.
@@ -65,6 +65,8 @@ export interface DownloadResult {
65
65
  declare class Downloader {
66
66
  /** 正在下载的 URL 集合(避免重复下载) */
67
67
  private _downloadingUrls;
68
+ /** 上一次选择的保存文件夹路径 */
69
+ private _lastSelectedDir;
68
70
  constructor();
69
71
  /**
70
72
  * 下载文件
@@ -99,9 +101,52 @@ declare class Downloader {
99
101
  * 显示保存对话框
100
102
  * @param url 下载 URL
101
103
  * @param options 下载选项
104
+ * @param defaultExt 默认扩展名(可选,用于 base64 URL)
102
105
  * @returns Promise<string | null> 用户选择的保存路径,如果取消则返回 null
103
106
  */
104
107
  private _showSaveDialog;
108
+ /**
109
+ * 检测并处理 base64 data URL
110
+ * @param url 资源URL
111
+ * @returns 如果是 base64 URL,返回 true、文件扩展名、MIME 类型和数据;否则返回 false
112
+ */
113
+ private _isBase64DataUrl;
114
+ /**
115
+ * 保存 base64 数据到文件
116
+ * @param base64Data base64 编码的数据
117
+ * @param filePath 目标文件路径
118
+ */
119
+ private _saveBase64ToFile;
120
+ /**
121
+ * 准备输出路径(确保目录存在、检查文件是否已存在)
122
+ * @param outputPath 输出路径
123
+ * @param options 下载选项
124
+ * @returns 如果文件已存在且不允许覆盖,返回结果;否则返回 null
125
+ */
126
+ private _prepareOutputPath;
127
+ /**
128
+ * 创建成功结果
129
+ * @param filePath 文件路径
130
+ * @param size 文件大小
131
+ * @returns DownloadResult
132
+ */
133
+ private _createSuccessResult;
134
+ /**
135
+ * 创建错误结果
136
+ * @param error 错误信息
137
+ * @returns DownloadResult
138
+ */
139
+ private _createErrorResult;
140
+ /**
141
+ * 处理 base64 data URL 的下载
142
+ * @param url base64 data URL
143
+ * @param base64Info base64 信息
144
+ * @param outputPath 已确定的输出路径
145
+ * @param options 下载选项
146
+ * @param progressCallback 进度回调
147
+ * @returns Promise<DownloadResult> 下载结果
148
+ */
149
+ private _handleBase64Download;
105
150
  /**
106
151
  * 生成唯一的文件名(如果文件已存在,添加数字后缀)
107
152
  * @param dir 目录路径
@@ -109,10 +154,17 @@ declare class Downloader {
109
154
  * @returns 唯一的文件名
110
155
  */
111
156
  private _generateUniqueFileName;
157
+ /**
158
+ * 从 URL 中提取文件名
159
+ * @param url 下载 URL
160
+ * @returns 文件名
161
+ */
162
+ private _extractFileNameFromUrl;
112
163
  /**
113
164
  * 解析输出路径
114
165
  * @param url 下载 URL
115
166
  * @param options 下载选项
167
+ * @param defaultExt 默认扩展名(可选,用于 base64 URL)
116
168
  * @returns 输出文件路径
117
169
  */
118
170
  private _resolveOutputPath;
@@ -1 +1 @@
1
- {"version":3,"file":"downloader.d.ts","sourceRoot":"","sources":["../../src/main/downloader.ts"],"names":[],"mappings":"AAuBA;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG,CAAC,QAAQ,EAAE;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACf,KAAK,IAAI,CAAC;AAEX;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,sBAAsB;IACtB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,mCAAmC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0BAA0B;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2BAA2B;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,sBAAsB;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wBAAwB;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW;IACX,KAAK,CAAC,EAAE;QACN,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,mBAAmB;IACnB,UAAU,CAAC,EAAE,wBAAwB,CAAC;IACtC,mBAAmB;IACnB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,mBAAmB;IACnB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,gBAAgB;IAChB,kBAAkB,CAAC,EAAE;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACjC,cAAc,CAAC,EAAE,OAAO,CAAC;QACzB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,WAAW;IACX,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,cAAM,UAAU;IACd,2BAA2B;IAC3B,OAAO,CAAC,gBAAgB,CAAuC;;IAiC/D;;;;;;OAMG;IACU,QAAQ,CACnB,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,eAAoB,EAC7B,UAAU,CAAC,EAAE,wBAAwB,GACpC,OAAO,CAAC,cAAc,CAAC;IA+J1B;;;;OAIG;IACI,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAUnC;;OAEG;IACI,SAAS,IAAI,IAAI;IAOxB;;;;OAIG;IACI,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAI1C;;;OAGG;IACI,kBAAkB,IAAI,MAAM,EAAE;IAIrC;;;;;OAKG;YACW,eAAe;IAmE7B;;;;;OAKG;IACH,OAAO,CAAC,uBAAuB;IAyB/B;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;CA8B3B;AAOD;;;GAGG;AACH,wBAAgB,aAAa,IAAI,UAAU,CAK1C;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,eAAoB,EAC7B,UAAU,CAAC,EAAE,wBAAwB,GACpC,OAAO,CAAC,cAAc,CAAC,CAGzB"}
1
+ {"version":3,"file":"downloader.d.ts","sourceRoot":"","sources":["../../src/main/downloader.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,21 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import mime from 'mime-types';
3
4
  import { DownloaderHelper } from 'node-downloader-helper';
4
- import { ipcMain, BrowserWindow, dialog } from 'electron';
5
+ import { ipcMain, app, BrowserWindow, dialog } from 'electron';
5
6
 
6
7
  /**
7
8
  * 最大重定向次数
8
9
  */
9
10
  const MAX_REDIRECTS = 5;
11
+ /**
12
+ * 默认文件扩展名
13
+ */
14
+ const DEFAULT_EXT = '.bin';
15
+ /**
16
+ * 默认 MIME 类型
17
+ */
18
+ const DEFAULT_MIME_TYPE = 'application/octet-stream';
10
19
  /**
11
20
  * 默认超时时间(毫秒):10 分钟
12
21
  */
@@ -18,6 +27,14 @@ const DEFAULT_RETRY = {
18
27
  maxRetries: 3,
19
28
  delay: 1000
20
29
  };
30
+ /**
31
+ * 默认文件名
32
+ */
33
+ const DEFAULT_FILENAME = 'download';
34
+ /**
35
+ * 最大文件名计数器(防止无限循环)
36
+ */
37
+ const MAX_FILENAME_COUNTER = 10000;
21
38
  /**
22
39
  * 下载器类:提供文件下载功能
23
40
  */
@@ -25,6 +42,8 @@ class Downloader {
25
42
  constructor() {
26
43
  /** 正在下载的 URL 集合(避免重复下载) */
27
44
  this._downloadingUrls = new Map();
45
+ /** 上一次选择的保存文件夹路径 */
46
+ this._lastSelectedDir = null;
28
47
  ipcMain.handle(`core:dowloader`, async (event, options) => {
29
48
  try {
30
49
  const { id, url } = options;
@@ -64,52 +83,40 @@ class Downloader {
64
83
  async download(url, options = {}, onProgress) {
65
84
  // 检查是否正在下载,避免重复下载
66
85
  if (this._downloadingUrls.has(url)) {
67
- return {
68
- success: false,
69
- error: `资源正在下载中: ${url}`
70
- };
86
+ return this._createErrorResult(`资源正在下载中: ${url}`);
71
87
  }
72
88
  // 合并进度回调:优先使用传入的 onProgress,其次使用 options.onProgress
73
89
  const progressCallback = onProgress || options.onProgress;
74
- // 如果需要显示保存对话框,先让用户选择保存路径
90
+ // 先检查是否是 base64 data URL(用于确定默认扩展名)
91
+ const base64Info = this._isBase64DataUrl(url);
92
+ const defaultExt = base64Info.isBase64 && base64Info.ext ? base64Info.ext : undefined;
93
+ // 确定输出路径
75
94
  let outputPath;
76
- if (options.showSaveDialog) {
77
- try {
78
- const savePath = await this._showSaveDialog(url, options);
95
+ try {
96
+ if (options.showSaveDialog) {
97
+ const savePath = await this._showSaveDialog(url, options, defaultExt);
79
98
  if (!savePath) {
80
- // 用户取消了保存对话框
81
- return {
82
- success: false,
83
- error: '用户取消了保存操作'
84
- };
99
+ return this._createErrorResult('用户取消了保存操作');
85
100
  }
86
101
  outputPath = savePath;
87
102
  }
88
- catch (error) {
89
- return {
90
- success: false,
91
- error: error instanceof Error ? error.message : '显示保存对话框失败'
92
- };
103
+ else {
104
+ outputPath = this._resolveOutputPath(url, options, defaultExt);
93
105
  }
94
106
  }
95
- else {
96
- // 确定输出路径
97
- outputPath = this._resolveOutputPath(url, options);
107
+ catch (error) {
108
+ return this._createErrorResult(error instanceof Error ? error.message : '确定输出路径失败');
109
+ }
110
+ // 准备输出路径(确保目录存在、检查文件是否已存在)
111
+ const pathCheckResult = await this._prepareOutputPath(outputPath, options);
112
+ if (pathCheckResult) {
113
+ return pathCheckResult;
98
114
  }
99
115
  const downloadDir = path.dirname(outputPath);
100
116
  const downloadFileName = path.basename(outputPath);
101
- // 确保目录存在
102
- if (!fs.existsSync(downloadDir)) {
103
- fs.mkdirSync(downloadDir, { recursive: true });
104
- }
105
- // 如果文件已存在且不允许覆盖,直接返回
106
- if (fs.existsSync(outputPath) && !(options.override ?? true)) {
107
- const stats = fs.statSync(outputPath);
108
- return {
109
- success: true,
110
- filePath: outputPath,
111
- size: stats.size
112
- };
117
+ // 如果是 base64 data URL,使用已确定的输出路径进行处理
118
+ if (base64Info.isBase64 && base64Info.ext && base64Info.mimeType && base64Info.data) {
119
+ return await this._handleBase64Download(url, { ext: base64Info.ext, mimeType: base64Info.mimeType, data: base64Info.data }, outputPath, options, progressCallback);
113
120
  }
114
121
  // 使用临时文件下载,避免下载中断时文件不完整
115
122
  const tempFilePath = `${outputPath}.cache`;
@@ -162,13 +169,9 @@ class Downloader {
162
169
  reject(error);
163
170
  }
164
171
  else {
165
- // 获取文件大小
172
+ // 获取文件大小并返回结果
166
173
  const stats = fs.statSync(outputPath);
167
- const result = {
168
- success: true,
169
- filePath: outputPath,
170
- size: stats.size
171
- };
174
+ const result = this._createSuccessResult(outputPath, stats.size);
172
175
  if (options.onComplete) {
173
176
  options.onComplete(outputPath);
174
177
  }
@@ -204,10 +207,7 @@ class Downloader {
204
207
  handleError(error);
205
208
  });
206
209
  }).catch((error) => {
207
- return {
208
- success: false,
209
- error: error instanceof Error ? error.message : '未知错误'
210
- };
210
+ return this._createErrorResult(error instanceof Error ? error.message : '未知错误');
211
211
  });
212
212
  }
213
213
  /**
@@ -252,9 +252,10 @@ class Downloader {
252
252
  * 显示保存对话框
253
253
  * @param url 下载 URL
254
254
  * @param options 下载选项
255
+ * @param defaultExt 默认扩展名(可选,用于 base64 URL)
255
256
  * @returns Promise<string | null> 用户选择的保存路径,如果取消则返回 null
256
257
  */
257
- async _showSaveDialog(url, options) {
258
+ async _showSaveDialog(url, options, defaultExt) {
258
259
  // 获取默认文件名
259
260
  let defaultFileName;
260
261
  if (options.defaultPath) {
@@ -265,15 +266,12 @@ class Downloader {
265
266
  }
266
267
  else {
267
268
  // 尝试从 URL 中提取文件名
268
- try {
269
- const urlObj = new URL(url);
270
- defaultFileName = path.basename(urlObj.pathname) || 'download';
271
- if (!path.extname(defaultFileName)) {
272
- defaultFileName += '.bin';
273
- }
269
+ if (defaultExt) {
270
+ // 如果提供了默认扩展名(base64 情况),使用它
271
+ defaultFileName = `${DEFAULT_FILENAME}.${defaultExt}`;
274
272
  }
275
- catch {
276
- defaultFileName = 'download.bin';
273
+ else {
274
+ defaultFileName = this._extractFileNameFromUrl(url);
277
275
  }
278
276
  }
279
277
  // 获取默认目录
@@ -284,6 +282,10 @@ class Downloader {
284
282
  else if (options.outputDir) {
285
283
  defaultDir = path.resolve(options.outputDir);
286
284
  }
285
+ else {
286
+ // 如果没有指定目录,使用上一次选择的文件夹,如果没有则使用下载目录
287
+ defaultDir = this._lastSelectedDir || app.getPath('downloads');
288
+ }
287
289
  // 如果指定了目录,检查文件是否存在并生成唯一文件名
288
290
  if (defaultDir) {
289
291
  defaultFileName = this._generateUniqueFileName(defaultDir, defaultFileName);
@@ -314,8 +316,157 @@ class Downloader {
314
316
  if (result.canceled || !result.filePath) {
315
317
  return null;
316
318
  }
319
+ // 记录用户选择的文件夹路径,用于下次默认使用
320
+ const selectedDir = path.dirname(result.filePath);
321
+ if (selectedDir) {
322
+ this._lastSelectedDir = selectedDir;
323
+ }
317
324
  return result.filePath;
318
325
  }
326
+ /**
327
+ * 检测并处理 base64 data URL
328
+ * @param url 资源URL
329
+ * @returns 如果是 base64 URL,返回 true、文件扩展名、MIME 类型和数据;否则返回 false
330
+ */
331
+ _isBase64DataUrl(url) {
332
+ if (!url.startsWith('data:')) {
333
+ return { isBase64: false };
334
+ }
335
+ try {
336
+ // 解析 data URL 格式:data:[<mediatype>][;base64],<data>
337
+ const commaIndex = url.indexOf(',');
338
+ if (commaIndex === -1) {
339
+ return { isBase64: false };
340
+ }
341
+ const header = url.substring(0, commaIndex);
342
+ const data = url.substring(commaIndex + 1);
343
+ // 检查是否包含 base64 标识
344
+ if (!header.includes('base64')) {
345
+ return { isBase64: false };
346
+ }
347
+ // 从 mediatype 中提取文件扩展名和 MIME 类型
348
+ // 例如:data:image/png;base64 -> png, image/png
349
+ // 例如:data:image/jpeg;base64 -> jpeg, image/jpeg
350
+ let ext = DEFAULT_EXT.replace(/^\./, '');
351
+ let mimeType = DEFAULT_MIME_TYPE;
352
+ const mimeMatch = header.match(/data:([^;]+)/);
353
+ if (mimeMatch && mimeMatch[1]) {
354
+ mimeType = mimeMatch[1];
355
+ // 使用 mime-types 包从 MIME 类型获取扩展名
356
+ const extension = mime.extension(mimeType);
357
+ if (extension) {
358
+ ext = extension;
359
+ }
360
+ }
361
+ return { isBase64: true, ext, mimeType, data };
362
+ }
363
+ catch (error) {
364
+ return { isBase64: false };
365
+ }
366
+ }
367
+ /**
368
+ * 保存 base64 数据到文件
369
+ * @param base64Data base64 编码的数据
370
+ * @param filePath 目标文件路径
371
+ */
372
+ async _saveBase64ToFile(base64Data, filePath) {
373
+ try {
374
+ // 解码 base64 数据
375
+ const buffer = Buffer.from(base64Data, 'base64');
376
+ // 使用 Promise 版本的 writeFile
377
+ // Buffer 继承自 Uint8Array,可以直接使用
378
+ await fs.promises.writeFile(filePath, buffer);
379
+ }
380
+ catch (error) {
381
+ throw new Error(`保存 base64 文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
382
+ }
383
+ }
384
+ /**
385
+ * 准备输出路径(确保目录存在、检查文件是否已存在)
386
+ * @param outputPath 输出路径
387
+ * @param options 下载选项
388
+ * @returns 如果文件已存在且不允许覆盖,返回结果;否则返回 null
389
+ */
390
+ async _prepareOutputPath(outputPath, options) {
391
+ const downloadDir = path.dirname(outputPath);
392
+ // 确保目录存在
393
+ if (!fs.existsSync(downloadDir)) {
394
+ fs.mkdirSync(downloadDir, { recursive: true });
395
+ }
396
+ // 如果文件已存在且不允许覆盖,直接返回
397
+ if (fs.existsSync(outputPath) && !(options.override ?? true)) {
398
+ const stats = fs.statSync(outputPath);
399
+ return this._createSuccessResult(outputPath, stats.size);
400
+ }
401
+ return null;
402
+ }
403
+ /**
404
+ * 创建成功结果
405
+ * @param filePath 文件路径
406
+ * @param size 文件大小
407
+ * @returns DownloadResult
408
+ */
409
+ _createSuccessResult(filePath, size) {
410
+ return {
411
+ success: true,
412
+ filePath,
413
+ size
414
+ };
415
+ }
416
+ /**
417
+ * 创建错误结果
418
+ * @param error 错误信息
419
+ * @returns DownloadResult
420
+ */
421
+ _createErrorResult(error) {
422
+ return {
423
+ success: false,
424
+ error
425
+ };
426
+ }
427
+ /**
428
+ * 处理 base64 data URL 的下载
429
+ * @param url base64 data URL
430
+ * @param base64Info base64 信息
431
+ * @param outputPath 已确定的输出路径
432
+ * @param options 下载选项
433
+ * @param progressCallback 进度回调
434
+ * @returns Promise<DownloadResult> 下载结果
435
+ */
436
+ async _handleBase64Download(url, base64Info, outputPath, options, progressCallback) {
437
+ if (!base64Info.data || !base64Info.ext) {
438
+ return this._createErrorResult(`无效的 base64 data URL: ${url}`);
439
+ }
440
+ try {
441
+ // 模拟进度回调(base64 数据通常很小,立即完成)
442
+ if (progressCallback) {
443
+ const dataLength = Buffer.from(base64Info.data, 'base64').length;
444
+ progressCallback({
445
+ url,
446
+ downloaded: dataLength,
447
+ total: dataLength,
448
+ percentage: 100,
449
+ speed: 0
450
+ });
451
+ }
452
+ // 保存 base64 数据到文件
453
+ await this._saveBase64ToFile(base64Info.data, outputPath);
454
+ // 获取文件大小
455
+ const stats = fs.statSync(outputPath);
456
+ const result = this._createSuccessResult(outputPath, stats.size);
457
+ if (options.onComplete) {
458
+ options.onComplete(outputPath);
459
+ }
460
+ return result;
461
+ }
462
+ catch (error) {
463
+ const err = error instanceof Error ? error : new Error('保存 base64 文件失败');
464
+ if (options.onError) {
465
+ options.onError(err);
466
+ }
467
+ return this._createErrorResult(err.message);
468
+ }
469
+ }
319
470
  /**
320
471
  * 生成唯一的文件名(如果文件已存在,添加数字后缀)
321
472
  * @param dir 目录路径
@@ -339,40 +490,53 @@ class Downloader {
339
490
  newFileName = `${nameWithoutExt}(${counter})${ext}`;
340
491
  newPath = path.join(dir, newFileName);
341
492
  counter++;
342
- } while (fs.existsSync(newPath) && counter < 10000); // 防止无限循环
493
+ } while (fs.existsSync(newPath) && counter < MAX_FILENAME_COUNTER);
343
494
  return newFileName;
344
495
  }
496
+ /**
497
+ * 从 URL 中提取文件名
498
+ * @param url 下载 URL
499
+ * @returns 文件名
500
+ */
501
+ _extractFileNameFromUrl(url) {
502
+ try {
503
+ const urlObj = new URL(url);
504
+ const fileName = path.basename(urlObj.pathname) || DEFAULT_FILENAME;
505
+ return path.extname(fileName) ? fileName : `${fileName}${DEFAULT_EXT}`;
506
+ }
507
+ catch {
508
+ return `${DEFAULT_FILENAME}${DEFAULT_EXT}`;
509
+ }
510
+ }
345
511
  /**
346
512
  * 解析输出路径
347
513
  * @param url 下载 URL
348
514
  * @param options 下载选项
515
+ * @param defaultExt 默认扩展名(可选,用于 base64 URL)
349
516
  * @returns 输出文件路径
350
517
  */
351
- _resolveOutputPath(url, options) {
518
+ _resolveOutputPath(url, options, defaultExt) {
352
519
  // 如果提供了完整路径,直接使用
353
520
  if (options.outputPath) {
354
521
  return path.resolve(options.outputPath);
355
522
  }
356
- // 确定输出目录
357
- const outputDir = options.outputDir ? path.resolve(options.outputDir) : process.cwd();
523
+ // 确定输出目录:优先使用指定的目录,否则使用 Electron 的下载目录
524
+ const outputDir = options.outputDir
525
+ ? path.resolve(options.outputDir)
526
+ : app.getPath('downloads');
358
527
  // 确定文件名
359
528
  let fileName;
360
529
  if (options.fileName) {
361
530
  fileName = options.fileName;
362
531
  }
363
532
  else {
364
- // 尝试从 URL 中提取文件名
365
- try {
366
- const urlObj = new URL(url);
367
- fileName = path.basename(urlObj.pathname) || 'download';
368
- // 如果没有扩展名,尝试从 Content-Type 或其他方式获取
369
- if (!path.extname(fileName)) {
370
- fileName += '.bin';
371
- }
533
+ // 如果提供了默认扩展名(base64 情况),使用它
534
+ if (defaultExt) {
535
+ fileName = `${DEFAULT_FILENAME}.${defaultExt}`;
372
536
  }
373
- catch {
374
- // 如果 URL 解析失败,使用默认文件名
375
- fileName = 'download.bin';
537
+ else {
538
+ // 尝试从 URL 中提取文件名
539
+ fileName = this._extractFileNameFromUrl(url);
376
540
  }
377
541
  }
378
542
  return path.join(outputDir, fileName);
@@ -1 +1 @@
1
- {"version":3,"file":"downloader.js","sources":["../../src/main/downloader.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { DownloaderHelper } from 'node-downloader-helper';\nimport { ipcMain, dialog, BrowserWindow } from 'electron';\n\n/**\n * 最大重定向次数\n */\nconst MAX_REDIRECTS = 5;\n\n/**\n * 默认超时时间(毫秒):10 分钟\n */\nconst DEFAULT_TIMEOUT = 600000;\n\n/**\n * 默认重试配置\n */\nconst DEFAULT_RETRY = {\n maxRetries: 3,\n delay: 1000\n};\n\n/**\n * 下载进度回调函数类型\n */\nexport type DownloadProgressCallback = (progress: {\n url: string; // 正在下载的 URL\n downloaded: number; // 已下载字节数\n total: number; // 总字节数(如果服务器提供 Content-Length)\n percentage: number; // 下载进度百分比 (0-100),如果 total 未知则为 -1\n speed: number; // 下载速度(字节/秒)\n}) => void;\n\n/**\n * 下载选项\n */\nexport interface DownloadOptions {\n /** 唯一标识(IPC 调用时必需) */\n id?: string;\n /** 输出文件路径(可选,如果不提供则使用 URL 的文件名) */\n outputPath?: string;\n /** 输出目录(可选,如果不提供则使用当前目录) */\n outputDir?: string;\n /** 输出文件名(可选,如果不提供则从 URL 提取) */\n fileName?: string;\n /** 是否覆盖已存在的文件(默认 true) */\n override?: boolean;\n /** 是否弹窗选择保存路径(默认 false) */\n showSaveDialog?: boolean;\n /** 保存对话框的默认文件名(可选) */\n defaultPath?: string;\n /** 超时时间(毫秒,默认 10 分钟) */\n timeout?: number;\n /** 重试配置 */\n retry?: {\n maxRetries?: number;\n delay?: number;\n };\n /** 下载进度回调函数(可选) */\n onProgress?: DownloadProgressCallback;\n /** 下载完成回调函数(可选) */\n onComplete?: (filePath: string) => void;\n /** 下载错误回调函数(可选) */\n onError?: (error: Error) => void;\n /** HTTP 请求选项 */\n httpRequestOptions?: {\n headers?: Record<string, string>;\n followRedirect?: boolean;\n maxRedirects?: number;\n };\n}\n\n/**\n * 下载结果\n */\nexport interface DownloadResult {\n /** 是否成功 */\n success: boolean;\n /** 下载的文件路径 */\n filePath?: string;\n /** 错误信息 */\n error?: string;\n /** 文件大小(字节) */\n size?: number;\n}\n\n/**\n * 下载器类:提供文件下载功能\n */\nclass Downloader {\n /** 正在下载的 URL 集合(避免重复下载) */\n private _downloadingUrls = new Map<string, DownloaderHelper>();\n\n constructor() {\n ipcMain.handle(`core:dowloader`, async (event: Electron.IpcMainInvokeEvent, options: DownloadOptions & { url: string; id: string }) => {\n try {\n const { id, url } = options;\n const sender = event.sender as Electron.WebContents;\n\n // 创建进度回调,通过 IPC 发送进度更新\n const progressCallback: DownloadProgressCallback = (progress) => {\n try {\n if (sender && typeof sender.send === 'function') {\n sender.send(`core:dowloader:progress`, {\n id,\n data: progress\n });\n }\n } catch (error) {\n console.error('发送下载进度失败:', error);\n }\n };\n\n const result = await this.download(url, options, progressCallback);\n return result;\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : '未知错误'\n };\n }\n });\n }\n\n /**\n * 下载文件\n * @param url 要下载的 URL\n * @param options 下载选项\n * @param onProgress 进度回调函数(可选,优先级高于 options.onProgress)\n * @returns Promise<DownloadResult> 下载结果\n */\n public async download(\n url: string,\n options: DownloadOptions = {},\n onProgress?: DownloadProgressCallback\n ): Promise<DownloadResult> {\n // 检查是否正在下载,避免重复下载\n if (this._downloadingUrls.has(url)) {\n return {\n success: false,\n error: `资源正在下载中: ${url}`\n };\n }\n\n // 合并进度回调:优先使用传入的 onProgress,其次使用 options.onProgress\n const progressCallback = onProgress || options.onProgress;\n\n // 如果需要显示保存对话框,先让用户选择保存路径\n let outputPath: string;\n if (options.showSaveDialog) {\n try {\n const savePath = await this._showSaveDialog(url, options);\n if (!savePath) {\n // 用户取消了保存对话框\n return {\n success: false,\n error: '用户取消了保存操作'\n };\n }\n outputPath = savePath;\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : '显示保存对话框失败'\n };\n }\n } else {\n // 确定输出路径\n outputPath = this._resolveOutputPath(url, options);\n }\n const downloadDir = path.dirname(outputPath);\n const downloadFileName = path.basename(outputPath);\n\n // 确保目录存在\n if (!fs.existsSync(downloadDir)) {\n fs.mkdirSync(downloadDir, { recursive: true });\n }\n\n // 如果文件已存在且不允许覆盖,直接返回\n if (fs.existsSync(outputPath) && !(options.override ?? true)) {\n const stats = fs.statSync(outputPath);\n return {\n success: true,\n filePath: outputPath,\n size: stats.size\n };\n }\n\n // 使用临时文件下载,避免下载中断时文件不完整\n const tempFilePath = `${outputPath}.cache`;\n\n return new Promise<DownloadResult>((resolve, reject) => {\n // 创建下载器实例\n const dl = new DownloaderHelper(url, downloadDir, {\n fileName: downloadFileName + '.cache',\n retry: {\n maxRetries: options.retry?.maxRetries ?? DEFAULT_RETRY.maxRetries,\n delay: options.retry?.delay ?? DEFAULT_RETRY.delay\n },\n timeout: options.timeout ?? DEFAULT_TIMEOUT,\n override: true, // 临时文件总是覆盖\n httpRequestOptions: {\n followRedirect: options.httpRequestOptions?.followRedirect ?? true,\n maxRedirects: options.httpRequestOptions?.maxRedirects ?? MAX_REDIRECTS,\n headers: options.httpRequestOptions?.headers\n }\n });\n\n // 记录正在下载的 URL\n this._downloadingUrls.set(url, dl);\n\n // 监听下载进度\n if (progressCallback) {\n dl.on('progress', (stats) => {\n try {\n progressCallback({\n url,\n downloaded: stats.downloaded || 0,\n total: stats.total || 0,\n percentage: stats.progress !== undefined ? Math.round(stats.progress) : -1,\n speed: stats.speed || 0\n });\n } catch (error) {\n // 忽略回调中的错误,避免影响下载\n console.error('进度回调执行失败:', error);\n }\n });\n }\n\n // 监听下载完成\n dl.on('end', () => {\n this._downloadingUrls.delete(url);\n // 将临时文件重命名为最终文件\n fs.rename(tempFilePath, outputPath, (renameErr) => {\n if (renameErr) {\n const error = new Error(`文件重命名失败 from ${tempFilePath} to ${outputPath}: ${renameErr.message}`);\n if (options.onError) {\n options.onError(error);\n }\n reject(error);\n } else {\n // 获取文件大小\n const stats = fs.statSync(outputPath);\n const result: DownloadResult = {\n success: true,\n filePath: outputPath,\n size: stats.size\n };\n if (options.onComplete) {\n options.onComplete(outputPath);\n }\n resolve(result);\n }\n });\n });\n\n // 统一的错误处理函数\n const handleError = (error: Error) => {\n this._downloadingUrls.delete(url);\n // 清理临时文件\n fs.promises.unlink(tempFilePath).catch(() => {\n // 忽略删除失败\n });\n if (options.onError) {\n options.onError(error);\n }\n reject(error);\n };\n\n // 监听下载错误\n dl.on('error', (err) => {\n const error = err instanceof Error ? err : new Error(`下载失败: ${url}`);\n handleError(error);\n });\n\n // 监听下载停止(取消)\n dl.on('stop', () => {\n const error = new Error(`下载已停止: ${url}`);\n handleError(error);\n });\n\n // 开始下载\n dl.start().catch((err) => {\n const error = err instanceof Error ? err : new Error(`启动下载失败: ${url}`);\n handleError(error);\n });\n }).catch((error): DownloadResult => {\n return {\n success: false,\n error: error instanceof Error ? error.message : '未知错误'\n };\n });\n }\n\n /**\n * 取消下载\n * @param url 要取消的 URL\n * @returns 是否成功取消\n */\n public cancel(url: string): boolean {\n const dl = this._downloadingUrls.get(url);\n if (dl) {\n dl.stop();\n this._downloadingUrls.delete(url);\n return true;\n }\n return false;\n }\n\n /**\n * 取消所有正在进行的下载\n */\n public cancelAll(): void {\n for (const [url, dl] of this._downloadingUrls.entries()) {\n dl.stop();\n }\n this._downloadingUrls.clear();\n }\n\n /**\n * 检查指定 URL 是否正在下载\n * @param url 要检查的 URL\n * @returns 是否正在下载\n */\n public isDownloading(url: string): boolean {\n return this._downloadingUrls.has(url);\n }\n\n /**\n * 获取正在下载的 URL 列表\n * @returns 正在下载的 URL 数组\n */\n public getDownloadingUrls(): string[] {\n return Array.from(this._downloadingUrls.keys());\n }\n\n /**\n * 显示保存对话框\n * @param url 下载 URL\n * @param options 下载选项\n * @returns Promise<string | null> 用户选择的保存路径,如果取消则返回 null\n */\n private async _showSaveDialog(url: string, options: DownloadOptions): Promise<string | null> {\n // 获取默认文件名\n let defaultFileName: string;\n if (options.defaultPath) {\n defaultFileName = path.basename(options.defaultPath);\n } else if (options.fileName) {\n defaultFileName = options.fileName;\n } else {\n // 尝试从 URL 中提取文件名\n try {\n const urlObj = new URL(url);\n defaultFileName = path.basename(urlObj.pathname) || 'download';\n if (!path.extname(defaultFileName)) {\n defaultFileName += '.bin';\n }\n } catch {\n defaultFileName = 'download.bin';\n }\n }\n\n // 获取默认目录\n let defaultDir: string | undefined;\n if (options.defaultPath) {\n defaultDir = path.dirname(options.defaultPath);\n } else if (options.outputDir) {\n defaultDir = path.resolve(options.outputDir);\n }\n\n // 如果指定了目录,检查文件是否存在并生成唯一文件名\n if (defaultDir) {\n defaultFileName = this._generateUniqueFileName(defaultDir, defaultFileName);\n }\n\n // 构建默认路径\n const defaultPath = defaultDir ? path.join(defaultDir, defaultFileName) : defaultFileName;\n\n // 获取当前活动的 BrowserWindow\n const focusedWindow = BrowserWindow.getFocusedWindow();\n const allWindows = BrowserWindow.getAllWindows();\n const parentWindow = focusedWindow || (allWindows.length > 0 ? allWindows[0] : undefined);\n\n // 显示保存对话框\n const dialogOptions = {\n defaultPath,\n title: '保存文件',\n buttonLabel: '保存',\n filters: [\n // 尝试从文件名推断文件类型\n ...(path.extname(defaultFileName) ? [{\n name: '所有文件',\n extensions: ['*']\n }] : [])\n ]\n };\n\n // 根据是否有父窗口调用不同的方法\n const result = parentWindow\n ? await dialog.showSaveDialog(parentWindow, dialogOptions)\n : await dialog.showSaveDialog(dialogOptions);\n\n if (result.canceled || !result.filePath) {\n return null;\n }\n\n return result.filePath;\n }\n\n /**\n * 生成唯一的文件名(如果文件已存在,添加数字后缀)\n * @param dir 目录路径\n * @param fileName 原始文件名\n * @returns 唯一的文件名\n */\n private _generateUniqueFileName(dir: string, fileName: string): string {\n // 分离文件名和扩展名\n const ext = path.extname(fileName);\n const nameWithoutExt = path.basename(fileName, ext);\n\n // 检查原始文件名是否存在\n const originalPath = path.join(dir, fileName);\n if (!fs.existsSync(originalPath)) {\n return fileName;\n }\n\n // 如果存在,尝试添加数字后缀\n let counter = 1;\n let newFileName: string;\n let newPath: string;\n\n do {\n newFileName = `${nameWithoutExt}(${counter})${ext}`;\n newPath = path.join(dir, newFileName);\n counter++;\n } while (fs.existsSync(newPath) && counter < 10000); // 防止无限循环\n\n return newFileName;\n }\n\n /**\n * 解析输出路径\n * @param url 下载 URL\n * @param options 下载选项\n * @returns 输出文件路径\n */\n private _resolveOutputPath(url: string, options: DownloadOptions): string {\n // 如果提供了完整路径,直接使用\n if (options.outputPath) {\n return path.resolve(options.outputPath);\n }\n\n // 确定输出目录\n const outputDir = options.outputDir ? path.resolve(options.outputDir) : process.cwd();\n\n // 确定文件名\n let fileName: string;\n if (options.fileName) {\n fileName = options.fileName;\n } else {\n // 尝试从 URL 中提取文件名\n try {\n const urlObj = new URL(url);\n fileName = path.basename(urlObj.pathname) || 'download';\n // 如果没有扩展名,尝试从 Content-Type 或其他方式获取\n if (!path.extname(fileName)) {\n fileName += '.bin';\n }\n } catch {\n // 如果 URL 解析失败,使用默认文件名\n fileName = 'download.bin';\n }\n }\n\n return path.join(outputDir, fileName);\n }\n}\n\n/**\n * 默认下载器实例(单例)\n */\nlet defaultDownloader: Downloader | null = null;\n\n/**\n * 获取默认下载器实例\n * @returns Downloader 实例\n */\nexport function getDownloader(): Downloader {\n if (!defaultDownloader) {\n defaultDownloader = new Downloader();\n }\n return defaultDownloader;\n}\n\n/**\n * 下载文件的便捷函数\n * @param url 要下载的 URL\n * @param options 下载选项\n * @param onProgress 进度回调函数(可选)\n * @returns Promise<DownloadResult> 下载结果\n */\nexport async function downloadFile(\n url: string,\n options: DownloadOptions = {},\n onProgress?: DownloadProgressCallback\n): Promise<DownloadResult> {\n const downloader = getDownloader();\n return downloader.download(url, options, onProgress);\n}\n\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"downloader.js","sources":["../../src/main/downloader.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport mime from 'mime-types';\nimport { DownloaderHelper } from 'node-downloader-helper';\nimport { ipcMain, dialog, BrowserWindow, app } from 'electron';\n\n/**\n * 最大重定向次数\n */\nconst MAX_REDIRECTS = 5;\n\n/**\n * 默认文件扩展名\n */\nconst DEFAULT_EXT = '.bin';\n\n/**\n * 默认 MIME 类型\n */\nconst DEFAULT_MIME_TYPE = 'application/octet-stream';\n\n/**\n * 默认超时时间(毫秒):10 分钟\n */\nconst DEFAULT_TIMEOUT = 600000;\n\n/**\n * 默认重试配置\n */\nconst DEFAULT_RETRY = {\n maxRetries: 3,\n delay: 1000\n};\n\n/**\n * 默认文件名\n */\nconst DEFAULT_FILENAME = 'download';\n\n/**\n * 最大文件名计数器(防止无限循环)\n */\nconst MAX_FILENAME_COUNTER = 10000;\n\n/**\n * 下载进度回调函数类型\n */\nexport type DownloadProgressCallback = (progress: {\n url: string; // 正在下载的 URL\n downloaded: number; // 已下载字节数\n total: number; // 总字节数(如果服务器提供 Content-Length)\n percentage: number; // 下载进度百分比 (0-100),如果 total 未知则为 -1\n speed: number; // 下载速度(字节/秒)\n}) => void;\n\n/**\n * 下载选项\n */\nexport interface DownloadOptions {\n /** 唯一标识(IPC 调用时必需) */\n id?: string;\n /** 输出文件路径(可选,如果不提供则使用 URL 的文件名) */\n outputPath?: string;\n /** 输出目录(可选,如果不提供则使用当前目录) */\n outputDir?: string;\n /** 输出文件名(可选,如果不提供则从 URL 提取) */\n fileName?: string;\n /** 是否覆盖已存在的文件(默认 true) */\n override?: boolean;\n /** 是否弹窗选择保存路径(默认 false) */\n showSaveDialog?: boolean;\n /** 保存对话框的默认文件名(可选) */\n defaultPath?: string;\n /** 超时时间(毫秒,默认 10 分钟) */\n timeout?: number;\n /** 重试配置 */\n retry?: {\n maxRetries?: number;\n delay?: number;\n };\n /** 下载进度回调函数(可选) */\n onProgress?: DownloadProgressCallback;\n /** 下载完成回调函数(可选) */\n onComplete?: (filePath: string) => void;\n /** 下载错误回调函数(可选) */\n onError?: (error: Error) => void;\n /** HTTP 请求选项 */\n httpRequestOptions?: {\n headers?: Record<string, string>;\n followRedirect?: boolean;\n maxRedirects?: number;\n };\n}\n\n/**\n * 下载结果\n */\nexport interface DownloadResult {\n /** 是否成功 */\n success: boolean;\n /** 下载的文件路径 */\n filePath?: string;\n /** 错误信息 */\n error?: string;\n /** 文件大小(字节) */\n size?: number;\n}\n\n/**\n * 下载器类:提供文件下载功能\n */\nclass Downloader {\n /** 正在下载的 URL 集合(避免重复下载) */\n private _downloadingUrls = new Map<string, DownloaderHelper>();\n /** 上一次选择的保存文件夹路径 */\n private _lastSelectedDir: string | null = null;\n\n constructor() {\n ipcMain.handle(`core:dowloader`, async (event: Electron.IpcMainInvokeEvent, options: DownloadOptions & { url: string; id: string }) => {\n try {\n const { id, url } = options;\n const sender = event.sender as Electron.WebContents;\n\n // 创建进度回调,通过 IPC 发送进度更新\n const progressCallback: DownloadProgressCallback = (progress) => {\n try {\n if (sender && typeof sender.send === 'function') {\n sender.send(`core:dowloader:progress`, {\n id,\n data: progress\n });\n }\n } catch (error) {\n console.error('发送下载进度失败:', error);\n }\n };\n\n const result = await this.download(url, options, progressCallback);\n return result;\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : '未知错误'\n };\n }\n });\n }\n\n /**\n * 下载文件\n * @param url 要下载的 URL\n * @param options 下载选项\n * @param onProgress 进度回调函数(可选,优先级高于 options.onProgress)\n * @returns Promise<DownloadResult> 下载结果\n */\n public async download(\n url: string,\n options: DownloadOptions = {},\n onProgress?: DownloadProgressCallback\n ): Promise<DownloadResult> {\n // 检查是否正在下载,避免重复下载\n if (this._downloadingUrls.has(url)) {\n return this._createErrorResult(`资源正在下载中: ${url}`);\n }\n\n // 合并进度回调:优先使用传入的 onProgress,其次使用 options.onProgress\n const progressCallback = onProgress || options.onProgress;\n\n // 先检查是否是 base64 data URL(用于确定默认扩展名)\n const base64Info = this._isBase64DataUrl(url);\n const defaultExt = base64Info.isBase64 && base64Info.ext ? base64Info.ext : undefined;\n\n // 确定输出路径\n let outputPath: string;\n try {\n if (options.showSaveDialog) {\n const savePath = await this._showSaveDialog(url, options, defaultExt);\n if (!savePath) {\n return this._createErrorResult('用户取消了保存操作');\n }\n outputPath = savePath;\n } else {\n outputPath = this._resolveOutputPath(url, options, defaultExt);\n }\n } catch (error) {\n return this._createErrorResult(\n error instanceof Error ? error.message : '确定输出路径失败'\n );\n }\n\n // 准备输出路径(确保目录存在、检查文件是否已存在)\n const pathCheckResult = await this._prepareOutputPath(outputPath, options);\n if (pathCheckResult) {\n return pathCheckResult;\n }\n\n const downloadDir = path.dirname(outputPath);\n const downloadFileName = path.basename(outputPath);\n\n // 如果是 base64 data URL,使用已确定的输出路径进行处理\n if (base64Info.isBase64 && base64Info.ext && base64Info.mimeType && base64Info.data) {\n return await this._handleBase64Download(\n url,\n { ext: base64Info.ext, mimeType: base64Info.mimeType, data: base64Info.data },\n outputPath,\n options,\n progressCallback\n );\n }\n\n // 使用临时文件下载,避免下载中断时文件不完整\n const tempFilePath = `${outputPath}.cache`;\n\n return new Promise<DownloadResult>((resolve, reject) => {\n // 创建下载器实例\n const dl = new DownloaderHelper(url, downloadDir, {\n fileName: downloadFileName + '.cache',\n retry: {\n maxRetries: options.retry?.maxRetries ?? DEFAULT_RETRY.maxRetries,\n delay: options.retry?.delay ?? DEFAULT_RETRY.delay\n },\n timeout: options.timeout ?? DEFAULT_TIMEOUT,\n override: true, // 临时文件总是覆盖\n httpRequestOptions: {\n followRedirect: options.httpRequestOptions?.followRedirect ?? true,\n maxRedirects: options.httpRequestOptions?.maxRedirects ?? MAX_REDIRECTS,\n headers: options.httpRequestOptions?.headers\n }\n });\n\n // 记录正在下载的 URL\n this._downloadingUrls.set(url, dl);\n\n // 监听下载进度\n if (progressCallback) {\n dl.on('progress', (stats) => {\n try {\n progressCallback({\n url,\n downloaded: stats.downloaded || 0,\n total: stats.total || 0,\n percentage: stats.progress !== undefined ? Math.round(stats.progress) : -1,\n speed: stats.speed || 0\n });\n } catch (error) {\n // 忽略回调中的错误,避免影响下载\n console.error('进度回调执行失败:', error);\n }\n });\n }\n\n // 监听下载完成\n dl.on('end', () => {\n this._downloadingUrls.delete(url);\n // 将临时文件重命名为最终文件\n fs.rename(tempFilePath, outputPath, (renameErr) => {\n if (renameErr) {\n const error = new Error(`文件重命名失败 from ${tempFilePath} to ${outputPath}: ${renameErr.message}`);\n if (options.onError) {\n options.onError(error);\n }\n reject(error);\n } else {\n // 获取文件大小并返回结果\n const stats = fs.statSync(outputPath);\n const result = this._createSuccessResult(outputPath, stats.size);\n if (options.onComplete) {\n options.onComplete(outputPath);\n }\n resolve(result);\n }\n });\n });\n\n // 统一的错误处理函数\n const handleError = (error: Error) => {\n this._downloadingUrls.delete(url);\n // 清理临时文件\n fs.promises.unlink(tempFilePath).catch(() => {\n // 忽略删除失败\n });\n if (options.onError) {\n options.onError(error);\n }\n reject(error);\n };\n\n // 监听下载错误\n dl.on('error', (err) => {\n const error = err instanceof Error ? err : new Error(`下载失败: ${url}`);\n handleError(error);\n });\n\n // 监听下载停止(取消)\n dl.on('stop', () => {\n const error = new Error(`下载已停止: ${url}`);\n handleError(error);\n });\n\n // 开始下载\n dl.start().catch((err) => {\n const error = err instanceof Error ? err : new Error(`启动下载失败: ${url}`);\n handleError(error);\n });\n }).catch((error): DownloadResult => {\n return this._createErrorResult(error instanceof Error ? error.message : '未知错误');\n });\n }\n\n /**\n * 取消下载\n * @param url 要取消的 URL\n * @returns 是否成功取消\n */\n public cancel(url: string): boolean {\n const dl = this._downloadingUrls.get(url);\n if (dl) {\n dl.stop();\n this._downloadingUrls.delete(url);\n return true;\n }\n return false;\n }\n\n /**\n * 取消所有正在进行的下载\n */\n public cancelAll(): void {\n for (const [url, dl] of this._downloadingUrls.entries()) {\n dl.stop();\n }\n this._downloadingUrls.clear();\n }\n\n /**\n * 检查指定 URL 是否正在下载\n * @param url 要检查的 URL\n * @returns 是否正在下载\n */\n public isDownloading(url: string): boolean {\n return this._downloadingUrls.has(url);\n }\n\n /**\n * 获取正在下载的 URL 列表\n * @returns 正在下载的 URL 数组\n */\n public getDownloadingUrls(): string[] {\n return Array.from(this._downloadingUrls.keys());\n }\n\n /**\n * 显示保存对话框\n * @param url 下载 URL\n * @param options 下载选项\n * @param defaultExt 默认扩展名(可选,用于 base64 URL)\n * @returns Promise<string | null> 用户选择的保存路径,如果取消则返回 null\n */\n private async _showSaveDialog(url: string, options: DownloadOptions, defaultExt?: string): Promise<string | null> {\n // 获取默认文件名\n let defaultFileName: string;\n if (options.defaultPath) {\n defaultFileName = path.basename(options.defaultPath);\n } else if (options.fileName) {\n defaultFileName = options.fileName;\n } else {\n // 尝试从 URL 中提取文件名\n if (defaultExt) {\n // 如果提供了默认扩展名(base64 情况),使用它\n defaultFileName = `${DEFAULT_FILENAME}.${defaultExt}`;\n } else {\n defaultFileName = this._extractFileNameFromUrl(url);\n }\n }\n\n // 获取默认目录\n let defaultDir: string | undefined;\n if (options.defaultPath) {\n defaultDir = path.dirname(options.defaultPath);\n } else if (options.outputDir) {\n defaultDir = path.resolve(options.outputDir);\n } else {\n // 如果没有指定目录,使用上一次选择的文件夹,如果没有则使用下载目录\n defaultDir = this._lastSelectedDir || app.getPath('downloads');\n }\n\n // 如果指定了目录,检查文件是否存在并生成唯一文件名\n if (defaultDir) {\n defaultFileName = this._generateUniqueFileName(defaultDir, defaultFileName);\n }\n\n // 构建默认路径\n const defaultPath = defaultDir ? path.join(defaultDir, defaultFileName) : defaultFileName;\n\n // 获取当前活动的 BrowserWindow\n const focusedWindow = BrowserWindow.getFocusedWindow();\n const allWindows = BrowserWindow.getAllWindows();\n const parentWindow = focusedWindow || (allWindows.length > 0 ? allWindows[0] : undefined);\n\n // 显示保存对话框\n const dialogOptions = {\n defaultPath,\n title: '保存文件',\n buttonLabel: '保存',\n filters: [\n // 尝试从文件名推断文件类型\n ...(path.extname(defaultFileName) ? [{\n name: '所有文件',\n extensions: ['*']\n }] : [])\n ]\n };\n\n // 根据是否有父窗口调用不同的方法\n const result = parentWindow\n ? await dialog.showSaveDialog(parentWindow, dialogOptions)\n : await dialog.showSaveDialog(dialogOptions);\n\n if (result.canceled || !result.filePath) {\n return null;\n }\n\n // 记录用户选择的文件夹路径,用于下次默认使用\n const selectedDir = path.dirname(result.filePath);\n if (selectedDir) {\n this._lastSelectedDir = selectedDir;\n }\n\n return result.filePath;\n }\n\n /**\n * 检测并处理 base64 data URL\n * @param url 资源URL\n * @returns 如果是 base64 URL,返回 true、文件扩展名、MIME 类型和数据;否则返回 false\n */\n private _isBase64DataUrl(url: string): { isBase64: boolean, ext?: string, mimeType?: string, data?: string } {\n if (!url.startsWith('data:')) {\n return { isBase64: false };\n }\n\n try {\n // 解析 data URL 格式:data:[<mediatype>][;base64],<data>\n const commaIndex = url.indexOf(',');\n if (commaIndex === -1) {\n return { isBase64: false };\n }\n\n const header = url.substring(0, commaIndex);\n const data = url.substring(commaIndex + 1);\n\n // 检查是否包含 base64 标识\n if (!header.includes('base64')) {\n return { isBase64: false };\n }\n\n // 从 mediatype 中提取文件扩展名和 MIME 类型\n // 例如:data:image/png;base64 -> png, image/png\n // 例如:data:image/jpeg;base64 -> jpeg, image/jpeg\n let ext = DEFAULT_EXT.replace(/^\\./, '');\n let mimeType = DEFAULT_MIME_TYPE;\n const mimeMatch = header.match(/data:([^;]+)/);\n if (mimeMatch && mimeMatch[1]) {\n mimeType = mimeMatch[1];\n // 使用 mime-types 包从 MIME 类型获取扩展名\n const extension = mime.extension(mimeType);\n if (extension) {\n ext = extension;\n }\n }\n\n return { isBase64: true, ext, mimeType, data };\n } catch (error) {\n return { isBase64: false };\n }\n }\n\n /**\n * 保存 base64 数据到文件\n * @param base64Data base64 编码的数据\n * @param filePath 目标文件路径\n */\n private async _saveBase64ToFile(base64Data: string, filePath: string): Promise<void> {\n try {\n // 解码 base64 数据\n const buffer = Buffer.from(base64Data, 'base64');\n\n // 使用 Promise 版本的 writeFile\n // Buffer 继承自 Uint8Array,可以直接使用\n await fs.promises.writeFile(filePath, buffer as Uint8Array);\n } catch (error) {\n throw new Error(`保存 base64 文件失败: ${error instanceof Error ? error.message : '未知错误'}`);\n }\n }\n\n /**\n * 准备输出路径(确保目录存在、检查文件是否已存在)\n * @param outputPath 输出路径\n * @param options 下载选项\n * @returns 如果文件已存在且不允许覆盖,返回结果;否则返回 null\n */\n private async _prepareOutputPath(\n outputPath: string,\n options: DownloadOptions\n ): Promise<DownloadResult | null> {\n const downloadDir = path.dirname(outputPath);\n\n // 确保目录存在\n if (!fs.existsSync(downloadDir)) {\n fs.mkdirSync(downloadDir, { recursive: true });\n }\n\n // 如果文件已存在且不允许覆盖,直接返回\n if (fs.existsSync(outputPath) && !(options.override ?? true)) {\n const stats = fs.statSync(outputPath);\n return this._createSuccessResult(outputPath, stats.size);\n }\n\n return null;\n }\n\n /**\n * 创建成功结果\n * @param filePath 文件路径\n * @param size 文件大小\n * @returns DownloadResult\n */\n private _createSuccessResult(filePath: string, size: number): DownloadResult {\n return {\n success: true,\n filePath,\n size\n };\n }\n\n /**\n * 创建错误结果\n * @param error 错误信息\n * @returns DownloadResult\n */\n private _createErrorResult(error: string): DownloadResult {\n return {\n success: false,\n error\n };\n }\n\n /**\n * 处理 base64 data URL 的下载\n * @param url base64 data URL\n * @param base64Info base64 信息\n * @param outputPath 已确定的输出路径\n * @param options 下载选项\n * @param progressCallback 进度回调\n * @returns Promise<DownloadResult> 下载结果\n */\n private async _handleBase64Download(\n url: string,\n base64Info: { ext: string; mimeType: string; data: string },\n outputPath: string,\n options: DownloadOptions,\n progressCallback?: DownloadProgressCallback\n ): Promise<DownloadResult> {\n if (!base64Info.data || !base64Info.ext) {\n return this._createErrorResult(`无效的 base64 data URL: ${url}`);\n }\n\n try {\n // 模拟进度回调(base64 数据通常很小,立即完成)\n if (progressCallback) {\n const dataLength = Buffer.from(base64Info.data, 'base64').length;\n progressCallback({\n url,\n downloaded: dataLength,\n total: dataLength,\n percentage: 100,\n speed: 0\n });\n }\n\n // 保存 base64 数据到文件\n await this._saveBase64ToFile(base64Info.data, outputPath);\n\n // 获取文件大小\n const stats = fs.statSync(outputPath);\n const result = this._createSuccessResult(outputPath, stats.size);\n\n if (options.onComplete) {\n options.onComplete(outputPath);\n }\n\n return result;\n } catch (error) {\n const err = error instanceof Error ? error : new Error('保存 base64 文件失败');\n if (options.onError) {\n options.onError(err);\n }\n return this._createErrorResult(err.message);\n }\n }\n\n /**\n * 生成唯一的文件名(如果文件已存在,添加数字后缀)\n * @param dir 目录路径\n * @param fileName 原始文件名\n * @returns 唯一的文件名\n */\n private _generateUniqueFileName(dir: string, fileName: string): string {\n // 分离文件名和扩展名\n const ext = path.extname(fileName);\n const nameWithoutExt = path.basename(fileName, ext);\n\n // 检查原始文件名是否存在\n const originalPath = path.join(dir, fileName);\n if (!fs.existsSync(originalPath)) {\n return fileName;\n }\n\n // 如果存在,尝试添加数字后缀\n let counter = 1;\n let newFileName: string;\n let newPath: string;\n\n do {\n newFileName = `${nameWithoutExt}(${counter})${ext}`;\n newPath = path.join(dir, newFileName);\n counter++;\n } while (fs.existsSync(newPath) && counter < MAX_FILENAME_COUNTER);\n\n return newFileName;\n }\n\n /**\n * 从 URL 中提取文件名\n * @param url 下载 URL\n * @returns 文件名\n */\n private _extractFileNameFromUrl(url: string): string {\n try {\n const urlObj = new URL(url);\n const fileName = path.basename(urlObj.pathname) || DEFAULT_FILENAME;\n return path.extname(fileName) ? fileName : `${fileName}${DEFAULT_EXT}`;\n } catch {\n return `${DEFAULT_FILENAME}${DEFAULT_EXT}`;\n }\n }\n\n /**\n * 解析输出路径\n * @param url 下载 URL\n * @param options 下载选项\n * @param defaultExt 默认扩展名(可选,用于 base64 URL)\n * @returns 输出文件路径\n */\n private _resolveOutputPath(url: string, options: DownloadOptions, defaultExt?: string): string {\n // 如果提供了完整路径,直接使用\n if (options.outputPath) {\n return path.resolve(options.outputPath);\n }\n\n // 确定输出目录:优先使用指定的目录,否则使用 Electron 的下载目录\n const outputDir = options.outputDir\n ? path.resolve(options.outputDir)\n : app.getPath('downloads');\n\n // 确定文件名\n let fileName: string;\n if (options.fileName) {\n fileName = options.fileName;\n } else {\n // 如果提供了默认扩展名(base64 情况),使用它\n if (defaultExt) {\n fileName = `${DEFAULT_FILENAME}.${defaultExt}`;\n } else {\n // 尝试从 URL 中提取文件名\n fileName = this._extractFileNameFromUrl(url);\n }\n }\n\n return path.join(outputDir, fileName);\n }\n}\n\n/**\n * 默认下载器实例(单例)\n */\nlet defaultDownloader: Downloader | null = null;\n\n/**\n * 获取默认下载器实例\n * @returns Downloader 实例\n */\nexport function getDownloader(): Downloader {\n if (!defaultDownloader) {\n defaultDownloader = new Downloader();\n }\n return defaultDownloader;\n}\n\n/**\n * 下载文件的便捷函数\n * @param url 要下载的 URL\n * @param options 下载选项\n * @param onProgress 进度回调函数(可选)\n * @returns Promise<DownloadResult> 下载结果\n */\nexport async function downloadFile(\n url: string,\n options: DownloadOptions = {},\n onProgress?: DownloadProgressCallback\n): Promise<DownloadResult> {\n const downloader = getDownloader();\n return downloader.download(url, options, onProgress);\n}\n\n"],"names":[],"mappings":"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"}