| 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 |
| } |