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 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
|
package fsutil
import (
"io/ioutil"
"os"
"path/filepath"
"runtime"
"sort"
strings "strings"
"github.com/pkg/errors"
)
func FollowLinks(root string, paths []string) ([]string, error) {
r := &symlinkResolver{root: root, resolved: map[string]struct{}{}}
for _, p := range paths {
if err := r.append(p); err != nil {
return nil, err
}
}
res := make([]string, 0, len(r.resolved))
for r := range r.resolved {
res = append(res, r)
}
sort.Strings(res)
return dedupePaths(res), nil
}
type symlinkResolver struct {
root string
resolved map[string]struct{}
}
func (r *symlinkResolver) append(p string) error {
p = filepath.Join(".", p)
current := "."
for {
parts := strings.SplitN(p, string(filepath.Separator), 2)
current = filepath.Join(current, parts[0])
targets, err := r.readSymlink(current, true)
if err != nil {
return err
}
p = ""
if len(parts) == 2 {
p = parts[1]
}
if p == "" || targets != nil {
if _, ok := r.resolved[current]; ok {
return nil
}
}
if targets != nil {
r.resolved[current] = struct{}{}
for _, target := range targets {
if err := r.append(filepath.Join(target, p)); err != nil {
return err
}
}
return nil
}
if p == "" {
r.resolved[current] = struct{}{}
return nil
}
}
}
func (r *symlinkResolver) readSymlink(p string, allowWildcard bool) ([]string, error) {
realPath := filepath.Join(r.root, p)
base := filepath.Base(p)
if allowWildcard && containsWildcards(base) {
fis, err := ioutil.ReadDir(filepath.Dir(realPath))
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, errors.Wrapf(err, "failed to read dir %s", filepath.Dir(realPath))
}
var out []string
for _, f := range fis {
if ok, _ := filepath.Match(base, f.Name()); ok {
res, err := r.readSymlink(filepath.Join(filepath.Dir(p), f.Name()), false)
if err != nil {
return nil, err
}
out = append(out, res...)
}
}
return out, nil
}
fi, err := os.Lstat(realPath)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, errors.Wrapf(err, "failed to lstat %s", realPath)
}
if fi.Mode()&os.ModeSymlink == 0 {
return nil, nil
}
link, err := os.Readlink(realPath)
if err != nil {
return nil, errors.Wrapf(err, "failed to readlink %s", realPath)
}
link = filepath.Clean(link)
if filepath.IsAbs(link) {
return []string{link}, nil
}
return []string{
filepath.Join(string(filepath.Separator), filepath.Join(filepath.Dir(p), link)),
}, nil
}
func containsWildcards(name string) bool {
isWindows := runtime.GOOS == "windows"
for i := 0; i < len(name); i++ {
ch := name[i]
if ch == '\\' && !isWindows {
i++
} else if ch == '*' || ch == '?' || ch == '[' {
return true
}
}
return false
}
// dedupePaths expects input as a sorted list
func dedupePaths(in []string) []string {
out := make([]string, 0, len(in))
var last string
for _, s := range in {
// if one of the paths is root there is no filter
if s == "." {
return nil
}
if strings.HasPrefix(s, last+string(filepath.Separator)) {
continue
}
out = append(out, s)
last = s
}
return out
}
|