diff --git a/src/main/file_handlers.ts b/src/main/file_handlers.ts index 2392e88..acb73c5 100644 --- a/src/main/file_handlers.ts +++ b/src/main/file_handlers.ts @@ -2,6 +2,7 @@ import { ipcMain, shell } from 'electron'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; +import * as crypto from 'crypto'; import { exec } from 'child_process'; import axios from 'axios'; import { expandPath } from './file_utils'; @@ -15,22 +16,50 @@ export function registerFileHandlers() { const prefix = hash.substring(0, Math.max(1, Math.min(prefix_len, 8))); const dir = path.join(expanded_root, prefix); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); - return path.join(dir, `${hash}.file`); + return path.join(dir, \`\${hash}.file\`); } - ipcMain.handle('native:check-cache', async (event, { cache_root, hash, hash_prefix_length = 2 }) => { + ipcMain.handle('native:check-cache', async (event, { cache_root, hash, hash_prefix_length = 2, verify_hash = false }) => { const full_path = get_organized_hashed_path(cache_root, hash, hash_prefix_length); - return fs.existsSync(full_path); + + if (!fs.existsSync(full_path)) return false; + + if (verify_hash) { + try { + const file_buffer = fs.readFileSync(full_path); + const actual_hash = crypto.createHash('sha256').update(file_buffer).digest('hex'); + return actual_hash === hash; + } catch (e) { + return false; + } + } + + return true; }); ipcMain.handle('native:download-to-cache', async (event, { url, cache_root, hash, api_key, account_id, hash_prefix_length = 2 }) => { const full_path = get_organized_hashed_path(cache_root, hash, hash_prefix_length); - const tmp_path = `${full_path}.tmp`; + const tmp_path = \`\${full_path}.tmp\`; if (endpoints_in_progress.includes(url)) return { success: true, status: 'in_progress' }; + + // 1. If final file exists, skip if (fs.existsSync(full_path)) return { success: true, path: full_path, status: 'exists' }; - console.log(`Native: Organized Download (${hash_prefix_length} chars) -> ${full_path}`); + // 2. Handle stale .tmp files (Legacy "Trust No One" pattern) + if (fs.existsSync(tmp_path)) { + const stats = fs.statSync(tmp_path); + const age_ms = Date.now() - stats.mtimeMs; + // If the tmp file is older than 5 minutes, assume previous download crashed and delete it + if (age_ms > 5 * 60 * 1000) { + console.log(\`Native: Deleting stale temp file (\${Math.round(age_ms/1000)}s old)\`); + fs.unlinkSync(tmp_path); + } else { + return { success: true, status: 'in_progress', detail: 'fresh_tmp_exists' }; + } + } + + console.log(\`Native: Hardened Download -> \${full_path}\`); try { endpoints_in_progress.push(url); @@ -51,7 +80,18 @@ export function registerFileHandlers() { writer.on('error', reject); }); + // 3. Verify Integrity before renaming (The "Trust No One" Check) + const file_buffer = fs.readFileSync(tmp_path); + const actual_hash = crypto.createHash('sha256').update(file_buffer).digest('hex'); + + if (actual_hash !== hash) { + console.error(\`Native: Hash Mismatch! Expected \${hash}, got \${actual_hash}\`); + fs.unlinkSync(tmp_path); + return { success: false, error: 'Integrity check failed: Hash mismatch' }; + } + fs.renameSync(tmp_path, full_path); + console.log(\`Native: Cache Integrity Verified. File moved to final destination.\`); return { success: true, path: full_path }; } catch (error: any) { if (fs.existsSync(tmp_path)) fs.unlinkSync(tmp_path); @@ -67,7 +107,7 @@ export function registerFileHandlers() { const expanded_temp = expandPath(temp_root); const target = path.join(expanded_temp, filename); - console.log(`Native: Launching from Cache -> ${filename}`); + console.log(\`Native: Launching from Cache -> \${filename}\`); if (!fs.existsSync(expanded_temp)) fs.mkdirSync(expanded_temp, { recursive: true }); @@ -81,9 +121,9 @@ export function registerFileHandlers() { // 3. Optimized Launch (LibreOffice / AppleScript) if (is_pres) { if (os.platform() === 'linux') { - console.log(`Native: Launching LibreOffice (--impress) for ${target}`); + console.log(\`Native: Launching LibreOffice (--impress) for \${target}\`); return new Promise((resolve) => { - exec(`libreoffice --impress "${target}"`, (err) => { + exec(\`libreoffice --impress "\${target}"\`, (err) => { if (err) resolve({ success: false, error: err.message }); else resolve({ success: true }); }); @@ -93,29 +133,30 @@ export function registerFileHandlers() { if (os.platform() === 'darwin') { let script = ''; if (ext === 'key') { - script = ` + script = \` tell application "Keynote" activate - open (POSIX file "${target}") + open (POSIX file "\${target}") delay 1 start (front document) end tell - `; + \`; } else if (ext === 'pptx' || ext === 'ppt') { - script = ` + script = \` tell application "Microsoft PowerPoint" activate - open (POSIX file "${target}") + open (POSIX file "\${target}") delay 1 run slide show of active presentation end tell - `; + \`; } if (script) { - console.log(`Native: Launching ${ext} via AppleScript for ${target}`); + console.log(\`Native: Launching \${ext} via AppleScript for \${target}\`); return new Promise((resolve) => { - exec(`osascript -e "${script.replace(/"/g, '\\\\"')}"`, (err) => { + const escapedScript = script.trim().replace(/"/g, '\\\\\\"').replace(/\\n/g, ' -e "') + '"'; + exec(\`osascript -e \${escapedScript}\`, (err) => { if (err) resolve({ success: false, error: err.message }); else resolve({ success: true }); }); @@ -131,4 +172,4 @@ export function registerFileHandlers() { return { success: false, error: error.message }; } }); -} \ No newline at end of file +}