From c0d6834178e192b24c253424e9b9a83b5b7f2baa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sat, 4 Jul 2026 11:38:26 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20impl=C3=A9menter=20r=C3=A9ellement=20la?= =?UTF-8?q?=20sauvegarde=20hebdomadaire=20de=20la=20base?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + config/cron-task.js | 15 ++++- src/utils/__tests__/backup-database.test.js | 62 +++++++++++++++++++++ src/utils/backup-database.js | 31 +++++++++++ 4 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 src/utils/__tests__/backup-database.test.js create mode 100644 src/utils/backup-database.js diff --git a/.gitignore b/.gitignore index 8e339ee..ebc9f8f 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,7 @@ $RECYCLE.BIN/ *.sql *.sqlite *.sqlite3 +backups/ ############################ diff --git a/config/cron-task.js b/config/cron-task.js index 97d5441..fc910a8 100644 --- a/config/cron-task.js +++ b/config/cron-task.js @@ -1,6 +1,19 @@ +const path = require('path'); +const {backupDatabase} = require('../src/utils/backup-database'); + module.exports = { myJob: { - task: ({ strapi }) => {console.log('TODO > save db')}, + task: ({ strapi }) => { + const dbPath = path.join(__dirname, '..', process.env.DATABASE_FILENAME || '.tmp/data.db'); + const backupsDir = path.join(__dirname, '..', 'backups'); + const backupPath = backupDatabase({dbPath, backupsDir}); + + if (backupPath) { + strapi.log.info(`Sauvegarde de la base effectuée : ${backupPath}`); + } else { + strapi.log.warn(`Sauvegarde de la base ignorée : fichier introuvable (${dbPath})`); + } + }, options: { rule: '0 0 * * SUN', tz: 'Indian/Reunion', diff --git a/src/utils/__tests__/backup-database.test.js b/src/utils/__tests__/backup-database.test.js new file mode 100644 index 0000000..74b43f2 --- /dev/null +++ b/src/utils/__tests__/backup-database.test.js @@ -0,0 +1,62 @@ +import {describe, it, expect, afterEach} from 'vitest' +import fs from 'fs' +import os from 'os' +import path from 'path' +import {backupDatabase} from '../backup-database.js' + +const tmpDirs = [] + +function makeTmpDir() { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'backup-database-test-')) + tmpDirs.push(dir) + return dir +} + +describe('backupDatabase', () => { + afterEach(() => { + for (const dir of tmpDirs.splice(0)) { + fs.rmSync(dir, {recursive: true, force: true}) + } + }) + + it("ne fait rien quand le fichier de base n'existe pas", () => { + const root = makeTmpDir() + const result = backupDatabase({ + dbPath: path.join(root, 'inexistant.db'), + backupsDir: path.join(root, 'backups') + }) + + expect(result).toBeNull() + expect(fs.existsSync(path.join(root, 'backups'))).toBe(false) + }) + + it('copie le fichier de base dans le dossier de sauvegardes', () => { + const root = makeTmpDir() + const dbPath = path.join(root, 'data.db') + fs.writeFileSync(dbPath, 'contenu-de-la-base') + const backupsDir = path.join(root, 'backups') + + const result = backupDatabase({dbPath, backupsDir}) + + expect(result).not.toBeNull() + expect(fs.existsSync(result)).toBe(true) + expect(fs.readFileSync(result, 'utf8')).toBe('contenu-de-la-base') + }) + + it('ne conserve que les 8 sauvegardes les plus récentes', () => { + const root = makeTmpDir() + const dbPath = path.join(root, 'data.db') + fs.writeFileSync(dbPath, 'contenu') + const backupsDir = path.join(root, 'backups') + fs.mkdirSync(backupsDir, {recursive: true}) + + for (let i = 0; i < 10; i++) { + fs.writeFileSync(path.join(backupsDir, `data-2020-01-0${i}.db`), 'ancien') + } + + backupDatabase({dbPath, backupsDir}) + + const remaining = fs.readdirSync(backupsDir).filter(name => name.startsWith('data-')) + expect(remaining).toHaveLength(8) + }) +}) diff --git a/src/utils/backup-database.js b/src/utils/backup-database.js new file mode 100644 index 0000000..28fe84a --- /dev/null +++ b/src/utils/backup-database.js @@ -0,0 +1,31 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const RETENTION = 8; + +function backupDatabase({dbPath, backupsDir}) { + if (!fs.existsSync(dbPath)) { + return null; + } + + fs.mkdirSync(backupsDir, {recursive: true}); + + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const backupPath = path.join(backupsDir, `data-${timestamp}.db`); + fs.copyFileSync(dbPath, backupPath); + + const backups = fs.readdirSync(backupsDir) + .filter(name => name.startsWith('data-') && name.endsWith('.db')) + .sort(); + + const toDelete = backups.slice(0, Math.max(backups.length - RETENTION, 0)); + for (const name of toDelete) { + fs.unlinkSync(path.join(backupsDir, name)); + } + + return backupPath; +} + +module.exports = {backupDatabase};