import express, { Request, Response } from 'express'; import path from 'path'; import fs from 'fs/promises'; import { createBackup, restoreBackup, listBackups, deleteBackup } from '../services/backup.service'; import multer from 'multer'; const router = express.Router(); // Configure multer for backup file uploads const upload = multer({ dest: '/tmp/basil-restore/', limits: { fileSize: 1024 * 1024 * 1024, // 1GB max }, }); // Get backup directory from env or use default const getBackupDir = (): string => { return process.env.BACKUP_PATH || path.join(__dirname, '../../../backups'); }; /** * POST /api/backup * Creates a new backup of all data and files */ router.post('/', async (req: Request, res: Response) => { try { const backupDir = getBackupDir(); await fs.mkdir(backupDir, { recursive: true }); const backupPath = await createBackup(backupDir); const fileName = path.basename(backupPath); const stats = await fs.stat(backupPath); res.json({ success: true, message: 'Backup created successfully', backup: { name: fileName, path: backupPath, size: stats.size, created: stats.birthtime, }, }); } catch (error) { console.error('Backup creation error:', error); res.status(500).json({ success: false, error: 'Failed to create backup', message: error instanceof Error ? error.message : 'Unknown error', }); } }); /** * GET /api/backup * Lists all available backups */ router.get('/', async (req: Request, res: Response) => { try { const backupDir = getBackupDir(); const backups = await listBackups(backupDir); res.json({ success: true, backups, }); } catch (error) { console.error('Error listing backups:', error); res.status(500).json({ success: false, error: 'Failed to list backups', message: error instanceof Error ? error.message : 'Unknown error', }); } }); /** * GET /api/backup/:filename * Downloads a specific backup file */ router.get('/:filename', async (req: Request, res: Response) => { try { const { filename } = req.params; const backupDir = getBackupDir(); const backupPath = path.join(backupDir, filename); // Security check: ensure the file is within the backup directory const resolvedPath = path.resolve(backupPath); const resolvedBackupDir = path.resolve(backupDir); if (!resolvedPath.startsWith(resolvedBackupDir)) { return res.status(403).json({ success: false, error: 'Access denied', }); } // Check if file exists try { await fs.access(backupPath); } catch { return res.status(404).json({ success: false, error: 'Backup file not found', }); } // Send file res.download(backupPath, filename, (err) => { if (err) { console.error('Error downloading backup:', err); if (!res.headersSent) { res.status(500).json({ success: false, error: 'Failed to download backup', }); } } }); } catch (error) { console.error('Error downloading backup:', error); res.status(500).json({ success: false, error: 'Failed to download backup', message: error instanceof Error ? error.message : 'Unknown error', }); } }); /** * POST /api/backup/restore * Restores data from a backup file * Accepts either: * - multipart/form-data with 'backup' file field * - JSON with 'filename' field (for existing backup in backup directory) */ router.post('/restore', upload.single('backup'), async (req: Request, res: Response) => { let backupPath: string | null = null; let isTemporaryFile = false; try { const backupDir = getBackupDir(); // Check if file was uploaded or filename provided if (req.file) { backupPath = req.file.path; isTemporaryFile = true; } else if (req.body.filename) { backupPath = path.join(backupDir, req.body.filename); // Security check const resolvedPath = path.resolve(backupPath); const resolvedBackupDir = path.resolve(backupDir); if (!resolvedPath.startsWith(resolvedBackupDir)) { return res.status(403).json({ success: false, error: 'Access denied', }); } // Check if file exists try { await fs.access(backupPath); } catch { return res.status(404).json({ success: false, error: 'Backup file not found', }); } } else { return res.status(400).json({ success: false, error: 'No backup file provided. Either upload a file or specify a filename.', }); } // Perform restore const metadata = await restoreBackup(backupPath, backupDir); // Clean up temporary file if it was uploaded if (isTemporaryFile && backupPath) { try { await fs.unlink(backupPath); } catch (err) { console.warn('Failed to clean up temporary file:', err); } } res.json({ success: true, message: 'Backup restored successfully', metadata, }); } catch (error) { console.error('Restore error:', error); // Clean up temporary file on error if (isTemporaryFile && backupPath) { try { await fs.unlink(backupPath); } catch {} } res.status(500).json({ success: false, error: 'Failed to restore backup', message: error instanceof Error ? error.message : 'Unknown error', }); } }); /** * DELETE /api/backup/:filename * Deletes a backup file */ router.delete('/:filename', async (req: Request, res: Response) => { try { const { filename } = req.params; const backupDir = getBackupDir(); const backupPath = path.join(backupDir, filename); // Security check const resolvedPath = path.resolve(backupPath); const resolvedBackupDir = path.resolve(backupDir); if (!resolvedPath.startsWith(resolvedBackupDir)) { return res.status(403).json({ success: false, error: 'Access denied', }); } // Check if file exists try { await fs.access(backupPath); } catch { return res.status(404).json({ success: false, error: 'Backup file not found', }); } await deleteBackup(backupPath); res.json({ success: true, message: 'Backup deleted successfully', }); } catch (error) { console.error('Error deleting backup:', error); res.status(500).json({ success: false, error: 'Failed to delete backup', message: error instanceof Error ? error.message : 'Unknown error', }); } }); export default router;