@lynker-desktop/electron-sdk 0.0.9-alpha.54 → 0.0.9-alpha.55
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 +18 -5
- package/esm/main/resource-cache.d.ts.map +1 -1
- package/esm/main/resource-cache.js +143 -102
- package/esm/main/resource-cache.js.map +1 -1
- package/main/resource-cache.d.ts +18 -5
- package/main/resource-cache.d.ts.map +1 -1
- package/main/resource-cache.js +143 -102
- package/main/resource-cache.js.map +1 -1
- package/package.json +7 -5
|
@@ -25,6 +25,8 @@ export declare class ResourceCache {
|
|
|
25
25
|
private _cachedMatchFunction?;
|
|
26
26
|
/** 缓存的来源校验函数(避免重复创建) */
|
|
27
27
|
private _cachedOriginFunction?;
|
|
28
|
+
/** 正在下载的 URL 集合(避免重复下载) */
|
|
29
|
+
private _downloadingUrls;
|
|
28
30
|
/**
|
|
29
31
|
* 构造函数
|
|
30
32
|
* @param session Electron session
|
|
@@ -69,19 +71,26 @@ export declare class ResourceCache {
|
|
|
69
71
|
* 下载资源到本地缓存(异步版本,返回 Promise)
|
|
70
72
|
* @param url 资源URL
|
|
71
73
|
* @param filePath 本地缓存路径
|
|
74
|
+
* @param redirectCount 当前重定向次数(内部使用)
|
|
72
75
|
* @returns Promise<void> 下载完成或失败
|
|
73
76
|
*/
|
|
74
|
-
downloadResourceAsync(url: string, filePath: string): Promise<void>;
|
|
77
|
+
downloadResourceAsync(url: string, filePath: string, redirectCount?: number): Promise<void>;
|
|
75
78
|
/**
|
|
76
79
|
* 下载资源到本地缓存(同步版本,不返回 Promise,用于拦截器)
|
|
77
80
|
* @param url 资源URL
|
|
78
81
|
* @param filePath 本地缓存路径
|
|
79
82
|
*/
|
|
80
83
|
downloadResource(url: string, filePath: string): void;
|
|
84
|
+
/**
|
|
85
|
+
* 从文件扩展名获取 MIME 类型
|
|
86
|
+
* @param ext 文件扩展名(带或不带点)
|
|
87
|
+
* @returns MIME 类型
|
|
88
|
+
*/
|
|
89
|
+
private _getMimeTypeFromExt;
|
|
81
90
|
/**
|
|
82
91
|
* 检测并处理 base64 data URL
|
|
83
92
|
* @param url 资源URL
|
|
84
|
-
* @returns 如果是 base64 URL,返回 true
|
|
93
|
+
* @returns 如果是 base64 URL,返回 true、文件扩展名、MIME 类型和数据;否则返回 false
|
|
85
94
|
*/
|
|
86
95
|
private _isBase64DataUrl;
|
|
87
96
|
/**
|
|
@@ -94,24 +103,28 @@ export declare class ResourceCache {
|
|
|
94
103
|
* 手动缓存指定 URL 的资源
|
|
95
104
|
* @param url 要缓存的资源 URL(支持普通 URL 和 base64 data URL)
|
|
96
105
|
* @param force 是否强制重新下载,即使缓存有效(默认 false)
|
|
97
|
-
* @returns Promise<{ filePath: string, hostPath: string }>
|
|
106
|
+
* @returns Promise<{ filePath: string, hostPath: string, mimeType: string, size: number }> 返回缓存文件路径、主机路径、MIME 类型和文件大小
|
|
98
107
|
* @throws 如果 URL 不匹配缓存规则或来源不允许,会抛出错误
|
|
99
108
|
*/
|
|
100
109
|
cacheUrl(url: string, force?: boolean, ignoreOrigin?: boolean): Promise<{
|
|
101
110
|
filePath: string;
|
|
102
111
|
hostPath: string;
|
|
112
|
+
mimeType: string;
|
|
113
|
+
size: number;
|
|
103
114
|
}>;
|
|
104
115
|
/**
|
|
105
116
|
* 批量缓存多个 URL 的资源
|
|
106
117
|
* @param urls 要缓存的资源 URL 数组
|
|
107
118
|
* @param force 是否强制重新下载,即使缓存有效(默认 false)
|
|
108
|
-
* @returns Promise<Array<{ url: string, success: boolean, filePath?: string, hostPath?: string, error?: string }>> 返回每个 URL 的缓存结果
|
|
119
|
+
* @returns Promise<Array<{ url: string, success: boolean, filePath?: string, hostPath?: string, mimeType?: string, size?: number, error?: string }>> 返回每个 URL 的缓存结果
|
|
109
120
|
*/
|
|
110
121
|
addCacheUrls(urls: string[], force?: boolean, ignoreOrigin?: boolean): Promise<Array<{
|
|
111
122
|
url: string;
|
|
112
123
|
success: boolean;
|
|
113
124
|
filePath?: string;
|
|
114
125
|
hostPath?: string;
|
|
126
|
+
mimeType?: string;
|
|
127
|
+
size?: number;
|
|
115
128
|
error?: string;
|
|
116
129
|
}>>;
|
|
117
130
|
/**
|
|
@@ -148,7 +161,7 @@ export declare class ResourceCache {
|
|
|
148
161
|
*/
|
|
149
162
|
private _cleanOldCache;
|
|
150
163
|
/**
|
|
151
|
-
*
|
|
164
|
+
* 清理所有缓存文件(完全异步版本,性能更好)
|
|
152
165
|
* @returns Promise<{ success: number, failed: number, totalSize: number }> 返回清理统计信息
|
|
153
166
|
*/
|
|
154
167
|
clearCache(): Promise<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource-cache.d.ts","sourceRoot":"","sources":["../../src/main/resource-cache.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"resource-cache.d.ts","sourceRoot":"","sources":["../../src/main/resource-cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,42 +4,24 @@ import http from 'node:http';
|
|
|
4
4
|
import https from 'node:https';
|
|
5
5
|
import md5 from 'md5';
|
|
6
6
|
import ipc__default from '@lynker-desktop/electron-ipc/main';
|
|
7
|
+
import mime from 'mime-types';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
+
* HTTP 重定向状态码
|
|
10
11
|
*/
|
|
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
|
-
};
|
|
12
|
+
const REDIRECT_STATUS_CODES = [301, 302, 307, 308];
|
|
13
|
+
/**
|
|
14
|
+
* 默认文件扩展名
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_EXT = '.res';
|
|
17
|
+
/**
|
|
18
|
+
* 默认 MIME 类型
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_MIME_TYPE = 'application/octet-stream';
|
|
21
|
+
/**
|
|
22
|
+
* 最大重定向次数
|
|
23
|
+
*/
|
|
24
|
+
const MAX_REDIRECTS = 5;
|
|
43
25
|
/**
|
|
44
26
|
* 默认配置
|
|
45
27
|
*/
|
|
@@ -68,6 +50,8 @@ class ResourceCache {
|
|
|
68
50
|
*/
|
|
69
51
|
constructor(session, options) {
|
|
70
52
|
this.cacheHost = `${ResourceCache.scheme}://-`;
|
|
53
|
+
/** 正在下载的 URL 集合(避免重复下载) */
|
|
54
|
+
this._downloadingUrls = new Set();
|
|
71
55
|
if (!session)
|
|
72
56
|
throw new Error('ResourceCache: session is required');
|
|
73
57
|
this.session = session;
|
|
@@ -94,29 +78,24 @@ class ResourceCache {
|
|
|
94
78
|
});
|
|
95
79
|
ipc__default.mainIPC.handleRenderer('core:cache', async (options) => {
|
|
96
80
|
try {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const data = await this.addCacheUrls(
|
|
103
|
-
return data[0];
|
|
81
|
+
switch (options.method) {
|
|
82
|
+
case 'clear':
|
|
83
|
+
return await this.clearCache();
|
|
84
|
+
case 'add': {
|
|
85
|
+
const urls = Array.isArray(options.urls) ? options.urls : options.urls ? [options.urls] : [];
|
|
86
|
+
const data = await this.addCacheUrls(urls, options.force ?? false, true);
|
|
87
|
+
return Array.isArray(options.urls) ? data : data[0];
|
|
104
88
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (typeof options.urls === 'string') {
|
|
110
|
-
const data = await this.deleteCacheUrls([options.urls]);
|
|
111
|
-
return data[0];
|
|
89
|
+
case 'delete': {
|
|
90
|
+
const urls = Array.isArray(options.urls) ? options.urls : options.urls ? [options.urls] : [];
|
|
91
|
+
const data = await this.deleteCacheUrls(urls);
|
|
92
|
+
return Array.isArray(options.urls) ? data : data[0];
|
|
112
93
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return await this.getCacheStats();
|
|
94
|
+
case 'stats':
|
|
95
|
+
return await this.getCacheStats();
|
|
96
|
+
default:
|
|
97
|
+
return undefined;
|
|
118
98
|
}
|
|
119
|
-
return undefined;
|
|
120
99
|
}
|
|
121
100
|
catch (error) {
|
|
122
101
|
return { success: false, error: error instanceof Error ? error.message : '未知错误' };
|
|
@@ -219,11 +198,11 @@ class ResourceCache {
|
|
|
219
198
|
// 尝试从 URL 中提取扩展名
|
|
220
199
|
try {
|
|
221
200
|
const urlObj = new URL(url);
|
|
222
|
-
ext = path.extname(urlObj.pathname) ||
|
|
201
|
+
ext = path.extname(urlObj.pathname) || DEFAULT_EXT;
|
|
223
202
|
}
|
|
224
203
|
catch {
|
|
225
204
|
// 如果 URL 解析失败(可能是 base64 data URL),使用默认扩展名
|
|
226
|
-
ext =
|
|
205
|
+
ext = DEFAULT_EXT;
|
|
227
206
|
}
|
|
228
207
|
}
|
|
229
208
|
return {
|
|
@@ -263,45 +242,53 @@ class ResourceCache {
|
|
|
263
242
|
* 下载资源到本地缓存(异步版本,返回 Promise)
|
|
264
243
|
* @param url 资源URL
|
|
265
244
|
* @param filePath 本地缓存路径
|
|
245
|
+
* @param redirectCount 当前重定向次数(内部使用)
|
|
266
246
|
* @returns Promise<void> 下载完成或失败
|
|
267
247
|
*/
|
|
268
|
-
downloadResourceAsync(url, filePath) {
|
|
248
|
+
downloadResourceAsync(url, filePath, redirectCount = 0) {
|
|
249
|
+
// 检查是否正在下载,避免重复下载
|
|
250
|
+
if (this._downloadingUrls.has(url)) {
|
|
251
|
+
return Promise.reject(new Error(`资源正在下载中: ${url}`));
|
|
252
|
+
}
|
|
253
|
+
// 检查重定向次数限制
|
|
254
|
+
if (redirectCount >= MAX_REDIRECTS) {
|
|
255
|
+
return Promise.reject(new Error(`重定向次数超过限制 (${MAX_REDIRECTS}): ${url}`));
|
|
256
|
+
}
|
|
257
|
+
this._downloadingUrls.add(url);
|
|
269
258
|
return new Promise((resolve, reject) => {
|
|
270
259
|
const tempFilePath = `${filePath}.cache`;
|
|
271
260
|
const lib = url.startsWith('https') ? https : http;
|
|
272
261
|
const file = fs.createWriteStream(tempFilePath);
|
|
273
262
|
let request;
|
|
274
263
|
const cleanupAndAbort = (errMsg, err) => {
|
|
275
|
-
|
|
276
|
-
console.log(errMsg, err);
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
279
|
-
console.log(errMsg);
|
|
280
|
-
}
|
|
264
|
+
this._downloadingUrls.delete(url);
|
|
281
265
|
if (request) {
|
|
282
266
|
request.destroy();
|
|
283
267
|
}
|
|
284
268
|
file.close(() => {
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
269
|
+
// 异步删除临时文件,不阻塞
|
|
270
|
+
fs.promises.unlink(tempFilePath).catch(() => {
|
|
271
|
+
// 忽略删除失败
|
|
272
|
+
});
|
|
289
273
|
});
|
|
290
|
-
|
|
274
|
+
const error = err instanceof Error ? err : new Error(errMsg);
|
|
275
|
+
reject(error);
|
|
291
276
|
};
|
|
292
277
|
request = lib.get(url, (res) => {
|
|
293
278
|
// 处理重定向
|
|
294
|
-
if (res.statusCode
|
|
279
|
+
if (res.statusCode && REDIRECT_STATUS_CODES.includes(res.statusCode)) {
|
|
295
280
|
const location = res.headers.location;
|
|
296
281
|
if (location) {
|
|
297
282
|
request.destroy();
|
|
298
283
|
file.close(() => {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
284
|
+
// 异步删除临时文件,不阻塞
|
|
285
|
+
fs.promises.unlink(tempFilePath).catch(() => {
|
|
286
|
+
// 忽略删除失败
|
|
287
|
+
});
|
|
302
288
|
});
|
|
303
|
-
|
|
304
|
-
|
|
289
|
+
this._downloadingUrls.delete(url);
|
|
290
|
+
// 递归处理重定向,增加重定向计数
|
|
291
|
+
this.downloadResourceAsync(location, filePath, redirectCount + 1).then(resolve).catch(reject);
|
|
305
292
|
return;
|
|
306
293
|
}
|
|
307
294
|
// 如果没有 location,继续处理为错误
|
|
@@ -319,6 +306,7 @@ class ResourceCache {
|
|
|
319
306
|
return cleanupAndAbort(`关闭临时文件流失败: ${tempFilePath}`, err);
|
|
320
307
|
}
|
|
321
308
|
fs.rename(tempFilePath, filePath, (renameErr) => {
|
|
309
|
+
this._downloadingUrls.delete(url);
|
|
322
310
|
if (renameErr) {
|
|
323
311
|
cleanupAndAbort(`缓存文件重命名失败 from ${tempFilePath} to ${filePath}`, renameErr);
|
|
324
312
|
}
|
|
@@ -334,6 +322,13 @@ class ResourceCache {
|
|
|
334
322
|
request.on('error', (err) => {
|
|
335
323
|
cleanupAndAbort(`下载资源请求失败: ${url}`, err);
|
|
336
324
|
});
|
|
325
|
+
// 添加超时处理(30秒)
|
|
326
|
+
const timeout = setTimeout(() => {
|
|
327
|
+
cleanupAndAbort(`下载超时: ${url}`);
|
|
328
|
+
}, 30000);
|
|
329
|
+
request.on('close', () => {
|
|
330
|
+
clearTimeout(timeout);
|
|
331
|
+
});
|
|
337
332
|
});
|
|
338
333
|
}
|
|
339
334
|
/**
|
|
@@ -343,14 +338,28 @@ class ResourceCache {
|
|
|
343
338
|
*/
|
|
344
339
|
downloadResource(url, filePath) {
|
|
345
340
|
// 异步执行,不等待完成
|
|
346
|
-
|
|
347
|
-
|
|
341
|
+
// 如果正在下载,跳过(避免重复下载)
|
|
342
|
+
if (this._downloadingUrls.has(url)) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
this.downloadResourceAsync(url, filePath).catch(() => {
|
|
346
|
+
// 静默处理错误,避免日志过多
|
|
348
347
|
});
|
|
349
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* 从文件扩展名获取 MIME 类型
|
|
351
|
+
* @param ext 文件扩展名(带或不带点)
|
|
352
|
+
* @returns MIME 类型
|
|
353
|
+
*/
|
|
354
|
+
_getMimeTypeFromExt(ext) {
|
|
355
|
+
const cleanExt = ext.replace(/^\./, '').toLowerCase();
|
|
356
|
+
const mimeType = mime.lookup(cleanExt);
|
|
357
|
+
return mimeType || DEFAULT_MIME_TYPE;
|
|
358
|
+
}
|
|
350
359
|
/**
|
|
351
360
|
* 检测并处理 base64 data URL
|
|
352
361
|
* @param url 资源URL
|
|
353
|
-
* @returns 如果是 base64 URL,返回 true
|
|
362
|
+
* @returns 如果是 base64 URL,返回 true、文件扩展名、MIME 类型和数据;否则返回 false
|
|
354
363
|
*/
|
|
355
364
|
_isBase64DataUrl(url) {
|
|
356
365
|
if (!url.startsWith('data:')) {
|
|
@@ -368,16 +377,21 @@ class ResourceCache {
|
|
|
368
377
|
if (!header.includes('base64')) {
|
|
369
378
|
return { isBase64: false };
|
|
370
379
|
}
|
|
371
|
-
// 从 mediatype
|
|
372
|
-
// 例如:data:image/png;base64 -> png
|
|
373
|
-
// 例如:data:image/jpeg;base64 -> jpeg
|
|
374
|
-
let ext = '
|
|
380
|
+
// 从 mediatype 中提取文件扩展名和 MIME 类型
|
|
381
|
+
// 例如:data:image/png;base64 -> png, image/png
|
|
382
|
+
// 例如:data:image/jpeg;base64 -> jpeg, image/jpeg
|
|
383
|
+
let ext = DEFAULT_EXT.replace(/^\./, '');
|
|
384
|
+
let mimeType = DEFAULT_MIME_TYPE;
|
|
375
385
|
const mimeMatch = header.match(/data:([^;]+)/);
|
|
376
386
|
if (mimeMatch && mimeMatch[1]) {
|
|
377
|
-
|
|
378
|
-
|
|
387
|
+
mimeType = mimeMatch[1];
|
|
388
|
+
// 使用 mime-types 包从 MIME 类型获取扩展名
|
|
389
|
+
const extension = mime.extension(mimeType);
|
|
390
|
+
if (extension) {
|
|
391
|
+
ext = extension;
|
|
392
|
+
}
|
|
379
393
|
}
|
|
380
|
-
return { isBase64: true, ext, data };
|
|
394
|
+
return { isBase64: true, ext, mimeType, data };
|
|
381
395
|
}
|
|
382
396
|
catch (error) {
|
|
383
397
|
return { isBase64: false };
|
|
@@ -404,7 +418,7 @@ class ResourceCache {
|
|
|
404
418
|
* 手动缓存指定 URL 的资源
|
|
405
419
|
* @param url 要缓存的资源 URL(支持普通 URL 和 base64 data URL)
|
|
406
420
|
* @param force 是否强制重新下载,即使缓存有效(默认 false)
|
|
407
|
-
* @returns Promise<{ filePath: string, hostPath: string }>
|
|
421
|
+
* @returns Promise<{ filePath: string, hostPath: string, mimeType: string, size: number }> 返回缓存文件路径、主机路径、MIME 类型和文件大小
|
|
408
422
|
* @throws 如果 URL 不匹配缓存规则或来源不允许,会抛出错误
|
|
409
423
|
*/
|
|
410
424
|
async cacheUrl(url, force = false, ignoreOrigin = false) {
|
|
@@ -417,13 +431,25 @@ class ResourceCache {
|
|
|
417
431
|
}
|
|
418
432
|
// 获取缓存路径(使用检测到的扩展名)
|
|
419
433
|
const cachePath = this.getCachedPath(url, base64Info.ext);
|
|
434
|
+
const mimeType = base64Info.mimeType || this._getMimeTypeFromExt(base64Info.ext);
|
|
420
435
|
// 如果缓存有效且不强制重新下载,直接返回
|
|
421
436
|
if (!force && await this.isCacheValidAsync(cachePath.filePath)) {
|
|
422
|
-
|
|
437
|
+
const stats = await fs.promises.stat(cachePath.filePath);
|
|
438
|
+
return {
|
|
439
|
+
...cachePath,
|
|
440
|
+
mimeType,
|
|
441
|
+
size: stats.size
|
|
442
|
+
};
|
|
423
443
|
}
|
|
424
444
|
// 保存 base64 数据到文件
|
|
425
445
|
await this._saveBase64ToFile(base64Info.data, cachePath.filePath);
|
|
426
|
-
|
|
446
|
+
// 获取文件大小
|
|
447
|
+
const stats = await fs.promises.stat(cachePath.filePath);
|
|
448
|
+
return {
|
|
449
|
+
...cachePath,
|
|
450
|
+
mimeType,
|
|
451
|
+
size: stats.size
|
|
452
|
+
};
|
|
427
453
|
}
|
|
428
454
|
// 处理普通 URL
|
|
429
455
|
const shouldCache = this._getMatchFunction();
|
|
@@ -438,19 +464,33 @@ class ResourceCache {
|
|
|
438
464
|
}
|
|
439
465
|
// 获取缓存路径
|
|
440
466
|
const cachePath = this.getCachedPath(url);
|
|
467
|
+
// 从文件扩展名获取 MIME 类型
|
|
468
|
+
const ext = path.extname(cachePath.filePath).replace(/^\./, '') || DEFAULT_EXT.replace(/^\./, '');
|
|
469
|
+
const mimeType = this._getMimeTypeFromExt(ext);
|
|
441
470
|
// 如果缓存有效且不强制重新下载,直接返回
|
|
442
471
|
if (!force && await this.isCacheValidAsync(cachePath.filePath)) {
|
|
443
|
-
|
|
472
|
+
const stats = await fs.promises.stat(cachePath.filePath);
|
|
473
|
+
return {
|
|
474
|
+
...cachePath,
|
|
475
|
+
mimeType,
|
|
476
|
+
size: stats.size
|
|
477
|
+
};
|
|
444
478
|
}
|
|
445
479
|
// 下载资源
|
|
446
480
|
await this.downloadResourceAsync(url, cachePath.filePath);
|
|
447
|
-
|
|
481
|
+
// 获取文件大小
|
|
482
|
+
const stats = await fs.promises.stat(cachePath.filePath);
|
|
483
|
+
return {
|
|
484
|
+
...cachePath,
|
|
485
|
+
mimeType,
|
|
486
|
+
size: stats.size
|
|
487
|
+
};
|
|
448
488
|
}
|
|
449
489
|
/**
|
|
450
490
|
* 批量缓存多个 URL 的资源
|
|
451
491
|
* @param urls 要缓存的资源 URL 数组
|
|
452
492
|
* @param force 是否强制重新下载,即使缓存有效(默认 false)
|
|
453
|
-
* @returns Promise<Array<{ url: string, success: boolean, filePath?: string, hostPath?: string, error?: string }>> 返回每个 URL 的缓存结果
|
|
493
|
+
* @returns Promise<Array<{ url: string, success: boolean, filePath?: string, hostPath?: string, mimeType?: string, size?: number, error?: string }>> 返回每个 URL 的缓存结果
|
|
454
494
|
*/
|
|
455
495
|
async addCacheUrls(urls, force = false, ignoreOrigin = false) {
|
|
456
496
|
const results = await Promise.allSettled(urls.map(url => this.cacheUrl(url, force, ignoreOrigin)));
|
|
@@ -461,7 +501,9 @@ class ResourceCache {
|
|
|
461
501
|
url,
|
|
462
502
|
success: true,
|
|
463
503
|
filePath: result.value.filePath,
|
|
464
|
-
hostPath: result.value.hostPath
|
|
504
|
+
hostPath: result.value.hostPath,
|
|
505
|
+
mimeType: result.value.mimeType,
|
|
506
|
+
size: result.value.size
|
|
465
507
|
};
|
|
466
508
|
}
|
|
467
509
|
else {
|
|
@@ -553,45 +595,44 @@ class ResourceCache {
|
|
|
553
595
|
}
|
|
554
596
|
}
|
|
555
597
|
/**
|
|
556
|
-
*
|
|
598
|
+
* 清理所有缓存文件(完全异步版本,性能更好)
|
|
557
599
|
* @returns Promise<{ success: number, failed: number, totalSize: number }> 返回清理统计信息
|
|
558
600
|
*/
|
|
559
601
|
async clearCache() {
|
|
560
602
|
try {
|
|
561
|
-
const files = fs.
|
|
603
|
+
const files = await fs.promises.readdir(this.options.cacheDir);
|
|
562
604
|
if (files.length === 0) {
|
|
563
|
-
console.log('缓存目录为空,无需清理');
|
|
564
605
|
return { success: 0, failed: 0, totalSize: 0 };
|
|
565
606
|
}
|
|
566
|
-
//
|
|
567
|
-
const fileInfos = files.map((file) => {
|
|
607
|
+
// 并行获取所有文件信息(避免并行时的竞态条件)
|
|
608
|
+
const fileInfos = await Promise.allSettled(files.map(async (file) => {
|
|
568
609
|
const filePath = path.join(this.options.cacheDir, file);
|
|
569
610
|
try {
|
|
570
|
-
const stats = fs.
|
|
611
|
+
const stats = await fs.promises.stat(filePath);
|
|
571
612
|
return { file, filePath, size: stats.size };
|
|
572
613
|
}
|
|
573
614
|
catch (error) {
|
|
574
615
|
return { file, filePath, size: 0, error };
|
|
575
616
|
}
|
|
576
|
-
});
|
|
617
|
+
}));
|
|
618
|
+
const validInfos = fileInfos
|
|
619
|
+
.filter((r) => r.status === 'fulfilled')
|
|
620
|
+
.map(r => r.value);
|
|
577
621
|
// 计算总大小
|
|
578
|
-
const totalSize =
|
|
622
|
+
const totalSize = validInfos.reduce((sum, info) => sum + info.size, 0);
|
|
579
623
|
// 并行删除文件,提升性能
|
|
580
|
-
const deleteResults = await Promise.allSettled(
|
|
624
|
+
const deleteResults = await Promise.allSettled(validInfos.map(async (info) => {
|
|
581
625
|
try {
|
|
582
626
|
await fs.promises.unlink(info.filePath);
|
|
583
627
|
return { success: true, file: info.file };
|
|
584
628
|
}
|
|
585
629
|
catch (error) {
|
|
586
|
-
console.log(`清理缓存文件失败: ${info.file}`, error);
|
|
587
630
|
throw error;
|
|
588
631
|
}
|
|
589
632
|
}));
|
|
590
633
|
// 统计成功和失败数量
|
|
591
634
|
const success = deleteResults.filter(r => r.status === 'fulfilled').length;
|
|
592
635
|
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
636
|
return { success, failed, totalSize };
|
|
596
637
|
}
|
|
597
638
|
catch (error) {
|