Release 2026.09.27.r001: support array disk rsync backups to mounted USB disks
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user