Sauvegarder une fois par séance plutôt qu'à chaque modification
L'interface enregistre après une seconde d'inactivité, et le serveur copiait le planning avant chaque écriture : une après-midi d'édition produisait des dizaines de fichiers quasi identiques. Le problème n'est pas la place occupée mais ce que la rotation en faisait — les cinquante emplacements se remplissaient en quelques heures et chassaient les états anciens, les seuls qu'on cherche à retrouver. Un filet qui ne remonte pas au-delà de la dernière demi-heure ne protège pas de la bêtise qu'on découvre le lendemain. La copie est désormais prise à l'ouverture, quand le planning est encore dans l'état d'avant la séance. Deux garde-fous : au plus une par jour si le serveur reste allumé longtemps, et rien du tout si le contenu n'a pas bougé depuis la dernière copie. Les cinquante conservées couvrent maintenant des mois. La copie et le remplacement partagent un état — la date de la dernière sauvegarde — donc un verrou les sérialise, ce qui protège au passage du cas où deux onglets enregistrent simultanément. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
78
main_test.go
78
main_test.go
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
"time"
|
||||
)
|
||||
|
||||
// serveurDeTest monte un serveur sur un dossier temporaire. Le fichier de
|
||||
@@ -253,11 +254,14 @@ func TestElaguerBackupsNeGardeQueLesPlusRecents(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSauvegardesMultiplesDansLaMemeSeconde(t *testing.T) {
|
||||
// Le point de la décision 23 : une séance d'édition ne doit pas remplir le
|
||||
// dossier de copies quasi identiques, sous peine d'en chasser les états
|
||||
// anciens — les seuls qu'on cherche vraiment à retrouver.
|
||||
func TestEcrituresRapprocheesNeProduisentQuUneSauvegarde(t *testing.T) {
|
||||
s, routes := serveurDeTest(t)
|
||||
ecrireFichier(t, s.fichierDonnees, `{"projects":[]}`)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
for i := 0; i < 10; i++ {
|
||||
rec := appeler(t, routes, http.MethodPut, "/api/data",
|
||||
fmt.Sprintf(`{"version":3,"projects":[],"n":%d}`, i))
|
||||
if rec.Code != http.StatusOK {
|
||||
@@ -265,6 +269,76 @@ func TestSauvegardesMultiplesDansLaMemeSeconde(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
sauvegardes, _ := filepath.Glob(filepath.Join(s.dossierBackups(), "projets-*.json"))
|
||||
if len(sauvegardes) != 1 {
|
||||
t.Errorf("sauvegardes = %d, attendu 1 pour dix écritures rapprochées", len(sauvegardes))
|
||||
}
|
||||
// Et c'est bien l'état d'avant la séance qui est conservé.
|
||||
if lireFichier(t, sauvegardes[0]) != `{"projects":[]}` {
|
||||
t.Errorf("la sauvegarde ne contient pas l'état initial : %s", lireFichier(t, sauvegardes[0]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSauvegardeDOuvertureEstToujoursPrise(t *testing.T) {
|
||||
s, _ := serveurDeTest(t)
|
||||
ecrireFichier(t, s.fichierDonnees, `{"projects":["avant"]}`)
|
||||
|
||||
// Une écriture vient d'avoir lieu : l'intervalle n'est pas écoulé.
|
||||
s.dernierBackup = time.Now()
|
||||
|
||||
if err := s.sauvegarderVersionPrecedente(true); err != nil {
|
||||
t.Fatalf("sauvegarde d'ouverture : %v", err)
|
||||
}
|
||||
|
||||
sauvegardes, _ := filepath.Glob(filepath.Join(s.dossierBackups(), "projets-*.json"))
|
||||
if len(sauvegardes) != 1 {
|
||||
t.Fatalf("sauvegardes = %d, attendu 1 — l'ouverture doit passer outre l'intervalle",
|
||||
len(sauvegardes))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOuvrirSansRienModifierNeDupliquePas(t *testing.T) {
|
||||
s, _ := serveurDeTest(t)
|
||||
ecrireFichier(t, s.fichierDonnees, `{"projects":["inchangé"]}`)
|
||||
|
||||
// Trois ouvertures successives sur un planning qu'on n'a pas touché.
|
||||
for i := 0; i < 3; i++ {
|
||||
if err := s.sauvegarderVersionPrecedente(true); err != nil {
|
||||
t.Fatalf("ouverture %d : %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
sauvegardes, _ := filepath.Glob(filepath.Join(s.dossierBackups(), "projets-*.json"))
|
||||
if len(sauvegardes) != 1 {
|
||||
t.Errorf("sauvegardes = %d, attendu 1 — le contenu est identique", len(sauvegardes))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSansFichierAucuneSauvegarde(t *testing.T) {
|
||||
s, _ := serveurDeTest(t)
|
||||
|
||||
if err := s.sauvegarderVersionPrecedente(true); err != nil {
|
||||
t.Fatalf("sauvegarde : %v", err)
|
||||
}
|
||||
|
||||
if existe(s.dossierBackups()) {
|
||||
t.Error("un dossier de sauvegardes a été créé alors qu'il n'y a pas de planning")
|
||||
}
|
||||
}
|
||||
|
||||
// Deux copies dans la même seconde ne doivent pas s'écraser. Le cas est devenu
|
||||
// rare depuis l'espacement des sauvegardes, mais il reste atteignable — deux
|
||||
// exemplaires lancés en même temps sur le même planning.
|
||||
func TestSauvegardesMultiplesDansLaMemeSeconde(t *testing.T) {
|
||||
s, _ := serveurDeTest(t)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
ecrireFichier(t, s.fichierDonnees, fmt.Sprintf(`{"projects":[],"n":%d}`, i))
|
||||
if err := s.sauvegarderVersionPrecedente(true); err != nil {
|
||||
t.Fatalf("sauvegarde %d : %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
sauvegardes, _ := filepath.Glob(filepath.Join(s.dossierBackups(), "projets-*.json"))
|
||||
if len(sauvegardes) != 3 {
|
||||
t.Errorf("sauvegardes = %d, attendu 3 — des copies se sont écrasées", len(sauvegardes))
|
||||
|
||||
Reference in New Issue
Block a user