107 lines
2.7 KiB
Go
107 lines
2.7 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"satoru/internal/store"
|
|
"satoru/internal/webui"
|
|
)
|
|
|
|
var usernamePattern = regexp.MustCompile(`^[a-z0-9._-]{3,32}$`)
|
|
|
|
func normalizeUsername(v string) string {
|
|
return strings.ToLower(strings.TrimSpace(v))
|
|
}
|
|
|
|
func validUsername(v string) bool {
|
|
return usernamePattern.MatchString(v)
|
|
}
|
|
|
|
func defaultWorkflowStages() []webui.WorkflowStage {
|
|
return []webui.WorkflowStage{
|
|
{Title: "Pull from Edge over SSH", Description: "Satoru connects to Linux edge hosts using local keys and pulls approved paths."},
|
|
{Title: "Stage on Backup Server", Description: "Pulled data lands on the backup host first, keeping edge systems isolated from B2."},
|
|
{Title: "Restic to B2", Description: "Restic runs centrally on this Satoru instance and uploads snapshots to Backblaze B2."},
|
|
{Title: "Audit and Recover", Description: "Each site records run output/status for operational visibility before full job history is added."},
|
|
}
|
|
}
|
|
|
|
func parsePathList(raw string) []string {
|
|
split := strings.FieldsFunc(raw, func(r rune) bool {
|
|
return r == '\n' || r == ',' || r == ';'
|
|
})
|
|
out := make([]string, 0, len(split))
|
|
for _, item := range split {
|
|
item = strings.TrimSpace(item)
|
|
if item == "" {
|
|
continue
|
|
}
|
|
out = append(out, item)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func parseLineList(raw string) []string {
|
|
split := strings.Split(raw, "\n")
|
|
seen := map[string]struct{}{}
|
|
out := make([]string, 0, len(split))
|
|
for _, item := range split {
|
|
item = strings.TrimSpace(item)
|
|
if item == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[item]; ok {
|
|
continue
|
|
}
|
|
seen[item] = struct{}{}
|
|
out = append(out, item)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func buildTargets(directoryPaths, sqlitePaths []string) []store.SiteTarget {
|
|
seen := map[string]struct{}{}
|
|
out := make([]store.SiteTarget, 0, len(directoryPaths)+len(sqlitePaths))
|
|
|
|
for _, p := range directoryPaths {
|
|
addTarget(&out, seen, store.SiteTarget{Path: p, Mode: "directory"})
|
|
}
|
|
for _, p := range sqlitePaths {
|
|
addTarget(&out, seen, store.SiteTarget{Path: p, Mode: "sqlite_dump"})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func addTarget(out *[]store.SiteTarget, seen map[string]struct{}, t store.SiteTarget) {
|
|
key := t.Mode + "\x00" + t.Path
|
|
if _, ok := seen[key]; ok {
|
|
return
|
|
}
|
|
seen[key] = struct{}{}
|
|
*out = append(*out, t)
|
|
}
|
|
|
|
func targetsAreValid(targets []store.SiteTarget) bool {
|
|
for _, t := range targets {
|
|
if t.Path == "" || !strings.HasPrefix(t.Path, "/") {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func parsePort(raw string) (int, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return 22, nil
|
|
}
|
|
port, err := strconv.Atoi(raw)
|
|
if err != nil || port < 1 || port > 65535 {
|
|
return 0, errors.New("invalid port")
|
|
}
|
|
return port, nil
|
|
}
|