204 lines
6.5 KiB
JavaScript
204 lines
6.5 KiB
JavaScript
require('dotenv').config();
|
|
const express = require('express');
|
|
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());
|
|
|
|
// 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}`);
|
|
});
|