Skip to content

Commit 2505b52

Browse files
committed
✨ feat: 准备缓存配置功能
1 parent f5db0f8 commit 2505b52

7 files changed

Lines changed: 546 additions & 13 deletions

File tree

.github/workflows/dev.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -235,9 +235,9 @@ jobs:
235235
uses: actions/upload-artifact@v4,
236236
with: { name: SPlayer-Linux-tar-arm64, path: dist/*-arm64.tar.gz },
237237
}
238-
- {
239-
name: Upload Artifacts - Linux Snap x64,
240-
if: runner.os == 'Linux',
241-
uses: actions/upload-artifact@v4,
242-
with: { name: SPlayer-Linux-snap-x64, path: dist/*-amd64.snap },
243-
}
238+
# - {
239+
# name: Upload Artifacts - Linux Snap x64,
240+
# if: runner.os == 'Linux',
241+
# uses: actions/upload-artifact@v4,
242+
# with: { name: SPlayer-Linux-snap-x64, path: dist/*-amd64.snap },
243+
# }

electron/main/ipc/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import initTrayIpc from "./ipc-tray";
88
import initUpdateIpc from "./ipc-update";
99
import initWindowsIpc from "./ipc-window";
1010
import initProtocolIpc from "./ipc-protocol";
11+
import initCacheIpc from "./ipc-cache";
1112

1213
/**
1314
* 初始化全部 IPC 通信
@@ -24,6 +25,7 @@ const initIpc = (): void => {
2425
initThumbarIpc();
2526
initShortcutIpc();
2627
initProtocolIpc();
28+
initCacheIpc();
2729
};
2830

2931
export default initIpc;

electron/main/ipc/ipc-cache.ts

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import { ipcMain } from "electron";
2+
import { existsSync, mkdirSync } from "fs";
3+
import { readdir, readFile, rm, stat, writeFile } from "fs/promises";
4+
import { join, resolve } from "path";
5+
import { useStore } from "../store";
6+
import type Store from "electron-store";
7+
import type { StoreType } from "../store";
8+
import { processLog } from "../logger";
9+
10+
/**
11+
* 缓存资源类型
12+
* - music: 音乐缓存
13+
* - lyrics: 歌词缓存
14+
* - local-data: 本地音乐数据缓存
15+
* - playlist-data: 歌单数据缓存
16+
*/
17+
type CacheResourceType = "music" | "lyrics" | "local-data" | "playlist-data";
18+
19+
/**
20+
* 缓存 IPC 通用返回结果
21+
* @template T 返回数据类型
22+
*/
23+
type CacheIpcResult<T = any> = {
24+
/** 是否成功 */
25+
success: boolean;
26+
/** 返回数据 */
27+
data?: T;
28+
/** 错误信息(失败时) */
29+
message?: string;
30+
};
31+
32+
/**
33+
* 缓存列表项信息
34+
*/
35+
type CacheListItem = {
36+
/** 缓存 key(文件名或相对路径) */
37+
key: string;
38+
/** 文件大小(字节) */
39+
size: number;
40+
/** 最后修改时间(毫秒时间戳) */
41+
mtime: number;
42+
};
43+
44+
/**
45+
* 不同缓存类型对应的子目录映射
46+
*/
47+
const CACHE_SUB_DIR: Record<CacheResourceType, string> = {
48+
music: "music",
49+
lyrics: "lyrics",
50+
"local-data": "local-data",
51+
"playlist-data": "playlist-data",
52+
};
53+
54+
/**
55+
* 确保缓存根目录及各子目录存在
56+
* @param basePath 缓存根路径
57+
*/
58+
const ensureCacheDirs = (basePath: string): void => {
59+
if (!existsSync(basePath)) mkdirSync(basePath, { recursive: true });
60+
Object.values(CACHE_SUB_DIR).forEach((sub) => {
61+
const dir = join(basePath, sub);
62+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
63+
});
64+
};
65+
66+
/**
67+
* 从 Store 中获取缓存根路径
68+
* @param store Electron Store 实例
69+
* @returns 缓存根路径
70+
* @throws 当未配置 cachePath 时抛出异常
71+
*/
72+
const getCacheBasePath = (store: Store<StoreType>): string => {
73+
const base = store.get("cachePath") as string | undefined;
74+
if (!base) {
75+
throw new Error("cachePath 未配置");
76+
}
77+
return base;
78+
};
79+
80+
/**
81+
* 解析并校验缓存文件路径,防止路径穿越
82+
* @param basePath 缓存根路径
83+
* @param type 缓存资源类型
84+
* @param key 缓存 key(文件名或相对路径)
85+
* @returns 目录与最终文件路径
86+
*/
87+
const resolveSafePath = (basePath: string, type: CacheResourceType, key: string) => {
88+
const dir = join(basePath, CACHE_SUB_DIR[type]);
89+
const target = resolve(dir, key);
90+
if (!target.startsWith(resolve(dir))) {
91+
throw new Error("非法的缓存 key");
92+
}
93+
return { dir, target };
94+
};
95+
96+
/**
97+
* 将多种类型的数据转换为 Buffer
98+
* @param data 输入数据(Buffer / Uint8Array / ArrayBuffer / string / Node Buffer JSON)
99+
* @returns 对应的 Buffer
100+
* @throws 不支持的类型时抛出异常
101+
*/
102+
const toBuffer = (data: any): Buffer => {
103+
if (Buffer.isBuffer(data)) return data;
104+
if (data instanceof Uint8Array) return Buffer.from(data);
105+
if (data instanceof ArrayBuffer) return Buffer.from(new Uint8Array(data));
106+
if (typeof data === "string") return Buffer.from(data, "utf-8");
107+
if (data?.type === "Buffer" && Array.isArray(data?.data)) {
108+
return Buffer.from(data.data);
109+
}
110+
throw new Error("不支持的缓存写入数据类型");
111+
};
112+
113+
/**
114+
* 通用错误捕获包装器,为 IPC 返回统一结果结构
115+
* @param action 实际执行的异步逻辑
116+
* @returns 包装后的结果对象
117+
*/
118+
const withErrorCatch = async <T>(action: () => Promise<T>): Promise<CacheIpcResult<T>> => {
119+
try {
120+
const data = await action();
121+
return { success: true, data };
122+
} catch (error: any) {
123+
processLog.error("❌ IPC cache error:", error);
124+
return { success: false, message: error?.message || String(error) };
125+
}
126+
};
127+
128+
/**
129+
* 初始化缓存相关 IPC 事件
130+
*/
131+
const initCacheIpc = (): void => {
132+
const store = useStore();
133+
if (!store) return;
134+
135+
try {
136+
const basePath = getCacheBasePath(store);
137+
ensureCacheDirs(basePath);
138+
} catch (error) {
139+
processLog.error("❌ 初始化缓存目录失败:", error);
140+
}
141+
142+
// 列出指定类型下的缓存文件
143+
ipcMain.handle(
144+
"cache-list",
145+
(_event, type: CacheResourceType): Promise<CacheIpcResult<CacheListItem[]>> => {
146+
return withErrorCatch(async () => {
147+
const basePath = getCacheBasePath(store);
148+
ensureCacheDirs(basePath);
149+
const dir = join(basePath, CACHE_SUB_DIR[type]);
150+
const files = await readdir(dir, { withFileTypes: true });
151+
const items: CacheListItem[] = [];
152+
153+
for (const file of files) {
154+
if (!file.isFile()) continue;
155+
const filePath = join(dir, file.name);
156+
const info = await stat(filePath);
157+
items.push({
158+
key: file.name,
159+
size: info.size,
160+
mtime: info.mtimeMs,
161+
});
162+
}
163+
164+
return items;
165+
});
166+
},
167+
);
168+
169+
// 读取指定缓存文件
170+
ipcMain.handle(
171+
"cache-get",
172+
(_event, type: CacheResourceType, key: string): Promise<CacheIpcResult<Buffer>> => {
173+
return withErrorCatch(async () => {
174+
const basePath = getCacheBasePath(store);
175+
ensureCacheDirs(basePath);
176+
const { target } = resolveSafePath(basePath, type, key);
177+
return await readFile(target);
178+
});
179+
},
180+
);
181+
182+
// 写入/更新缓存文件
183+
ipcMain.handle(
184+
"cache-put",
185+
(
186+
_event,
187+
type: CacheResourceType,
188+
key: string,
189+
data: Buffer | Uint8Array | ArrayBuffer | string,
190+
): Promise<CacheIpcResult<null>> => {
191+
return withErrorCatch(async () => {
192+
const basePath = getCacheBasePath(store);
193+
ensureCacheDirs(basePath);
194+
const { dir, target } = resolveSafePath(basePath, type, key);
195+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
196+
const buffer = toBuffer(data);
197+
await writeFile(target, buffer);
198+
return null;
199+
});
200+
},
201+
);
202+
203+
// 删除单个缓存文件
204+
ipcMain.handle(
205+
"cache-remove",
206+
(_event, type: CacheResourceType, key: string): Promise<CacheIpcResult<null>> => {
207+
return withErrorCatch(async () => {
208+
const basePath = getCacheBasePath(store);
209+
ensureCacheDirs(basePath);
210+
const { target } = resolveSafePath(basePath, type, key);
211+
await rm(target, { force: true });
212+
return null;
213+
});
214+
},
215+
);
216+
217+
// 清空指定类型的缓存目录
218+
ipcMain.handle(
219+
"cache-clear",
220+
(_event, type: CacheResourceType): Promise<CacheIpcResult<null>> => {
221+
return withErrorCatch(async () => {
222+
const basePath = getCacheBasePath(store);
223+
const dir = join(basePath, CACHE_SUB_DIR[type]);
224+
await rm(dir, { recursive: true, force: true });
225+
ensureCacheDirs(basePath);
226+
return null;
227+
});
228+
},
229+
);
230+
};
231+
232+
export default initCacheIpc;

electron/main/store/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { screen } from "electron";
1+
import { app, screen } from "electron";
22
import { storeLog } from "../logger";
33
import type { LyricConfig } from "../../../src/types/desktop-lyric";
44
import { defaultAMLLDbServer } from "../utils/config";
5+
import { join } from "path";
56
import defaultLyricConfig from "../../../src/assets/data/lyricConfig";
67
import Store from "electron-store";
78

@@ -27,6 +28,8 @@ export interface StoreType {
2728
proxy: string;
2829
// amll-db-server
2930
amllDbServer: string;
31+
// 缓存地址
32+
cachePath: string;
3033
}
3134

3235
/**
@@ -51,6 +54,7 @@ export const useStore = () => {
5154
},
5255
proxy: "",
5356
amllDbServer: defaultAMLLDbServer,
57+
cachePath: join(app.getPath("userData"), "cache-data"),
5458
},
5559
});
5660
};

0 commit comments

Comments
 (0)