v0.26.0: Storage namespace felhom-data/ + test node wipe script

All felhom-managed data on external drives now lives under felhom-data/
subdirectory, cleanly separating controller data from user files.

- backup/paths.go: add FelhomDataDir constant, update 8 path helpers
- stacks/delete.go: add local felhomDataDir constant (circular import
  boundary), update ProtectedHDDPaths + GetStackBackupData
- storage/migrate_drive.go: import backup pkg, fix conflict check, verify,
  rsync excludes (felhom-data/backups/*/restic/), size estimation
- storage/migrate.go: import backup pkg, fix DB dump paths
- web/handlers.go: fix legacy 'storage' path -> backup.AppDataDir()
- storage/format_linux.go: create felhom-data/ instead of storage/
- storage/attach_linux.go: create felhom-data/ instead of storage/
- scripts/felhom-wipe.sh: new multi-level test node wipe script
  (soft/controller/full/nuclear)
- CHANGELOG.md, controller/README.md, scripts/README.md: updated docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 10:10:51 +01:00
parent e238474b33
commit 7abd1c5954
12 changed files with 596 additions and 52 deletions
+25 -12
View File
@@ -12,6 +12,7 @@ import (
"sync"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
)
@@ -180,7 +181,7 @@ func (dm *DriveMigrator) MigrateDrive(ctx context.Context, req DriveMigrateReque
// Check for conflicts on destination
for _, app := range appsToMigrate {
destAppData := filepath.Join(req.DestPath, "appdata", app.Name)
destAppData := backup.AppDataDir(req.DestPath, app.Name)
if info, err := os.Stat(destAppData); err == nil && info.IsDir() {
entries, _ := os.ReadDir(destAppData)
if len(entries) > 0 {
@@ -192,18 +193,30 @@ func (dm *DriveMigrator) MigrateDrive(ctx context.Context, req DriveMigrateReque
}
}
// Estimate total size (exclude restic repos)
// Estimate total size (exclude restic repos inside felhom-data/backups/)
var totalBytes int64
entries, _ := os.ReadDir(req.SourcePath)
for _, entry := range entries {
if !entry.IsDir() {
continue
}
entryPath := filepath.Join(req.SourcePath, entry.Name())
if entry.IsDir() {
// Skip restic repos in size estimate
if entry.Name() == "backups" {
totalBytes += dirSizeExcluding(entryPath, "restic")
} else {
totalBytes += dirSize(entryPath)
if entry.Name() == backup.FelhomDataDir {
// Scan inside namespace dir, excluding restic repos from estimate
subEntries, _ := os.ReadDir(entryPath)
for _, sub := range subEntries {
if !sub.IsDir() {
continue
}
subPath := filepath.Join(entryPath, sub.Name())
if sub.Name() == "backups" {
totalBytes += dirSizeExcluding(subPath, "restic")
} else {
totalBytes += dirSize(subPath)
}
}
} else {
totalBytes += dirSize(entryPath)
}
}
@@ -252,8 +265,8 @@ func (dm *DriveMigrator) MigrateDrive(ctx context.Context, req DriveMigrateReque
send("copying", "Adatok másolása...", 10)
rsyncCmd := exec.CommandContext(ctx, "rsync", "-a", "--info=progress2",
"--exclude=backups/primary/restic/",
"--exclude=backups/secondary/restic/",
"--exclude=felhom-data/backups/primary/restic/",
"--exclude=felhom-data/backups/secondary/restic/",
req.SourcePath+"/", req.DestPath+"/",
)
@@ -322,11 +335,11 @@ func (dm *DriveMigrator) MigrateDrive(ctx context.Context, req DriveMigrateReque
send("verifying", "Másolat ellenőrzése...", 62)
for _, app := range appsToMigrate {
destAppData := filepath.Join(req.DestPath, "appdata", app.Name)
destAppData := backup.AppDataDir(req.DestPath, app.Name)
if _, err := os.Stat(destAppData); os.IsNotExist(err) {
// appdata might not exist for all apps (SSD-only apps that share the drive)
// Only warn, don't fail
dm.Logger.Printf("[WARN] Drive migration: %s/appdata/%s not found on destination (may be SSD-only)", req.DestPath, app.Name)
dm.Logger.Printf("[WARN] Drive migration: %s not found on destination (may be SSD-only)", destAppData)
}
}