Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions rules/darwin/browsers.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,23 @@
"$schema": "../schema/rules.schema.json",
"type": "browsers",
"chromiumCacheDirs": {
"cache": "Cache",
"codeCache": "Code Cache",
"gpuCache": "GpuCache",
"serviceWorker": "Service Worker/CacheStorage"
"profile": [
{ "dir": "Cache", "label": "Cache" },
{ "dir": "Code Cache", "label": "Code Cache" },
{ "dir": "GpuCache", "label": "GPU Cache" },
{ "dir": "Service Worker/CacheStorage", "label": "Service Worker Cache" },
{ "dir": "DawnWebGPUCache", "label": "WebGPU Shader Cache", "description": "Dawn's compiled WebGPU shaders — rebuilt on demand" },
{ "dir": "DawnGraphiteCache", "label": "Graphite Shader Cache", "description": "Dawn/Skia Graphite pipeline cache — rebuilt on demand" }
],
"shared": [
{ "dir": "component_crx_cache", "label": "Component Extension Cache", "description": "Downloaded CRX packages for component extensions — re-fetched on next update check, not the installed extensions themselves" },
{ "dir": "extensions_crx_cache", "label": "Extension Update Cache", "description": "Downloaded CRX packages awaiting install — re-fetched on next update check" },
{ "dir": "GrShaderCache", "label": "Skia Shader Cache", "description": "Skia GPU shader cache — rebuilt on demand" },
{ "dir": "ShaderCache", "label": "Shader Cache", "description": "Legacy GPU shader cache — rebuilt on demand" },
{ "dir": "GraphiteDawnCache", "label": "Graphite Pipeline Cache", "description": "Where newer Chromium builds keep the Graphite pipeline cache — rebuilt on demand" },
{ "dir": "GPUPersistentCache", "label": "Persistent GPU Cache", "description": "Where newer Chromium builds keep the on-disk GPU cache — rebuilt on demand" },
{ "dir": "PnaclTranslationCache", "label": "PNaCl Translation Cache", "description": "Translated PNaCl modules — regenerated on demand" }
]
},
"chromium": [
{ "key": "chrome", "base": "${APP_SUPPORT}/Google/Chrome" },
Expand Down
21 changes: 17 additions & 4 deletions rules/linux/browsers.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,23 @@
"$schema": "../schema/rules.schema.json",
"type": "browsers",
"chromiumCacheDirs": {
"cache": "Cache",
"codeCache": "Code Cache",
"gpuCache": "GpuCache",
"serviceWorker": "Service Worker/CacheStorage"
"profile": [
{ "dir": "Cache", "label": "Cache" },
{ "dir": "Code Cache", "label": "Code Cache" },
{ "dir": "GpuCache", "label": "GPU Cache" },
{ "dir": "Service Worker/CacheStorage", "label": "Service Worker Cache" },
{ "dir": "DawnWebGPUCache", "label": "WebGPU Shader Cache", "description": "Dawn's compiled WebGPU shaders — rebuilt on demand" },
{ "dir": "DawnGraphiteCache", "label": "Graphite Shader Cache", "description": "Dawn/Skia Graphite pipeline cache — rebuilt on demand" }
],
"shared": [
{ "dir": "component_crx_cache", "label": "Component Extension Cache", "description": "Downloaded CRX packages for component extensions — re-fetched on next update check, not the installed extensions themselves" },
{ "dir": "extensions_crx_cache", "label": "Extension Update Cache", "description": "Downloaded CRX packages awaiting install — re-fetched on next update check" },
{ "dir": "GrShaderCache", "label": "Skia Shader Cache", "description": "Skia GPU shader cache — rebuilt on demand" },
{ "dir": "ShaderCache", "label": "Shader Cache", "description": "Legacy GPU shader cache — rebuilt on demand" },
{ "dir": "GraphiteDawnCache", "label": "Graphite Pipeline Cache", "description": "Where newer Chromium builds keep the Graphite pipeline cache — rebuilt on demand" },
{ "dir": "GPUPersistentCache", "label": "Persistent GPU Cache", "description": "Where newer Chromium builds keep the on-disk GPU cache — rebuilt on demand" },
{ "dir": "PnaclTranslationCache", "label": "PNaCl Translation Cache", "description": "Translated PNaCl modules — regenerated on demand" }
]
},
"chromium": [
{ "key": "chrome", "base": "${CONFIG}/google-chrome" },
Expand Down
32 changes: 27 additions & 5 deletions rules/schema/rules.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,21 @@
}
},

"ChromiumCacheDir": {
"type": "object",
"required": ["dir", "label"],
"additionalProperties": false,
"properties": {
"dir": {
"type": "string",
"minLength": 1,
"description": "Directory relative to the profile (or to the user-data base, for shared entries). Use forward slashes — the loader converts to OS-native separators."
},
"label": { "type": "string", "minLength": 1, "description": "Display label appended to the browser (and profile) name" },
"description": { "type": "string", "description": "Optional explanation of what this directory holds and why it's safe to delete" }
}
},

"ChromiumBrowser": {
"type": "object",
"required": ["key", "base"],
Expand Down Expand Up @@ -147,13 +162,20 @@
"type": { "const": "browsers" },
"chromiumCacheDirs": {
"type": "object",
"required": ["cache", "codeCache", "gpuCache", "serviceWorker"],
"required": ["profile"],
"additionalProperties": false,
"properties": {
"cache": { "type": "string" },
"codeCache": { "type": "string" },
"gpuCache": { "type": "string" },
"serviceWorker": { "type": "string" }
"profile": {
"type": "array",
"items": { "$ref": "#/definitions/ChromiumCacheDir" },
"minItems": 1,
"description": "Cache directories that live inside each browser profile (Default, Profile 1, …)"
},
"shared": {
"type": "array",
"items": { "$ref": "#/definitions/ChromiumCacheDir" },
"description": "Cache directories shared by every profile, sitting directly under the user-data base"
}
}
},
"chromium": {
Expand Down
21 changes: 17 additions & 4 deletions rules/win32/browsers.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,23 @@
"$schema": "../schema/rules.schema.json",
"type": "browsers",
"chromiumCacheDirs": {
"cache": "Cache/Cache_Data",
"codeCache": "Code Cache",
"gpuCache": "GPUCache",
"serviceWorker": "Service Worker/CacheStorage"
"profile": [
{ "dir": "Cache/Cache_Data", "label": "Cache" },
{ "dir": "Code Cache", "label": "Code Cache" },
{ "dir": "GPUCache", "label": "GPU Cache" },
{ "dir": "Service Worker/CacheStorage", "label": "Service Worker Cache" },
{ "dir": "DawnWebGPUCache", "label": "WebGPU Shader Cache", "description": "Dawn's compiled WebGPU shaders — rebuilt on demand" },
{ "dir": "DawnGraphiteCache", "label": "Graphite Shader Cache", "description": "Dawn/Skia Graphite pipeline cache — rebuilt on demand" }
],
"shared": [
{ "dir": "component_crx_cache", "label": "Component Extension Cache", "description": "Downloaded CRX packages for component extensions — re-fetched on next update check, not the installed extensions themselves" },
{ "dir": "extensions_crx_cache", "label": "Extension Update Cache", "description": "Downloaded CRX packages awaiting install — re-fetched on next update check" },
{ "dir": "GrShaderCache", "label": "Skia Shader Cache", "description": "Skia GPU shader cache — rebuilt on demand" },
{ "dir": "ShaderCache", "label": "Shader Cache", "description": "Legacy GPU shader cache — rebuilt on demand" },
{ "dir": "GraphiteDawnCache", "label": "Graphite Pipeline Cache", "description": "Where newer Chromium builds keep the Graphite pipeline cache — rebuilt on demand" },
{ "dir": "GPUPersistentCache", "label": "Persistent GPU Cache", "description": "Where newer Chromium builds keep the on-disk GPU cache — rebuilt on demand" },
{ "dir": "PnaclTranslationCache", "label": "PNaCl Translation Cache", "description": "Translated PNaCl modules — regenerated on demand" }
]
},
"chromium": [
{ "key": "chrome", "base": "${LOCALAPPDATA}/Google/Chrome/User Data" },
Expand Down
65 changes: 9 additions & 56 deletions src/main/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { readdir } from 'fs/promises'
import { join } from 'path'
import { scanDirectory, scanFile, scanMultipleDirectories, scanDirectoriesAsItems, resolveChildSubdirs, cleanItems, getDirectorySize } from './services/file-utils'
import { cacheItems } from './services/scan-cache'
import { BROWSER_CACHE_RECENCY, chromiumBrowsers, chromiumCacheTargets } from './services/chromium-cache'
import { CleanerType } from '../shared/enums'
import type { ScanResult, CleanResult } from '../shared/types'
import { getPlatform } from './platform'
Expand Down Expand Up @@ -182,48 +183,11 @@ async function scanBrowserCli(): Promise<ScanResult[]> {
const results: ScanResult[] = []
const category = CleanerType.Browser
const browserPaths = getPlatform().paths.browserPaths()
const chromiumBrowsers = [
{ label: 'Chrome', ...browserPaths.chrome, hasProfiles: true },
{ label: 'Edge', ...browserPaths.edge, hasProfiles: true },
{ label: 'Brave', ...browserPaths.brave, hasProfiles: true },
{ label: 'Vivaldi', ...browserPaths.vivaldi, hasProfiles: true },
{ label: 'Opera', ...browserPaths.opera, hasProfiles: false },
{ label: 'Opera GX', ...browserPaths.operaGX, hasProfiles: false },
{ label: 'Arc', ...browserPaths.arc, hasProfiles: true },
{ label: 'Chromium', ...browserPaths.chromium, hasProfiles: true },
{ label: 'Thorium', ...browserPaths.thorium, hasProfiles: true },
{ label: 'Supermium', ...browserPaths.supermium, hasProfiles: true },
{ label: 'Helium', ...browserPaths.helium, hasProfiles: true },
{ label: 'Cromite', ...browserPaths.cromite, hasProfiles: true },
{ label: 'CatsXP', ...browserPaths.catsxp, hasProfiles: true },
]
for (const browser of chromiumBrowsers) {
if (!existsSync(browser.base)) continue
if (browser.hasProfiles) {
const profiles = await getChromiumProfiles(browser.base)
for (const profile of profiles) {
for (const { dir, label } of [
{ dir: browser.cache, label: 'Cache' }, { dir: browser.codeCache, label: 'Code Cache' },
{ dir: browser.gpuCache, label: 'GPU Cache' }, { dir: browser.serviceWorker, label: 'Service Worker Cache' },
]) {
const cachePath = join(browser.base, profile, dir)
if (existsSync(cachePath)) {
const result = await scanDirectory(cachePath, category, `${browser.label} - ${profile} ${label}`)
if (result.items.length > 0) { cacheItems(result.items); results.push(result) }
}
}
}
} else {
for (const { dir, label } of [
{ dir: browser.cache, label: 'Cache' }, { dir: browser.codeCache, label: 'Code Cache' },
{ dir: browser.gpuCache, label: 'GPU Cache' }, { dir: browser.serviceWorker, label: 'Service Worker Cache' },
]) {
const cachePath = join(browser.base, dir)
if (existsSync(cachePath)) {
const result = await scanDirectory(cachePath, category, `${browser.label} - ${label}`)
if (result.items.length > 0) { cacheItems(result.items); results.push(result) }
}
}
const recency = BROWSER_CACHE_RECENCY
for (const browser of chromiumBrowsers(browserPaths)) {
for (const target of await chromiumCacheTargets(browser)) {
const result = await scanDirectory(target.path, category, target.label, recency)
if (result.items.length > 0) { cacheItems(result.items); results.push(result) }
}
}
if (existsSync(browserPaths.firefox.cache)) {
Expand All @@ -233,7 +197,7 @@ async function scanBrowserCli(): Promise<ScanResult[]> {
if (dir.isDirectory()) {
const cachePath = join(browserPaths.firefox.cache, dir.name, 'cache2', 'entries')
if (existsSync(cachePath)) {
const result = await scanDirectory(cachePath, category, `Firefox - ${dir.name} Cache`)
const result = await scanDirectory(cachePath, category, `Firefox - ${dir.name} Cache`, recency)
if (result.items.length > 0) { cacheItems(result.items); results.push(result) }
}
}
Expand All @@ -254,7 +218,7 @@ async function scanBrowserCli(): Promise<ScanResult[]> {
if (dir.isDirectory()) {
const cachePath = join(fork.cache, dir.name, 'cache2')
if (existsSync(cachePath)) {
const result = await scanDirectory(cachePath, category, `${fork.label} - ${dir.name} Cache`)
const result = await scanDirectory(cachePath, category, `${fork.label} - ${dir.name} Cache`, recency)
if (result.items.length > 0) { cacheItems(result.items); results.push(result) }
}
}
Expand All @@ -263,7 +227,7 @@ async function scanBrowserCli(): Promise<ScanResult[]> {
}
// Safari (macOS only) — cache directory only, never cookies/history/bookmarks
if (browserPaths.safari && existsSync(browserPaths.safari.cache)) {
const result = await scanDirectory(browserPaths.safari.cache, category, 'Safari - Cache')
const result = await scanDirectory(browserPaths.safari.cache, category, 'Safari - Cache', recency)
if (result.items.length > 0) { cacheItems(result.items); results.push(result) }
}
return results
Expand Down Expand Up @@ -471,17 +435,6 @@ async function cleanDatabasesCli(itemIds: string[]): Promise<CleanResult> {
return { totalCleaned, filesDeleted, filesSkipped, errors, needsElevation: errors.some((e) => e.reason === 'permission-denied') }
}

async function getChromiumProfiles(basePath: string): Promise<string[]> {
const profiles = ['Default']
try {
const entries = await readdir(basePath, { withFileTypes: true })
for (const entry of entries) {
if (entry.isDirectory() && entry.name.startsWith('Profile ')) profiles.push(entry.name)
}
} catch { /* skip */ }
return profiles
}

// ─── Help text ───────────────────────────────────────────────

function printHelp(): void {
Expand Down
75 changes: 62 additions & 13 deletions src/main/ipc/browser-cleaner.ipc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,21 +59,33 @@ function getHandler(channel: string): (...args: unknown[]) => unknown {
return call[1] as (...args: unknown[]) => unknown
}

const PROFILE_CACHES = [
{ dir: 'Cache', label: 'Cache' },
{ dir: 'Code Cache', label: 'Code Cache' },
{ dir: 'GPUCache', label: 'GPU Cache' },
{ dir: 'Service Worker', label: 'Service Worker Cache' },
]
const SHARED_CACHES = [{ dir: 'GrShaderCache', label: 'Skia Shader Cache' }]

function browser(base: string) {
return { base, profileCaches: PROFILE_CACHES, sharedCaches: SHARED_CACHES }
}

function makeBrowserPaths() {
return {
chrome: { base: '/home/user/.config/google-chrome', cache: 'Cache', codeCache: 'Code Cache', gpuCache: 'GPUCache', serviceWorker: 'Service Worker' },
edge: { base: '/home/user/.config/microsoft-edge', cache: 'Cache', codeCache: 'Code Cache', gpuCache: 'GPUCache', serviceWorker: 'Service Worker' },
brave: { base: '/fake/brave', cache: 'Cache', codeCache: 'Code Cache', gpuCache: 'GPUCache', serviceWorker: 'Service Worker' },
vivaldi: { base: '/fake/vivaldi', cache: 'Cache', codeCache: 'Code Cache', gpuCache: 'GPUCache', serviceWorker: 'Service Worker' },
opera: { base: '/fake/opera', cache: 'Cache', codeCache: 'Code Cache', gpuCache: 'GPUCache', serviceWorker: 'Service Worker' },
operaGX: { base: '/fake/operaGX', cache: 'Cache', codeCache: 'Code Cache', gpuCache: 'GPUCache', serviceWorker: 'Service Worker' },
arc: { base: '/fake/arc', cache: 'Cache', codeCache: 'Code Cache', gpuCache: 'GPUCache', serviceWorker: 'Service Worker' },
chromium: { base: '/fake/chromium', cache: 'Cache', codeCache: 'Code Cache', gpuCache: 'GPUCache', serviceWorker: 'Service Worker' },
thorium: { base: '/fake/thorium', cache: 'Cache', codeCache: 'Code Cache', gpuCache: 'GPUCache', serviceWorker: 'Service Worker' },
supermium: { base: '/fake/supermium', cache: 'Cache', codeCache: 'Code Cache', gpuCache: 'GPUCache', serviceWorker: 'Service Worker' },
helium: { base: '/fake/helium', cache: 'Cache', codeCache: 'Code Cache', gpuCache: 'GPUCache', serviceWorker: 'Service Worker' },
cromite: { base: '/fake/cromite', cache: 'Cache', codeCache: 'Code Cache', gpuCache: 'GPUCache', serviceWorker: 'Service Worker' },
catsxp: { base: '/fake/catsxp', cache: 'Cache', codeCache: 'Code Cache', gpuCache: 'GPUCache', serviceWorker: 'Service Worker' },
chrome: browser('/home/user/.config/google-chrome'),
edge: browser('/home/user/.config/microsoft-edge'),
brave: browser('/fake/brave'),
vivaldi: browser('/fake/vivaldi'),
opera: browser('/fake/opera'),
operaGX: browser('/fake/operaGX'),
arc: browser('/fake/arc'),
chromium: browser('/fake/chromium'),
thorium: browser('/fake/thorium'),
supermium: browser('/fake/supermium'),
helium: browser('/fake/helium'),
cromite: browser('/fake/cromite'),
catsxp: browser('/fake/catsxp'),
firefox: { cache: '/fake/firefox-cache' },
librewolf: { cache: '' },
waterfox: { cache: '' },
Expand Down Expand Up @@ -150,6 +162,43 @@ describe('BROWSER_SCAN handler', () => {
expect(mockCacheItems).toHaveBeenCalled()
})

// Issue #265: judging a directory by its own mtime made scanDirectory drop
// `Code Cache` whole — it holds only `js` and `wasm`, both touched by any
// recent browsing.
it('scans browser caches with directories judged by their contents', async () => {
mockExistsSync.mockReturnValue(true)
mockReaddir.mockResolvedValue([])
mockScanDirectory.mockResolvedValue({ category: 'browser', subcategory: 'test', items: [], totalSize: 0, itemCount: 0 })

registerBrowserCleanerIpc(() => mockWindow() as any)
await getHandler('cleaner:browser:scan')()

expect(mockScanDirectory).toHaveBeenCalled()
for (const call of mockScanDirectory.mock.calls) {
expect(call[3]).toEqual({ deepRecencyCheck: true })
}
})

it('scans the shared caches that sit beside the profiles', async () => {
const chromeBase = '/home/user/.config/google-chrome'
mockExistsSync.mockImplementation((p: string) => p === chromeBase || p.startsWith(join(chromeBase, 'GrShaderCache')))
mockReaddir.mockResolvedValue([])
mockScanDirectory.mockResolvedValue({
category: 'browser',
subcategory: 'Chrome - Skia Shader Cache',
items: [{ id: '1', path: '/test', size: 100 }],
totalSize: 100,
itemCount: 1,
})

registerBrowserCleanerIpc(() => mockWindow() as any)
await getHandler('cleaner:browser:scan')()

expect(mockScanDirectory).toHaveBeenCalledWith(
join(chromeBase, 'GrShaderCache'), 'browser', 'Chrome - Skia Shader Cache', { deepRecencyCheck: true }
)
})

it('scans Opera-style browsers without profiles', async () => {
const paths = makeBrowserPaths()
// Only opera exists
Expand Down
Loading
Loading