package backup import ( "errors" "fmt" "io/fs" "path/filepath" "strings" "gitea.dooplex.hu/admin/felhom-controller/internal/settings" "gitea.dooplex.hu/admin/felhom-controller/internal/system" "gitea.dooplex.hu/admin/felhom-controller/internal/util" ) // restoreDirError (R-362) turns a failure to create a restore's directory into the truth when the // cause is a drive that went away. Measured 2026-08-21: the guest-visible bind was unmounted 4 s into // a scratch restore and the household read „restore dir: mkdir /mnt/felhom-drives/hdd_1/backups: // permission denied" — a correct refusal that sent the reader after a permissions problem that did // not exist. The settings flag can lag a detach by seconds, so the drive is also checked directly: // a drive under the managed parent that is no longer a mount point is gone, whatever the flag says. // Any other failure keeps its original wording. Pinned by TestR362_DetachedDriveIsNamed. func (m *Manager) restoreDirError(dir string, err error) error { if err == nil { return nil } if !errors.Is(err, fs.ErrPermission) && !errors.Is(err, fs.ErrNotExist) { return fmt.Errorf("restore dir: %w", err) } sp, ok := m.registeredDriveHolding(dir) if !ok || !m.driveGone(sp) { return fmt.Errorf("restore dir: %w", err) } name := sp.Label if strings.TrimSpace(name) == "" { name = sp.Path } m.logger.Printf("[WARN] [backup] restore dir %s: %v — the drive %s (%s) is not connected; reported as a missing drive (R-362)", dir, err, sp.Path, name) return util.MsgError("err.backup.restore_drive_gone", name) } // registeredDriveHolding returns the registered storage path that contains dir (longest match). func (m *Manager) registeredDriveHolding(dir string) (settings.StoragePath, bool) { if m == nil || m.settings == nil { return settings.StoragePath{}, false } clean := filepath.Clean(dir) var best settings.StoragePath found := false for _, sp := range m.settings.GetStoragePaths() { root := filepath.Clean(sp.Path) if root == "" || root == "/" { continue } if clean == root || strings.HasPrefix(clean, root+string(filepath.Separator)) { if !found || len(root) > len(filepath.Clean(best.Path)) { best, found = sp, true } } } return best, found } // driveGone: the registry already says disconnected, or the drive lives under the managed parent // (where every enrolled drive is a bind mount) and is no longer a mount point. func (m *Manager) driveGone(sp settings.StoragePath) bool { if sp.Disconnected { return true } parent := filepath.Clean(settings.NetworkMountRoot) + string(filepath.Separator) if !strings.HasPrefix(filepath.Clean(sp.Path)+string(filepath.Separator), parent) { return false // a legacy path outside the managed parent: only the flag can say it is gone } mounted := system.IsMountPoint if m.driveMountedFn != nil { mounted = m.driveMountedFn } return !mounted(sp.Path) }