v0.275.0: a backup's data and its version travel together (R-696, 07 §6.6, D4 option A); R-695, R-691, R-694
gates / gates (push) Successful in 23s
gates / gates (push) Successful in 23s
The unit's data files are stamped with the versions that wrote them; the capture keeps the definition the data belongs to; a restore never starts data under another version's definition (unit restores refuse a mismatch; the off-site restore writes the snapshot's definition); every tier's time is its data's; the conversion-copy release needs a dump on the new engine. File-browser sync single-flight + no empty kept folder (R-695); the kept view joins the folder's owning group, language switch resyncs (R-691); a restore-generated login is not shown as the password (R-694). Red-proofs in felhom.eu/documentation/audits/version-travel-2026-09-26/. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -31,9 +31,7 @@ const restorePointShortID = "helyi"
|
||||
// known at all (found=false → the caller should 404). A known stack with no recovery unit on
|
||||
// disk returns an EMPTY list (a valid answer — "no backup yet"), not an error.
|
||||
//
|
||||
// The single point's Time is the newest mtime among the unit's artifacts (manifest.json,
|
||||
// db-dumps/*.sql, volume-dumps/*.tar): the manifest is only rewritten when the app's config
|
||||
// changes (checksum-skip), so the nightly-refreshed dumps are usually the freshest artifact.
|
||||
// The single point's Time is the unit's DATA time (unitNewestArtifact, v0.275.0 — R-696).
|
||||
func (m *Manager) ListRestorePoints(stackName string) (points []RestorePoint, found bool) {
|
||||
if m.stackProvider == nil {
|
||||
return nil, false
|
||||
@@ -61,13 +59,11 @@ func (m *Manager) ListRestorePoints(stackName string) (points []RestorePoint, fo
|
||||
return []RestorePoint{}, true
|
||||
}
|
||||
|
||||
fi, err := os.Stat(RecoveryUnitManifestPath(nsRoot, stackName))
|
||||
if err != nil {
|
||||
// v0.275.0 (R-696): the unit's DATA time (unitNewestArtifact), never the manifest's refresh time.
|
||||
newest, ok := unitNewestArtifact(RecoveryUnitPath(nsRoot, stackName))
|
||||
if !ok {
|
||||
return []RestorePoint{}, true // no recovery unit yet — "no backup" is a valid answer
|
||||
}
|
||||
newest := fi.ModTime()
|
||||
newest = newestArtifact(AppDBDumpPath(nsRoot, stackName), ".sql", newest)
|
||||
newest = newestArtifact(AppVolumeDumpPath(nsRoot, stackName), ".tar", newest)
|
||||
|
||||
return []RestorePoint{{
|
||||
Time: newest.UTC().Format(time.RFC3339),
|
||||
@@ -77,15 +73,16 @@ func (m *Manager) ListRestorePoints(stackName string) (points []RestorePoint, fo
|
||||
}}, true
|
||||
}
|
||||
|
||||
// newestArtifact returns the newest mtime among cur and the files with the given extension in
|
||||
// dir (non-recursive; a missing dir contributes nothing).
|
||||
func newestArtifact(dir, ext string, cur time.Time) time.Time {
|
||||
// newestDataFile returns the newest mtime among cur and the DATA files with the given extension in dir
|
||||
// (non-recursive; a missing dir contributes nothing). The undo copies (`pre-restore-*`) are not data of
|
||||
// the unit (R-361) and never date it.
|
||||
func newestDataFile(dir, ext string, cur time.Time) time.Time {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return cur
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ext) {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ext) || strings.HasPrefix(e.Name(), preRestoreDumpPrefix) {
|
||||
continue
|
||||
}
|
||||
if info, err := e.Info(); err == nil && info.ModTime().After(cur) {
|
||||
|
||||
Reference in New Issue
Block a user