diff --git a/fluxer_app/src/features/platform/types/Electron.ts b/fluxer_app/src/features/platform/types/Electron.ts index 0ba724ad4..71766784f 100644 --- a/fluxer_app/src/features/platform/types/Electron.ts +++ b/fluxer_app/src/features/platform/types/Electron.ts @@ -319,7 +319,6 @@ export interface ElectronAPI { clearThemeLocalFiles: () => Promise; importThemeDirectory: () => Promise>; cacheVoiceBackgroundMedia: (options: VoiceBackgroundMediaCacheRequest) => Promise; - resolveVoiceBackgroundMedia: (id: string) => Promise; readVoiceBackgroundMedia: (id: string) => Promise; deleteVoiceBackgroundMedia: (id: string) => Promise; onUpdaterEvent: (callback: (event: UpdaterEvent) => void) => () => void; diff --git a/fluxer_app/src/types/electron.d.ts b/fluxer_app/src/types/electron.d.ts index 28e32162d..1f83d3099 100644 --- a/fluxer_app/src/types/electron.d.ts +++ b/fluxer_app/src/types/electron.d.ts @@ -377,7 +377,6 @@ export interface ElectronAPI { clearThemeLocalFiles?(): Promise; importThemeDirectory?(): Promise>; cacheVoiceBackgroundMedia?(options: VoiceBackgroundMediaCacheRequest): Promise; - resolveVoiceBackgroundMedia?(id: string): Promise; readVoiceBackgroundMedia?(id: string): Promise; deleteVoiceBackgroundMedia?(id: string): Promise; getDesktopTroubleshootingSettings?(): Promise; diff --git a/fluxer_desktop/src/common/Types.ts b/fluxer_desktop/src/common/Types.ts index f6ba2309e..a035b0116 100644 --- a/fluxer_desktop/src/common/Types.ts +++ b/fluxer_desktop/src/common/Types.ts @@ -656,7 +656,6 @@ export interface ElectronAPI { clearThemeLocalFiles: () => Promise; importThemeDirectory: () => Promise>; cacheVoiceBackgroundMedia: (options: VoiceBackgroundMediaCacheRequest) => Promise; - resolveVoiceBackgroundMedia: (id: string) => Promise; readVoiceBackgroundMedia: (id: string) => Promise; deleteVoiceBackgroundMedia: (id: string) => Promise; getDesktopTroubleshootingSettings: () => Promise; diff --git a/fluxer_desktop/src/main/ChromiumRuntime.test.mjs b/fluxer_desktop/src/main/ChromiumRuntime.test.mjs index 8dfd0515d..651ad766f 100644 --- a/fluxer_desktop/src/main/ChromiumRuntime.test.mjs +++ b/fluxer_desktop/src/main/ChromiumRuntime.test.mjs @@ -87,3 +87,19 @@ describe('ChromiumRuntime Windows capture policy', () => { assert.deepEqual([...features], ['ExistingFeature']); }); }); + +describe('ChromiumRuntime configured switch allowlist', () => { + test('rejects settings-supplied switches that would disable or shrink the HTTP cache', () => { + const {appendedSwitches, module} = loadChromiumRuntime('win32'); + + module.appendConfiguredChromiumSwitches([ + 'disable-http-cache', + 'disk-cache-size', + 'disk-cache-dir', + 'disable-gpu-shader-disk-cache', + 'disable_metal', + ]); + + assert.deepEqual(appendedSwitches, [{name: 'disable_metal', value: undefined}]); + }); +}); diff --git a/fluxer_desktop/src/main/VoiceBackgroundMediaCache.ts b/fluxer_desktop/src/main/VoiceBackgroundMediaCache.ts index 970bcac90..becf8d84c 100644 --- a/fluxer_desktop/src/main/VoiceBackgroundMediaCache.ts +++ b/fluxer_desktop/src/main/VoiceBackgroundMediaCache.ts @@ -15,6 +15,8 @@ const CACHE_DIR_NAME = 'voice-background-media'; const MAX_BACKGROUND_MEDIA_BYTES = 10 * 1024 * 1024; const MAX_BACKGROUND_MEDIA_CACHE_FILES = 16; const MAX_BACKGROUND_MEDIA_ID_LENGTH = 128; +const MAX_BACKGROUND_MEDIA_TEMP_AGE_MS = 60 * 60 * 1000; +const WEBP_HEADER_BYTES = 1024; const MEDIA_TYPES = new Map< string, @@ -82,7 +84,7 @@ function isAnimatedWebp(data: Buffer): boolean { if (data.subarray(12, 16).toString('ascii') === 'VP8X') { return (data[20] & 0b0000_0010) !== 0; } - return data.includes(Buffer.from('ANMF', 'ascii')); + return data.subarray(0, WEBP_HEADER_BYTES).includes(Buffer.from('ANMF', 'ascii')); } function classifyMediaKind( @@ -95,6 +97,20 @@ function classifyMediaKind( return mediaType.mediaKind; } +async function readMediaHeader(filePath: string, byteLength: number): Promise { + const handle = await fs.open(filePath, 'r').catch(() => null); + if (!handle) return null; + try { + const header = Buffer.alloc(byteLength); + const {bytesRead} = await handle.read(header, 0, byteLength, 0); + return header.subarray(0, bytesRead); + } catch { + return null; + } finally { + await handle.close().catch(() => undefined); + } +} + async function findCachedVoiceBackgroundMedia( id: string, ): Promise<{path: string; mimeType: string; mediaKind: VoiceBackgroundMediaKind} | null> { @@ -110,9 +126,9 @@ async function findCachedVoiceBackgroundMedia( const filePath = path.join(cacheDir, entry.name); let mediaKind = mediaType.mediaKind; if (mediaType.extension === 'webp') { - const data = await fs.readFile(filePath).catch(() => null); - if (!data) continue; - mediaKind = classifyMediaKind(mediaType, data); + const header = await readMediaHeader(filePath, WEBP_HEADER_BYTES); + if (!header) continue; + mediaKind = classifyMediaKind(mediaType, header); } return { path: filePath, @@ -124,20 +140,29 @@ async function findCachedVoiceBackgroundMedia( } async function trimVoiceBackgroundMediaCache(cacheDir: string): Promise { - const entries = await fs.readdir(cacheDir, {withFileTypes: true}); - const files = await Promise.all( - entries - .filter((entry) => entry.isFile()) - .map(async (entry) => { - const filePath = path.join(cacheDir, entry.name); - const stats = await fs.stat(filePath); - return {filePath, mtimeMs: stats.mtimeMs}; - }), + const entries = await fs.readdir(cacheDir, {withFileTypes: true}).catch(() => []); + const now = Date.now(); + const cachedMedia: Array<{filePath: string; mtimeMs: number}> = []; + for (const entry of entries) { + if (!entry.isFile()) continue; + const filePath = path.join(cacheDir, entry.name); + const stats = await fs.stat(filePath).catch(() => null); + if (!stats) continue; + const extension = path.extname(entry.name).slice(1).toLowerCase(); + if (!MEDIA_TYPES_BY_EXTENSION.has(extension)) { + if (now - stats.mtimeMs > MAX_BACKGROUND_MEDIA_TEMP_AGE_MS) { + await fs.rm(filePath, {force: true}).catch(() => undefined); + } + continue; + } + cachedMedia.push({filePath, mtimeMs: stats.mtimeMs}); + } + if (cachedMedia.length <= MAX_BACKGROUND_MEDIA_CACHE_FILES) return; + cachedMedia.sort((a, b) => a.mtimeMs - b.mtimeMs); + const deleteCount = cachedMedia.length - MAX_BACKGROUND_MEDIA_CACHE_FILES; + await Promise.all( + cachedMedia.slice(0, deleteCount).map(({filePath}) => fs.rm(filePath, {force: true}).catch(() => undefined)), ); - if (files.length <= MAX_BACKGROUND_MEDIA_CACHE_FILES) return; - files.sort((a, b) => a.mtimeMs - b.mtimeMs); - const deleteCount = files.length - MAX_BACKGROUND_MEDIA_CACHE_FILES; - await Promise.all(files.slice(0, deleteCount).map(({filePath}) => fs.rm(filePath, {force: true}))); } async function cacheVoiceBackgroundMedia( @@ -165,12 +190,6 @@ async function cacheVoiceBackgroundMedia( return {path: filePath, mediaKind}; } -async function resolveVoiceBackgroundMedia(id: string): Promise { - const cached = await findCachedVoiceBackgroundMedia(id); - if (!cached) return null; - return {path: cached.path, mediaKind: cached.mediaKind}; -} - async function readVoiceBackgroundMedia(id: string): Promise { const cached = await findCachedVoiceBackgroundMedia(id); if (!cached) return null; @@ -200,7 +219,6 @@ export function registerVoiceBackgroundMediaCacheHandlers(): void { ipcMain.handle('voice-background-media-cache:write', (_event, options: VoiceBackgroundMediaCacheRequest) => cacheVoiceBackgroundMedia(options), ); - ipcMain.handle('voice-background-media-cache:resolve', (_event, id: string) => resolveVoiceBackgroundMedia(id)); ipcMain.handle('voice-background-media-cache:read', (_event, id: string) => readVoiceBackgroundMedia(id)); ipcMain.handle('voice-background-media-cache:delete', (_event, id: string) => deleteVoiceBackgroundMedia(id)); } diff --git a/fluxer_desktop/src/preload/index.ts b/fluxer_desktop/src/preload/index.ts index 9baaa9d28..8bfb8e5dd 100644 --- a/fluxer_desktop/src/preload/index.ts +++ b/fluxer_desktop/src/preload/index.ts @@ -357,7 +357,6 @@ const api: ElectronAPI = { clearThemeLocalFiles: () => ipcRenderer.invoke('theme-local-files-clear'), importThemeDirectory: () => ipcRenderer.invoke('theme-directory-import'), cacheVoiceBackgroundMedia: (options) => ipcRenderer.invoke('voice-background-media-cache:write', options), - resolveVoiceBackgroundMedia: (id) => ipcRenderer.invoke('voice-background-media-cache:resolve', id), readVoiceBackgroundMedia: (id) => ipcRenderer.invoke('voice-background-media-cache:read', id), deleteVoiceBackgroundMedia: (id) => ipcRenderer.invoke('voice-background-media-cache:delete', id), getDesktopTroubleshootingSettings: (): Promise =>