v0.12.5: drive-type-aware cross-drive backup validation
Fix cross-drive backup failing for system-drive destinations. The /mnt/hdd_placeholder folder is on the internal SSD, so IsMountPoint() returned false and the old code hard-blocked it. - ValidateDestination() (crossdrive.go): onSystemDrive flag replaces boolean-only check; system drives require ≥10 GB free and <90% usage to protect OS stability; external drives just need ≥100 MB free. - CheckBackupDestination() (mounts_linux.go): Tier 4 now branches on h.SystemDrive; system drive blocks at <10 GB free or ≥90% usage (matches runner enforcement); external drive unchanged (warn 90%, block 95%). Removes the && h.Severity == "ok" guard that was preventing system-drive critical messages from overriding warnings. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,24 @@
|
||||
## Changelog
|
||||
|
||||
### What was just completed (2026-02-18 session 41)
|
||||
- **v0.12.5 — Cross-Drive Backup Validation Fix:**
|
||||
|
||||
**Root cause:** Immich cross-drive backup failed with `destination /mnt/hdd_placeholder is not a mount point` because `ValidateDestination()` hard-blocked non-mount-point destinations. The `/mnt/hdd_placeholder` folder is on the internal SSD (not a separate mount), so the device-ID check returned false.
|
||||
|
||||
**Fix 1: Drive-type-aware space checks in `ValidateDestination` (`internal/backup/crossdrive.go`)**
|
||||
- `onSystemDrive` flag replaces the previous boolean-only mount-point check
|
||||
- System-drive destinations: require **≥10 GB free** and **<90% usage** to protect OS stability
|
||||
- External-drive destinations: require **≥100 MB free** (original threshold)
|
||||
- Updated function comment to reflect the new tiered logic
|
||||
|
||||
**Fix 2: Aligned `CheckBackupDestination` UI thresholds for system drives (`internal/system/mounts_linux.go`)**
|
||||
- Tier 4 disk checks now branch on `h.SystemDrive` flag (set in Tier 3)
|
||||
- System drive: block at <10 GB free OR ≥90% used (matches runner enforcement); Hungarian warning messages
|
||||
- External drive: warn at ≥90% used, block at ≥95% used (unchanged)
|
||||
- Removed the `&& h.Severity == "ok"` guard that prevented system-drive warnings from being overridden properly
|
||||
|
||||
**Files modified (2):** `internal/backup/crossdrive.go`, `internal/system/mounts_linux.go`
|
||||
|
||||
### What was just completed (2026-02-18 session 40)
|
||||
- **v0.12.4 — Correctness & Robustness Bug Fixes (TASK.md — 15 bugs fixed):**
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
A single, lightweight Go container that replaces Portainer + scattered systemd scripts with a unified, Hungarian-language web dashboard for managing Docker Compose stacks, backups, storage, monitoring, and notifications on customer hardware.
|
||||
|
||||
**Current version: v0.12.2**
|
||||
**Current version: v0.12.5**
|
||||
|
||||
---
|
||||
|
||||
@@ -192,7 +192,11 @@ Implements the 3-2-1 backup rule by copying data to a different physical drive.
|
||||
- **rsync** — Simple mirror with `--delete` (fast, no versioning)
|
||||
- **restic** — Versioned, deduplicated, encrypted (shared repo across apps)
|
||||
- Per-app configuration: destination path, method, schedule (daily/weekly/manual)
|
||||
- Safety guards: destination != source, mount point check, writable check
|
||||
- **Drive-type-aware validation** (`ValidateDestination` / `CheckBackupDestination`):
|
||||
- External mount: block if <100 MB free; warn/block at 90%/95% usage
|
||||
- System drive (same block device as `/`): require ≥10 GB free AND <90% usage to protect OS stability; allowed with a logged warning (no hard block for non-mount-point destinations)
|
||||
- Web UI `CheckBackupDestination` matches runner thresholds — no surprise divergence between UI and actual enforcement
|
||||
- Safety guards: destination ≠ source, path-overlap check, writable check
|
||||
- **Chained execution**: cross-drive runs immediately after nightly restic backup (daily apps every night, weekly apps on Sundays) for DB/file consistency
|
||||
- Per-app concurrency lock prevents overlapping runs
|
||||
- Status tracking (last_run, duration, size, error) persisted to settings.json
|
||||
|
||||
@@ -159,9 +159,9 @@ func (r *CrossDriveRunner) IsRunning(stackName string) bool {
|
||||
}
|
||||
|
||||
// ValidateDestination checks that the destination path exists, is writable,
|
||||
// and has sufficient free space (at least 100MB).
|
||||
// A non-mount-point destination (e.g. a folder on the system drive) is allowed
|
||||
// with a logged warning — consistent with the web UI's CheckBackupDestination.
|
||||
// and has sufficient free space. System-drive destinations get stricter limits
|
||||
// (≥10 GB free, <90% used) to protect OS stability; external drives just need
|
||||
// ≥100 MB. Non-mount-point destinations are allowed with a logged warning.
|
||||
func (r *CrossDriveRunner) ValidateDestination(path string) error {
|
||||
if path == "" {
|
||||
return fmt.Errorf("destination path is empty")
|
||||
@@ -169,15 +169,28 @@ func (r *CrossDriveRunner) ValidateDestination(path string) error {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return fmt.Errorf("destination %s does not exist", path)
|
||||
}
|
||||
if !system.IsMountPoint(path) {
|
||||
onSystemDrive := !system.IsMountPoint(path)
|
||||
if onSystemDrive {
|
||||
r.logger.Printf("[WARN] Destination %s is not a separate mount point (system drive) — backup will proceed but data is not protected against drive failure", path)
|
||||
}
|
||||
if !system.IsWritable(path) {
|
||||
return fmt.Errorf("destination %s is not writable", path)
|
||||
}
|
||||
di := system.GetDiskUsage(path)
|
||||
if di != nil && di.AvailGB < 0.1 {
|
||||
return fmt.Errorf("destination %s has insufficient free space (%.1f GB)", path, di.AvailGB)
|
||||
if di := system.GetDiskUsage(path); di != nil {
|
||||
if onSystemDrive {
|
||||
// System drive: protect OS stability — require ≥10 GB free and <90% used
|
||||
if di.AvailGB < 10 {
|
||||
return fmt.Errorf("destination %s is on the system drive with only %.1f GB free — at least 10 GB required to protect OS stability", path, di.AvailGB)
|
||||
}
|
||||
if di.UsedPercent >= 90 {
|
||||
return fmt.Errorf("destination %s is on the system drive at %.0f%% capacity — maximum 90%% allowed", path, di.UsedPercent)
|
||||
}
|
||||
} else {
|
||||
// External drive: just ensure it's not completely full
|
||||
if di.AvailGB < 0.1 {
|
||||
return fmt.Errorf("destination %s has insufficient free space (%.1f GB free)", path, di.AvailGB)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -172,13 +172,28 @@ func CheckBackupDestination(path string) DestinationHealth {
|
||||
if di := GetDiskUsage(path); di != nil {
|
||||
h.UsedPercent = di.UsedPercent
|
||||
h.FreeGB = di.AvailGB
|
||||
if di.UsedPercent >= 95 {
|
||||
h.Warning = fmt.Sprintf("A mentési meghajtó megtelt (%.0f%% használt)!", di.UsedPercent)
|
||||
h.Blocked = true
|
||||
h.Severity = "critical"
|
||||
} else if di.UsedPercent >= 90 && h.Severity == "ok" {
|
||||
h.Warning = fmt.Sprintf("A mentési meghajtó majdnem megtelt (%.0f%% használt).", di.UsedPercent)
|
||||
h.Severity = "warning"
|
||||
if h.SystemDrive {
|
||||
// System drive: stricter limits to protect OS stability
|
||||
if di.AvailGB < 10 {
|
||||
h.Warning = fmt.Sprintf("A rendszermeghajtón csak %.1f GB szabad — legalább 10 GB szükséges a rendszer stabilitásához!", di.AvailGB)
|
||||
h.Blocked = true
|
||||
h.Severity = "critical"
|
||||
} else if di.UsedPercent >= 90 {
|
||||
h.Warning = fmt.Sprintf("A rendszermeghajtó %.0f%%-ban megtelt — maximum 90%% megengedett.", di.UsedPercent)
|
||||
h.Blocked = true
|
||||
h.Severity = "critical"
|
||||
}
|
||||
// If neither triggers, keep the Tier 3 system-drive warning
|
||||
} else {
|
||||
// External drive: original thresholds
|
||||
if di.UsedPercent >= 95 {
|
||||
h.Warning = fmt.Sprintf("A mentési meghajtó megtelt (%.0f%% használt)!", di.UsedPercent)
|
||||
h.Blocked = true
|
||||
h.Severity = "critical"
|
||||
} else if di.UsedPercent >= 90 {
|
||||
h.Warning = fmt.Sprintf("A mentési meghajtó majdnem megtelt (%.0f%% használt).", di.UsedPercent)
|
||||
h.Severity = "warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user