import { dialog, type BrowserWindow } from 'electron' import { readFileSync, writeFileSync } from 'fs' import { getSettings, setSettings } from './settings' import { listTemplates, replaceTemplates } from './templates' import type { BackupExportResult, BackupImportResult, Settings, CommandTemplate } from '@shared/ipc' interface BackupFile { version: 1 settings: Settings templates: CommandTemplate[] } export async function exportBackup(win: BrowserWindow | undefined): Promise { const res = await dialog.showSaveDialog(win!, { defaultPath: 'aerofetch-backup.json', filters: [{ name: 'JSON', extensions: ['json'] }] }) if (res.canceled || !res.filePath) return { ok: false } const payload: BackupFile = { version: 1, settings: getSettings(), templates: listTemplates() } try { writeFileSync(res.filePath, JSON.stringify(payload, null, 2)) return { ok: true, path: res.filePath } } catch (e) { return { ok: false, error: (e as Error).message } } } export async function importBackup(win: BrowserWindow | undefined): Promise { const res = await dialog.showOpenDialog(win!, { properties: ['openFile'], filters: [{ name: 'JSON', extensions: ['json'] }] }) if (res.canceled || !res.filePaths[0]) return { ok: false } let parsed: unknown try { parsed = JSON.parse(readFileSync(res.filePaths[0], 'utf8')) } catch (e) { return { ok: false, error: `Could not read backup file: ${(e as Error).message}` } } if (!parsed || typeof parsed !== 'object') { return { ok: false, error: 'Not a valid AeroFetch backup file.' } } // setSettings/replaceTemplates validate and sanitize every field, so a // hand-edited or partial backup file degrades gracefully rather than // corrupting the store. const file = parsed as Partial const incomingSettings = file.settings && typeof file.settings === 'object' ? (file.settings as Partial) : undefined const incomingTemplates = Array.isArray(file.templates) ? (file.templates as CommandTemplate[]) : [] // A custom-command template's `args` are extra yt-dlp flags that get spawned on // the next download. A backup that arrives with customCommandEnabled + a default // template carrying args would silently enable code execution the moment a // download starts — so require an explicit, informed confirmation before // honouring that, and import the templates in a *disabled* state if declined. const commandTemplates = incomingTemplates.filter( (t) => t && typeof t === 'object' && typeof t.args === 'string' && t.args.trim() !== '' ) const wouldEnableCustomCommands = incomingSettings?.customCommandEnabled === true && commandTemplates.length > 0 let applyCustomCommands = true if (wouldEnableCustomCommands) { const preview = commandTemplates .slice(0, 10) .map((t) => { const name = (t.name ?? '').trim() || 'Untitled command' const args = t.args.length > 100 ? `${t.args.slice(0, 100)}…` : t.args return `• ${name}: ${args}` }) .join('\n') const more = commandTemplates.length > 10 ? `\n…and ${commandTemplates.length - 10} more.` : '' const choice = await dialog.showMessageBox(win!, { type: 'warning', buttons: ['Enable custom commands', 'Import without enabling', 'Cancel'], defaultId: 1, cancelId: 2, title: 'Backup contains custom commands', message: `This backup enables ${commandTemplates.length} custom-command template${ commandTemplates.length === 1 ? '' : 's' }.`, detail: 'Custom commands run extra yt-dlp flags on every download. Only enable them if you trust this backup file.\n\n' + preview + more }) if (choice.response === 2) return { ok: false } // Cancel — change nothing applyCustomCommands = choice.response === 0 } if (incomingSettings) { // When the user declined to enable custom commands (or there were none to // enable), force the toggle off so an imported defaultTemplateId can't auto-run. const safeSettings = applyCustomCommands ? incomingSettings : { ...incomingSettings, customCommandEnabled: false } setSettings(safeSettings) } if (incomingTemplates.length > 0 || Array.isArray(file.templates)) { replaceTemplates(incomingTemplates) } return { ok: true } }