Refactor code structure for improved readability and maintainability

This commit is contained in:
Alexander Sabino
2026-05-09 10:43:50 +01:00
parent b763b3ca90
commit 990b0dea4d
9 changed files with 2188 additions and 112 deletions
+1
View File
@@ -166,6 +166,7 @@ class BackupService {
mode: effectiveMode,
backupScope,
backupDir: profile.backupDir,
basedOnFullBackupId: options.basedOnFullBackupId || null,
status: 'ok',
containers: [],
};
+196 -12
View File
@@ -2,12 +2,20 @@ const express = require('express');
const path = require('path');
const fs = require('fs/promises');
const crypto = require('crypto');
const { execFile } = require('child_process');
const { promisify } = require('util');
const execFileAsync = promisify(execFile);
const config = require('./config');
const JsonStore = require('./store');
const DockerService = require('./dockerService');
const BackupService = require('./backupService');
function hashPassword(password) {
return crypto.createHash('sha256').update(password).digest('hex');
}
async function main() {
await fs.mkdir(config.dataDir, { recursive: true });
@@ -25,13 +33,134 @@ async function main() {
const app = express();
app.use(express.json());
// ─── Auth middleware ──────────────────────────────────────
async function authMiddleware(request, response, next) {
const settings = await store.getSettings();
if (!settings.requireAuth) {
return next();
}
const token = request.headers['x-auth-token'];
if (!token) {
return response.status(401).json({ error: 'Unauthorized' });
}
const expected = hashPassword(`${settings.username}:${settings.passwordHash}:${settings.username}`);
if (token !== expected) {
return response.status(401).json({ error: 'Unauthorized' });
}
return next();
}
// Static files served without auth (login page needs to load)
app.use(express.static(path.join(process.cwd(), 'public')));
app.get('/api/health', (_request, response) => {
response.json({ ok: true });
});
app.get('/api/containers', async (_request, response) => {
// Login endpoint (public)
app.post('/api/login', async (request, response) => {
try {
const settings = await store.getSettings();
if (!settings.requireAuth) {
return response.json({ token: null, requireAuth: false });
}
const { username, password } = request.body || {};
if (!username || !password) {
return response.status(400).json({ error: 'Informe usuario e senha.' });
}
if (username !== settings.username || hashPassword(password) !== settings.passwordHash) {
return response.status(401).json({ error: 'Usuario ou senha incorretos.' });
}
const token = hashPassword(`${settings.username}:${settings.passwordHash}:${settings.username}`);
return response.json({ token, requireAuth: true });
} catch (error) {
return response.status(500).json({ error: error.message });
}
});
// Auth check endpoint (public)
app.get('/api/auth-status', async (_request, response) => {
try {
const settings = await store.getSettings();
response.json({ requireAuth: settings.requireAuth });
} catch (error) {
response.status(500).json({ error: error.message });
}
});
// Settings endpoints (auth-protected)
app.get('/api/settings', authMiddleware, async (_request, response) => {
try {
const settings = await store.getSettings();
response.json({ language: settings.language, requireAuth: settings.requireAuth, username: settings.username });
} catch (error) {
response.status(500).json({ error: error.message });
}
});
app.post('/api/settings', authMiddleware, async (request, response) => {
try {
const payload = request.body || {};
const current = await store.getSettings();
const update = {
language: payload.language || current.language,
requireAuth: typeof payload.requireAuth === 'boolean' ? payload.requireAuth : current.requireAuth,
username: payload.username !== undefined ? String(payload.username).trim() : current.username,
passwordHash: current.passwordHash,
};
if (payload.password) {
update.passwordHash = hashPassword(payload.password);
}
if (update.requireAuth && (!update.username || !update.passwordHash)) {
return response.status(400).json({ error: 'Defina usuario e senha para ativar autenticacao.' });
}
await store.saveSettings(update);
response.json({ ok: true });
} catch (error) {
response.status(500).json({ error: error.message });
}
});
// About endpoint (auth-protected)
app.get('/api/about', authMiddleware, async (_request, response) => {
try {
const pkgPath = path.join(process.cwd(), 'package.json');
const pkgRaw = await fs.readFile(pkgPath, 'utf8');
const pkg = JSON.parse(pkgRaw);
response.json({ currentVersion: pkg.version, name: pkg.name || 'dockerbackup' });
} catch (error) {
response.status(500).json({ error: error.message });
}
});
// Update endpoint (auth-protected)
app.post('/api/update', authMiddleware, async (_request, response) => {
try {
await execFileAsync('git', ['pull', '--ff-only', 'origin', 'main'], { cwd: process.cwd() });
try {
await execFileAsync('npm', ['install', '--omit=dev'], { cwd: process.cwd() });
} catch {
// Non-fatal: deps may already be up to date
}
response.json({ ok: true });
setTimeout(() => process.exit(0), 500);
} catch (error) {
response.status(500).json({ error: error.message });
}
});
app.get('/api/containers', authMiddleware, async (_request, response) => {
try {
const containers = await dockerService.listContainers();
response.json(containers);
@@ -40,7 +169,7 @@ async function main() {
}
});
app.get('/api/containers/:containerId/mounts', async (request, response) => {
app.get('/api/containers/:containerId/mounts', authMiddleware, async (request, response) => {
try {
const inspect = await dockerService.inspectContainer(request.params.containerId);
const mounts = (inspect.Mounts || [])
@@ -58,7 +187,7 @@ async function main() {
}
});
app.get('/api/profiles', async (_request, response) => {
app.get('/api/profiles', authMiddleware, async (_request, response) => {
try {
const profiles = await store.listProfiles();
response.json(profiles);
@@ -67,11 +196,27 @@ async function main() {
}
});
app.post('/api/profiles', async (request, response) => {
app.post('/api/profiles', authMiddleware, async (request, response) => {
try {
const payload = request.body || {};
if (!payload.name || !payload.backupDir || !Array.isArray(payload.containerIds) || !payload.containerIds.length) {
response.status(400).json({ error: 'Informe nome, diretorio de backup e ao menos um container.' });
if (!payload.name || !Array.isArray(payload.containerIds) || !payload.containerIds.length) {
response.status(400).json({ error: 'Informe nome, local de armazenamento e ao menos um container.' });
return;
}
let resolvedBackupDir = payload.backupDir;
if (payload.storageLocationId) {
const locations = await store.listStorageLocations();
const loc = locations.find((l) => l.id === payload.storageLocationId);
if (!loc) {
response.status(400).json({ error: 'Local de armazenamento nao encontrado.' });
return;
}
resolvedBackupDir = loc.directory;
}
if (!resolvedBackupDir) {
response.status(400).json({ error: 'Informe o local de armazenamento.' });
return;
}
@@ -85,7 +230,8 @@ async function main() {
id: payload.id,
createdAt: existing?.createdAt,
name: payload.name.trim(),
backupDir: payload.backupDir.trim(),
backupDir: resolvedBackupDir.trim(),
storageLocationId: payload.storageLocationId || existing?.storageLocationId || null,
containerIds: payload.containerIds,
mode: existing?.mode || 'full',
backupScope: payload.backupScope || existing?.backupScope || 'volumes',
@@ -98,7 +244,7 @@ async function main() {
}
});
app.delete('/api/profiles/:profileId', async (request, response) => {
app.delete('/api/profiles/:profileId', authMiddleware, async (request, response) => {
try {
const profile = await store.getProfile(request.params.profileId);
await store.deleteProfile(request.params.profileId);
@@ -115,7 +261,7 @@ async function main() {
}
});
app.get('/api/profiles/:profileId/backups', async (request, response) => {
app.get('/api/profiles/:profileId/backups', authMiddleware, async (request, response) => {
try {
const backups = await store.listBackups(request.params.profileId);
response.json(backups);
@@ -124,10 +270,11 @@ async function main() {
}
});
app.post('/api/profiles/:profileId/run', async (request, response) => {
app.post('/api/profiles/:profileId/run', authMiddleware, async (request, response) => {
try {
const profileId = request.params.profileId;
const requestedMode = request.body?.mode;
const basedOnFullBackupId = request.body?.basedOnFullBackupId || null;
if (requestedMode && !['full', 'incremental'].includes(requestedMode)) {
response.status(400).json({ error: 'Modo de backup invalido.' });
return;
@@ -155,6 +302,7 @@ async function main() {
void backupService.runProfile(profileId, {
mode: requestedMode,
basedOnFullBackupId,
onProgress: (progressSnapshot) => {
const currentJob = runJobs.get(runId);
if (!currentJob) {
@@ -189,7 +337,7 @@ async function main() {
}
});
app.get('/api/runs/:runId', (request, response) => {
app.get('/api/runs/:runId', authMiddleware, (request, response) => {
const job = runJobs.get(request.params.runId);
if (!job) {
response.status(404).json({ error: 'Execucao nao encontrada.' });
@@ -209,7 +357,7 @@ async function main() {
});
});
app.post('/api/profiles/:profileId/restore', async (request, response) => {
app.post('/api/profiles/:profileId/restore', authMiddleware, async (request, response) => {
try {
const profileId = request.params.profileId;
if (!request.body?.backupId) {
@@ -279,6 +427,42 @@ async function main() {
}
});
app.get('/api/storage-locations', authMiddleware, async (_request, response) => {
try {
const locations = await store.listStorageLocations();
response.json(locations);
} catch (error) {
response.status(500).json({ error: error.message });
}
});
app.post('/api/storage-locations', authMiddleware, async (request, response) => {
try {
const payload = request.body || {};
if (!payload.name || !payload.directory) {
response.status(400).json({ error: 'Informe nome e diretorio do local de armazenamento.' });
return;
}
const location = await store.saveStorageLocation({
id: payload.id,
name: payload.name.trim(),
directory: payload.directory.trim(),
});
response.status(payload.id ? 200 : 201).json(location);
} catch (error) {
response.status(500).json({ error: error.message });
}
});
app.delete('/api/storage-locations/:id', authMiddleware, async (request, response) => {
try {
await store.deleteStorageLocation(request.params.id);
response.status(204).end();
} catch (error) {
response.status(500).json({ error: error.message });
}
});
app.listen(config.port, () => {
console.log(`Docker Backup app ouvindo na porta ${config.port}`);
});
+59 -1
View File
@@ -13,7 +13,7 @@ class JsonStore {
try {
await fs.access(this.filePath);
} catch {
await fs.writeFile(this.filePath, JSON.stringify({ profiles: [], backups: [] }, null, 2));
await fs.writeFile(this.filePath, JSON.stringify({ profiles: [], backups: [], storageLocations: [], settings: {} }, null, 2));
}
}
@@ -22,6 +22,8 @@ class JsonStore {
const parsed = JSON.parse(raw);
parsed.profiles ||= [];
parsed.backups ||= [];
parsed.storageLocations ||= [];
parsed.settings ||= {};
return parsed;
}
@@ -136,6 +138,62 @@ class JsonStore {
return [];
}
async listStorageLocations() {
const data = await this.read();
return data.storageLocations;
}
async saveStorageLocation(input) {
const now = new Date().toISOString();
const location = {
id: input.id || randomUUID(),
name: input.name,
directory: input.directory,
updatedAt: now,
createdAt: input.createdAt || now,
};
await this.write((data) => {
const index = data.storageLocations.findIndex((item) => item.id === location.id);
if (index >= 0) {
data.storageLocations[index] = location;
} else {
data.storageLocations.push(location);
}
return data;
});
return location;
}
async deleteStorageLocation(locationId) {
await this.write((data) => {
data.storageLocations = data.storageLocations.filter((item) => item.id !== locationId);
return data;
});
}
async getSettings() {
const data = await this.read();
return {
language: data.settings.language || 'pt-BR',
requireAuth: data.settings.requireAuth || false,
username: data.settings.username || '',
passwordHash: data.settings.passwordHash || '',
};
}
async saveSettings(input) {
await this.write((data) => {
data.settings = {
...data.settings,
...input,
};
return data;
});
return this.getSettings();
}
async getLastContainerBackupTime(profileId, containerId) {
const backups = await this.listBackups(profileId);
const ordered = backups