@lynker-desktop/electron-sdk 0.0.9-alpha.54 → 0.0.9-alpha.56
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/esm/main/resource-cache.d.ts +58 -7
- package/esm/main/resource-cache.d.ts.map +1 -1
- package/esm/main/resource-cache.js +309 -117
- package/esm/main/resource-cache.js.map +1 -1
- package/main/resource-cache.d.ts +58 -7
- package/main/resource-cache.d.ts.map +1 -1
- package/main/resource-cache.js +309 -117
- package/main/resource-cache.js.map +1 -1
- package/package.json +7 -5
|
@@ -2,44 +2,27 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import http from 'node:http';
|
|
4
4
|
import https from 'node:https';
|
|
5
|
+
import { Transform } from 'node:stream';
|
|
5
6
|
import md5 from 'md5';
|
|
6
|
-
import
|
|
7
|
+
import mime from 'mime-types';
|
|
8
|
+
import { ipcMain } from 'electron';
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
|
-
*
|
|
11
|
+
* HTTP 重定向状态码
|
|
10
12
|
*/
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
'font/woff': 'woff',
|
|
25
|
-
'font/woff2': 'woff2',
|
|
26
|
-
'font/ttf': 'ttf',
|
|
27
|
-
'font/otf': 'otf',
|
|
28
|
-
'application/font-woff': 'woff',
|
|
29
|
-
'application/font-woff2': 'woff2',
|
|
30
|
-
'video/mp4': 'mp4',
|
|
31
|
-
'video/webm': 'webm',
|
|
32
|
-
'video/ogg': 'ogg',
|
|
33
|
-
'audio/mpeg': 'mp3',
|
|
34
|
-
'audio/wav': 'wav',
|
|
35
|
-
'text/css': 'css',
|
|
36
|
-
'text/javascript': 'js',
|
|
37
|
-
'application/javascript': 'js',
|
|
38
|
-
'application/json': 'json',
|
|
39
|
-
'text/xml': 'xml',
|
|
40
|
-
'text/plain': 'txt',
|
|
41
|
-
'application/pdf': 'pdf',
|
|
42
|
-
};
|
|
13
|
+
const REDIRECT_STATUS_CODES = [301, 302, 307, 308];
|
|
14
|
+
/**
|
|
15
|
+
* 默认文件扩展名
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_EXT = '.res';
|
|
18
|
+
/**
|
|
19
|
+
* 默认 MIME 类型
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_MIME_TYPE = 'application/octet-stream';
|
|
22
|
+
/**
|
|
23
|
+
* 最大重定向次数
|
|
24
|
+
*/
|
|
25
|
+
const MAX_REDIRECTS = 5;
|
|
43
26
|
/**
|
|
44
27
|
* 默认配置
|
|
45
28
|
*/
|
|
@@ -68,6 +51,8 @@ class ResourceCache {
|
|
|
68
51
|
*/
|
|
69
52
|
constructor(session, options) {
|
|
70
53
|
this.cacheHost = `${ResourceCache.scheme}://-`;
|
|
54
|
+
/** 正在下载的 URL 集合(避免重复下载) */
|
|
55
|
+
this._downloadingUrls = new Set();
|
|
71
56
|
if (!session)
|
|
72
57
|
throw new Error('ResourceCache: session is required');
|
|
73
58
|
this.session = session;
|
|
@@ -92,31 +77,39 @@ class ResourceCache {
|
|
|
92
77
|
this._cleanOldCache().catch(err => {
|
|
93
78
|
console.log('初始化时清理过期缓存失败:', err);
|
|
94
79
|
});
|
|
95
|
-
|
|
80
|
+
ipcMain.handle('core:cache', async (event, options) => {
|
|
96
81
|
try {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
82
|
+
switch (options.method) {
|
|
83
|
+
case 'clear':
|
|
84
|
+
return await this.clearCache();
|
|
85
|
+
case 'add': {
|
|
86
|
+
const { id } = options;
|
|
87
|
+
const sender = event.sender;
|
|
88
|
+
const urls = Array.isArray(options.urls) ? options.urls : options.urls ? [options.urls] : [];
|
|
89
|
+
const data = await this.addCacheUrls(urls, options.force ?? false, true, (data) => {
|
|
90
|
+
try {
|
|
91
|
+
if (sender) {
|
|
92
|
+
if (sender) {
|
|
93
|
+
sender?.send?.(`core:cache:progress:${id}`, data);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.log('发送缓存进度回调失败:', error);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
return Array.isArray(options.urls) ? data : data[0];
|
|
104
102
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (typeof options.urls === 'string') {
|
|
110
|
-
const data = await this.deleteCacheUrls([options.urls]);
|
|
111
|
-
return data[0];
|
|
103
|
+
case 'delete': {
|
|
104
|
+
const urls = Array.isArray(options.urls) ? options.urls : options.urls ? [options.urls] : [];
|
|
105
|
+
const data = await this.deleteCacheUrls(urls);
|
|
106
|
+
return Array.isArray(options.urls) ? data : data[0];
|
|
112
107
|
}
|
|
113
|
-
|
|
114
|
-
|
|
108
|
+
case 'stats':
|
|
109
|
+
return await this.getCacheStats();
|
|
110
|
+
default:
|
|
111
|
+
return undefined;
|
|
115
112
|
}
|
|
116
|
-
if (options.method === 'stats') {
|
|
117
|
-
return await this.getCacheStats();
|
|
118
|
-
}
|
|
119
|
-
return undefined;
|
|
120
113
|
}
|
|
121
114
|
catch (error) {
|
|
122
115
|
return { success: false, error: error instanceof Error ? error.message : '未知错误' };
|
|
@@ -219,11 +212,11 @@ class ResourceCache {
|
|
|
219
212
|
// 尝试从 URL 中提取扩展名
|
|
220
213
|
try {
|
|
221
214
|
const urlObj = new URL(url);
|
|
222
|
-
ext = path.extname(urlObj.pathname) ||
|
|
215
|
+
ext = path.extname(urlObj.pathname) || DEFAULT_EXT;
|
|
223
216
|
}
|
|
224
217
|
catch {
|
|
225
218
|
// 如果 URL 解析失败(可能是 base64 data URL),使用默认扩展名
|
|
226
|
-
ext =
|
|
219
|
+
ext = DEFAULT_EXT;
|
|
227
220
|
}
|
|
228
221
|
}
|
|
229
222
|
return {
|
|
@@ -263,45 +256,84 @@ class ResourceCache {
|
|
|
263
256
|
* 下载资源到本地缓存(异步版本,返回 Promise)
|
|
264
257
|
* @param url 资源URL
|
|
265
258
|
* @param filePath 本地缓存路径
|
|
259
|
+
* @param redirectCount 当前重定向次数(内部使用)
|
|
260
|
+
* @param onProgress 下载进度回调函数(可选)
|
|
266
261
|
* @returns Promise<void> 下载完成或失败
|
|
267
262
|
*/
|
|
268
|
-
downloadResourceAsync(url, filePath) {
|
|
263
|
+
downloadResourceAsync(url, filePath, redirectCount = 0, onProgress) {
|
|
264
|
+
// 检查是否正在下载,避免重复下载
|
|
265
|
+
if (this._downloadingUrls.has(url)) {
|
|
266
|
+
return Promise.reject(new Error(`资源正在下载中: ${url}`));
|
|
267
|
+
}
|
|
268
|
+
// 检查重定向次数限制
|
|
269
|
+
if (redirectCount >= MAX_REDIRECTS) {
|
|
270
|
+
return Promise.reject(new Error(`重定向次数超过限制 (${MAX_REDIRECTS}): ${url}`));
|
|
271
|
+
}
|
|
272
|
+
this._downloadingUrls.add(url);
|
|
269
273
|
return new Promise((resolve, reject) => {
|
|
270
274
|
const tempFilePath = `${filePath}.cache`;
|
|
271
275
|
const lib = url.startsWith('https') ? https : http;
|
|
272
276
|
const file = fs.createWriteStream(tempFilePath);
|
|
273
277
|
let request;
|
|
278
|
+
// 下载进度跟踪
|
|
279
|
+
let downloaded = 0;
|
|
280
|
+
let total = 0;
|
|
281
|
+
let lastProgressTime = Date.now();
|
|
282
|
+
let lastDownloaded = 0;
|
|
283
|
+
let progressInterval = null;
|
|
274
284
|
const cleanupAndAbort = (errMsg, err) => {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
console.log(errMsg);
|
|
285
|
+
this._downloadingUrls.delete(url);
|
|
286
|
+
if (progressInterval) {
|
|
287
|
+
clearInterval(progressInterval);
|
|
288
|
+
progressInterval = null;
|
|
280
289
|
}
|
|
281
290
|
if (request) {
|
|
282
291
|
request.destroy();
|
|
283
292
|
}
|
|
284
293
|
file.close(() => {
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
294
|
+
// 异步删除临时文件,不阻塞
|
|
295
|
+
fs.promises.unlink(tempFilePath).catch(() => {
|
|
296
|
+
// 忽略删除失败
|
|
297
|
+
});
|
|
289
298
|
});
|
|
290
|
-
|
|
299
|
+
const error = err instanceof Error ? err : new Error(errMsg);
|
|
300
|
+
reject(error);
|
|
301
|
+
};
|
|
302
|
+
const reportProgress = () => {
|
|
303
|
+
if (!onProgress)
|
|
304
|
+
return;
|
|
305
|
+
const now = Date.now();
|
|
306
|
+
const timeDiff = (now - lastProgressTime) / 1000; // 秒
|
|
307
|
+
const downloadedDiff = downloaded - lastDownloaded;
|
|
308
|
+
const speed = timeDiff > 0 ? downloadedDiff / timeDiff : 0;
|
|
309
|
+
const percentage = total > 0
|
|
310
|
+
? Math.round((downloaded / total) * 100)
|
|
311
|
+
: -1;
|
|
312
|
+
onProgress({
|
|
313
|
+
url,
|
|
314
|
+
downloaded,
|
|
315
|
+
total,
|
|
316
|
+
percentage,
|
|
317
|
+
speed
|
|
318
|
+
});
|
|
319
|
+
lastProgressTime = now;
|
|
320
|
+
lastDownloaded = downloaded;
|
|
291
321
|
};
|
|
292
322
|
request = lib.get(url, (res) => {
|
|
293
323
|
// 处理重定向
|
|
294
|
-
if (res.statusCode
|
|
324
|
+
if (res.statusCode && REDIRECT_STATUS_CODES.includes(res.statusCode)) {
|
|
295
325
|
const location = res.headers.location;
|
|
296
326
|
if (location) {
|
|
297
327
|
request.destroy();
|
|
298
328
|
file.close(() => {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
329
|
+
// 异步删除临时文件,不阻塞
|
|
330
|
+
fs.promises.unlink(tempFilePath).catch(() => {
|
|
331
|
+
// 忽略删除失败
|
|
332
|
+
});
|
|
302
333
|
});
|
|
303
|
-
|
|
304
|
-
|
|
334
|
+
this._downloadingUrls.delete(url);
|
|
335
|
+
// 递归处理重定向,增加重定向计数,传递进度回调
|
|
336
|
+
this.downloadResourceAsync(location, filePath, redirectCount + 1, onProgress).then(resolve).catch(reject);
|
|
305
337
|
return;
|
|
306
338
|
}
|
|
307
339
|
// 如果没有 location,继续处理为错误
|
|
@@ -311,14 +343,40 @@ class ResourceCache {
|
|
|
311
343
|
cleanupAndAbort(`下载失败,状态码: ${res.statusCode} for ${url}`);
|
|
312
344
|
return;
|
|
313
345
|
}
|
|
314
|
-
|
|
346
|
+
// 获取文件总大小(如果服务器提供)
|
|
347
|
+
const contentLength = res.headers['content-length'];
|
|
348
|
+
if (contentLength) {
|
|
349
|
+
total = parseInt(contentLength, 10);
|
|
350
|
+
}
|
|
351
|
+
// 使用 Transform 流来跟踪下载进度,避免与 pipe 冲突
|
|
352
|
+
const progressStream = new Transform({
|
|
353
|
+
transform(chunk, encoding, callback) {
|
|
354
|
+
downloaded += chunk.length;
|
|
355
|
+
callback(null, chunk);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
// 定期报告进度(每 200ms 一次)
|
|
359
|
+
if (onProgress) {
|
|
360
|
+
progressInterval = setInterval(reportProgress, 200);
|
|
361
|
+
}
|
|
362
|
+
// 将响应流通过进度跟踪流再写入文件
|
|
363
|
+
res.pipe(progressStream).pipe(file);
|
|
315
364
|
});
|
|
316
365
|
file.on('finish', () => {
|
|
366
|
+
if (progressInterval) {
|
|
367
|
+
clearInterval(progressInterval);
|
|
368
|
+
progressInterval = null;
|
|
369
|
+
}
|
|
370
|
+
// 最终进度报告
|
|
371
|
+
if (onProgress) {
|
|
372
|
+
reportProgress();
|
|
373
|
+
}
|
|
317
374
|
file.close((err) => {
|
|
318
375
|
if (err) {
|
|
319
376
|
return cleanupAndAbort(`关闭临时文件流失败: ${tempFilePath}`, err);
|
|
320
377
|
}
|
|
321
378
|
fs.rename(tempFilePath, filePath, (renameErr) => {
|
|
379
|
+
this._downloadingUrls.delete(url);
|
|
322
380
|
if (renameErr) {
|
|
323
381
|
cleanupAndAbort(`缓存文件重命名失败 from ${tempFilePath} to ${filePath}`, renameErr);
|
|
324
382
|
}
|
|
@@ -334,6 +392,13 @@ class ResourceCache {
|
|
|
334
392
|
request.on('error', (err) => {
|
|
335
393
|
cleanupAndAbort(`下载资源请求失败: ${url}`, err);
|
|
336
394
|
});
|
|
395
|
+
// 添加超时处理(30秒)
|
|
396
|
+
const timeout = setTimeout(() => {
|
|
397
|
+
cleanupAndAbort(`下载超时: ${url}`);
|
|
398
|
+
}, 30000);
|
|
399
|
+
request.on('close', () => {
|
|
400
|
+
clearTimeout(timeout);
|
|
401
|
+
});
|
|
337
402
|
});
|
|
338
403
|
}
|
|
339
404
|
/**
|
|
@@ -343,14 +408,28 @@ class ResourceCache {
|
|
|
343
408
|
*/
|
|
344
409
|
downloadResource(url, filePath) {
|
|
345
410
|
// 异步执行,不等待完成
|
|
346
|
-
|
|
347
|
-
|
|
411
|
+
// 如果正在下载,跳过(避免重复下载)
|
|
412
|
+
if (this._downloadingUrls.has(url)) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
this.downloadResourceAsync(url, filePath).catch(() => {
|
|
416
|
+
// 静默处理错误,避免日志过多
|
|
348
417
|
});
|
|
349
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* 从文件扩展名获取 MIME 类型
|
|
421
|
+
* @param ext 文件扩展名(带或不带点)
|
|
422
|
+
* @returns MIME 类型
|
|
423
|
+
*/
|
|
424
|
+
_getMimeTypeFromExt(ext) {
|
|
425
|
+
const cleanExt = ext.replace(/^\./, '').toLowerCase();
|
|
426
|
+
const mimeType = mime.lookup(cleanExt);
|
|
427
|
+
return mimeType || DEFAULT_MIME_TYPE;
|
|
428
|
+
}
|
|
350
429
|
/**
|
|
351
430
|
* 检测并处理 base64 data URL
|
|
352
431
|
* @param url 资源URL
|
|
353
|
-
* @returns 如果是 base64 URL,返回 true
|
|
432
|
+
* @returns 如果是 base64 URL,返回 true、文件扩展名、MIME 类型和数据;否则返回 false
|
|
354
433
|
*/
|
|
355
434
|
_isBase64DataUrl(url) {
|
|
356
435
|
if (!url.startsWith('data:')) {
|
|
@@ -368,16 +447,21 @@ class ResourceCache {
|
|
|
368
447
|
if (!header.includes('base64')) {
|
|
369
448
|
return { isBase64: false };
|
|
370
449
|
}
|
|
371
|
-
// 从 mediatype
|
|
372
|
-
// 例如:data:image/png;base64 -> png
|
|
373
|
-
// 例如:data:image/jpeg;base64 -> jpeg
|
|
374
|
-
let ext = '
|
|
450
|
+
// 从 mediatype 中提取文件扩展名和 MIME 类型
|
|
451
|
+
// 例如:data:image/png;base64 -> png, image/png
|
|
452
|
+
// 例如:data:image/jpeg;base64 -> jpeg, image/jpeg
|
|
453
|
+
let ext = DEFAULT_EXT.replace(/^\./, '');
|
|
454
|
+
let mimeType = DEFAULT_MIME_TYPE;
|
|
375
455
|
const mimeMatch = header.match(/data:([^;]+)/);
|
|
376
456
|
if (mimeMatch && mimeMatch[1]) {
|
|
377
|
-
|
|
378
|
-
|
|
457
|
+
mimeType = mimeMatch[1];
|
|
458
|
+
// 使用 mime-types 包从 MIME 类型获取扩展名
|
|
459
|
+
const extension = mime.extension(mimeType);
|
|
460
|
+
if (extension) {
|
|
461
|
+
ext = extension;
|
|
462
|
+
}
|
|
379
463
|
}
|
|
380
|
-
return { isBase64: true, ext, data };
|
|
464
|
+
return { isBase64: true, ext, mimeType, data };
|
|
381
465
|
}
|
|
382
466
|
catch (error) {
|
|
383
467
|
return { isBase64: false };
|
|
@@ -404,10 +488,12 @@ class ResourceCache {
|
|
|
404
488
|
* 手动缓存指定 URL 的资源
|
|
405
489
|
* @param url 要缓存的资源 URL(支持普通 URL 和 base64 data URL)
|
|
406
490
|
* @param force 是否强制重新下载,即使缓存有效(默认 false)
|
|
407
|
-
* @
|
|
491
|
+
* @param ignoreOrigin 是否忽略来源检查(默认 false)
|
|
492
|
+
* @param onDownloadProgress 下载进度回调函数(可选)
|
|
493
|
+
* @returns Promise<{ filePath: string, hostPath: string, mimeType: string, size: number }> 返回缓存文件路径、主机路径、MIME 类型和文件大小
|
|
408
494
|
* @throws 如果 URL 不匹配缓存规则或来源不允许,会抛出错误
|
|
409
495
|
*/
|
|
410
|
-
async cacheUrl(url, force = false, ignoreOrigin = false) {
|
|
496
|
+
async cacheUrl(url, force = false, ignoreOrigin = false, onDownloadProgress) {
|
|
411
497
|
// 检查是否是 base64 data URL
|
|
412
498
|
const base64Info = this._isBase64DataUrl(url);
|
|
413
499
|
if (base64Info.isBase64) {
|
|
@@ -417,16 +503,28 @@ class ResourceCache {
|
|
|
417
503
|
}
|
|
418
504
|
// 获取缓存路径(使用检测到的扩展名)
|
|
419
505
|
const cachePath = this.getCachedPath(url, base64Info.ext);
|
|
506
|
+
const mimeType = base64Info.mimeType || this._getMimeTypeFromExt(base64Info.ext);
|
|
420
507
|
// 如果缓存有效且不强制重新下载,直接返回
|
|
421
508
|
if (!force && await this.isCacheValidAsync(cachePath.filePath)) {
|
|
422
|
-
|
|
509
|
+
const stats = await fs.promises.stat(cachePath.filePath);
|
|
510
|
+
return {
|
|
511
|
+
...cachePath,
|
|
512
|
+
mimeType,
|
|
513
|
+
size: stats.size
|
|
514
|
+
};
|
|
423
515
|
}
|
|
424
516
|
// 保存 base64 数据到文件
|
|
425
517
|
await this._saveBase64ToFile(base64Info.data, cachePath.filePath);
|
|
426
|
-
|
|
518
|
+
// 获取文件大小
|
|
519
|
+
const stats = await fs.promises.stat(cachePath.filePath);
|
|
520
|
+
return {
|
|
521
|
+
...cachePath,
|
|
522
|
+
mimeType,
|
|
523
|
+
size: stats.size
|
|
524
|
+
};
|
|
427
525
|
}
|
|
428
526
|
// 处理普通 URL
|
|
429
|
-
const shouldCache = this._getMatchFunction();
|
|
527
|
+
const shouldCache = ignoreOrigin ? () => true : this._getMatchFunction();
|
|
430
528
|
const isAllowedOrigin = ignoreOrigin ? () => true : this._getOriginAllowFunction();
|
|
431
529
|
// 检查是否匹配缓存规则
|
|
432
530
|
if (!shouldCache(url)) {
|
|
@@ -438,40 +536,135 @@ class ResourceCache {
|
|
|
438
536
|
}
|
|
439
537
|
// 获取缓存路径
|
|
440
538
|
const cachePath = this.getCachedPath(url);
|
|
539
|
+
// 从文件扩展名获取 MIME 类型
|
|
540
|
+
const ext = path.extname(cachePath.filePath).replace(/^\./, '') || DEFAULT_EXT.replace(/^\./, '');
|
|
541
|
+
const mimeType = this._getMimeTypeFromExt(ext);
|
|
441
542
|
// 如果缓存有效且不强制重新下载,直接返回
|
|
442
543
|
if (!force && await this.isCacheValidAsync(cachePath.filePath)) {
|
|
443
|
-
|
|
544
|
+
const stats = await fs.promises.stat(cachePath.filePath);
|
|
545
|
+
return {
|
|
546
|
+
...cachePath,
|
|
547
|
+
mimeType,
|
|
548
|
+
size: stats.size
|
|
549
|
+
};
|
|
444
550
|
}
|
|
445
551
|
// 下载资源
|
|
446
|
-
await this.downloadResourceAsync(url, cachePath.filePath);
|
|
447
|
-
|
|
552
|
+
await this.downloadResourceAsync(url, cachePath.filePath, 0, onDownloadProgress);
|
|
553
|
+
// 获取文件大小
|
|
554
|
+
const stats = await fs.promises.stat(cachePath.filePath);
|
|
555
|
+
return {
|
|
556
|
+
...cachePath,
|
|
557
|
+
mimeType,
|
|
558
|
+
size: stats.size
|
|
559
|
+
};
|
|
448
560
|
}
|
|
449
561
|
/**
|
|
450
562
|
* 批量缓存多个 URL 的资源
|
|
451
563
|
* @param urls 要缓存的资源 URL 数组
|
|
452
564
|
* @param force 是否强制重新下载,即使缓存有效(默认 false)
|
|
453
|
-
* @
|
|
565
|
+
* @param ignoreOrigin 是否忽略来源检查(默认 false)
|
|
566
|
+
* @param onProgress 进度回调函数(可选)
|
|
567
|
+
* @returns Promise<Array<{ url: string, success: boolean, filePath?: string, hostPath?: string, mimeType?: string, size?: number, error?: string }>> 返回每个 URL 的缓存结果
|
|
454
568
|
*/
|
|
455
|
-
async addCacheUrls(urls, force = false, ignoreOrigin = false) {
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
569
|
+
async addCacheUrls(urls, force = false, ignoreOrigin = false, onProgress) {
|
|
570
|
+
const total = urls.length;
|
|
571
|
+
const results = [];
|
|
572
|
+
let completed = 0;
|
|
573
|
+
// 存储每个 URL 的下载进度
|
|
574
|
+
const downloadProgressMap = new Map();
|
|
575
|
+
// 进度回调的包装函数,确保线程安全
|
|
576
|
+
const reportProgress = (url, success, result) => {
|
|
577
|
+
if (onProgress) {
|
|
578
|
+
completed++;
|
|
579
|
+
const downloadProgress = downloadProgressMap.get(url);
|
|
580
|
+
onProgress({
|
|
581
|
+
current: completed,
|
|
582
|
+
total,
|
|
583
|
+
url,
|
|
584
|
+
success,
|
|
585
|
+
result,
|
|
586
|
+
percentage: Math.round((completed / total) * 100),
|
|
587
|
+
downloadProgress: downloadProgress ? {
|
|
588
|
+
downloaded: downloadProgress.downloaded,
|
|
589
|
+
total: downloadProgress.total,
|
|
590
|
+
percentage: downloadProgress.percentage,
|
|
591
|
+
speed: downloadProgress.speed
|
|
592
|
+
} : undefined
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
// 并行处理所有 URL,但跟踪每个的完成状态
|
|
597
|
+
const promises = urls.map(async (url, index) => {
|
|
598
|
+
// 为每个 URL 创建下载进度回调
|
|
599
|
+
const onDownloadProgress = onProgress
|
|
600
|
+
? (progress) => {
|
|
601
|
+
// 更新下载进度
|
|
602
|
+
downloadProgressMap.set(url, {
|
|
603
|
+
downloaded: progress.downloaded,
|
|
604
|
+
total: progress.total,
|
|
605
|
+
percentage: progress.percentage,
|
|
606
|
+
speed: progress.speed
|
|
607
|
+
});
|
|
608
|
+
// 实时报告下载进度(不增加 completed 计数)
|
|
609
|
+
if (onProgress) {
|
|
610
|
+
onProgress({
|
|
611
|
+
current: completed,
|
|
612
|
+
total,
|
|
613
|
+
url,
|
|
614
|
+
success: true, // 下载中视为进行中
|
|
615
|
+
result: undefined,
|
|
616
|
+
percentage: Math.round((completed / total) * 100),
|
|
617
|
+
downloadProgress: {
|
|
618
|
+
downloaded: progress.downloaded,
|
|
619
|
+
total: progress.total,
|
|
620
|
+
percentage: progress.percentage,
|
|
621
|
+
speed: progress.speed
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
: undefined;
|
|
627
|
+
try {
|
|
628
|
+
const result = await this.cacheUrl(url, force, ignoreOrigin, onDownloadProgress);
|
|
629
|
+
const item = {
|
|
461
630
|
url,
|
|
462
631
|
success: true,
|
|
463
|
-
filePath: result.
|
|
464
|
-
hostPath: result.
|
|
632
|
+
filePath: result.filePath,
|
|
633
|
+
hostPath: result.hostPath,
|
|
634
|
+
mimeType: result.mimeType,
|
|
635
|
+
size: result.size
|
|
465
636
|
};
|
|
637
|
+
results[index] = item;
|
|
638
|
+
// 清除下载进度(已完成)
|
|
639
|
+
downloadProgressMap.delete(url);
|
|
640
|
+
// 调用进度回调
|
|
641
|
+
reportProgress(url, true, {
|
|
642
|
+
filePath: result.filePath,
|
|
643
|
+
hostPath: result.hostPath,
|
|
644
|
+
mimeType: result.mimeType,
|
|
645
|
+
size: result.size
|
|
646
|
+
});
|
|
647
|
+
return item;
|
|
466
648
|
}
|
|
467
|
-
|
|
468
|
-
|
|
649
|
+
catch (error) {
|
|
650
|
+
const item = {
|
|
469
651
|
url,
|
|
470
652
|
success: false,
|
|
471
|
-
error:
|
|
653
|
+
error: error instanceof Error ? error.message : '未知错误'
|
|
472
654
|
};
|
|
655
|
+
results[index] = item;
|
|
656
|
+
// 清除下载进度(失败)
|
|
657
|
+
downloadProgressMap.delete(url);
|
|
658
|
+
// 调用进度回调
|
|
659
|
+
reportProgress(url, false, {
|
|
660
|
+
error: item.error
|
|
661
|
+
});
|
|
662
|
+
return item;
|
|
473
663
|
}
|
|
474
664
|
});
|
|
665
|
+
// 等待所有请求完成
|
|
666
|
+
await Promise.allSettled(promises);
|
|
667
|
+
return results;
|
|
475
668
|
}
|
|
476
669
|
/**
|
|
477
670
|
* 删除多个 URL 的资源
|
|
@@ -553,45 +746,44 @@ class ResourceCache {
|
|
|
553
746
|
}
|
|
554
747
|
}
|
|
555
748
|
/**
|
|
556
|
-
*
|
|
749
|
+
* 清理所有缓存文件(完全异步版本,性能更好)
|
|
557
750
|
* @returns Promise<{ success: number, failed: number, totalSize: number }> 返回清理统计信息
|
|
558
751
|
*/
|
|
559
752
|
async clearCache() {
|
|
560
753
|
try {
|
|
561
|
-
const files = fs.
|
|
754
|
+
const files = await fs.promises.readdir(this.options.cacheDir);
|
|
562
755
|
if (files.length === 0) {
|
|
563
|
-
console.log('缓存目录为空,无需清理');
|
|
564
756
|
return { success: 0, failed: 0, totalSize: 0 };
|
|
565
757
|
}
|
|
566
|
-
//
|
|
567
|
-
const fileInfos = files.map((file) => {
|
|
758
|
+
// 并行获取所有文件信息(避免并行时的竞态条件)
|
|
759
|
+
const fileInfos = await Promise.allSettled(files.map(async (file) => {
|
|
568
760
|
const filePath = path.join(this.options.cacheDir, file);
|
|
569
761
|
try {
|
|
570
|
-
const stats = fs.
|
|
762
|
+
const stats = await fs.promises.stat(filePath);
|
|
571
763
|
return { file, filePath, size: stats.size };
|
|
572
764
|
}
|
|
573
765
|
catch (error) {
|
|
574
766
|
return { file, filePath, size: 0, error };
|
|
575
767
|
}
|
|
576
|
-
});
|
|
768
|
+
}));
|
|
769
|
+
const validInfos = fileInfos
|
|
770
|
+
.filter((r) => r.status === 'fulfilled')
|
|
771
|
+
.map(r => r.value);
|
|
577
772
|
// 计算总大小
|
|
578
|
-
const totalSize =
|
|
773
|
+
const totalSize = validInfos.reduce((sum, info) => sum + info.size, 0);
|
|
579
774
|
// 并行删除文件,提升性能
|
|
580
|
-
const deleteResults = await Promise.allSettled(
|
|
775
|
+
const deleteResults = await Promise.allSettled(validInfos.map(async (info) => {
|
|
581
776
|
try {
|
|
582
777
|
await fs.promises.unlink(info.filePath);
|
|
583
778
|
return { success: true, file: info.file };
|
|
584
779
|
}
|
|
585
780
|
catch (error) {
|
|
586
|
-
console.log(`清理缓存文件失败: ${info.file}`, error);
|
|
587
781
|
throw error;
|
|
588
782
|
}
|
|
589
783
|
}));
|
|
590
784
|
// 统计成功和失败数量
|
|
591
785
|
const success = deleteResults.filter(r => r.status === 'fulfilled').length;
|
|
592
786
|
const failed = deleteResults.filter(r => r.status === 'rejected').length;
|
|
593
|
-
const sizeMB = (totalSize / (1024 * 1024)).toFixed(2);
|
|
594
|
-
console.log(`缓存清理完成: 成功 ${success} 个, 失败 ${failed} 个, 释放空间 ${sizeMB} MB`);
|
|
595
787
|
return { success, failed, totalSize };
|
|
596
788
|
}
|
|
597
789
|
catch (error) {
|