Created
February 17, 2026 12:00
-
-
Save fdstevex/e0ec128b0342ee3b1772dd0c85b372b3 to your computer and use it in GitHub Desktop.
Chrome Cache Viewer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """Chrome Cache Viewer - A web tool for browsing Chrome's disk cache.""" | |
| import http.server | |
| import json | |
| import mimetypes | |
| import os | |
| import struct | |
| import sys | |
| import urllib.parse | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| CACHE_DIR = Path.home() / "Library/Caches/Google/Chrome/Default/Cache/Cache_Data" | |
| SIMPLE_CACHE_MAGIC = 0xFCFB6D1BA7725C30 | |
| EOF_MAGIC = 0xF4FA6F45970D41D8 | |
| BODY_PREFIX_SIZE = 4 # Chrome adds a 4-byte prefix before the body data | |
| PORT = 8088 | |
| def parse_cache_entry(filepath): | |
| """Parse a Chrome Simple Cache entry file and return metadata.""" | |
| try: | |
| with open(filepath, "rb") as f: | |
| data = f.read() | |
| except OSError: | |
| return None | |
| if len(data) < 24: | |
| return None | |
| magic, version, key_len, key_hash = struct.unpack("<QIII", data[:20]) | |
| if magic != SIMPLE_CACHE_MAGIC: | |
| return None | |
| if 20 + key_len > len(data): | |
| return None | |
| key_raw = data[20 : 20 + key_len] | |
| key_str = key_raw.lstrip(b"\x00").decode("utf-8", errors="replace") | |
| # Extract the actual URL from the partition key format | |
| # Format: "1/0/_dk_<top-frame-origin> <requesting-origin> <url>" | |
| # or just a plain URL | |
| url = key_str | |
| if "_dk_" in key_str: | |
| parts = key_str.split(" ") | |
| url = parts[-1] if len(parts) >= 3 else parts[-1] | |
| elif key_str.startswith("1/0/"): | |
| url = key_str[4:] | |
| # Find the body region (between key end and first EOF magic) | |
| body_start = 20 + key_len | |
| eof_pos = None | |
| for i in range(body_start, min(body_start + 10_000_000, len(data) - 8)): | |
| if struct.unpack("<Q", data[i : i + 8])[0] == EOF_MAGIC: | |
| eof_pos = i | |
| break | |
| # Extract HTTP headers (find "HTTP/" in the data after the first EOF) | |
| headers = {} | |
| status_line = "" | |
| http_pos = data.find(b"HTTP/", body_start) | |
| if http_pos > 0: | |
| hdr_end = data.find(b"\x00\x00", http_pos) | |
| if hdr_end < 0: | |
| hdr_end = min(http_pos + 4000, len(data)) | |
| for h in data[http_pos:hdr_end].split(b"\x00"): | |
| hs = h.decode("utf-8", errors="replace") | |
| if hs.startswith("HTTP/"): | |
| status_line = hs | |
| elif ":" in hs: | |
| name, _, value = hs.partition(":") | |
| headers[name.strip().lower()] = value.strip() | |
| content_type = headers.get("content-type", "") | |
| content_length = headers.get("content-length", "") | |
| # Body data: skip the 4-byte prefix | |
| body_offset = body_start + BODY_PREFIX_SIZE | |
| body_end = eof_pos if eof_pos else len(data) | |
| body_size = max(0, body_end - body_offset) | |
| # File modification time | |
| try: | |
| mtime = os.path.getmtime(filepath) | |
| modified = datetime.fromtimestamp(mtime, tz=timezone.utc).isoformat() | |
| except OSError: | |
| modified = "" | |
| return { | |
| "filename": os.path.basename(filepath), | |
| "url": url, | |
| "full_key": key_str, | |
| "status": status_line, | |
| "content_type": content_type, | |
| "content_length": content_length, | |
| "body_size": body_size, | |
| "file_size": len(data), | |
| "modified": modified, | |
| "headers": headers, | |
| "body_offset": body_offset, | |
| "body_end": body_end, | |
| } | |
| def get_body_data(filepath, entry): | |
| """Extract the body data from a cache entry file.""" | |
| try: | |
| with open(filepath, "rb") as f: | |
| f.seek(entry["body_offset"]) | |
| return f.read(entry["body_end"] - entry["body_offset"]) | |
| except OSError: | |
| return b"" | |
| def scan_cache(limit=2000): | |
| """Scan the cache directory and return parsed entries.""" | |
| entries = [] | |
| cache_files = sorted( | |
| (f for f in CACHE_DIR.iterdir() if f.name.endswith("_0") and f.name != "index"), | |
| key=lambda f: f.stat().st_mtime, | |
| reverse=True, | |
| ) | |
| for fpath in cache_files[:limit]: | |
| entry = parse_cache_entry(fpath) | |
| if entry and entry["url"]: | |
| entries.append(entry) | |
| return entries | |
| HTML_PAGE = r"""<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Chrome Cache Viewer</title> | |
| <style> | |
| :root { | |
| --bg: #1a1a2e; | |
| --surface: #16213e; | |
| --surface2: #0f3460; | |
| --accent: #e94560; | |
| --accent2: #533483; | |
| --text: #eee; | |
| --text2: #aab; | |
| --border: #2a2a4a; | |
| --green: #4ecca3; | |
| --yellow: #f0c040; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace; | |
| background: var(--bg); | |
| color: var(--text); | |
| font-size: 13px; | |
| line-height: 1.5; | |
| } | |
| header { | |
| background: var(--surface); | |
| border-bottom: 1px solid var(--border); | |
| padding: 12px 20px; | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| } | |
| header h1 { | |
| font-size: 16px; | |
| font-weight: 600; | |
| color: var(--accent); | |
| white-space: nowrap; | |
| } | |
| .controls { | |
| display: flex; | |
| gap: 10px; | |
| flex: 1; | |
| align-items: center; | |
| } | |
| input[type="text"] { | |
| background: var(--bg); | |
| border: 1px solid var(--border); | |
| color: var(--text); | |
| padding: 6px 12px; | |
| border-radius: 4px; | |
| font-family: inherit; | |
| font-size: 13px; | |
| flex: 1; | |
| max-width: 500px; | |
| } | |
| input[type="text"]:focus { | |
| outline: none; | |
| border-color: var(--accent); | |
| } | |
| select { | |
| background: var(--bg); | |
| border: 1px solid var(--border); | |
| color: var(--text); | |
| padding: 6px 10px; | |
| border-radius: 4px; | |
| font-family: inherit; | |
| font-size: 13px; | |
| } | |
| .stats { | |
| color: var(--text2); | |
| font-size: 12px; | |
| white-space: nowrap; | |
| } | |
| .main { | |
| display: flex; | |
| height: calc(100vh - 49px); | |
| } | |
| .list-panel { | |
| width: 55%; | |
| overflow-y: auto; | |
| border-right: 1px solid var(--border); | |
| } | |
| .detail-panel { | |
| width: 45%; | |
| overflow-y: auto; | |
| background: var(--surface); | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| } | |
| th { | |
| background: var(--surface); | |
| padding: 6px 10px; | |
| text-align: left; | |
| font-weight: 600; | |
| color: var(--text2); | |
| font-size: 11px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| position: sticky; | |
| top: 0; | |
| z-index: 10; | |
| border-bottom: 1px solid var(--border); | |
| cursor: pointer; | |
| user-select: none; | |
| } | |
| th:hover { color: var(--accent); } | |
| th.sorted { color: var(--accent); } | |
| td { | |
| padding: 5px 10px; | |
| border-bottom: 1px solid var(--border); | |
| max-width: 0; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| tr.entry-row { cursor: pointer; } | |
| tr.entry-row:hover { background: var(--surface2); } | |
| tr.entry-row.selected { background: var(--accent2); } | |
| .url-cell { | |
| color: var(--green); | |
| } | |
| .type-cell { | |
| color: var(--yellow); | |
| } | |
| .size-cell { | |
| text-align: right; | |
| color: var(--text2); | |
| } | |
| .date-cell { | |
| color: var(--text2); | |
| font-size: 12px; | |
| } | |
| .status-ok { color: var(--green); } | |
| .status-redirect { color: var(--yellow); } | |
| .status-error { color: var(--accent); } | |
| .detail-empty { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| height: 100%; | |
| color: var(--text2); | |
| } | |
| .detail-content { padding: 16px; } | |
| .detail-section { | |
| margin-bottom: 16px; | |
| } | |
| .detail-section h3 { | |
| font-size: 12px; | |
| color: var(--accent); | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| margin-bottom: 8px; | |
| padding-bottom: 4px; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .detail-url { | |
| word-break: break-all; | |
| color: var(--green); | |
| font-size: 12px; | |
| margin-bottom: 8px; | |
| } | |
| .detail-url a { | |
| color: var(--green); | |
| text-decoration: none; | |
| } | |
| .detail-url a:hover { text-decoration: underline; } | |
| .header-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| } | |
| .header-table td { | |
| padding: 3px 8px; | |
| border-bottom: 1px solid var(--border); | |
| font-size: 12px; | |
| max-width: none; | |
| white-space: normal; | |
| word-break: break-all; | |
| } | |
| .header-table td:first-child { | |
| color: var(--yellow); | |
| white-space: nowrap; | |
| width: 160px; | |
| } | |
| .preview-container { | |
| background: var(--bg); | |
| border: 1px solid var(--border); | |
| border-radius: 4px; | |
| padding: 12px; | |
| text-align: center; | |
| min-height: 60px; | |
| } | |
| .preview-container img { | |
| max-width: 100%; | |
| max-height: 400px; | |
| image-rendering: auto; | |
| } | |
| .preview-container pre { | |
| text-align: left; | |
| white-space: pre-wrap; | |
| word-break: break-all; | |
| max-height: 400px; | |
| overflow-y: auto; | |
| font-size: 12px; | |
| color: var(--text); | |
| } | |
| .preview-container audio, | |
| .preview-container video { | |
| max-width: 100%; | |
| } | |
| .btn { | |
| background: var(--surface2); | |
| color: var(--text); | |
| border: 1px solid var(--border); | |
| padding: 4px 12px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-family: inherit; | |
| font-size: 12px; | |
| } | |
| .btn:hover { background: var(--accent2); } | |
| .loading { | |
| color: var(--text2); | |
| padding: 40px; | |
| text-align: center; | |
| } | |
| .badge { | |
| display: inline-block; | |
| padding: 1px 6px; | |
| border-radius: 3px; | |
| font-size: 11px; | |
| font-weight: 600; | |
| } | |
| .badge-img { background: #2d4a3e; color: #4ecca3; } | |
| .badge-js { background: #4a4a2d; color: #f0c040; } | |
| .badge-css { background: #2d3a5a; color: #60a0ff; } | |
| .badge-html { background: #4a2d3a; color: #e94560; } | |
| .badge-json { background: #3a2d4a; color: #b060ff; } | |
| .badge-font { background: #3a3a2d; color: #c0a060; } | |
| .badge-other { background: #2d2d3a; color: #8888aa; } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>Chrome Cache Viewer</h1> | |
| <div class="controls"> | |
| <input type="text" id="search" placeholder="Filter by URL or content type..." autofocus> | |
| <select id="type-filter"> | |
| <option value="">All Types</option> | |
| <option value="image">Images</option> | |
| <option value="text/javascript">JavaScript</option> | |
| <option value="text/css">CSS</option> | |
| <option value="text/html">HTML</option> | |
| <option value="application/json">JSON</option> | |
| <option value="font">Fonts</option> | |
| <option value="video">Video</option> | |
| <option value="audio">Audio</option> | |
| </select> | |
| <span class="stats" id="stats">Loading...</span> | |
| </div> | |
| </header> | |
| <div class="main"> | |
| <div class="list-panel"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th data-sort="type" style="width:70px">Type</th> | |
| <th data-sort="url">URL</th> | |
| <th data-sort="status" style="width:50px">Status</th> | |
| <th data-sort="size" style="width:80px" class="size-cell">Size</th> | |
| <th data-sort="modified" style="width:130px">Modified</th> | |
| </tr> | |
| </thead> | |
| <tbody id="entries"></tbody> | |
| </table> | |
| </div> | |
| <div class="detail-panel" id="detail"> | |
| <div class="detail-empty">Select a cache entry to view details</div> | |
| </div> | |
| </div> | |
| <script> | |
| let allEntries = []; | |
| let sortCol = 'modified'; | |
| let sortAsc = false; | |
| let selectedFile = null; | |
| async function loadEntries() { | |
| const resp = await fetch('/api/entries'); | |
| allEntries = await resp.json(); | |
| renderEntries(); | |
| } | |
| function getTypeBadge(ct) { | |
| if (!ct) return '<span class="badge badge-other">???</span>'; | |
| if (ct.includes('image')) return '<span class="badge badge-img">IMG</span>'; | |
| if (ct.includes('javascript')) return '<span class="badge badge-js">JS</span>'; | |
| if (ct.includes('css')) return '<span class="badge badge-css">CSS</span>'; | |
| if (ct.includes('html')) return '<span class="badge badge-html">HTML</span>'; | |
| if (ct.includes('json')) return '<span class="badge badge-json">JSON</span>'; | |
| if (ct.includes('font') || ct.includes('woff') || ct.includes('ttf')) return '<span class="badge badge-font">FONT</span>'; | |
| if (ct.includes('video')) return '<span class="badge badge-img">VID</span>'; | |
| if (ct.includes('audio')) return '<span class="badge badge-img">AUD</span>'; | |
| return '<span class="badge badge-other">' + ct.split('/').pop().split(';')[0].substring(0,6).toUpperCase() + '</span>'; | |
| } | |
| function formatSize(bytes) { | |
| if (!bytes || bytes <= 0) return '-'; | |
| if (bytes < 1024) return bytes + ' B'; | |
| if (bytes < 1024*1024) return (bytes/1024).toFixed(1) + ' KB'; | |
| return (bytes/(1024*1024)).toFixed(1) + ' MB'; | |
| } | |
| function getStatusClass(status) { | |
| if (!status) return ''; | |
| const code = parseInt(status.split(' ')[1]); | |
| if (code >= 200 && code < 300) return 'status-ok'; | |
| if (code >= 300 && code < 400) return 'status-redirect'; | |
| return 'status-error'; | |
| } | |
| function getStatusCode(status) { | |
| if (!status) return ''; | |
| const parts = status.split(' '); | |
| return parts.length > 1 ? parts[1] : ''; | |
| } | |
| function formatDate(iso) { | |
| if (!iso) return ''; | |
| const d = new Date(iso); | |
| const month = String(d.getMonth()+1).padStart(2,'0'); | |
| const day = String(d.getDate()).padStart(2,'0'); | |
| const hours = String(d.getHours()).padStart(2,'0'); | |
| const mins = String(d.getMinutes()).padStart(2,'0'); | |
| return `${d.getFullYear()}-${month}-${day} ${hours}:${mins}`; | |
| } | |
| function getShortUrl(url) { | |
| try { | |
| const u = new URL(url); | |
| let path = u.pathname + u.search; | |
| if (path.length > 80) path = path.substring(0, 80) + '...'; | |
| return u.hostname + path; | |
| } catch { | |
| if (url.length > 100) return url.substring(0, 100) + '...'; | |
| return url; | |
| } | |
| } | |
| function filterEntries() { | |
| const q = document.getElementById('search').value.toLowerCase(); | |
| const tf = document.getElementById('type-filter').value.toLowerCase(); | |
| return allEntries.filter(e => { | |
| if (q && !e.url.toLowerCase().includes(q) && !e.content_type.toLowerCase().includes(q)) return false; | |
| if (tf && !e.content_type.toLowerCase().includes(tf)) return false; | |
| return true; | |
| }); | |
| } | |
| function sortEntries(entries) { | |
| return entries.sort((a, b) => { | |
| let va, vb; | |
| switch(sortCol) { | |
| case 'url': va = a.url; vb = b.url; break; | |
| case 'type': va = a.content_type; vb = b.content_type; break; | |
| case 'status': va = a.status; vb = b.status; break; | |
| case 'size': va = a.body_size; vb = b.body_size; break; | |
| case 'modified': va = a.modified; vb = b.modified; break; | |
| default: va = a.modified; vb = b.modified; | |
| } | |
| if (va < vb) return sortAsc ? -1 : 1; | |
| if (va > vb) return sortAsc ? 1 : -1; | |
| return 0; | |
| }); | |
| } | |
| function renderEntries() { | |
| let entries = filterEntries(); | |
| entries = sortEntries(entries); | |
| document.getElementById('stats').textContent = | |
| `${entries.length} of ${allEntries.length} entries`; | |
| const tbody = document.getElementById('entries'); | |
| // Render in chunks for performance | |
| const frag = document.createDocumentFragment(); | |
| const limit = Math.min(entries.length, 2000); | |
| for (let i = 0; i < limit; i++) { | |
| const e = entries[i]; | |
| const tr = document.createElement('tr'); | |
| tr.className = 'entry-row' + (e.filename === selectedFile ? ' selected' : ''); | |
| tr.dataset.filename = e.filename; | |
| tr.innerHTML = ` | |
| <td>${getTypeBadge(e.content_type)}</td> | |
| <td class="url-cell" title="${e.url}">${getShortUrl(e.url)}</td> | |
| <td class="${getStatusClass(e.status)}">${getStatusCode(e.status)}</td> | |
| <td class="size-cell">${formatSize(e.body_size)}</td> | |
| <td class="date-cell">${formatDate(e.modified)}</td> | |
| `; | |
| tr.onclick = () => showDetail(e); | |
| frag.appendChild(tr); | |
| } | |
| tbody.replaceChildren(frag); | |
| // Update sort headers | |
| document.querySelectorAll('th[data-sort]').forEach(th => { | |
| th.classList.toggle('sorted', th.dataset.sort === sortCol); | |
| th.textContent = th.textContent.replace(/ [▲▼]$/, ''); | |
| if (th.dataset.sort === sortCol) { | |
| th.textContent += sortAsc ? ' ▲' : ' ▼'; | |
| } | |
| }); | |
| } | |
| async function showDetail(entry) { | |
| selectedFile = entry.filename; | |
| renderEntries(); | |
| const detail = document.getElementById('detail'); | |
| detail.innerHTML = `<div class="detail-content"> | |
| <div class="detail-section"> | |
| <h3>URL</h3> | |
| <div class="detail-url"><a href="${entry.url}" target="_blank">${entry.url}</a></div> | |
| </div> | |
| <div class="detail-section"> | |
| <h3>Response ${entry.status}</h3> | |
| <table class="header-table"> | |
| ${Object.entries(entry.headers).map(([k,v]) => | |
| `<tr><td>${k}</td><td>${v}</td></tr>` | |
| ).join('')} | |
| </table> | |
| </div> | |
| <div class="detail-section"> | |
| <h3>Preview <button class="btn" onclick="downloadEntry('${entry.filename}', '${entry.content_type}')">Download</button></h3> | |
| <div class="preview-container" id="preview">Loading...</div> | |
| </div> | |
| <div class="detail-section"> | |
| <h3>Cache File</h3> | |
| <table class="header-table"> | |
| <tr><td>File</td><td>${entry.filename}</td></tr> | |
| <tr><td>File Size</td><td>${formatSize(entry.file_size)}</td></tr> | |
| <tr><td>Body Size</td><td>${formatSize(entry.body_size)}</td></tr> | |
| <tr><td>Full Key</td><td style="font-size:11px">${entry.full_key}</td></tr> | |
| </table> | |
| </div> | |
| </div>`; | |
| loadPreview(entry); | |
| } | |
| async function loadPreview(entry) { | |
| const container = document.getElementById('preview'); | |
| const ct = entry.content_type.toLowerCase(); | |
| const url = '/api/body/' + entry.filename; | |
| if (entry.body_size <= 0) { | |
| container.textContent = '(empty body)'; | |
| return; | |
| } | |
| if (ct.includes('image')) { | |
| const img = document.createElement('img'); | |
| img.src = url; | |
| img.alt = 'Cached image'; | |
| img.onerror = () => { container.textContent = '(could not render image)'; }; | |
| container.replaceChildren(img); | |
| } else if (ct.includes('video')) { | |
| container.innerHTML = `<video controls src="${url}"></video>`; | |
| } else if (ct.includes('audio')) { | |
| container.innerHTML = `<audio controls src="${url}"></audio>`; | |
| } else if (ct.includes('javascript') || ct.includes('css') || ct.includes('json') || | |
| ct.includes('html') || ct.includes('xml') || ct.includes('text') || | |
| ct.includes('svg')) { | |
| try { | |
| const resp = await fetch(url); | |
| let text = await resp.text(); | |
| if (text.length > 50000) text = text.substring(0, 50000) + '\n... (truncated)'; | |
| const pre = document.createElement('pre'); | |
| pre.textContent = text; | |
| container.replaceChildren(pre); | |
| } catch { | |
| container.textContent = '(could not load text content)'; | |
| } | |
| } else if (ct.includes('font') || ct.includes('woff') || ct.includes('ttf') || ct.includes('otf')) { | |
| container.textContent = `Font file (${formatSize(entry.body_size)}) - use Download to save`; | |
| } else { | |
| container.textContent = `Binary data (${formatSize(entry.body_size)}) - use Download to save`; | |
| } | |
| } | |
| function downloadEntry(filename, contentType) { | |
| const a = document.createElement('a'); | |
| a.href = '/api/body/' + filename; | |
| a.download = filename; | |
| a.click(); | |
| } | |
| document.querySelectorAll('th[data-sort]').forEach(th => { | |
| th.addEventListener('click', () => { | |
| if (sortCol === th.dataset.sort) { | |
| sortAsc = !sortAsc; | |
| } else { | |
| sortCol = th.dataset.sort; | |
| sortAsc = sortCol === 'url' || sortCol === 'type'; | |
| } | |
| renderEntries(); | |
| }); | |
| }); | |
| let searchTimeout; | |
| document.getElementById('search').addEventListener('input', () => { | |
| clearTimeout(searchTimeout); | |
| searchTimeout = setTimeout(renderEntries, 200); | |
| }); | |
| document.getElementById('type-filter').addEventListener('change', renderEntries); | |
| loadEntries(); | |
| </script> | |
| </body> | |
| </html>""" | |
| class CacheViewerHandler(http.server.BaseHTTPRequestHandler): | |
| _entries_cache = None | |
| _entries_cache_time = 0 | |
| def log_message(self, fmt, *args): | |
| # Quiet logging | |
| pass | |
| def _set_headers(self, content_type="text/html", status=200): | |
| self.send_response(status) | |
| self.send_header("Content-Type", content_type) | |
| self.send_header("Cache-Control", "no-cache") | |
| self.end_headers() | |
| def do_GET(self): | |
| parsed = urllib.parse.urlparse(self.path) | |
| path = parsed.path | |
| if path == "/" or path == "": | |
| self._set_headers("text/html") | |
| self.wfile.write(HTML_PAGE.encode()) | |
| elif path == "/api/entries": | |
| # Cache the scan results for 30 seconds | |
| now = __import__("time").time() | |
| if ( | |
| CacheViewerHandler._entries_cache is None | |
| or now - CacheViewerHandler._entries_cache_time > 30 | |
| ): | |
| entries = scan_cache() | |
| CacheViewerHandler._entries_cache = entries | |
| CacheViewerHandler._entries_cache_time = now | |
| else: | |
| entries = CacheViewerHandler._entries_cache | |
| self._set_headers("application/json") | |
| # Send only what the frontend needs (exclude body_offset/body_end for listing) | |
| slim = [] | |
| for e in entries: | |
| slim.append( | |
| { | |
| "filename": e["filename"], | |
| "url": e["url"], | |
| "full_key": e["full_key"], | |
| "status": e["status"], | |
| "content_type": e["content_type"], | |
| "body_size": e["body_size"], | |
| "file_size": e["file_size"], | |
| "modified": e["modified"], | |
| "headers": e["headers"], | |
| } | |
| ) | |
| self.wfile.write(json.dumps(slim).encode()) | |
| elif path.startswith("/api/body/"): | |
| filename = path[len("/api/body/") :] | |
| if "/" in filename or ".." in filename: | |
| self._set_headers("text/plain", 400) | |
| self.wfile.write(b"Invalid filename") | |
| return | |
| filepath = CACHE_DIR / filename | |
| if not filepath.exists(): | |
| self._set_headers("text/plain", 404) | |
| self.wfile.write(b"Not found") | |
| return | |
| entry = parse_cache_entry(filepath) | |
| if not entry: | |
| self._set_headers("text/plain", 500) | |
| self.wfile.write(b"Could not parse cache entry") | |
| return | |
| body = get_body_data(filepath, entry) | |
| ct = entry["content_type"] or "application/octet-stream" | |
| # Strip charset for binary types | |
| mime = ct.split(";")[0].strip() | |
| self._set_headers(mime) | |
| self.wfile.write(body) | |
| else: | |
| self._set_headers("text/plain", 404) | |
| self.wfile.write(b"Not found") | |
| def main(): | |
| port = PORT | |
| if len(sys.argv) > 1: | |
| port = int(sys.argv[1]) | |
| server = http.server.HTTPServer(("127.0.0.1", port), CacheViewerHandler) | |
| print(f"Chrome Cache Viewer running at http://127.0.0.1:{port}") | |
| print(f"Cache directory: {CACHE_DIR}") | |
| print("Press Ctrl+C to stop") | |
| try: | |
| server.serve_forever() | |
| except KeyboardInterrupt: | |
| print("\nStopped.") | |
| server.server_close() | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment