1/*
2Copyright (c) for portions of walk.go are held by The Go Authors, 2009 and are
3provided under the BSD license.
4
5https://github.com/golang/go/blob/master/LICENSE
6
7Copyright The Helm Authors.
8Licensed under the Apache License, Version 2.0 (the "License");
9you may not use this file except in compliance with the License.
10You may obtain a copy of the License at
11
12http://www.apache.org/licenses/LICENSE-2.0
13
14Unless required by applicable law or agreed to in writing, software
15distributed under the License is distributed on an "AS IS" BASIS,
16WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17See the License for the specific language governing permissions and
18limitations under the License.
19*/
20
21package sympath
22
23import (
24	"log"
25	"os"
26	"path/filepath"
27	"sort"
28
29	"github.com/pkg/errors"
30)
31
32// Walk walks the file tree rooted at root, calling walkFn for each file or directory
33// in the tree, including root. All errors that arise visiting files and directories
34// are filtered by walkFn. The files are walked in lexical order, which makes the
35// output deterministic but means that for very large directories Walk can be
36// inefficient. Walk follows symbolic links.
37func Walk(root string, walkFn filepath.WalkFunc) error {
38	info, err := os.Lstat(root)
39	if err != nil {
40		err = walkFn(root, nil, err)
41	} else {
42		err = symwalk(root, info, walkFn)
43	}
44	if err == filepath.SkipDir {
45		return nil
46	}
47	return err
48}
49
50// readDirNames reads the directory named by dirname and returns
51// a sorted list of directory entries.
52func readDirNames(dirname string) ([]string, error) {
53	f, err := os.Open(dirname)
54	if err != nil {
55		return nil, err
56	}
57	names, err := f.Readdirnames(-1)
58	f.Close()
59	if err != nil {
60		return nil, err
61	}
62	sort.Strings(names)
63	return names, nil
64}
65
66// symwalk recursively descends path, calling walkFn.
67func symwalk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error {
68	// Recursively walk symlinked directories.
69	if IsSymlink(info) {
70		resolved, err := filepath.EvalSymlinks(path)
71		if err != nil {
72			return errors.Wrapf(err, "error evaluating symlink %s", path)
73		}
74		log.Printf("found symbolic link in path: %s resolves to %s", path, resolved)
75		if info, err = os.Lstat(resolved); err != nil {
76			return err
77		}
78		if err := symwalk(path, info, walkFn); err != nil && err != filepath.SkipDir {
79			return err
80		}
81		return nil
82	}
83
84	if err := walkFn(path, info, nil); err != nil {
85		return err
86	}
87
88	if !info.IsDir() {
89		return nil
90	}
91
92	names, err := readDirNames(path)
93	if err != nil {
94		return walkFn(path, info, err)
95	}
96
97	for _, name := range names {
98		filename := filepath.Join(path, name)
99		fileInfo, err := os.Lstat(filename)
100		if err != nil {
101			if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir {
102				return err
103			}
104		} else {
105			err = symwalk(filename, fileInfo, walkFn)
106			if err != nil {
107				if (!fileInfo.IsDir() && !IsSymlink(fileInfo)) || err != filepath.SkipDir {
108					return err
109				}
110			}
111		}
112	}
113	return nil
114}
115
116// IsSymlink is used to determine if the fileinfo is a symbolic link.
117func IsSymlink(fi os.FileInfo) bool {
118	return fi.Mode()&os.ModeSymlink != 0
119}
120