2018-06-08 21:07:42 -04:00
|
|
|
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 {
|
2020-11-17 05:31:07 -05:00
|
|
|
if errors.Is(err, os.ErrNotExist) {
|
2018-06-08 21:07:42 -04:00
|
|
|
return nil, nil
|
|
|
|
}
|
2020-11-17 05:31:07 -05:00
|
|
|
return nil, errors.Wrap(err, "readdir")
|
2018-06-08 21:07:42 -04:00
|
|
|
}
|
|
|
|
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 {
|
2020-11-17 05:31:07 -05:00
|
|
|
if errors.Is(err, os.ErrNotExist) {
|
2018-06-08 21:07:42 -04:00
|
|
|
return nil, nil
|
|
|
|
}
|
2020-11-17 05:31:07 -05:00
|
|
|
return nil, errors.WithStack(err)
|
2018-06-08 21:07:42 -04:00
|
|
|
}
|
|
|
|
if fi.Mode()&os.ModeSymlink == 0 {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
link, err := os.Readlink(realPath)
|
|
|
|
if err != nil {
|
2020-11-17 05:31:07 -05:00
|
|
|
return nil, errors.WithStack(err)
|
2018-06-08 21:07:42 -04:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|