| import fs from "fs/promises"; |
| import path from "path"; |
| import os from "os"; |
|
|
| const BACKUP_DIR = path.join(os.homedir(), ".omniroute", "backups"); |
| const MAX_BACKUPS_PER_TOOL = 5; |
|
|
| |
| |
| |
| function getToolBackupDir(toolId: string) { |
| return path.join(BACKUP_DIR, toolId); |
| } |
|
|
| |
| |
| |
| async function ensureBackupDir(toolId: string) { |
| const dir = getToolBackupDir(toolId); |
| await fs.mkdir(dir, { recursive: true }); |
| return dir; |
| } |
|
|
| |
| |
| |
| function makeBackupName(originalPath: string) { |
| const ext = path.extname(originalPath); |
| const base = path.basename(originalPath, ext); |
| const ts = new Date().toISOString().replace(/[:.]/g, "-"); |
| return `${base}_${ts}${ext}`; |
| } |
|
|
| |
| |
| |
| |
| export async function createBackup(toolId: string, filePath: string) { |
| try { |
| await fs.access(filePath); |
| } catch { |
| |
| return null; |
| } |
|
|
| const dir = await ensureBackupDir(toolId); |
| const backupName = makeBackupName(filePath); |
| const backupPath = path.join(dir, backupName); |
|
|
| await fs.copyFile(filePath, backupPath); |
|
|
| |
| const metaPath = backupPath + ".meta.json"; |
| await fs.writeFile( |
| metaPath, |
| JSON.stringify({ |
| originalPath: filePath, |
| backupName, |
| toolId, |
| createdAt: new Date().toISOString(), |
| }) |
| ); |
|
|
| |
| await rotateBackups(toolId); |
|
|
| return backupPath; |
| } |
|
|
| |
| |
| |
| |
| export async function createMultiBackup(toolId: string, filePaths: string[]) { |
| const results: (string | null)[] = []; |
| for (const filePath of filePaths) { |
| const result = await createBackup(toolId, filePath); |
| results.push(result); |
| } |
| return results; |
| } |
|
|
| |
| |
| |
| export async function listBackups(toolId: string) { |
| const dir = getToolBackupDir(toolId); |
|
|
| let entries; |
| try { |
| entries = await fs.readdir(dir); |
| } catch { |
| return []; |
| } |
|
|
| const metaFiles = entries.filter((e) => e.endsWith(".meta.json")); |
| const backups: any[] = []; |
|
|
| for (const metaFile of metaFiles) { |
| try { |
| const metaPath = path.join(dir, metaFile); |
| const raw = await fs.readFile(metaPath, "utf-8"); |
| const meta = JSON.parse(raw); |
|
|
| const backupFile = metaFile.replace(".meta.json", ""); |
| const backupPath = path.join(dir, backupFile); |
|
|
| let size = 0; |
| try { |
| const stat = await fs.stat(backupPath); |
| size = stat.size; |
| } catch { |
| |
| continue; |
| } |
|
|
| backups.push({ |
| id: backupFile, |
| toolId: meta.toolId, |
| originalPath: meta.originalPath, |
| createdAt: meta.createdAt, |
| size, |
| }); |
| } catch { |
| |
| } |
| } |
|
|
| |
| backups.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); |
| return backups; |
| } |
|
|
| |
| |
| |
| export async function restoreBackup(toolId: string, backupId: string) { |
| const dir = getToolBackupDir(toolId); |
| const backupPath = path.join(dir, backupId); |
| const metaPath = backupPath + ".meta.json"; |
|
|
| |
| let meta; |
| try { |
| const raw = await fs.readFile(metaPath, "utf-8"); |
| meta = JSON.parse(raw); |
| } catch { |
| throw new Error(`Backup metadata not found: ${backupId}`); |
| } |
|
|
| |
| try { |
| await fs.access(backupPath); |
| } catch { |
| throw new Error(`Backup file not found: ${backupId}`); |
| } |
|
|
| |
| await createBackup(toolId, meta.originalPath); |
|
|
| |
| const targetDir = path.dirname(meta.originalPath); |
| await fs.mkdir(targetDir, { recursive: true }); |
| await fs.copyFile(backupPath, meta.originalPath); |
|
|
| return { |
| restored: true, |
| backupId, |
| originalPath: meta.originalPath, |
| }; |
| } |
|
|
| |
| |
| |
| export async function deleteBackup(toolId: string, backupId: string) { |
| const dir = getToolBackupDir(toolId); |
| const backupPath = path.join(dir, backupId); |
| const metaPath = backupPath + ".meta.json"; |
|
|
| try { |
| await fs.unlink(backupPath); |
| } catch { |
| |
| } |
| try { |
| await fs.unlink(metaPath); |
| } catch { |
| |
| } |
|
|
| return { deleted: true, backupId }; |
| } |
|
|
| |
| |
| |
| |
| async function rotateBackups(toolId: string) { |
| const all = await listBackups(toolId); |
|
|
| |
| const groups: Record<string, any[]> = {}; |
| for (const b of all) { |
| const key = path.basename(b.originalPath); |
| if (!groups[key]) groups[key] = []; |
| groups[key].push(b); |
| } |
|
|
| for (const [, group] of Object.entries(groups) as [string, any[]][]) { |
| |
| if (group.length > MAX_BACKUPS_PER_TOOL) { |
| const toDelete = group.slice(MAX_BACKUPS_PER_TOOL); |
| for (const old of toDelete) { |
| await deleteBackup(toolId, old.id); |
| } |
| } |
| } |
| } |
|
|