Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | /**
* Service Backup Scheduler — Exécute les backups planifiés automatiquement.
*
* Vérifie toutes les 15 minutes si un backup doit être lancé.
* Supprime les anciens backups selon la rétention configurée.
* Envoie un webhook en cas d'échec.
*
* @module services/backupScheduler
*/
const { pool: db } = require('../db');
const { exec } = require('./ssh');
const { shellEscape } = require('../utils/shell');
const { notify } = require('./webhookNotifier');
const CHECK_INTERVAL_MS = 15 * 60 * 1000; // Vérifier toutes les 15 min
const BACKUP_DIR = '/var/lib/iliacloud-backups';
const FREQUENCY_MS = {
hourly: 60 * 60 * 1000,
daily: 24 * 60 * 60 * 1000,
weekly: 7 * 24 * 60 * 60 * 1000,
};
/**
* Construit la commande de dump pour un schedule donne.
* @param {Object} schedule
* @param {string} remotePath — Chemin du fichier de sortie
* @param {string} userId
* @param {string} serverId
* @returns {Promise<string>}
*/
async function buildDumpCommand(schedule, remotePath, userId, serverId) {
const safeContainer = schedule.db_container ? shellEscape(schedule.db_container) : null;
const safeDbName = shellEscape(schedule.db_name);
if (schedule.db_type === 'postgresql') {
const safeUser = shellEscape(schedule.db_user);
return safeContainer
? `docker exec ${safeContainer} pg_dump -U ${safeUser} ${safeDbName} > ${remotePath}`
: `pg_dump -U ${safeUser} ${safeDbName} > ${remotePath}`;
}
// MySQL : recuperer le mot de passe root
const pwd = safeContainer
? await getMysqlPassword(userId, serverId, safeContainer)
: '';
const pwdFlag = pwd ? `-p'${pwd}'` : '';
return safeContainer
? `docker exec ${safeContainer} mysqldump -u root ${pwdFlag} --no-tablespaces ${safeDbName} 2>/dev/null > ${remotePath}`
: `mysqldump -u root ${pwdFlag} --no-tablespaces ${safeDbName} > ${remotePath}`;
}
/**
* Recupere le mot de passe root MySQL depuis les secrets ou env vars du container.
*/
async function getMysqlPassword(userId, serverId, safeContainer) {
const { stdout: pwdOut } = await exec(userId, serverId,
`docker exec ${safeContainer} sh -c 'cat /run/secrets/mysql_root_password 2>/dev/null || echo $MYSQL_ROOT_PASSWORD' 2>/dev/null`,
10000,
);
return pwdOut.trim();
}
/**
* Execute le dump et met a jour le backup en base.
* @returns {boolean} — true si le dump a reussi
*/
async function executeDump(userId, serverId, schedule, backupId, remotePath) {
await exec(userId, serverId, `mkdir -p ${BACKUP_DIR} && chmod 700 ${BACKUP_DIR}`, 10000);
const cmd = await buildDumpCommand(schedule, remotePath, userId, serverId);
const { stderr, code } = await exec(userId, serverId, cmd, 300000);
if (code !== 0) {
await db.query('UPDATE backups SET status=$1, error=$2 WHERE id=$3', ['error', stderr, backupId]);
await notify(userId, 'backup_error', {
title: `Backup échoué — ${schedule.db_name}`,
message: stderr || 'Erreur inconnue',
});
return false;
}
const { stdout: sizeOut } = await exec(userId, serverId, `stat -c '%s' ${remotePath} 2>/dev/null || echo 0`);
const fileSize = Number.parseInt(sizeOut.trim()) || 0;
await db.query(
'UPDATE backups SET status=$1, file_path=$2, file_size=$3 WHERE id=$4',
['done', remotePath, fileSize, backupId],
);
const sizeMB = (fileSize / (1024 * 1024)).toFixed(1);
await notify(userId, 'backup_success', {
title: `Backup reussi — ${schedule.db_name}`,
message: `Backup automatique de ${schedule.db_name} termine (${sizeMB} Mo).`,
fields: [
{ name: 'Base', value: schedule.db_name, inline: true },
{ name: 'Taille', value: `${sizeMB} Mo`, inline: true },
{ name: 'Frequence', value: schedule.frequency, inline: true },
],
});
console.log(`[backup-scheduler] OK: ${schedule.db_name} (${fileSize} bytes)`);
return true;
}
/**
* Supprime les anciens backups au-dela de la retention configuree.
*/
async function cleanupOldBackups(userId, serverId, dbName, retention) {
const { rows: oldBackups } = await db.query(
`SELECT id, file_path FROM backups
WHERE server_id = $1 AND db_name = $2 AND status = 'done'
ORDER BY created_at DESC OFFSET $3`,
[serverId, dbName, retention],
);
for (const old of oldBackups) {
if (old.file_path) {
await exec(userId, serverId, `rm -f ${shellEscape(old.file_path)}`, 10000).catch(() => {});
}
await db.query('DELETE FROM backups WHERE id = $1', [old.id]);
}
}
/**
* Traite un schedule individuel.
*/
async function processSchedule(schedule) {
const userId = schedule.user_id;
const serverId = schedule.server_id;
const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-');
const filename = `${schedule.db_name}_auto_${timestamp}.sql`;
const remotePath = `${BACKUP_DIR}/${filename}`;
const label = `Auto — ${schedule.db_name} (${schedule.frequency})`;
await db.query('UPDATE backup_schedules SET last_run = NOW() WHERE id = $1', [schedule.id]);
const { rows } = await db.query(
`INSERT INTO backups (server_id, label, db_type, db_name, db_user, db_container, status)
VALUES ($1, $2, $3, $4, $5, $6, 'running') RETURNING id`,
[serverId, label, schedule.db_type, schedule.db_name, schedule.db_user, schedule.db_container],
);
const backupId = rows[0].id;
try {
await executeDump(userId, serverId, schedule, backupId, remotePath); // NOSONAR — executeDump est async (retourne Promise)
} catch (err) {
await db.query('UPDATE backups SET status=$1, error=$2 WHERE id=$3', ['error', err.message, backupId]);
await notify(userId, 'backup_error', {
title: `Backup échoué — ${schedule.db_name}`,
message: err.message,
});
}
try {
await cleanupOldBackups(userId, serverId, schedule.db_name, schedule.retention);
} catch {}
}
/**
* Verifie si un schedule doit etre execute.
*/
function isDue(schedule, now) {
const lastRun = schedule.last_run ? new Date(schedule.last_run).getTime() : 0;
const interval = FREQUENCY_MS[schedule.frequency] || FREQUENCY_MS.daily;
return now - lastRun >= interval;
}
async function runScheduledBackups() {
try {
const { rows: schedules } = await db.query(`
SELECT bs.*, s.user_id AS srv_user_id
FROM backup_schedules bs
JOIN servers s ON s.id = bs.server_id
WHERE bs.enabled = TRUE
`);
const now = Date.now();
for (const schedule of schedules) {
if (!isDue(schedule, now)) continue;
await processSchedule(schedule);
}
} catch (err) {
console.error('[backup-scheduler] Erreur:', err.message);
}
}
function startBackupScheduler() {
console.log('[backup-scheduler] Démarrage du planificateur de backups');
setInterval(runScheduledBackups, CHECK_INTERVAL_MS);
setTimeout(runScheduledBackups, 30000); // Premier check après 30s
}
module.exports = { startBackupScheduler };
|