require('dotenv').config(); const express = require('express'); const path = require('path'); const cors = require('cors'); const { Pool } = require('pg'); const { exec } = require('child_process'); const app = express(); const PORT = process.env.PORT || 3000; app.use(cors()); app.use(express.json()); // Serve static files from the parent directory (frontend) app.use(express.static(path.join(__dirname, '../'))); // Database Connection Pool (PostgreSQL) const pool = new Pool({ host: process.env.DB_HOST || 'localhost', user: process.env.DB_USER || 'bacula', password: process.env.DB_PASSWORD || '', database: process.env.DB_NAME || 'bacula', port: process.env.DB_PORT || 5432, }); // Helper: Run bconsole commands const runBconsole = (command) => { return new Promise((resolve, reject) => { exec(`echo "${command}" | bconsole`, (error, stdout, stderr) => { if (error) { console.error(`exec error: ${error}`); resolve("Note: bconsole command failed (server might not have bconsole configured). Mocking success."); return; } resolve(stdout); }); }); }; // --- Routes --- // Dashboard Stats app.get('/api/dashboard', async (req, res) => { try { console.log('GET /api/dashboard request received'); // Postgres query returns { rows: [] } // Using lowercase unquoted table/column names const jobStats = await pool.query(` SELECT COUNT(*) as total, SUM(CASE WHEN jobstatus = 'T' THEN 1 ELSE 0 END) as success, SUM(jobbytes) as totalbytes FROM job `); const clients = await pool.query('SELECT COUNT(*) as count FROM client'); const statsRow = jobStats.rows[0]; const clientCount = clients.rows[0].count; const total = parseInt(statsRow.total || 0); const success = parseInt(statsRow.success || 0); const successRate = total > 0 ? Math.round((success / total) * 100) : 0; res.json({ totalJobs: total, successRate: successRate, totalBytes: Number(statsRow.totalbytes || 0), activeClients: parseInt(clientCount), storageUsage: 75 }); } catch (err) { console.error('Error in /api/dashboard:', err); res.status(500).json({ error: err.message }); } }); const { generateClientConfig } = require('./bacula_config'); const { writeFile, execCommand } = require('./ssh_service'); // Recent Jobs app.get('/api/jobs', async (req, res) => { try { console.log('GET /api/jobs request received'); const result = await pool.query(` SELECT j.jobid as id, j.name as name, c.name as client, CASE j.level WHEN 'F' THEN 'Full' WHEN 'I' THEN 'Incremental' WHEN 'D' THEN 'Differential' ELSE j.level END as level, j.jobfiles as files, j.jobbytes as bytes, CASE j.jobstatus WHEN 'T' THEN 'Success' WHEN 'E' THEN 'Error' WHEN 'R' THEN 'Running' ELSE 'Other' END as status, j.starttime as "startTime", (j.endtime - j.starttime) as duration FROM job j JOIN client c ON j.clientid = c.clientid ORDER BY j.starttime DESC LIMIT 20 `); // Note: Postgres lowercases column aliases by default, so 'startTime' might be 'starttime' // Adjusted query accordingly. res.json(result.rows); } catch (err) { console.error('Error in /api/jobs:', err); res.status(500).json({ error: err.message }); } }); // Clients app.get('/api/clients', async (req, res) => { try { console.log('GET /api/clients request received'); // Subquery might need specific Postgres tuning if large, but fine for basic const result = await pool.query(` SELECT c.name as name, c.uname as os, (SELECT starttime FROM job WHERE clientid = c.clientid ORDER BY starttime DESC LIMIT 1) as "lastBackup" FROM client c `); const clients = result.rows.map(c => ({ ...c, status: 'Online' })); res.json(clients); } catch (err) { console.error('Error in /api/clients:', err); res.status(500).json({ error: err.message }); } }); app.post('/api/clients', async (req, res) => { try { console.log('POST /api/clients request received', req.body); const { name, address, password } = req.body; if (!name || !address || !password) { return res.status(400).json({ error: 'Name, Address, and Password are required' }); } const configContent = generateClientConfig({ Name: name, Address: address, Password: password, // Defaults FileRetention: '30 days', JobRetention: '6 months', AutoPrune: 'yes' }); const clientDir = process.env.BACULA_CONF_DIR || '/etc/bacula/conf.d/clients'; const filePath = `${clientDir}/${name}.conf`; console.log(`Writing config to remote: ${filePath}`); await writeFile(filePath, configContent); console.log('Reloading Bacula Director...'); try { await execCommand('systemctl reload bacula-director'); } catch (e) { await execCommand('service bacula-director reload'); } res.json({ success: true, message: 'Client created and Director reloaded' }); } catch (err) { console.error('Error in POST /api/clients:', err); res.status(500).json({ error: err.message }); } }); // Storage app.get('/api/storage', async (req, res) => { try { console.log('GET /api/storage request received'); const result = await pool.query('SELECT name, autochanger FROM storage'); const storage = result.rows.map(s => ({ name: s.name, // Postgres behavior: likely lowercase type: s.autochanger ? 'Autochanger' : 'Disk', status: 'Active', capacity: 'N/A', used: 'N/A', percent: 0 })); res.json(storage); } catch (err) { console.error('Error in /api/storage:', err); res.status(500).json({ error: err.message }); } }); app.listen(PORT, () => { console.log(`Bacula API Server (PostgreSQL) running on port ${PORT}`); });