summaryrefslogtreecommitdiff
path: root/internal/backend/sftp/config.go
blob: 76d6d145df352ca46695ca42eaf7fa64f5a5e473 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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)"`
	Command string `option:"command" help:"specify command to create sftp connection"`

	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) (interface{}, 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.Fatal("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
}