Files
backstor-ui/server/index.js
2025-12-08 12:12:07 +07:00

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}`);
});