clonetool/spec.go
2026-09-05 23:02:29 +02:00

77 lines
2.1 KiB
Go

package main
import (
"fmt"
"path/filepath"
"strings"
)
// Spec is a parsed source/dest location: [user@]host:path, or a bare local
// path (Host == "").
type Spec struct {
Raw string
User string
Host string
Path string
}
func (s Spec) IsLocal() bool { return s.Host == "" }
func (s Spec) String() string {
if s.IsLocal() {
return s.Path
}
if s.User != "" {
return fmt.Sprintf("%s@%s:%s", s.User, s.Host, s.Path)
}
return fmt.Sprintf("%s:%s", s.Host, s.Path)
}
// parseSpec parses "[user@]host:path" or a local "path". A leading "/",
// "./" or "../", or the absence of any colon, is treated as a local path so
// that ordinary absolute/relative paths are never mistaken for a host spec.
func parseSpec(raw string) (Spec, error) {
if raw == "" {
return Spec{}, fmt.Errorf("empty location")
}
if strings.HasPrefix(raw, "/") || strings.HasPrefix(raw, "./") || strings.HasPrefix(raw, "../") || !strings.Contains(raw, ":") {
return Spec{Raw: raw, Path: raw}, nil
}
idx := strings.Index(raw, ":")
hostpart := raw[:idx]
path := raw[idx+1:]
if hostpart == "" || path == "" || strings.ContainsAny(hostpart, "/ ") {
return Spec{}, fmt.Errorf("cannot parse location %q (expected [user@]host:path or a local path)", raw)
}
user := ""
host := hostpart
if at := strings.Index(hostpart, "@"); at >= 0 {
user = hostpart[:at]
host = hostpart[at+1:]
}
if host == "" {
return Spec{}, fmt.Errorf("cannot parse location %q: empty host", raw)
}
return Spec{Raw: raw, User: user, Host: host, Path: path}, nil
}
// checkNotSame does a best-effort local check that source and dest don't
// refer to the exact same path, to avoid an obviously destructive mistake.
// It cannot resolve whether two different remote hostnames are actually the
// same machine.
func checkNotSame(src, dst Spec) error {
if src.IsLocal() != dst.IsLocal() {
return nil
}
if !src.IsLocal() && !strings.EqualFold(src.Host, dst.Host) {
return nil
}
if !src.IsLocal() && src.User != dst.User {
return nil
}
if filepath.Clean(src.Path) == filepath.Clean(dst.Path) {
return fmt.Errorf("source and destination resolve to the same path (%s)", src.Path)
}
return nil
}