summaryrefslogtreecommitdiff
path: root/internal/fsutil/path.go
blob: c872a5b6e05246573280cda4eeaaba98a1ad6d1d (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
94
95
96
97
98
99
package fsutil

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
	"os/user"
	"path/filepath"
	"strings"
)

// ResolveRootPath expands and validates the supplied root path. When the
// caller did not specify a value, defaultValue is used and created on demand.
func ResolveRootPath(input, defaultValue string) (string, error) {
	value, isDefault := normalizeRootInput(input, defaultValue)
	expanded, err := expandPath(value)
	if err != nil {
		return "", fmt.Errorf("cannot expand root path %q: %w", value, err)
	}
	abs, err := filepath.Abs(expanded)
	if err != nil {
		return "", fmt.Errorf("cannot resolve root path %q: %w", expanded, err)
	}
	info, err := ensureRootExists(abs, isDefault)
	if err != nil {
		return "", err
	}
	if !info.IsDir() && !info.Mode().IsRegular() {
		return "", fmt.Errorf("root path %q is not a file or directory", abs)
	}
	return abs, nil
}

func normalizeRootInput(input, defaultValue string) (value string, isDefault bool) {
	trimmed := strings.TrimSpace(input)
	if trimmed == "" {
		return defaultValue, true
	}
	return trimmed, false
}

func ensureRootExists(path string, allowCreate bool) (fs.FileInfo, error) {
	info, err := os.Stat(path)
	if err == nil {
		return info, nil
	}
	if !errors.Is(err, fs.ErrNotExist) {
		return nil, fmt.Errorf("cannot access root path %q: %w", path, err)
	}
	if !allowCreate {
		return nil, fmt.Errorf("root path does not exist: %s", path)
	}
	if mkErr := os.MkdirAll(path, 0o755); mkErr != nil {
		return nil, fmt.Errorf("cannot create default directory %q: %w", path, mkErr)
	}
	info, err = os.Stat(path)
	if err != nil {
		return nil, fmt.Errorf("cannot stat default directory %q: %w", path, err)
	}
	return info, nil
}

func expandPath(p string) (string, error) {
	if p == "" || p[0] != '~' {
		return p, nil
	}
	if len(p) == 1 {
		home, err := os.UserHomeDir()
		if err != nil {
			return "", err
		}
		return home, nil
	}
	if p[1] == '/' {
		home, err := os.UserHomeDir()
		if err != nil {
			return "", err
		}
		return filepath.Join(home, p[2:]), nil
	}
	username, rest := splitUserPath(p)
	usr, err := user.Lookup(username)
	if err != nil {
		return "", err
	}
	if rest == "" {
		return usr.HomeDir, nil
	}
	return filepath.Join(usr.HomeDir, rest), nil
}

func splitUserPath(p string) (string, string) {
	sep := strings.IndexRune(p, '/')
	if sep == -1 {
		return p[1:], ""
	}
	return p[1:sep], p[sep:]
}