backupmanager_ynhVersion 5.0 — 2026-07-17
Application YunoHost centralisant toutes les sauvegardes d'un serveur, s'appuyant au maximum sur les outils YunoHost natifs, extensible en écosystème fédéré multi-instances.
Périmètre : Apps YNH · Système YNH · Répertoires custom · MySQL · PostgreSQL
┌──────────────┐ API ┌──────────────┐ API ┌──────────────┐
│ Instance A │◄─────►│ Instance B │◄─────►│ Instance C │
│ (maître) │ │ (nœud) │ │ (nœud) │
└──────────────┘ └──────────────┘ └──────────────┘
N'importe quelle instance peut être "maître"
| Sujet | Décision |
|---|---|
| Langage | Python 3 + Flask |
| Scheduler | APScheduler + SQLite |
| DB | SQLite + SQLAlchemy |
| Frontend | Jinja2 + TailwindCSS CDN |
| Proxy/Service | Nginx + systemd YunoHost natifs |
| Archives | /home/yunohost.backup/archives/ |
| Nommage | jerry_nocodb_20260508.tar |
| Auth dashboard | SSO YunoHost — admins uniquement |
| Auth API | /api ouvert SSOwat + token X-BackupManager-Key |
| Transfert | HTTP chunked (fédération) + SSH/rsync (destinations SSH) |
| Restauration | Complète : fichiers + user système + systemd + DB |
| Destinations | Many-to-many : un job peut cibler plusieurs destinations |
backupmanager_ynh/
├── manifest.toml
├── scripts/
│ ├── _common.sh
│ ├── install / remove / backup / restore / upgrade
├── conf/
│ ├── nginx.conf
│ ├── systemd.service
│ ├── app.conf.j2
│ └── sudoers
├── sources/
│ ├── app.py # Bootstrap Flask + enregistrement blueprints
│ ├── scheduler.py # APScheduler
│ ├── db.py # SQLAlchemy (Job, Run, Destination, Setting, …)
│ ├── retention.py # Moteur count/daily/gfs + apply_ssh_retention
│ ├── notifications.py # Email SMTP (succès/erreur)
│ ├── helpers.py # read_archive_info, get_ynh_apps
│ ├── blueprints/
│ │ ├── jobs.py # Dashboard local, CRUD jobs, archives, restauration
│ │ ├── destinations.py # CRUD destinations SSH
│ │ ├── network.py # Fédération, push/pull inter-instances
│ │ ├── settings.py # Paramètres SMTP, export/import config JSON
│ │ ├── api.py # API REST v1 (auth par token)
│ │ └── overview.py # Vue globale des archives (local + SSH + instances)
│ ├── jobs/
│ │ ├── ynh_backup.py # Point d'entrée execute_job, ynh_app, ynh_system
│ │ ├── custom_dir.py # tar + rsync chemins libres
│ │ ├── db_dump.py # mysqldump / pg_dump
│ │ ├── transfer.py # rsync SSH + push HTTP chunked
│ │ └── utils.py # sudo_exists/getsize/listdir/rm/rm_archive
│ ├── federation/
│ │ └── client.py # FederationClient + sync_instance
│ ├── tests/
│ │ └── test_retention.py # Tests pytest (count / daily / GFS)
│ └── templates/
│ ├── base.html
│ ├── dashboard_local.html
│ ├── dashboard_network.html
│ ├── job_form.html
│ ├── job_history.html
│ ├── archives.html
│ ├── archives_overview.html # Vue globale des archives
│ ├── restore_confirm.html
│ ├── settings.html
│ ├── destinations.html
│ ├── remote_instances.html
│ ├── remote_instance_form.html
│ └── federation.html
├── v3/
│ └── apps.json # Catalogue YunoHost personnalisé (version disponible)
└── doc/
-- Jobs de sauvegarde
CREATE TABLE jobs (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL, -- ynh_app|ynh_system|custom_dir|mysql|postgresql
config_json TEXT,
cron_expr TEXT NOT NULL, -- ex: "0 3 * * 1" ou "" (manuel uniquement)
retention_mode TEXT NOT NULL, -- count|daily|gfs
retention_value INTEGER NOT NULL,
retention_gfs_config TEXT, -- JSON {"daily":N,"weekly":M,"monthly":P}
enabled BOOLEAN DEFAULT 1,
core_only BOOLEAN DEFAULT 0,
created_at DATETIME,
updated_at DATETIME
);
-- Relation many-to-many Job ↔ Destination
CREATE TABLE job_destinations (
id INTEGER PRIMARY KEY,
job_id INTEGER REFERENCES jobs(id) ON DELETE CASCADE,
dest_type TEXT NOT NULL, -- ssh | instance
dest_id INTEGER, -- FK → destinations.id (si ssh)
instance_id INTEGER -- FK → remote_instances.id (si instance)
);
-- Destinations de transfert SSH/rsync
CREATE TABLE destinations (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
host TEXT NOT NULL,
port INTEGER DEFAULT 22,
user TEXT NOT NULL DEFAULT 'root',
remote_path TEXT NOT NULL,
key_name TEXT, -- fichier clé dans data_dir/keys/
enabled BOOLEAN DEFAULT 1,
created_at DATETIME
);
-- Historique des exécutions (backup ET restauration)
CREATE TABLE runs (
id INTEGER PRIMARY KEY,
job_id INTEGER REFERENCES jobs(id),
started_at DATETIME,
finished_at DATETIME,
status TEXT, -- running|success|warning|error
log_text TEXT, -- préfixé [RESTAURATION] si restauration
archive_name TEXT,
size_bytes INTEGER
);
-- Paramètres SMTP et notifications
CREATE TABLE settings (
key TEXT PRIMARY KEY, -- smtp_host, smtp_port, smtp_user, …
value TEXT NOT NULL DEFAULT ''
);
-- Instances distantes enregistrées (fédération)
CREATE TABLE remote_instances (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL, -- ex: "tom"
url TEXT NOT NULL, -- https://tom.domaine.fr
api_key TEXT NOT NULL,
last_seen DATETIME,
status TEXT, -- online|offline|error
created_at DATETIME
);
-- Cache états distants
CREATE TABLE remote_runs (
id INTEGER PRIMARY KEY,
instance_id INTEGER REFERENCES remote_instances(id),
job_id INTEGER,
job_name TEXT,
job_type TEXT,
last_run_at DATETIME,
last_status TEXT,
last_archive_name TEXT,
last_size_bytes INTEGER
);
-- Transferts chunked en cours
CREATE TABLE uploads (
upload_id TEXT PRIMARY KEY, -- uuid4
filename TEXT,
total_size INTEGER,
chunk_size INTEGER,
chunks_received INTEGER DEFAULT 0,
checksum TEXT, -- SHA256
started_at DATETIME,
status TEXT -- pending|in_progress|complete|error
);
ynh_appyunohost backup create --apps <app_id> --name <archive_name>
BACKUP_CORE_ONLY=1 yunohost backup create --apps nextcloud --name <archive_name>
{ "app_id": "nocodb", "core_only": false }
Le nom du job est auto-rempli depuis le label de l'application YunoHost sélectionnée.
ynh_systemyunohost backup create --system [hook1 hook2 …] --name <archive_name>
{ "hooks": ["conf_nginx", "conf_ssowat"] }
Les hooks système disponibles sont listés dynamiquement. La liste vide déclenche une sauvegarde système complète.
custom_dir{
"source_path": "/opt/hermes-agent",
"excludes": ["cache/", "logs/", "*.tmp"],
"restore": {
"system_user": { "name": "hermes-agent", "home": "/opt/hermes-agent", "shell": "/bin/false" },
"systemd_service": { "name": "hermes-agent", "service_file": "/opt/hermes-agent/hermes-agent.service" },
"permissions": { "owner": "hermes-agent:hermes-agent", "mode": "750" },
"post_restore_commands": ["systemctl restart hermes-agent"]
}
}
Format archive compatible YunoHost :
jerry_hermes-agent_20260508.tar
├── backup.csv # requis YNH
├── backup_info.json # métadonnées BackupManager
└── data/custom/opt/hermes-agent/...
jerry_hermes-agent_20260508.info.json # SÉPARÉ hors tar, requis YNH
mysql{ "database": "mabase", "user": "mabase_user" }
postgresql{ "database": "mabase", "user": "postgres" }
La rétention est appliquée après chaque backup réussi, localement et sur chaque destination. Les échecs de suppression sont loggés explicitement dans le run (plus de silence silencieux).
count→ Garde les N dernières archives par job
→ Supprime les plus anciennes au-delà de N
daily — fenêtre glissanteretention_value: 30
→ 1 archive par jour sur les 30 derniers jours
→ Fenêtre glissante : toute archive > J-30 supprimée
→ Doublons du même jour éliminés (garde le plus récent)
gfs — Grandfather-Father-Son{ "daily": 7, "weekly": 4, "monthly": 12 }
Après chaque transfert rsync SSH réussi, la même politique est appliquée sur le serveur distant via SSH (find pour lister, rm -f pour supprimer). Les échecs de connexion ou de suppression sont loggés sans bloquer le job.
Après chaque push HTTP chunked, la rétention est appliquée via l'API REST de l'instance distante (DELETE /api/v1/archives/<name>).
Format : {instance}_{label}_{YYYYMMDD}.tar
jerry_nocodb_20260508.tar
jerry_nextcloud_20260508.tar
jerry_system_20260508.tar
jerry_hermes-agent_20260508.tar
jerry_mysql_mabase_20260508.tar
Si le nom du jour existe déjà (backup manuel + automatique le même jour), un suffixe est ajouté : jerry_nextcloud_20260508_2.tar.
backup_info.json embarqué dans le tar :
{
"instance_name": "jerry",
"instance_url": "https://jerry.mondomaine.fr",
"type": "ynh_app",
"created_at": "2026-05-08T03:00:00",
"backupmanager_version": "1.0.0"
}
Un job peut avoir plusieurs destinations : aucune (local uniquement), une ou plusieurs SSH, une ou plusieurs instances fédérées, ou toute combinaison.
Après chaque backup réussi :
| Outil YNH | Usage |
|---|---|
yunohost backup create |
Jobs ynh_app et ynh_system |
yunohost backup restore |
Restauration webadmin |
yunohost app list --output-as json |
Formulaire job |
/home/yunohost.backup/archives/ |
Toutes les archives |
| Nginx + systemd + SSOwat + Let's Encrypt | Infra app |
SSOwat (manifest.toml) :
[resources.permissions.main]
url = "/"
allowed = "admins"
[resources.permissions.api]
url = "/api"
allowed = "visitors" # sécurisé par token Flask
auth_header = false
protected = true
Sudoers : les commandes autorisées couvrent yunohost backup, mysqldump, pg_dump, stat, find, rsync, tar, rm, mkdir, chown, chmod, useradd, systemctl.
Tous les endpoints protégés par X-BackupManager-Key.
GET /api/v1/health
GET /api/v1/summary
GET /api/v1/jobs
GET /api/v1/jobs/<id>/runs
POST /api/v1/jobs/<id>/run
GET /api/v1/running
GET /api/v1/archives
GET /api/v1/archives/<name>/info
DELETE /api/v1/archives/<name>
GET /api/v1/archives/<name>/download
GET /api/v1/archives/<name>/info-json-download
POST /api/v1/archives/<name>/restore
GET /api/v1/archives/<name>/restore/status
POST /api/v1/archives/upload/start
POST /api/v1/archives/upload/<id>/chunk/<n>
POST /api/v1/archives/upload/<id>/finish
DELETE /api/v1/archives/upload/<id>
Chunks : 50 MB | Reprise : upload_id SQLite | Vérif : SHA256 | Transport : HTTPS
ssh-keygen -t ed25519 -f $data_dir/keys/dest_<name>_ed25519 -N "" -C "backupmanager@<dest>"
rsync -az -e "ssh -i $key -p $port" archive.tar archive.info.json user@host:/remote/path/
| Action | Dashboard BM | Webadmin YNH |
|---|---|---|
| App YNH | ✅ (via YNH) | ✅ natif |
| Système YNH | ✅ (via YNH) | ✅ natif |
| Fichiers custom_dir | ✅ complet | ✅ partiel |
| User système | ✅ useradd |
❌ |
| Service systemd | ✅ systemctl |
❌ |
| Permissions | ✅ chown/chmod |
❌ |
| Post-restore commands | ✅ | ❌ |
| MySQL / PostgreSQL | ✅ | ❌ |
| Instance distante | ✅ | ❌ |
| Élément | Mesure |
|---|---|
| Dashboard | SSO YunoHost admins |
| Token API | secrets.token_hex(32), hashé bcrypt |
| Clé SSH | ed25519, $data_dir/keys/, permissions 600 |
| Credentials DB | Jamais en clair, config protégée (chmod 600) |
| Logs | Sans credentials |
| Inter-instances | HTTPS obligatoire |
| sudo_rm | Vérifie le code retour, logue les échecs explicitement |
sudo cat non autorisé → remplacé par sudo rsync → open → sudo rmfinally avec sudo rm -rfurl_for('blueprint.fonction'))/archives — tableau filtrable, actions Restaurer · Pousser · Télécharger · Supprimer/api/v1/running toutes les 5 s, rechargement autobtn-primary/secondary/ghost/danger)sudo_rm vérifie le code retour, échecs loggés/overview : local + SSH + instances, suppression manuelle, rétention à la demandeThreadPoolExecutor)(connect=5s, read=15s) au lieu de 15s globalcore_only : BACKUP_CORE_ONLY passé via env_keep sudoers (plus de sudo env)find -printf (GNU only) par find + stat portablev3/apps.json) — mises à jour détectables depuis l'interface webapp = current_app._get_current_object() avant le thread, with app.app_context() dans le thread@bp.before_request (pas de filtre global)/tmp owned root → cleanup via sudo rm -rf (sudoers)ALTER TABLE dans app.py au démarragecd {dir} && find . -maxdepth 1 -name '*.tar' | stat -c%s — portable (pas de find -printf absent sur BusyBox/BSD)BACKUP_CORE_ONLY=1 transmise via subprocess(env=) + env_keep dans sudoers (pas via sudo env non autorisé)ThreadPoolExecutor — une destination lente ne bloque pas les autresCréer /etc/yunohost/apps_catalog.yml :
- id: default
url: https://app.yunohost.org/default
- id: backupmanager-hansen
url: https://git.hansen.tl/YunoHost-Apps/backupmanager_ynh/raw/main
Rafraîchir le cache : sudo yunohost tools update apps
YunoHost construit l'URL du catalogue en ajoutant /v3/apps.json à l'URL de base,
soit : https://git.hansen.tl/YunoHost-Apps/backupmanager_ynh/raw/main/v3/apps.json
1.1~ynh1 → 1.2~ynh1) :
manifest.toml → champ versionv3/apps.json → champ manifest.versionmainsudo yunohost tools update apps) et propose la mise à jour dans l'interface webbackupmanager_ynh — CDC v5.0 — 2026-07-17 | Phase 1 ✅ | Phase 2 ✅ | Phase 3 ✅ | Phase 3bis ✅ | Phase 4 ✅ | Phase 5 ✅