Conception du stockage et de la synchronisation cloud
État : les étapes 1-4 sont implémentées au 2026-09-17 ; l'étape 5 dispose d'une référence reproductible. Des reçus durables de sortie/d'entrée, des marqueurs de modification, une session de synchronisation locale au processus et un exécuteur indépendant du fournisseur avec injection de pannes MemoryTransport sont implémentés dans crates/core/src/sync.rs. Les adaptateurs de fichiers OneDrive/Google Drive, le renouvellement des jetons et la CLI cloud sync sont implémentés et testés avec des jeux de test locaux. Les comptes de fournisseurs réels n'ont pas été validés. La connexion mobile AppAuth et les commandes de synchronisation au premier plan sont implémentées ; le consentement natif nécessite des inscriptions de plateforme. Voir cloud-authorization.md pour le périmètre d'autorisation actuel. Ce document étend la demande OneDrive/Google Drive avec l'exigence que les appareils concurrents ne doivent pas corrompre ni verrouiller mutuellement leurs fichiers de base de données.
Décision
Conserver SQLite comme stockage transactionnel local à l'appareil. Ajouter un protocole de synchronisation indépendant du fournisseur qui échange des objets immuables chiffrés et authentifiés via les API cloud. Chaque appareil possède sa propre base de données, son identité, son mot de passe et son composant d'écriture local. Le stockage cloud est un transport d'objets non fiable, et non une base de données montée.
Ne jamais placer le fichier actif memories.db, son journal, WAL, SHM ou fichier de verrouillage dans un répertoire de synchronisation géré par le cloud. Ne pas ouvrir la base de données via un système de fichiers distant. Conserver l'emplacement par défaut des données d'application du système d'exploitation. La détection/le rejet des racines OneDrive/Drive connues constituent une future protection de configuration, actuellement non appliquée ; les clients de synchronisation et points de montage arbitraires ne peuvent pas être détectés de manière fiable.
flowchart LR
A[Device A local SQLite] --> QA[Durable outgoing queue]
QA --> C[OneDrive or Google Drive encrypted objects]
C --> VB[Download and verify]
VB --> B[Device B local SQLite]
B --> QB[Durable outgoing queue]
QB --> C
C --> VA[Download and verify]
VA --> A
Comportement existant et limites
crates/core/src/store.rs: un verrou exclusif du système d'exploitation rejette un second propriétaire local du stockage. SQLite utilisejournal_mode=DELETE,synchronous=FULLet des mises à jour transactionnelles de l'état chiffré accompagnées de points de contrôle signés. Cela protège les processus locaux coopérants ; ce n'est pas un verrou distribué entre des répliques de fichiers indépendantes.- Chaque enregistrement sérialise et chiffre l'intégralité de
State; le déverrouillage vérifie tout l'historique des points de contrôle. Des bibliothèques plus grandes augmenteront le coût d'écriture et le temps de démarrage. Il s'agit d'un problème de passage à l'échelle, et non d'une preuve que SQLite lui-même doit être remplacé. Aucun benchmark de charge n'a été réalisé pour cette évaluation. - L'initialisation de la base de données conserve le verrou du système d'exploitation de la destination pendant la construction d'une base de préparation privée dans le même répertoire. Elle valide l'état chiffré et le point de contrôle initiaux, ferme SQLite, synchronise le fichier et le publie sans remplacer une destination existante. Un arrêt brutal avant publication laisse le chemin final disponible pour une nouvelle tentative. Des fichiers
.memories-init-*abandonnés peuvent subsister après un arrêt brutal ; ils ne sont jamais chargés comme base active et ne sont pas supprimés automatiquement. Servicetraite les mutations sur un état cloné et les valide avant de les accepter.Vault::mergevalide un candidat avant de remplacer le coffre actuel. Les historiques de révisions signés préservent déjà les modifications concurrentes et les marqueurs de suppression.- L'IPC de bureau accepte jusqu'à 16 requêtes concurrentes ; la distribution de l'état détient le mutex du service. Le mobile utilise des verrous de service individuels et une brève recherche dans le registre. Une requête HTTP cloud effectuée directement dans ces sections critiques retarderait d'autres opérations, dont le verrouillage. Il s'agit d'un blocage, et non d'un interblocage circulaire démontré. Les travailleurs cloud doivent libérer les verrous de service avant les E/S réseau ; le traitement des requêtes de bureau utilise désormais des connexions concurrentes limitées.
- Les exportations de synchronisation existantes sont des enveloppes contenant un coffre complet, chiffrées pour le destinataire. Un appareil nouvellement authentifié dans le cloud nécessite toujours une approbation d'appareil avant de pouvoir déchiffrer ou importer un coffre. Une connexion cloud ne doit pas remplacer l'appairage cryptographique des appareils.
Autres bases de données possibles
| Option | Pertinence pour ce dépôt | Décision |
|---|---|---|
| SQLite local avec objets cloud immuables | Réutilise les transactions, le chiffrement, l'audit et la gestion des conflits ; aucun serveur nécessaire | Recommandé |
| redb ou LMDB comme stockage local | Le stockage transactionnel embarqué peut convenir, mais changer de moteur n'apporte ni fusion entre appareils ni verrouillage cloud | Aucune migration justifiée pour le moment |
| Une base de données partagée ou une archive chiffrée dans un dossier synchronisé | Les téléversements de fichiers indépendants peuvent écraser les versions ; les verrous annexes et journaux ne sont pas coordonnés | À rejeter pour une synchronisation active |
| PostgreSQL avec une API de synchronisation dédiée | Centralise la concurrence côté serveur, mais nécessite un service exploité et un protocole hors ligne/de chiffrement distinct | À envisager seulement si les exigences produit incluent un serveur |
| Bibliothèques CRDT telles qu'Automerge ou Yjs | Potentiellement utiles pour une future édition collaborative de texte ; ne remplacent ni le stockage durable ni les vérifications d'autorisations signées | Ne pas introduire pour les identifiants ou les changements de politique |
Passer SQLite à WAL n'est pas une solution de synchronisation cloud. WAL pourrait ensuite améliorer la concurrence des lectures locales après mesure, mais nécessite une coordination sur le même hôte et ne supprime pas le besoin d'un composant d'écriture local unique.
Contrat des objets cloud
Commencer par le format d'enveloppe existant contenant un coffre complet ; éviter de modifier simultanément le stockage, la cryptographie et la granularité de synchronisation. N'ajouter des révisions/blocs chiffrés incrémentaux qu'après validation du chemin d'enveloppes immuables.
- Générer chaque enveloppe une seule fois et enregistrer ses octets exacts dans la file de sortie locale avant le téléversement. Utiliser le SHA-256 de l'objet chiffré complet comme ID logique. Les nouvelles tentatives réutilisent ces octets ; rechiffrer à chaque tentative crée un nouvel objet.
- Les objets et descripteurs de publication sont immuables. Il n'y a pas de
latest.jsonpartagé modifiable, de fichier de verrouillage partagé, de séquence cloud globale ou d'écrasement de base de données. Toute future tête/cache mutable est indicatif et récupérable par énumération des objets. - Publier d'abord les objets de données, puis un descripteur signé immuable liant la version du format, la référence opaque du coffre, l'expéditeur, le destinataire, les empreintes et tailles des objets, et le prédécesseur/la séquence de chaque appareil. Utiliser des identifiants de texte chiffré dans les noms de fichiers, pas des titres en clair, des noms de comptes ou des empreintes de mots de passe.
- Un descripteur peut devenir visible avant ses dépendances, car l'énumération et la livraison cloud peuvent être retardées ou réordonnées. Le maintenir en attente jusqu'à ce que chaque objet soit présent et validé. Ne jamais l'importer partiellement ni faire avancer son état appliqué tant que des dépendances manquent.
- Vérifier la longueur, l'empreinte du texte chiffré, la signature, le destinataire, le périmètre du coffre, l'autorisation actuelle et les invariants de l'historique existant avant validation. Les ETags et empreintes du fournisseur sont des métadonnées de transport, pas une autorité cryptographique.
- Les objets rejoués et fichiers fournisseur en double sont sans danger : dédupliquer par ID logique d'objet vérifié, pas par nom de fichier. Des fichiers cloud manquants n'impliquent jamais une suppression au niveau de l'application ; une suppression est un marqueur signé.
Un fournisseur cloud peut supprimer ou retenir des objets. Les têtes/points de contrôle connus localement peuvent détecter certains retours en arrière, mais un nouvel appareil sans point de contrôle plus récent fiable ne peut pas prouver qu'il dispose de l'état le plus récent. Cette conception fournit l'intégrité et des conflits explicites, pas une disponibilité ni une fraîcheur garanties face à un fournisseur malveillant.
Transactions et cycle de vie des travailleurs
Le composant d'écriture local possède l'état de l'application, les mises à jour des points de contrôle, l'état des files de sortie/d'entrée et les accusés de synchronisation. Utiliser deux courtes sections critiques autour de l'activité réseau :
- Préparer localement : autoriser, capturer une exportation cohérente et valider la tâche exacte de téléversement chiffré ainsi que la liaison de génération/session. Une modification utilisateur et son marqueur de modification/exportation doivent être validés atomiquement pour qu'un arrêt brutal ne perde pas de travail.
- Transférer sans verrou : un travailleur distinct effectue le renouvellement OAuth, le téléversement, le téléchargement, la pagination et les nouvelles tentatives sans détenir le mutex du service ni une transaction d'écriture de base de données. Limiter la concurrence, les tailles de réponse, les tentatives et les délais de connexion/lecture. Respecter les limitations du fournisseur et
Retry-After. - Valider localement : reprendre le composant d'écriture, vérifier l'autorisation et la génération de session, fusionner dans l'état actuel plutôt que dans une ancienne copie capturée, puis valider ensemble l'état, le point de contrôle, les ID appliqués et l'état de la file d'entrée. Un curseur d'énumération fournisseur ne peut avancer que lorsque le travail découvert est durable.
Le verrouillage/la déconnexion annulent les tâches et invalident les résultats tardifs. Aucun nouveau déchiffrement/import ne peut se terminer dans une session invalidée. L'annulation ne peut rappeler un téléversement chiffré déjà accepté par le fournisseur. Conserver les tâches incomplètes sous une forme reprenable ; un téléversement réussi signifie une persistance cloud, pas un accusé d'un autre appareil.
Les jetons OAuth restent chiffrés et locaux à l'appareil, exclus des objets cloud, des paquets d'approbation d'appareil, des sauvegardes exportées, de la recherche et de MCP. La rotation des jetons de renouvellement doit être enregistrée avant que les opérations suivantes utilisent le nouveau jeton ; les renouvellements concurrents sont sérialisés par compte, sans détenir le verrou de service pendant l'échange HTTP. Ne jamais ajouter d'opération publique de lecture de jeton pour les travailleurs.
Comportement propre à chaque fournisseur
| Fournisseur | Stockage d'application | Exigences de téléversement/nouvelle tentative |
|---|---|---|
| OneDrive | Dossier d'application Microsoft Graph, Files.ReadWrite.AppFolder |
Utiliser des noms d'objets immuables et le comportement de conflit des sessions de téléversement fail ; pour un objet existant, le télécharger/vérifier plutôt que le remplacer. N'utiliser les requêtes conditionnelles que lorsque ce point de terminaison en documente la prise en charge. |
| Google Drive | appDataFolder, portée drive.appdata |
Prégénérer et enregistrer les ID de fichiers avec les tâches afin de réessayer sans risque les créations ambiguës ; ne pas considérer les noms de fichiers comme uniques. Vérifier les ID/contenus existants après un conflit. |
Les deux adaptateurs doivent implémenter la pagination, les transferts reprenables, les téléchargements limités, la récupération après perte de réponse et la gestion d'erreurs propre au fournisseur. Ne pas supposer que Drive prend en charge le renommage/verrouillage du système de fichiers ou les mêmes préconditions que Graph. Les URL de téléchargement redirigées et de téléversement reprenable nécessitent des règles explicites de transmission des identifiants afin que les jetons porteurs OAuth ne soient pas envoyés à des hôtes arbitraires.
La conception initiale par dossier d'application sert les appareils appairés d'un utilisateur connectés au même compte fournisseur et à la même application. Le stockage de données d'application Google ne permet pas le partage de fichiers entre comptes différents. Le partage de coffres entre utilisateurs nécessite une conception explicite de transport/partage ; il ne doit pas élargir silencieusement les portées OAuth. OneDrive et Google Drive constituent des répliques indépendantes lorsque les deux sont activés, pas des dossiers synchronisés entre eux. Suivre séparément les transferts en attente et réussis.
Les inscriptions de clients OAuth et la configuration du consentement du fournisseur sont nécessaires avant une vérification avec un compte réel. L'autorisation CLI doit utiliser OAuth par navigateur avec PKCE, validation de l'état et rappel de bouclage. Le mobile nécessite son flux d'autorisation natif pris en charge et une gestion du cycle de vie ; les hypothèses des rappels de bureau ne doivent pas être copiées telles quelles sur mobile.
Conflits, récupération et conservation
- Préserver les deux révisions concurrentes d'identifiant/carte. Exiger une sélection ou résolution explicite avant utilisation ; ne pas choisir l'horodatage d'horloge le plus récent ni fusionner des chaînes de mots de passe. Réutiliser les règles existantes de bifurcation de politique et de quarantaine des anciennes époques.
- Un échec de somme de contrôle/signature rejette l'objet entrant et conserve l'état local précédent. Distinguer dans l'état les entrées corrompues, dépendances manquantes, connexions expirées, quotas et conflits de contenu, plutôt que de tout signaler comme erreur de verrouillage. Une réponse dont les octets ne correspondent pas à son empreinte reste réessayable avec temporisation progressive. Les objets dont l'empreinte a été vérifiée mais qui sont invalides ont des reçus de rejet distincts ; ils ne comptent jamais comme dépendances appliquées avec succès. Les rejets empêchent de déclarer une synchronisation réussie et restent visibles par fournisseur dans l'état cloud. Déconnecter un fournisseur efface ses reçus de rejet pour permettre une nouvelle analyse lors d'une connexion ultérieure. Les anciens reçus mélangeant rejets et imports réussis sont effacés une fois ; les reçus existants de paquets signés empêchent toujours les imports en double.
- La découverte de répertoire dispose d'un budget de 4096 entrées dans la file d'entrée, plus 16 emplacements réservés aux dépendances. Les descripteurs vérifiés planifient les objets manquants directement par empreinte, avant les téléchargements ordinaires. Les tampons d'enveloppes non référencés peuvent être abandonnés sous pression puis récupérés lorsque leur descripteur arrive. Une page de répertoire partielle ne fait jamais avancer le curseur au-delà des ID non admis.
- Chaque validation d'état supprime les exportations en attente vers les destinataires dont l'accès actuel au coffre a été révoqué. Les travailleurs valident leur session entre les opérations sur objets, y compris après l'allocation d'ID par le fournisseur. Les ID de fichiers Google sont enregistrés dans la file de sortie chiffrée avant le premier téléversement, afin que les accusés perdus et redémarrages réutilisent le même objet fournisseur immuable.
- Conserver initialement les anciens objets cloud. Un nettoyage sûr nécessite des accusés par appareil, une politique pour les appareils hors ligne, une gestion des révocations et une politique de conservation pour la récupération. L'absence dans une énumération et le temps écoulé ne suffisent pas à eux seuls.
- Conserver des sauvegardes chiffrées indépendantes. La synchronisation entre appareils distribue aussi les suppressions valides et ne remplace pas une récupération à un instant donné.
- Ajouter un chemin de migration tenant compte de la récupération avant toute migration de schéma : verrouiller localement, conserver une sauvegarde vérifiée, migrer transactionnellement, vérifier, puis activer. Une migration échouée doit laisser le stockage précédent utilisable.
Séquence d'implémentation
- Terminé. Requêtes d'écriture limitées (
CloudSyncEnqueue/CloudSyncWork/CloudSyncAck) et reçus durables de sortie/d'entrée dansStatechiffré. Le chiffrement SQLite,journal_mode=DELETEet les enregistrements de l'état complet sont inchangés. Aucune migration vers WAL ou d'un moteur de base de données. - Terminé, transport local uniquement. Exécuteur d'objets immuables indépendant du fournisseur (
sync::execute) avec injection de pannesMemoryTransport. Les tests couvrent la nouvelle tentative avec les octets exacts, la réouverture après validation, la vérification des écritures en double, le descripteur avant les données, les objets corrompus, l'invalidation par verrouillage/déconnexion et deux appareils appairés. Ce transport de test n'utilise pas de comptes cloud réels. - Terminé, jeux de test locaux uniquement. Renouvellement des jetons, adaptateurs d'objets immuables OneDrive/Google Drive,
CloudSyncRundu démon (HTTP hors du verrou d'écriture) etmemories cloud sync. Aucun compte Microsoft/Google réel n'a été autorisé. - Implémenté, consentement natif réel en attente. La connexion mobile utilise AppAuth ; la synchronisation au premier plan partage l'exécuteur du cœur. Le passage en arrière-plan verrouille tous les handles et annule les résultats tardifs. Les tests de régression des widgets et de la FFI native couvrent l'annulation ; le consentement avec un fournisseur/appareil réel reste une condition de publication. La planification en arrière-plan n'est volontairement pas activée.
- Référence disponible. Voir les performances pour le benchmark jetable en mode release de la taille de bibliothèque, des latences d'enregistrement/déverrouillage/recherche et du trafic de synchronisation. Si nécessaire, migrer le blob local d'état complet vers des lignes chiffrées par coffre/révision/clé avec des reçus transactionnels, puis introduire des objets cloud incrémentaux. Préserver les ID signés, l'historique des révisions, la confidentialité du texte chiffré et la récupération par sauvegarde.
Preuves de validation et conditions restantes
Sous Windows, crates/core/tests/storage_resilience.rs utilise des stockages jetables et des sous-processus. Trois scénarios ont réussi pendant cette évaluation :
- Un processus distinct est rejeté lorsqu'un autre possède le stockage ; tuer le propriétaire libère le verrou du système d'exploitation et l'état chiffré original peut être rouvert.
- Un enfant démarre une transaction d'écriture SQLite, force les pages modifiées dans un journal de restauration et est tué avant validation.
Store::unlockvalide l'état/le point de contrôle original etPRAGMA integrity_checkrenvoieok. - Deux appareils autorisés modifient indépendamment le même élément. Les deux ordres de fusion convergent, le rejeu est idempotent, les deux têtes sont conservées et un instantané entrant avec un parent manquant est rejeté sans modifier l'état valide.
L'assistant volontairement ignoré est explicitement lancé par les tests parents. Exécuter :
cargo test -p memories-core --test storage_resilience --locked -j 1 -- --test-threads=1
cargo test -p memories-core --lib store::tests --locked -j 1 -- --test-threads=1
Les tests de régression d'initialisation dans store.rs tuent un sous-processus après préparation mais avant publication, puis recréent/déverrouillent le stockage final. Ils vérifient aussi que le verrou de destination reste détenu et qu'une destination existante ou nouvellement apparue n'est jamais écrasée. Les tests OAuth couvrent l'invalidation des résultats tardifs après verrouillage.
Il s'agit de tests de modèle et d'arrêts brutaux de processus locaux, pas de preuves face à une véritable coupure de courant, aux pannes de disque/contrôleur, au remplacement de fichiers par un client cloud ou au comportement réel d'un fournisseur. Les échecs dus à un disque plein, les grandes bibliothèques, les coupures de courant à la publication et les frontières de nouvelle tentative/publication cloud nécessitent encore des tests. Aucun compte cloud réel n'a été autorisé ni écrit pendant cette évaluation.
Références
Documentation officielle vérifiée le 2026-09-09 :
- SQLite sur un réseau
- Contraintes de SQLite WAL
- Causes de corruption SQLite
- Dossiers d'application OneDrive
- Sessions de téléversement Graph et comportement de conflit
- Données d'application Google Drive
- Téléversements Google Drive et ID prégénérés
- OAuth Google pour applications natives
- Fonctionnalités de redb