feat(file): implement hardened caching with SHA-256 verification
- Added .tmp -> .file download pattern with integrity checks.\n- Implemented auto-purge for stale temp files (>5 mins).\n- Added verify_hash support to check-cache handler.\n- Improved error handling for network interruptions.
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user