first commit

This commit is contained in:
Rutra committed 2026-10-12 14:25:22 +02:00
commit 3c0479faf0
169 files changed
+27617

No files matched your search

+171
View File
@@ -0,0 +1,171 @@
import { mkdir, readdir, rm, stat } from 'node:fs/promises'
import { basename, dirname, join } from 'node:path'
import { DateTime } from 'luxon'
import logger from '@adonisjs/core/services/logger'
import vpsConfig from '#config/vps'
import UserError from '#exceptions/user_error'
import jobRunner, { type JobMeta } from '#services/job_runner'
/**
* db-2026-10-11_0300.sql.gz (all the MariaDB databases)
* srv-2026-10-11_0300.tar.gz (the /srv folder)
*/
const BACKUP_FILE = /^(db|srv)-\d{4}-\d{2}-\d{2}_\d{4}\.(sql\.gz|tar\.gz)$/
export interface BackupFile {
name: string
type: 'db' | 'srv'
size: number
createdAt: number
}
class BackupService {
#timer: NodeJS.Timeout | null = null
#lastScheduledDay: string | null = null
async list(): Promise<BackupFile[]> {
let names: string[] = []
try {
names = await readdir(vpsConfig.backups.dir)
} catch {
return []
}
const files = await Promise.all(
names
.filter((name) => BACKUP_FILE.test(name))
.map(async (name) => {
const info = await stat(join(vpsConfig.backups.dir, name))
return {
name,
type: name.startsWith('db-') ? 'db' : 'srv',
size: info.size,
createdAt: Math.floor(info.mtimeMs / 1000),
} satisfies BackupFile
})
)
return files.sort((a, b) => b.createdAt - a.createdAt)
}
/**
* Absolute path of a backup, after checking its name (no path traversal).
*/
path(name: string) {
if (!BACKUP_FILE.test(name)) throw new UserError('Sauvegarde introuvable.', 404)
return join(vpsConfig.backups.dir, name)
}
async delete(name: string) {
await rm(this.path(name))
}
run(meta: Omit<JobMeta, 'action' | 'target' | 'lockKey'>) {
return jobRunner.start(
{ ...meta, action: 'Sauvegarde', target: vpsConfig.backups.dir, lockKey: 'backup' },
async (ctx) => {
const dir = vpsConfig.backups.dir
const stamp = DateTime.now().toFormat('yyyy-LL-dd_HHmm')
await mkdir(dir, { recursive: true })
const dbFile = join(dir, `db-${stamp}.sql.gz`)
ctx.log('1/2 Export de toutes les bases MariaDB')
try {
await ctx.run(
'docker',
[
'exec',
vpsConfig.mariadb.container,
'sh',
'-c',
'exec "$(command -v mariadb-dump || command -v mysqldump)" -u root -p"${MARIADB_ROOT_PASSWORD:-$MYSQL_ROOT_PASSWORD}" --all-databases --single-transaction --routines --events',
],
{
display: `docker exec ${vpsConfig.mariadb.container} mariadb-dump --all-databases > ${basename(dbFile)}`,
stdoutFile: dbFile,
gzipStdout: true,
}
)
} catch (error) {
await rm(dbFile, { force: true })
throw error
}
/**
* node_modules can be reinstalled and db_data holds the raw MariaDB
* files, already covered (consistently) by the export above.
*/
const srvFile = join(dir, `srv-${stamp}.tar.gz`)
ctx.log(`2/2 Archive de ${vpsConfig.srvRoot}`)
try {
await ctx.run(
'tar',
[
'czf',
srvFile,
'--exclude=node_modules',
'--exclude=db_data',
'-C',
dirname(vpsConfig.srvRoot),
basename(vpsConfig.srvRoot),
],
/**
* 1 = some files changed while being read (logs…): not an error.
*/
{ okCodes: [0, 1] }
)
} catch (error) {
await rm(srvFile, { force: true })
throw error
}
const removed = await this.#applyRetention()
if (removed.length) ctx.log(`Anciennes sauvegardes supprimées : ${removed.join(', ')}`)
}
)
}
/**
* Keeps the last N backups of each type.
*/
async #applyRetention() {
const files = await this.list()
const removed: string[] = []
for (const type of ['db', 'srv'] as const) {
const old = files.filter((file) => file.type === type).slice(vpsConfig.backups.keep)
for (const file of old) {
await rm(join(vpsConfig.backups.dir, file.name), { force: true })
removed.push(file.name)
}
}
return removed
}
get schedule() {
return { dailyAt: vpsConfig.backups.dailyAt, keep: vpsConfig.backups.keep }
}
/**
* Daily backup at BACKUP_DAILY_AT (server time).
*/
startSchedule() {
const dailyAt = vpsConfig.backups.dailyAt
if (!dailyAt) return
this.#timer = setInterval(() => {
const now = DateTime.now()
const today = now.toISODate()
if (now.toFormat('HH:mm') !== dailyAt || this.#lastScheduledDay === today) return
this.#lastScheduledDay = today
this.run({ username: 'automatique', ip: null }).catch((error) =>
logger.error({ err: error }, 'scheduled backup failed to start')
)
}, 30_000)
}
stopSchedule() {
if (this.#timer) clearInterval(this.#timer)
}
}
const backupService = new BackupService()
export default backupService