Local-first runtime cleanup and recovery hardening
This commit is contained in:
@@ -88,6 +88,9 @@ func EnsureRotatingLocalBackup(dbPath, configPath string) ([]string, error) {
|
||||
}
|
||||
|
||||
root := resolveBackupRoot(dbPath)
|
||||
if err := validateBackupRoot(root); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := backupNow()
|
||||
|
||||
created := make([]string, 0)
|
||||
@@ -111,6 +114,40 @@ func resolveBackupRoot(dbPath string) string {
|
||||
return filepath.Join(filepath.Dir(dbPath), "backups")
|
||||
}
|
||||
|
||||
func validateBackupRoot(root string) error {
|
||||
absRoot, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve backup root: %w", err)
|
||||
}
|
||||
|
||||
if gitRoot, ok := findGitWorktreeRoot(absRoot); ok {
|
||||
return fmt.Errorf("backup root must stay outside git worktree: %s is inside %s", absRoot, gitRoot)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findGitWorktreeRoot(path string) (string, bool) {
|
||||
current := filepath.Clean(path)
|
||||
info, err := os.Stat(current)
|
||||
if err == nil && !info.IsDir() {
|
||||
current = filepath.Dir(current)
|
||||
}
|
||||
|
||||
for {
|
||||
gitPath := filepath.Join(current, ".git")
|
||||
if _, err := os.Stat(gitPath); err == nil {
|
||||
return current, true
|
||||
}
|
||||
|
||||
parent := filepath.Dir(current)
|
||||
if parent == current {
|
||||
return "", false
|
||||
}
|
||||
current = parent
|
||||
}
|
||||
}
|
||||
|
||||
func isBackupDisabled() bool {
|
||||
val := strings.ToLower(strings.TrimSpace(os.Getenv(envBackupDisable)))
|
||||
return val == "1" || val == "true" || val == "yes"
|
||||
|
||||
@@ -3,6 +3,7 @@ package appstate
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -69,7 +70,7 @@ func TestEnsureRotatingLocalBackupEnvControls(t *testing.T) {
|
||||
if _, err := EnsureRotatingLocalBackup(dbPath, cfgPath); err != nil {
|
||||
t.Fatalf("backup with env: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(backupRoot, "daily", "meta.json")); err != nil {
|
||||
if _, err := os.Stat(filepath.Join(backupRoot, "daily", ".period.json")); err != nil {
|
||||
t.Fatalf("expected backup in custom dir: %v", err)
|
||||
}
|
||||
|
||||
@@ -77,7 +78,35 @@ func TestEnsureRotatingLocalBackupEnvControls(t *testing.T) {
|
||||
if _, err := EnsureRotatingLocalBackup(dbPath, cfgPath); err != nil {
|
||||
t.Fatalf("backup disabled: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(backupRoot, "daily", "meta.json")); err != nil {
|
||||
if _, err := os.Stat(filepath.Join(backupRoot, "daily", ".period.json")); err != nil {
|
||||
t.Fatalf("backup should remain from previous run: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureRotatingLocalBackupRejectsGitWorktree(t *testing.T) {
|
||||
temp := t.TempDir()
|
||||
repoRoot := filepath.Join(temp, "repo")
|
||||
if err := os.MkdirAll(filepath.Join(repoRoot, ".git"), 0755); err != nil {
|
||||
t.Fatalf("mkdir git dir: %v", err)
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(repoRoot, "data", "qfs.db")
|
||||
cfgPath := filepath.Join(repoRoot, "data", "config.yaml")
|
||||
if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil {
|
||||
t.Fatalf("mkdir data dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(dbPath, []byte("db"), 0644); err != nil {
|
||||
t.Fatalf("write db: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(cfgPath, []byte("cfg"), 0644); err != nil {
|
||||
t.Fatalf("write cfg: %v", err)
|
||||
}
|
||||
|
||||
_, err := EnsureRotatingLocalBackup(dbPath, cfgPath)
|
||||
if err == nil {
|
||||
t.Fatal("expected git worktree backup root to be rejected")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "outside git worktree") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user