mirror of
https://github.com/asabino2/dockerbackup.git
synced 2026-09-24 06:36:59 +00:00
Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -166,6 +166,7 @@ class BackupService {
|
||||
mode: effectiveMode,
|
||||
backupScope,
|
||||
backupDir: profile.backupDir,
|
||||
basedOnFullBackupId: options.basedOnFullBackupId || null,
|
||||
status: 'ok',
|
||||
containers: [],
|
||||
};
|
||||
|
||||
+196
-12
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user