EDIT: La version à jour se trouve ici, plus facile à maintenir pour moi: Gnoobix.net | Sauvegarde Yunohost avec Restic
Sauvegarde Yunohost avec restic
Sommaire
- Description
- Configuration du serveur cible
- Configuration du serveur source
- Vérifiez que tout fonctionne jusque-là
- Script de sauvegarde
- Script de planification
- Restauration d’une sauvegarde
Description
Cette procedure est une adaptation de celle-ci. J’ai décidé d’utiliser restic au lieu de borg parce que:
- J’ai essayé la version packagée de borg pour Yunohost et je n’ai pas réussi à la faire fonctionner
- Je ne voulais pas installer de dépendance sur mon serveur cible, restic ne nécessite qu’un accès à un serveur sftp ce que fait n’importe quelle machine avec le paquet openssh-server.
- restic dispose des mêmes fonctionalités que borg (du moins celles qui m’intéressent)
- restic étant écrit en go, l’installation se résume à la copie d’un fichier, c’est très simple et évite d’avoir à installer plusieurs autres paquets sur mon serveur yunohost.
- j’avais déjà utilisé restic et j’en étais content
- Je peux le faire, tout simplement. Ça fait une alternative de plus aux options existantes. Chacun peut choisir celle qu’il préfère
L’objectif ici est de sauvegarder mon instance Yunohost quotidiennement et de tirer partie des fonctionalités de déduplication et de snapshots de restic pour garder de multiples versions de mes configurations et fichiers.
Tout au long de cette procédure, on partira du postulat suivant:
-
Méthode de sauvegarde:
myrestic
-
Server hébergeant l’instance Yunohost à sauvegarder
- nom
mysourceserver
- adresse
mysourceserver.mysourcedomain.tld
- clé privée ssh
/root/.ssh/id_rsa_restic
- clé publique ssh
/root/.ssh/id_rsa_restic.pub
- nom
-
Serveur vers lequel on veut sauvegarder Yunohost:
- nom
mytargetserver
- adresse
mytargetserver.mytargetdomain.tld
- utilisateur sftp
mytargetuser
- port sftp
2222
- répertoire de stockage des sauvegardes:
/home/backup/mysourceserver.mysourcedomain.tld
- nom
Pensez bien à adapter ces informations à votre cas dans tout ce qui va suivre
Vous pouvez aussi utiliser un répertoire local (sur un disque dur externe monté par exemple) et bénéficier quand même des fonctionalités de restic.
Dans ce cas vous pouvez vous passer de la configuration du serveur cible, il suffit simplement de s’assurer que le répertoire cible de la sauvegarde existe.
Vous n’aurez pas non plus besoin de configurer ssh.
Configuration du serveur cible
Sur le serveur où vous voulez envoyer vos sauvegardes:
- Créez l’utilisateur
mytargetuser
et définissez son mot de passe. Je vous renvoie à la documentation de votre distribution GNU/Linux pour cela mais pour indication ça devrait ressembler à quelque chose comme ça:
# en tant que root ou alors à précéder d'un `sudo `
useradd -m mytargetuser
passwd mytargetuser
- Créez le répertoire de destination des sauvegardes et définissez le propriétaire à
mytargetuser
mkdir /home/backup/mysourceserver.mysourcedomain.tld
chown mytargetuser: /home/backup/mysourceserver.mysourcedomain.tld
On pourrait améliorer la sécurité:
- En désactivant le login ssh pour le compte utilisateur
mytargetuser
- En limitant la connexion de
mytargetuser
à une prison sftp
Configuration du serveur source
Sur le serveur Yunohost à sauvegarder (tout ceci doit être fait en tant que root):
- Installer restic
cd /tmp
wget https://github.com/restic/restic/releases/download/v0.9.6/restic_0.9.6_linux_amd64.bz2
bunzip2 restic_*.bz2 -c > /usr/local/bin/restic
chmod +x /usr/local/bin/restic
Choisissez la dernière version correspondant à votre architecture sur cette page ( linux_arm64
si vous êtes sur Raspberry Pi par exemple).
Vous pouvez choisir n’importe quelle autre méthode d’installation, tout est là
- Configurez SSH
Génération d’une paire de clés SSH
ssh-keygen -b 4096 -t rsa -f /root/.ssh/id_rsa_restic -q -N ""
Création du fichier de configuration SSH
cat <<EOCONF >> ~/.ssh/config
Host mytargetserver
Hostname mytargetserver.mytargetdomain.tld
Port 2222
User mytargetuser
IdentityFile /root/.ssh/id_rsa_restic
EOCONF
- Copiez la clé ssh sur le serveur cible
ssh-copy-id -i /root/.ssh/id_rsa_restic mytargetserver
Vérifiez que tout fonctionne jusque-là
Maintenant qu’on a configuré nos serveurs source et de destination, on veut être sûr que restic sait faire des sauvegardes entre les deux.
Sur mysourceserver
en tant que root:
Essai d’initialisation de dépôt sur mytargetserver
depuis mysourceserver
restic -r sftp:mytargetserver:/home/backup/mysourceserver.mysourcedomain.tld/test-repository init
Restic devrait vous demander un mot de passe (mettez ce que vous voulez) et terminer sans erreur.
Script de sauvegarde
Il est temps de créer le script qui va sauvegarder l’ensemble de notre instance
- Créez un fichier
/etc/yunohost/hooks.d/backup_method/05-myrestic
avec ce contenu:
#!/bin/bash
set -e
RESTIC_PASSWORD="mysupersecretpassword" # changez le mot de passe MAIS ne le perdez pas, il est irrécupérable!
RESTIC_REPOSITORY_BASE=sftp:mytargetserver:/home/backup/mysourceserver.mysourcedomain.tld
RESTIC_COMMAND=/usr/local/bin/restic
do_need_mount() {
work_dir="$1"
name="$2"
repo="$3"
size="$4"
description="$5"
export RESTIC_PASSWORD
export RESTIC_REPOSITORY=${RESTIC_REPOSITORY_BASE}/$name
# On essaie de lister les snapshots, sinon on initialise le dépôt
$RESTIC_COMMAND list snapshots || $RESTIC_COMMAND init
}
do_backup() {
work_dir="$1"
name="$2"
repo="$3"
size="$4"
description="$5"
export RESTIC_PASSWORD
export RESTIC_REPOSITORY=${RESTIC_REPOSITORY_BASE}/$name
LOGFILE=/var/log/backup_restic.log
ERRFILE=/var/log/backup_restic.err
current_date=$(date +"%d_%m_%y_%H:%M")
pushd $work_dir
$RESTIC_COMMAND backup ./ >> $LOGFILE 2>> $ERRFILE
return_code="$?"
popd
# On ne nettoie que si la sauvegarde s'est bien passée
if [ "$return_code" -eq "0" ];then
$RESTIC_COMMAND forget --keep-daily 7 --keep-weekly 8 --keep-monthly 12 >> $LOGFILE 2>> $ERRFILE
fi
}
work_dir=$2
name=$3
size=$5
description=$6
case "$1" in
need_mount)
do_need_mount $work_dir $name $repo $size $description
;;
backup)
do_backup $work_dir $name $repo $size $description
;;
mount)
do_need_mount $work_dir $name $repo $size $description
;;
*)
echo "hook called with unknown argument \`$1'" >&2
exit 1
;;
esac
exit 0
- Rendez le script exécutable et limitez-y l’accès puisqu’il contient des informations secrètes
chmod u=rwx,go= /etc/yunohost/hooks.d/backup_method/05-myrestic
- Testez le script
rm -rf /tmp/test-backup/; mkdir /tmp/test-backup; yunohost backup create --system conf_ldap -n conf_ldap --methods myrestic --debug -r -o /tmp/test-backup; rm -rf /tmp/test-backup
Ne passez à l’étape suivante que si cette commande s’est exécutée sans erreurs.
Il peut subsiter des avertissemnts ( Warning ), celui-ci peut être ignoré par exemple:
Fatal: unable to open config file: Lstat: file does not exist
Is there a repository at the following location?
sftp:mytargetserver:/home/backup/mysourceserver.mysourcedomain.tld/xxxx
C’est ce que restic affiche quand on essaie d’accéder à un dépôt non initialisé.
Il reste quelques modifications à apporter à ce script selon moi:
- Utiliser tous les paramètres passés au script ou les supprimer
- Ajuster la stratégie de nettoyage, je me suis contenté de garder celle du script original avec borg
- Comprendre l’utilisation des arguments need_mount et mount , je les ai gardés du script d’origine car je me suis dit que c’était utile mais je n’ai pas vraiment saisi comment et pourquoi les utiliser
- Lancer une vérification des sauvegardes transférées, peut-être dans un script séparé car selon les options choisies elle peut être longue et consommer beaucoup de ressources. Voir la documentation restic à ce sujet
Script de planification
Puisqu’on sait que notre script de sauvegarde fonctionne, on peut le planifier. La première sauvegarde peut être très longue, ça dépendra du volume de données à transférer. Les sauvegardes suivantes seront beaucoup plus rapides grâce à la déduplication.
- créez un fichier
/root/yunohost-99-backup
avec ce contenu:
#!/bin/bash
LOCK_FILE=/tmp/yunohost-99-backup.lock
if [ -f "$LOCK_FILE" ];then
echo "Backup already launched by process $(grep '.*' $LOCK_FILE), canceling this one" >&2
exit 1
fi
echo $ > "$LOCK_FILE"
if yunohost -v | grep "version: 2." > /dev/null; then
ignore_apps="--ignore-apps"
ignore_system="--ignore-system"
else
ignore_apps=""
ignore_system=""
fi
filter_hooks() {
ls /usr/share/yunohost/hooks/backup/ /etc/yunohost/hooks.d/backup/ | grep "\-$1_" | cut -d"-" -f2 | uniq
}
# Backup system part conf
yunohost backup create $ignore_apps -n auto_conf --methods myrestic --system $(filter_hooks conf)
# Backup system data
yunohost backup create $ignore_apps -n auto_data --methods myrestic --system $(filter_hooks data)
# Backup all apps independently
for app in $(yunohost app list --installed -b | grep id: | cut -d: -f2); do
backup_methods=$(yunohost app setting $app backup_methods)
if [ -z "$backup_methods" ]; then
backup_methods=myrestic
fi
if [ "$backup_methods" != "none" ]; then
yunohost backup create $ignore_system -n auto_$app --methods $backup_methods --apps $app
fi
done
rm "$LOCK_FILE"
- Rendez-le exécutable
chmod +x /etc/cron.daily/yunohost-99-backup
J’ai apporté quelques modifications au script d’origine:
- J’utilise un fichier de verrou pour éviter le lancement concurrent de plusieurs sauvegardes
- Je n’ai pas planifié directement ce script pour la raison qui suit
J’ai remarqué que la sauvegarde de gitlab bloquait car elle attendait que je réponde à une question par un “y”.
J’ai cherché dans yunohost backup --help
pour savoir comment y répondre sans succès.
J’ai essayé quelques astuces avec la commande yes ou un simple echo “y” passé à la commande de backup par un pipe, même résultat.
Il fallait absolument que la sauvegarde se fasse sans interaction, j’ai donc utilisé le programme expect pour répondre à la question pour moi.
Ça a l’avantage d’être sûr de la question à laquelle on répond
Pour info la question est une demande de confirmation avant de prendre un peu plus d’espace disque que prévu temporairement pour faire la sauvegarde. Si vous êtes juste en place ça peut poser problème.
Voilà comment faire:
- Installer expect
apt install expect -y
- Créer un fichier /etc/cron.daily/yunohost-99-backup-answerbot avec ce contenu
#!/usr/bin/expect -f
set timeout -1
spawn /root/yunohost-99-backup
expect -re "Some files couldn't be prepared.*Do you agree?"
send -- "y\r"
expect eof
Ce script va lancer le script de sauvegarde et répondre par “y” quand on lui demandera.
Restauration d’une sauvegarde
Vous pouriez avoir envie (ou besoin) de restaurer des données d’une sauvegarde (c’est tout l’intérêt :p)
Je vous renvoie à la documentation officielle pour les détails des commandes.
Voici un simple exemple de restauration pour l’application Piwigo
- Listez les snapshots existants
restic -r sftp:mytargetserver:/home/backup/mysourceserver.mysourcedomain.tld/auto_piwigo snapshots
# enter password for repository:
# repository xxxxxx opened successfully, password is correct
# created new cache in /home/mytargetuser/.cache/restic
# ID Time Host Tags Paths
# -------------------------------------------------------------------------------------------------------------------
# yyyyyyyy 2020-02-10 22:03:22 mysourceserver.mysourcedomain.tld /home/yunohost.backup/tmp/auto_piwigo
# -------------------------------------------------------------------------------------------------------------------
# 1 snapshots
- Restaurez à partir du snapshot
yyyyyyyy
mkdir /tmp/restore
restic -r sftp:mytargetserver:/home/backup/mysourceserver.mysourcedomain.tld/auto_piwigo restore yyyyyyyy --target /tmp/restore
# enter password for repository:
# repository xxxxxx opened successfully, password is correct
# restoring <Snapshot yyyyyyyy of [/home/yunohost.backup/tmp/auto_piwigo] at 2020-02-10 22:03:22.602905984 +0100 CET by root@mysourceserver.mydomain.tld to /tmp/restore
- Vérifiez que vos fichiers se trouvent bien dans le répertoire de restauration temporaire, référez-vous à la documentation de Yunohost ou du paquet de l’application pour savoir où et quels fichiers restaurer.
tree /tmp/restore/ -L 4
# /tmp/restore/
# ├── apps
# │ └── piwigo
# │ ├── backup
# │ │ ├── db.sql
# │ │ ├── etc
# │ │ ├── home
# │ │ └── var
# │ └── settings
# │ ├── conf
# │ ├── manifest.json
# │ ├── scripts
# │ ├── settings.yml
# │ └── status.json
# ├── backup.csv
# └── info.json
#
# 9 directories, 6 files
EDIT: The up-to-date version here, easier to maintain for me: Gnoobix.net | Yunohost backup with restic
Yunohost restic backup
Table of Contents
- Description
- Target server configuration
- Source server configuration
- Check that everything works up to that point
- Backup script
- Scheduled script
- Restore from backup
Description
This procedure is adapted from this one. I decided to use restic instead of Borg because:
- I tried the yunohost packaged borg but failed to make it work
- I did not want to have to install a dependency on the target server, restic only needs a sftp server as target which qualifies to any gnu/linux server with openssh-server installed on.
- restic has the same features as borg (at least all the ones that matter to me)
- restic being written in go has no dependency, you copy the binary and that’s it, no packaging problem
- I was used to restic
- Why not? It makes an alternative to the existing methods. Everybody is free to choose their favorite one
The goal here is to backup my Yunohost instance daily and leverage restic’s deduplication and snapshots to keep multiple archives of my data and configuration.
Here we consider the following use case:
-
Backup method:
myrestic
-
Server hosting the Yunohost instance to be backed up
- name
mysourceserver
- address
mysourceserver.mysourcedomain.tld
- private key
/root/.ssh/id_rsa_restic
- public key
/root/.ssh/id_rsa_restic.pub
- name
-
Server to backup Yunohost to:
- Name
mytargetserver
- address
mytargetserver.mytargetdomain.tld
- sftp login user
mytargetuser
- sftp port
2222
- directory hosting the backup repositories:
/home/backup/mysourceserver.mysourcedomain.tld
- Name
Adapt those to your needs in the following steps.
You could also use a local directory on an external drive for example and benefit restic’s deduplication and snapshots features.
In that case you would not need any target server configuration other than creating the directory where the backups should be stored on your yunohost server.
You would not need source server ssh configuration either.
Target server configuration
On the server where you want to send your backups
- Create a user
mytargetuser
and set its password. Refer to your distribution documentation for this but it should look like something like this:
# as root or using sudo
useradd -m mytargetuser
passwd mytargetuser
- Create the directory where backups will be stored and make
mytargetuser
the owner
mkdir /home/backup/mysourceserver.mysourcedomain.tld
chown mytargetuser: /home/backup/mysourceserver.mysourcedomain.tld
Security could be hardened:
- Disable ssh and shell login for
mytargetuser
- restrain
mytargetuser
to a sftp jail
Source server configuration
On the Yunohost server to be backed up (all this should be done with the root user):
- Install restic
cd /tmp
wget https://github.com/restic/restic/releases/download/v0.9.6/restic_0.9.6_linux_amd64.bz2
bunzip2 restic_*.bz2 -c > /usr/local/bin/restic
chmod +x /usr/local/bin/restic
Update this with the latest release that can be found on the release page and select the one matching your architecture (e.g: linux_arm64
for a raspberry).
Any other installation method would do, check here
- Configure SSH
Generate an SSH keypair
ssh-keygen -b 4096 -t rsa -f /root/.ssh/id_rsa_restic -q -N ""
Set ssh config file
cat <<EOCONF >> ~/.ssh/config
Host mytargetserver
Hostname mytargetserver.mytargetdomain.tld
Port 2222
User mytargetuser
IdentityFile /root/.ssh/id_rsa_restic
EOCONF
- Copy ssh key to target server
ssh-copy-id -i /root/.ssh/id_rsa_restic mytargetserver
Check that everything works up to that point
Now that we have configured both our source and target server, we want to make sure we can actually make a restic backup from source to target.
On mysourceserver
as root:
Try to initialize a repository on mytargetserver
from mysourceserver
restic -r sftp:mytargetserver:/home/backup/mysourceserver.mysourcedomain.tld/test-repository init
This should ask for a password (use any one you like) and return a success.
Backup script
It’s time to create the script that will backup our system and applications using restic.
- Create a file
/etc/yunohost/hooks.d/backup_method/05-myrestic
with this content:
#!/bin/bash
set -e
RESTIC_PASSWORD="mysupersecretpassword" # change it but DO NOT loose this password, you can't recover it
RESTIC_REPOSITORY_BASE=sftp:mytargetserver:/home/backup/mysourceserver.mysourcedomain.tld
RESTIC_COMMAND=/usr/local/bin/restic
do_need_mount() {
work_dir="$1"
name="$2"
repo="$3"
size="$4"
description="$5"
export RESTIC_PASSWORD
export RESTIC_REPOSITORY=${RESTIC_REPOSITORY_BASE}/$name
# Try to list snapshot from repository, otherwise initialize repository
$RESTIC_COMMAND list snapshots || $RESTIC_COMMAND init
}
do_backup() {
work_dir="$1"
name="$2"
repo="$3"
size="$4"
description="$5"
export RESTIC_PASSWORD
export RESTIC_REPOSITORY=${RESTIC_REPOSITORY_BASE}/$name
LOGFILE=/var/log/backup_restic.log
ERRFILE=/var/log/backup_restic.err
current_date=$(date +"%d_%m_%y_%H:%M")
pushd $work_dir
$RESTIC_COMMAND backup ./ >> $LOGFILE 2>> $ERRFILE
return_code="$?"
popd
# cleanup old archives only if the backup succeeded
if [ "$return_code" -eq "0" ];then
$RESTIC_COMMAND forget --keep-daily 7 --keep-weekly 8 --keep-monthly 12 >> $LOGFILE 2>> $ERRFILE
fi
}
work_dir=$2
name=$3
size=$5
description=$6
case "$1" in
need_mount)
do_need_mount $work_dir $name $repo $size $description
;;
backup)
do_backup $work_dir $name $repo $size $description
;;
mount)
do_need_mount $work_dir $name $repo $size $description
;;
*)
echo "hook called with unknown argument \`$1'" >&2
exit 1
;;
esac
exit 0
- Make it executable and set safer permissions as it contains sensitive information
chmod u=rwx,go= /etc/yunohost/hooks.d/backup_method/05-myrestic
- Test it
rm -rf /tmp/test-backup/; mkdir /tmp/test-backup; yunohost backup create --system conf_ldap -n conf_ldap --methods myrestic --debug -r -o /tmp/test-backup; rm -rf /tmp/test-backup
Proceed to the next step only if this ran without errors.
Some warnings may be ignored as this one:
Fatal: unable to open config file: Lstat: file does not exist
Is there a repository at the following location?
sftp:mytargetserver:/home/backup/mysourceserver.mysourcedomain.tld/xxxx
this is generated when restic is trying to access the remote repository the first time.
There are still some changes left to be made in that script:
- Make some use of all the parameters passed to the script or remove them
- Tune cleanup policy, I kept the one used in the original borg script
- Understand the use off need_mount and mount arguments, I kept them from the original script as I guess this is a required interface but I am not sure of how to use them and why
- Add a restic check to make sure backups are consitent but maybe add it to a separate script planed weekly or monthly as this is resource and time consuming you may not want to do it daily. Check restic documentation about that
Scheduled script
Since we know our backup script works, we can schedule it to run daily. The first backup can take a long time to finish depending on the amount of data to be processed. The following backups should be a lot faster since changes will be incremental and take advantage of deduplication for snapshots.
- Create a file
/root/yunohost-99-backup
with this content:
#!/bin/bash
LOCK_FILE=/tmp/yunohost-99-backup.lock
if [ -f "$LOCK_FILE" ];then
echo "Backup already launched by process $(grep '.*' $LOCK_FILE), canceling this one" >&2
exit 1
fi
echo $ > "$LOCK_FILE"
if yunohost -v | grep "version: 2." > /dev/null; then
ignore_apps="--ignore-apps"
ignore_system="--ignore-system"
else
ignore_apps=""
ignore_system=""
fi
filter_hooks() {
ls /usr/share/yunohost/hooks/backup/ /etc/yunohost/hooks.d/backup/ | grep "\-$1_" | cut -d"-" -f2 | uniq
}
# Backup system part conf
yunohost backup create $ignore_apps -n auto_conf --methods myrestic --system $(filter_hooks conf)
# Backup system data
yunohost backup create $ignore_apps -n auto_data --methods myrestic --system $(filter_hooks data)
# Backup all apps independently
for app in $(yunohost app list --installed -b | grep id: | cut -d: -f2); do
backup_methods=$(yunohost app setting $app backup_methods)
if [ -z "$backup_methods" ]; then
backup_methods=myrestic
fi
if [ "$backup_methods" != "none" ]; then
yunohost backup create $ignore_system -n auto_$app --methods $backup_methods --apps $app
fi
done
rm "$LOCK_FILE"
- Make it executable
chmod +x /etc/cron.daily/yunohost-99-backup
I made some changes to the original script:
- Used a lock file to make sure multiple backups don’t run at the same time
- I did not directly schedule this script, the reason is given in the note below
I noticed that the gitlab backup spawned a question and required an interaction (a “y” answer) for the backup to proceed.
I looked into the yunohost backup --help
with no luck.
I tried some tricks using the yes command or a basic piped echo "y"
, nothing worked.
Since I want a totally unattented backup script I had to use the expect program to answer this question for me.
This has the advantage of being able to select which question I want to answer.
Note that this question is whether you are okay with taking a little more space temporarily to make the backup.
This could be a problem in a low space environment.
Here’s how to do it
- Install the expect program
apt install expect -y
- Create a file
/etc/cron.daily/yunohost-99-backup-answerbot
with this content
#!/usr/bin/expect -f
set timeout -1
spawn /root/yunohost-99-backup
expect -re "Some files couldn't be prepared.*Do you agree?"
send -- "y\r"
expect eof
This will launch the backup script and automatically answer “y” when prompted.
Restore from backup
You may want or need to restore data from your backups someday.
See official restic documentation for details about the commands.
Here is a simple example for piwigo application restoration.
- List existing snapshots
restic -r sftp:mytargetserver:/home/backup/mysourceserver.mysourcedomain.tld/auto_piwigo snapshots
# enter password for repository:
# repository xxxxxx opened successfully, password is correct
# created new cache in /home/mytargetuser/.cache/restic
# ID Time Host Tags Paths
# -------------------------------------------------------------------------------------------------------------------
# yyyyyyyy 2020-02-10 22:03:22 mysourceserver.mysourcedomain.tld /home/yunohost.backup/tmp/auto_piwigo
# -------------------------------------------------------------------------------------------------------------------
# 1 snapshots
- Restore files from snapshot
yyyyyyyy
mkdir /tmp/restore
restic -r sftp:mytargetserver:/home/backup/mysourceserver.mysourcedomain.tld/auto_piwigo restore yyyyyyyy --target /tmp/restore
# enter password for repository:
# repository xxxxxx opened successfully, password is correct
# restoring <Snapshot yyyyyyyy of [/home/yunohost.backup/tmp/auto_piwigo] at 2020-02-10 22:03:22.602905984 +0100 CET by root@mysourceserver.mydomain.tld to /tmp/restore
- Check that you have the files restored to temporary directory, choose what to restore and how according to yunohost or package documentation.
tree /tmp/restore/ -L 4
# /tmp/restore/
# ├── apps
# │ └── piwigo
# │ ├── backup
# │ │ ├── db.sql
# │ │ ├── etc
# │ │ ├── home
# │ │ └── var
# │ └── settings
# │ ├── conf
# │ ├── manifest.json
# │ ├── scripts
# │ ├── settings.yml
# │ └── status.json
# ├── backup.csv
# └── info.json
#
# 9 directories, 6 files