package sftp import ( "net/url" "path" "strings" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/options" ) // Config collects all information required to connect to an sftp server. type Config struct { User, Host, Port, Path string Layout string `option:"layout" help:"use this backend directory layout (default: auto-detect) (deprecated)"` Command string `option:"command" help:"specify command to create sftp connection"` Args string `option:"args" help:"specify arguments for ssh"` Connections uint `option:"connections" help:"set a limit for the number of concurrent connections (default: 5)"` } // NewConfig returns a new config with default options applied. func NewConfig() Config { return Config{ Connections: 5, } } func init() { options.Register("sftp", Config{}) } // ParseConfig parses the string s and extracts the sftp config. The // supported configuration formats are sftp://user@host[:port]/directory // and sftp:user@host:directory. The directory will be path Cleaned and can // be an absolute path if it starts with a '/' (e.g. // sftp://user@host//absolute and sftp:user@host:/absolute). func ParseConfig(s string) (*Config, error) { var user, host, port, dir string switch { case strings.HasPrefix(s, "sftp://"): // parse the "sftp://user@host/path" url format url, err := url.Parse(s) if err != nil { return nil, errors.WithStack(err) } if url.User != nil { user = url.User.Username() } host = url.Hostname() port = url.Port() dir = url.Path if dir == "" { return nil, errors.Errorf("invalid backend %q, no directory specified", s) } dir = dir[1:] case strings.HasPrefix(s, "sftp:"): // parse the sftp:user@host:path format, which means we'll get // "user@host:path" in s s = s[5:] // split user@host and path at the colon var colon bool host, dir, colon = strings.Cut(s, ":") if !colon { return nil, errors.New("sftp: invalid format, hostname or path not found") } // split user and host at the "@" data := strings.SplitN(host, "@", 3) if len(data) == 3 { user = data[0] + "@" + data[1] host = data[2] } else if len(data) == 2 { user = data[0] host = data[1] } default: return nil, errors.New(`invalid format, does not start with "sftp:"`) } p := path.Clean(dir) if strings.HasPrefix(p, "~") { return nil, errors.New("sftp path starts with the tilde (~) character, that fails for most sftp servers.\nUse a relative directory, most servers interpret this as relative to the user's home directory") } cfg := NewConfig() cfg.User = user cfg.Host = host cfg.Port = port cfg.Path = p return &cfg, nil }