Initial commit
This commit is contained in:
203
server/index.js
Normal file
203
server/index.js
Normal file
@@ -0,0 +1,203 @@
|
||||
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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user