package platform import ( "fmt" "os" "path/filepath" "regexp" "strings" "git.casaderoll.de/michael/urbm/internal/model" ) var arrayDiskName = regexp.MustCompile(`^disk[1-9][0-9]*$`) func isArrayDiskPath(path string) bool { parts := strings.Split(strings.TrimPrefix(filepath.Clean(path), "/"), "/") return len(parts) >= 2 && parts[0] == "mnt" && arrayDiskName.MatchString(parts[1]) } func mountedPaths() ([]string, error) { data, err := os.ReadFile("/proc/self/mountinfo") if err != nil { return nil, err } return parseMountPaths(string(data)), nil } func parseMountPaths(data string) []string { var paths []string unescape := strings.NewReplacer(`\040`, " ", `\011`, "\t", `\012`, "\n", `\134`, `\`) for _, line := range strings.Split(data, "\n") { fields := strings.Fields(line) if len(fields) >= 10 { paths = append(paths, unescape.Replace(fields[4])) } } return paths } func storageBrowseRoots(mounts []string) []string { roots := append([]string{}, browseRoots...) seen := make(map[string]bool) for _, mount := range mounts { if filepath.Dir(mount) == "/mnt" && isArrayDiskPath(mount) && !seen[mount] { roots = append(roots, mount) seen[mount] = true } } return roots } func pathUnder(path, root string) bool { return path == root || strings.HasPrefix(path, root+string(os.PathSeparator)) } // Require an actual mount for removable disks and array sources. An empty // directory left behind by an unmount must never become a backup destination. func validateRsyncMounts(job model.Job, mounts []string) error { paths := []string{job.Rsync.Target} for _, source := range job.Sources { paths = append(paths, source.Path) } for _, path := range paths { clean := filepath.Clean(path) if !isArrayDiskPath(clean) && !pathUnder(clean, "/mnt/disks") { continue } resolved, err := filepath.EvalSymlinks(clean) if err != nil { return fmt.Errorf("environment: rsync disk unavailable: %s: %w", clean, err) } if !diskPathMounted(resolved, mounts) { return fmt.Errorf("environment: rsync disk is not mounted: %s", clean) } } return nil } func diskPathMounted(resolved string, mounts []string) bool { for _, mount := range mounts { if ((isArrayDiskPath(mount) && filepath.Dir(mount) == "/mnt") || strings.HasPrefix(mount, "/mnt/disks/")) && pathUnder(resolved, mount) { return true } } return false }