Historique des versions : snapshots horodatés, différences, restauration

- Avant d'écraser un schéma, le contenu précédent est copié dans
  DATA_DIR/.history/<chemin>/<date>.excalidraw, au plus une version par
  HISTORY_INTERVAL (10 min) ; rétention HISTORY_KEEP (50) et HISTORY_MAX_AGE.
  Pas de git : l'image reste distroless.
- API : GET /api/history, GET /api/history/version, POST /api/history/snapshot,
  POST /api/history/restore (le contenu remplacé devient une version).
- L'historique suit renommages et déplacements, part à la corbeille avec le
  schéma supprimé.
- Dialogue Historique (barre du haut, menu de l'explorateur) : aperçu,
  différences colorées avec l'état actuel, restaurer, restaurer en copie,
  créer une version.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H6J7vKwXx7uopxKhWfFJpm
This commit is contained in:
claude BotandClaude Opus 5.5 committed 2026-10-07 18:03:08 +02:00
1 parent 88f7750577
commit 9f9ec3b3a4
14 files changed
+1005 -7

No files matched your search

+11 -1
View File
@@ -6,6 +6,7 @@
package storage
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
@@ -52,6 +53,9 @@ type Entry struct {
type Store struct {
root string
// History règle l'historique des versions (DefaultHistory par défaut).
History HistoryPolicy
settingsMu sync.Mutex // sérialise les lectures-modifications de .settings.json
}
@@ -69,7 +73,7 @@ func New(root string) (*Store, error) {
if abs, err = filepath.EvalSymlinks(abs); err != nil {
return nil, err
}
return &Store{root: abs}, nil
return &Store{root: abs, History: DefaultHistory}, nil
}
// Clean normalise un chemin relatif : "/a/../b/" -> "b", "" -> "".
@@ -269,6 +273,10 @@ func (s *Store) Write(rel string, data []byte, ifMatch string, create bool) (Ent
return Entry{}, "", ErrConflict
case err != nil && !errors.Is(err, fs.ErrNotExist):
return Entry{}, "", err
case err == nil && !bytes.Equal(cur, data):
if fi, err := os.Stat(abs); err == nil {
s.autoSnapshot(c, cur, fi.ModTime())
}
}
if err := os.MkdirAll(filepath.Dir(abs), 0o755); err != nil {
return Entry{}, "", err
@@ -363,6 +371,7 @@ func (s *Store) Move(from, to string) (Entry, error) {
return Entry{}, mapErr(err)
}
s.moveThumb(cf, ct, fi.IsDir())
s.moveHistory(cf, ct)
fi, err = os.Stat(at)
if err != nil {
return Entry{}, err
@@ -463,6 +472,7 @@ func (s *Store) Delete(rel string, recursive bool) error {
return err
}
s.removeThumb(c, fi.IsDir())
s.trashHistory(c, trash)
return nil
}