Woodstock Backup v2.0.0 - La réécriture complète en Rust

Posté le 26 avril 2026 dans Woodstock Tags: woodstock, backup, sauvegarde, rust, grpcTemps de lecture: 22 min

Bonjour à tous,

Six ans. Il m'aura fallu six ans entre la première version de Woodstock Backup et cette v2. Si vous m'aviez dit en 2020 que je passerais la moitié de la décennie à réécrire trois fois le même logiciel de sauvegarde... j'aurais quand même foncé tête baissée. C'est ma façon de faire. Me voilà donc avec une version 2 stable, entièrement réécrite en Rust, qui tourne en production sur ma petite infrastructure depuis plus d'un an. Et je suis vraiment content du résultat. 😄

Pour ceux qui me lisent depuis longtemps, voici un récapitulatif des articles qui ont précédé celui-ci :

ArticleDateSujet
Woodstock Backup v1.0.02020-09-20Présentation du projet, prototype TypeScript + rsync
Woodstock Backup - Btrfs2021-01-12Abandon de Btrfs, écriture d'un pool custom
Woodstock Backup - Protocole et Langage de sauvegarde2021-04-18Protocole gRPC maison
Woodstock Backup - Optimiser Node.js avec Rust2023-05-10NAPI-RS et bindings Rust pour réduire la consommation mémoire
Woodstock Backup - Reverse engineering de BackupPC2024-05-07Migration du pool BackupPC vers Woodstock

Pour les nouveaux, je résume : Woodstock Backup est mon logiciel de sauvegarde personnel, centralisé, qui sauvegarde toutes les machines de mon réseau local et mes serveurs distants sur un NAS. L'idée de départ était simple. Le résultat est... un peu plus complexe. :)

Pool of chunks

Le long chemin de 2020 à 2026

Prototype 1 : TypeScript + rsync + Btrfs (2020)

Tout a commencé avec un prototype en TypeScript qui utilisait rsync pour copier les fichiers et Btrfs pour les snapshots incrémentaux. L'idée était élégante sur le papier : rsync calcule le delta, Btrfs déduplique, tout le monde est content.

En pratique, je me suis rapidement retrouvé avec des problèmes de stabilité de Btrfs lorsque le nombre de snapshots devient élevé et que le système de fichiers approche de la saturation. J'en ai parlé en détail dans mon article sur Btrfs. En résumé : abandonné.

Prototype 2 : TypeScript + gRPC + pool custom (2021)

L'abandon de Btrfs m'a forcé à écrire mon propre pool de stockage basé sur le principe de Content-Addressable Storage (CAS) : chaque bloc de fichier (chunk) est identifié par son hash, et si deux fichiers partagent des blocs identiques, ces blocs ne sont stockés qu'une seule fois. La déduplication est donc native.

Pour transférer les fichiers entre les machines à sauvegarder et le serveur, j'ai abandonné rsync et développé mon propre protocole basé sur gRPC. gRPC s'appuie sur HTTP/2, offre une compression et un TLS natifs, et les bibliothèques existent pour tous les langages. Parfait.

Ce prototype, toujours en TypeScript, fonctionnait. Mais le moteur JavaScript de Node.js a ses limites. En particulier, la représentation en mémoire des objets JavaScript est beaucoup plus gourmande que celle d'un langage compilé. Pour un outil qui doit gérer des millions de fichiers, c'est un vrai problème.

L'ère des bindings Rust (2023)

L'idée de cette phase était séduisante : garder toute la partie cœur en Rust pour les performances, et conserver le TypeScript pour la couche GraphQL et la logique applicative. Rust pour ce qui est critique, TypeScript pour ce qui est lisible et rapide à écrire. C'est ce que j'ai décrit dans mon article de 2023.

Bien sûr, je me trompais. En pratique, il fallait des bindings d'exposition NAPI-RS pour chaque interface entre les deux langages, des DTOs côté TypeScript qui doublonnaient les structures Rust, et surtout, plus le temps passait, plus le cœur métier migrait vers Rust — laissant de moins en moins de code côté TypeScript. La complexité était réelle : gérer des équivalents d'Observable ou du streaming en passant par des bindings NAPI-RS n'est pas trivial, même si les dernières versions de NAPI-RS ont depuis apporté des solutions à ces problèmes.

Le tout en restant performant malgré le coût du passage par les bindings TypeScript. C'était jouable, mais c'était de la jonglerie.

La migration du pool BackupPC (2024)

En parallèle, j'ai travaillé sur la migration de mon pool BackupPC existant vers le format Woodstock. La première approche envisagée consistait à monter les sauvegardes BackupPC via FUSE et à les lire comme un système de fichiers normal. J'ai finalement opté pour une approche plus directe : lire directement le format interne de BackupPC, ce qui impliquait un travail de rétro-ingénierie non négligeable. La migration a été un succès, et j'ai pu enfin abandonner complètement BackupPC.

La décision de tout réécrire en Rust (2024)

À ce stade, la situation était devenue évidente : la partie TypeScript avait tellement rétréci qu'il ne restait plus grand-chose dedans. Un serveur NestJS (framework Node.js) pour l'API, une interface web en Vue.js, et quelques couches de glue — le reste était déjà en Rust. Maintenir deux écosystèmes pour si peu de code TypeScript n'avait plus aucun sens.

J'ai donc décidé de sauter le pas : supprimer les bindings, réécrire intégralement le backend en Rust. Plus de NAPI-RS, plus de DTOs en double, plus de jonglerie entre deux compilateurs. Du Rust pur, de bout en bout.

Woodstock Backup v2 : la version stable (2026)

La version 2.0.0 est en production depuis plus d'un an maintenant, et je la considère comme stable. Les sauvegardes tournent tous les jours, les restaurations fonctionnent, et je dors tranquille.

Architecture de Woodstock Backup v2

Vue d'ensemble : le modèle Pull

L'architecture reste fondée sur le modèle pull : c'est le serveur de sauvegarde qui initie les connexions vers les périphériques, et non l'inverse. Cela offre une garantie de sécurité importante : un périphérique compromis ne peut pas écrire de données arbitraires dans le serveur de sauvegarde.

Architecture Pull - Woodstock v2

Les composants Rust

L'ensemble de la solution est écrit en Rust. Côté serveur, quatre microservices ; côté client, un démon déployé sur chaque machine à sauvegarder :

ComposantDéployé surRôle
api_serverServeurAPI REST/GraphQL pour l'interface Vue.js — hors du chemin de sauvegarde
client_api_serverServeurReçoit les connexions mTLS de ws_client_daemon pour le signalement online/offline
job_workerServeurExécute les sauvegardes : connexion gRPC vers ws_client_daemon, transfert des chunks, déduplication
schedulerServeurGère le planning, déclenche les jobs selon les règles définies
ws_client_daemonChaque périphériqueReçoit les connexions de job_worker, crée les snapshots, envoie les chunks

La gestion des jobs de sauvegarde est assurée par Apalis, qui s'appuie sur Redis/Valkey comme backend de file d'attente. Apalis remplace ici BullMQ, qui remplissait ce rôle dans l'ancienne version NestJS. C'est une architecture simple et fiable, qui permet également de distribuer les workers si un jour l'infrastructure venait à grossir.

Le pool de stockage : CAS Blake3+Zstd

Le cœur du système est le pool CAS (Content-Addressable Storage). Son fonctionnement est le suivant :

  1. Chaque fichier est découpé en chunks de taille variable.
  2. Chaque chunk est haché avec Blake3 (un algorithme de hachage moderne, très rapide).
  3. Si le hash du chunk est déjà présent dans le pool, il n'est pas retransféré ni re-stocké.
  4. Si le chunk est nouveau, il est compressé avec Zstd avant d'être écrit sur disque.

La déduplication est donc réalisée au niveau des chunks, et non au niveau des fichiers entiers. Cela signifie que si un fichier de 10 Go n'a été modifié qu'à 1 %, seul 1 % sera retransféré et stocké.

Le pool maintient également un compteur de références (refcount) par chunk : quand une sauvegarde est supprimée, les chunks qui ne sont plus référencés sont supprimés du pool. Ce mécanisme de garbage collection permet de garder le pool propre sans intervention manuelle.

Sauvegardes Windows : VSS natif

Depuis la version alpha.57, le client Windows utilise le Volume Shadow Copy Service (VSS) de Windows pour créer un snapshot cohérent du système de fichiers avant la sauvegarde. Cela permet de sauvegarder des fichiers verrouillés (comme les bases de données, les fichiers de profil Outlook, etc.) sans erreur.

Plus besoin de rsync, de Cygwin, ou d'outils tiers : le client ws_client_daemon est un binaire Rust natif pour Windows, compilé avec la cible x86_64-pc-windows-msvc, qui utilise les APIs Win32 directement. C'est nettement plus propre que l'ancienne approche.

Sauvegardes Linux : snapshots Btrfs

Sur les machines Linux dont le système de fichiers est Btrfs, le client crée un snapshot en lecture seule avant de lancer la sauvegarde. Cela garantit la cohérence des données sauvegardées, même si des fichiers sont modifiés pendant la sauvegarde.

Contrairement au premier prototype qui utilisait Btrfs côté serveur (ce qui causait les problèmes évoqués plus haut), ici les snapshots sont créés côté client et uniquement pour la durée de la sauvegarde. C'est une utilisation beaucoup plus conservative de Btrfs.

Les défis techniques

Le refcounting : un problème de cohérence

Le plus grand défi de cette réécriture a été l'implémentation correcte du compteur de références du pool CAS.

Le problème est le suivant : lorsqu'une sauvegarde est en cours, des chunks sont ajoutés au pool et leur refcount est incrémenté. Si la sauvegarde est interrompue (coupure réseau, arrêt du serveur, etc.), le pool peut se retrouver dans un état incohérent : des chunks présents dans le pool sans être référencés par aucune sauvegarde complète.

C'est la solution la plus complexe et qui a nécessité le plus de travail pour être implémentée de manière fiable. En effet, il faut s'assurer que le comptage de référence est bon si on ne veux pas se retrouver à éliminer des chunks encore référencés.

Afin de garantir que le comptage de référence est bon, un outil de récupération a été développé : il analyse le pool et les manifestes des sauvegardes, et corrige les refcounts en cas d'incohérence. Il n'est normallement nécessaire qu'en cas de crash du serveur (coupure de courant, etc.) pendant une sauvegarde.

Actuellement la structure du pool repose sur le système de fichiers. L'inconvénient est à aujourd'hui la durée d'execution du fsck qui peut être très longue (taille du pool). En échange les opérations de lecture/écriture sont très rapides.

Les BREAKING CHANGES

Par rapport à la version 1, on est sur une réécriture complète. Vu le peu de monde qui utilise cette version 1, il n'y a pas de migration prévue. La version 2 est un nouveau projet, avec une nouvelle API, et des changements majeurs dans la façon dont les sauvegardes sont gérées.

Windows sans rsync : binaire natif cross-compilé

Dans la version 1, le client Windows était un serveur rsyncd couplé à Cygwin. C'était fonctionnel, mais peu élégant, difficile à installer, et les permissions NTFS n'étaient pas correctement sauvegardées.

Dans la version 2, le client Windows est un binaire Rust compilé en cross-compilation depuis Linux avec la cible x86_64-pc-windows-msvc et le linker LLVM/Clang. Le binaire est distribué seul, sans dépendance. Il se connecte au serveur de sauvegarde via gRPC mTLS, crée un snapshot VSS, parcourt le système de fichiers et envoie les chunks.

Les ACLs NTFS, les attributs étendus, les points de jonction et les liens symboliques Windows sont correctement gérés. C'est un vrai progrès.

La sécurité : mTLS de bout en bout

Toutes les communications impliquant ws_client_daemon sont chiffrées et authentifiées par mutual TLS (mTLS). Chaque périphérique possède un certificat client signé par une autorité de certification interne au serveur Woodstock.

Deux canaux mTLS distincts :

  • job_workerws_client_daemon (gRPC) : le canal de sauvegarde, à l'initiative du serveur.
  • ws_client_daemonclient_api_server (REST) : le canal de présence, à l'initiative du client, qui lui permet de signaler son statut online/offline.

Cela garantit que :

  • Les données sauvegardées sont chiffrées en transit.
  • Seul le serveur Woodstock peut déclencher une sauvegarde sur un périphérique enregistré.
  • Un périphérique ne peut pas usurper l'identité d'un autre.

À noter : api_server, qui sert l'interface Vue.js, ne dispose pas encore d'authentification. C'est prévu, mais pas encore fait (voir plus bas).

En production depuis plus d'un an

Les machines sauvegardées

Voici un tableau récapitulatif de mes neuf machines sauvegardées au 26 avril 2026 :

MachineRôleSauvegardesFichiersTaille bruteCompressé
localhostNAS local (Debian)4313 3461,3 Go0,8 Go
pc-evePC principal de la famille (Windows)211563 333473 Go413 Go
pc-m-eveOrdinateur portable familial (Windows)26146 24321,6 Go13,5 Go
pc-m-ulrichMon portable personnel (Linux)5343 185125 Go64,5 Go
pc-ulrichMon PC de bureau (Linux)3221 385 001108 Go76,7 Go
pc-alex-linuxPC Linux secondaire4210 00625 Go14,5 Go
pc-alex-windowsPC Windows secondaire360429 368147 Go108 Go
serverServeur dédié OVH principal450243 074810 Go752 Go
server-ovh-6Second serveur OVH472493 441194 Go173 Go

Quelques observations :

  • server et server-ovh-6 ont le plus grand nombre de sauvegardes (450 et 472). Ce sont des serveurs qui tournent 24/7, avec des données critiques.
  • pc-ulrich cumule plus de 1,3 million de fichiers sauvegardés, ce qui en fait la machine avec le plus grand nombre de fichiers individuels. Mon répertoire de développement est manifestement très fragmenté. 😄
  • La compression Zstd est particulièrement efficace sur pc-m-ulrich (49 % d'économie) et beaucoup moins sur server (7 %), ce qui s'explique par la nature des données stockées : données de développement vs. données déjà compressées (archives, images Docker, etc.).

Le pool global

Le pool CAS central agrège toutes les sauvegardes :

StatistiqueValeur
Chunks uniques3 365 564
Références totales60 888 193
Espace pool compressé1,95 To
Espace disque total9,6 To
Espace disque utilisé6,4 To

Le ratio références/chunks (60,9 M / 3,4 M ≈ 18x) illustre l'efficacité de la déduplication : chaque chunk unique est en moyenne référencé 18 fois par différentes sauvegardes. L'historique long des sauvegardes explique cette valeur élevée.

Interface web

L'interface web Vue.js 3 / Vuetify permet de visualiser l'état de l'infrastructure, du pool, et de l'historique des sauvegardes de chaque machine.

Page Devices - liste des machines sauvegardées

La page principale liste les neuf machines avec leur état, le nombre de sauvegardes, la taille brute et compressée.

Page Pool - statistiques du pool CAS

La page pool affiche les statistiques globales du stockage : occupation disque, nombre de chunks, nombre de références, et l'évolution depuis le mois précédent.

Page Backups - détail des sauvegardes d'une machine

La page de détail d'une machine liste l'historique complet de ses sauvegardes avec la date, la durée, et la liste des partitions sauvegardées.

Liste des sauvegardes avec état, durée et politique de rétention

Chaque entrée de la liste indique le numéro de sauvegarde, la date de démarrage, la durée, le nombre de fichiers totaux et nouveaux, les fichiers modifiés et supprimés, le compteur d'erreurs, et la politique de rétention appliquée : Horaire, Quotidien, Hebdo ou Mensuel.

Navigation dans les fichiers d'une sauvegarde

En cliquant sur une sauvegarde, on accède à la vue détaillée : statistiques complètes (fichiers, tailles, durée, vitesse), partitions sauvegardées avec leur type (Btrfs ou VSS), et un explorateur de fichiers permettant de naviguer dans l'arborescence et de télécharger ou restaurer individuellement n'importe quel fichier.

Perspectives

Archivage hors-site sur disque USB

Une fonctionnalité que je veux mettre en place depuis la v1 est l'archivage de la dernière version des sauvegardes sur un disque dur USB, qui est ensuite stocké hors-site. L'idée est d'avoir une copie physique des données dans un autre lieu en cas de sinistre (incendie, vol, etc.).

Dans la version 1 avec BackupPC, j'avais un script qui utilisait le connecteur FUSE de BackupPC pour monter les sauvegardes et les synchroniser avec rsync vers le disque USB. Ce script posait des problèmes avec les gros fichiers et les permissions Windows, comme je l'avais mentionné dans le tout premier article.

Dans Woodstock v2, l'outil en ligne de commande ws_console dispose d'une commande mount qui permet de monter une sauvegarde comme un système de fichiers FUSE. Il sera donc possible de faire un rsync depuis ce point de montage vers le disque USB, avec une gestion correcte des permissions et des gros fichiers.

Ce n'est pas encore automatisé, mais c'est la prochaine chose que je veux mettre en place.

Ajout d'un format de stockage

J'envisage également de pouvoir stocker directement le pool sur un bucket S3 ou compatible (SeaweedFS, RustFS). Avant de me lancer dans cette implémentation, je veux d'abord tester les performances d'un tel choix.

Comparaison avec les solutions existantes

Avant de conclure, voici une comparaison honnête avec les alternatives réalistes. Si un autre outil correspond mieux à vos besoins, utilisez-le. Sans rancune.

Les outils couverts : Restic, BorgBackup, BackupPC, URBackup et Kopia. Ils représentent les principales solutions open-source pour une infrastructure self-hostée multi-machines — ce qui correspond grosso modo au problème que Woodstock cherche à résoudre.

CritèreWoodstock v2ResticBorgBackupBackupPCURBackupKopia
LangageRustGoPython + CPerlC++Go
LicenceMITBSD-2BSDGPL v2+AGPLv3+Apache 2.0
Développement actif✅ 2026✅ 2025✅ 2024❌ 2020¹✅ 2026✅ 2025
Modèle de synchronisationPull (initié par le serveur)Push (le client pousse)Push (SSH ou local)Pull (rsync/tar/SMB)Pull-like (découverte LAN)Push / mode serveur
Agent requis sur le client✅ daemon❌ binaire unique❌ accès SSH❌ rsync/SMB✅ daemon❌ / ✅ serveur optionnel
Planificateur intégré✅ (Apalis + Redis)❌ (cron/systemd)❌ (cron/Borgmatic)✅ (mode serveur)
Backends cloud/distants❌ self-hosted uniquement✅ S3, B2, SFTP, Azure, GCS, rclone…⚠️ SSH / BorgBase❌ disque local uniquement❌ self-hosted uniquement✅ S3, Azure, GCS, B2, SFTP, rclone…
Format de stockagePool CAS, fichiers sur disquePack files CASJournal de segments + indexPool MD5 (niveau fichier) + reverse deltasSnapshots de fichiers + images de blocsPack files CAS
Granularité de déduplicationChunk (CDC)Chunk (CDC)Chunk (BUZHASH CDC)Fichier (MD5 fichier complet, sans hardlinks en v4)FichierChunk (CDC)
Dédup cross-machines✅ (un pool partagé)⚠️ uniquement si dépôt partagé⚠️ uniquement au sein d'un dépôt✅ (pool MD5 partagé)✅ (niveau fichier)⚠️ uniquement au sein d'un dépôt
CompressionZstdZstd (depuis 0.14)lz4, zstd, zlib, lzmagzip / bzip2lzo / zstd (optionnel)zstd, lz4, gzip…
Chiffrement au repos❌ pool en clair✅ AES-256-CTR + Poly1305 (obligatoire)✅ AES-256-CTR + HMAC (optionnel)❌ pool en clair⚠️ optionnel✅ AES-256-GCM ou ChaCha20 (optionnel)
Chiffrement en transit✅ mTLS (gRPC + REST)⚠️ dépend du backend⚠️ SSH ou local⚠️ SSH ou SMB (optionnel)⚠️ TLS optionnel⚠️ TLS en mode serveur (optionnel)
Vérification d'intégritéHash Blake3 par chunkSHA-256 par blobHMAC-SHA256 / BLAKE2bMD5 fichier complet (v4+)HashBLAKE2B ou SHA256
Modèle d'authentificationmTLS par appareil (CA interne)Mot de passe + fichiers clésClés SSH + passphraseClés SSHCertificats clients optionnelsMot de passe + TLS optionnel
Linux
macOS❌²
Windows (natif)✅ (binaire MSVC)❌ (WSL2 uniquement)⚠️ (rsync/Cygwin ou SMB)
Snapshots VSS (Windows)✅ (depuis 0.12)
Snapshots Btrfs (Linux)❌ (hooks manuels)❌ (hooks manuels)
Sauvegarde image / bare-metal✅ (Windows + Linux)
Montage FUSE
Interface web✅ (sans auth pour l'instant)❌ (tiers : Restic Browser)❌ (tiers : Vorta)✅ (CGI/Perl)✅ (KopiaUI)

¹ La dernière version de BackupPC date de juin 2020. La base de code est stable mais n'est plus maintenue.

² La prise en charge de macOS n'est pas encore implémentée dans Woodstock.

Quelques points à souligner :

Le modèle pull (Woodstock, BackupPC, URBackup) signifie qu'un client compromis ne peut pas écrire de données arbitraires sur le serveur de sauvegarde. Le modèle push (Restic, Borg, Kopia) est plus simple à mettre en place, mais accorde davantage de confiance à chaque client.

La déduplication au niveau des chunks signifie que si un fichier de 10 Go est modifié d'1 Ko, seul cet 1 Ko est transféré et stocké. La déduplication au niveau fichier (BackupPC, URBackup) signifie que si un fichier change, le nouveau fichier entier est stocké dans le pool. Pour BackupPC spécifiquement, rsync gère le transfert de manière efficiente — seuls les blocs modifiés transitent sur le réseau — mais comme la correspondance dans le pool est basée sur un MD5 du fichier complet, un fichier modifié génère toujours une nouvelle entrée complète dans le pool. Pour les gros fichiers mutables — bases de données, disques de machines virtuelles, fichiers PST Outlook — la différence d'efficacité de stockage est considérable.

La déduplication cross-machines est moins courante. Si dix machines possèdent chacune une copie du même installeur de 500 Mo, Woodstock ne le stocke qu'une seule fois. Restic et Borg ne dédupliquent qu'au sein d'un même dépôt — pour obtenir une déduplication cross-machines, il faudrait mettre toutes les machines dans le même dépôt, ce qui crée des problèmes de contention de verrous et de contrôle d'accès.

La principale faiblesse de Woodstock : le pool n'est pas chiffré au repos. Si quelqu'un obtient un accès physique ou système à votre NAS, il peut lire vos données de sauvegarde directement. L'approche de Restic — tout chiffrer, de manière obligatoire — est clairement plus solide si quelqu'un met la main sur votre disque. Sur un NAS de LAN privé que vous contrôlez physiquement, c'est un compromis que j'accepte. Pour un stockage distant ou dans le cloud, c'est une vraie limitation. Il est possible de contrer ce problème en chiffrant le disque lui-même (LUKS, BitLocker, etc.), mais c'est une couche supplémentaire à gérer.

Si vous avez besoin d'une restauration bare-metal, URBackup est le seul outil de cette liste qui le fait nativement. Si vous avez besoin de BorgBackup sur Windows, c'est impossible sans WSL2.

Conclusion

Ce projet m'a appris énormément de choses : Rust, gRPC, mTLS, la gestion de pools CAS, le VSS Windows, les snapshots Btrfs, la gestion de jobs distribués avec Redis, et probablement une dizaine d'autres sujets que j'ai oubliés depuis.

Est-ce que c'était raisonnable de passer six ans à construire son propre logiciel de sauvegarde alors qu'il en existe des dizaines ? Bien sur que oui 😄.

Le logiciel est disponible sur mon instance Gitea et la documentation est sur woodstock.shadoware.org.

À bientôt !