1package fileutils
2
3import (
4	"errors"
5	"fmt"
6	"io"
7	"io/ioutil"
8	"os"
9	"path/filepath"
10	"strings"
11
12	"github.com/fsouza/go-dockerclient/external/github.com/Sirupsen/logrus"
13)
14
15// exclusion return true if the specified pattern is an exclusion
16func exclusion(pattern string) bool {
17	return pattern[0] == '!'
18}
19
20// empty return true if the specified pattern is empty
21func empty(pattern string) bool {
22	return pattern == ""
23}
24
25// CleanPatterns takes a slice of patterns returns a new
26// slice of patterns cleaned with filepath.Clean, stripped
27// of any empty patterns and lets the caller know whether the
28// slice contains any exception patterns (prefixed with !).
29func CleanPatterns(patterns []string) ([]string, [][]string, bool, error) {
30	// Loop over exclusion patterns and:
31	// 1. Clean them up.
32	// 2. Indicate whether we are dealing with any exception rules.
33	// 3. Error if we see a single exclusion marker on it's own (!).
34	cleanedPatterns := []string{}
35	patternDirs := [][]string{}
36	exceptions := false
37	for _, pattern := range patterns {
38		// Eliminate leading and trailing whitespace.
39		pattern = strings.TrimSpace(pattern)
40		if empty(pattern) {
41			continue
42		}
43		if exclusion(pattern) {
44			if len(pattern) == 1 {
45				return nil, nil, false, errors.New("Illegal exclusion pattern: !")
46			}
47			exceptions = true
48		}
49		pattern = filepath.Clean(pattern)
50		cleanedPatterns = append(cleanedPatterns, pattern)
51		if exclusion(pattern) {
52			pattern = pattern[1:]
53		}
54		patternDirs = append(patternDirs, strings.Split(pattern, "/"))
55	}
56
57	return cleanedPatterns, patternDirs, exceptions, nil
58}
59
60// Matches returns true if file matches any of the patterns
61// and isn't excluded by any of the subsequent patterns.
62func Matches(file string, patterns []string) (bool, error) {
63	file = filepath.Clean(file)
64
65	if file == "." {
66		// Don't let them exclude everything, kind of silly.
67		return false, nil
68	}
69
70	patterns, patDirs, _, err := CleanPatterns(patterns)
71	if err != nil {
72		return false, err
73	}
74
75	return OptimizedMatches(file, patterns, patDirs)
76}
77
78// OptimizedMatches is basically the same as fileutils.Matches() but optimized for archive.go.
79// It will assume that the inputs have been preprocessed and therefore the function
80// doen't need to do as much error checking and clean-up. This was done to avoid
81// repeating these steps on each file being checked during the archive process.
82// The more generic fileutils.Matches() can't make these assumptions.
83func OptimizedMatches(file string, patterns []string, patDirs [][]string) (bool, error) {
84	matched := false
85	parentPath := filepath.Dir(file)
86	parentPathDirs := strings.Split(parentPath, "/")
87
88	for i, pattern := range patterns {
89		negative := false
90
91		if exclusion(pattern) {
92			negative = true
93			pattern = pattern[1:]
94		}
95
96		match, err := filepath.Match(pattern, file)
97		if err != nil {
98			return false, err
99		}
100
101		if !match && parentPath != "." {
102			// Check to see if the pattern matches one of our parent dirs.
103			if len(patDirs[i]) <= len(parentPathDirs) {
104				match, _ = filepath.Match(strings.Join(patDirs[i], "/"),
105					strings.Join(parentPathDirs[:len(patDirs[i])], "/"))
106			}
107		}
108
109		if match {
110			matched = !negative
111		}
112	}
113
114	if matched {
115		logrus.Debugf("Skipping excluded path: %s", file)
116	}
117
118	return matched, nil
119}
120
121// CopyFile copies from src to dst until either EOF is reached
122// on src or an error occurs. It verifies src exists and remove
123// the dst if it exists.
124func CopyFile(src, dst string) (int64, error) {
125	cleanSrc := filepath.Clean(src)
126	cleanDst := filepath.Clean(dst)
127	if cleanSrc == cleanDst {
128		return 0, nil
129	}
130	sf, err := os.Open(cleanSrc)
131	if err != nil {
132		return 0, err
133	}
134	defer sf.Close()
135	if err := os.Remove(cleanDst); err != nil && !os.IsNotExist(err) {
136		return 0, err
137	}
138	df, err := os.Create(cleanDst)
139	if err != nil {
140		return 0, err
141	}
142	defer df.Close()
143	return io.Copy(df, sf)
144}
145
146// GetTotalUsedFds Returns the number of used File Descriptors by
147// reading it via /proc filesystem.
148func GetTotalUsedFds() int {
149	if fds, err := ioutil.ReadDir(fmt.Sprintf("/proc/%d/fd", os.Getpid())); err != nil {
150		logrus.Errorf("Error opening /proc/%d/fd: %s", os.Getpid(), err)
151	} else {
152		return len(fds)
153	}
154	return -1
155}
156
157// ReadSymlinkedDirectory returns the target directory of a symlink.
158// The target of the symbolic link may not be a file.
159func ReadSymlinkedDirectory(path string) (string, error) {
160	var realPath string
161	var err error
162	if realPath, err = filepath.Abs(path); err != nil {
163		return "", fmt.Errorf("unable to get absolute path for %s: %s", path, err)
164	}
165	if realPath, err = filepath.EvalSymlinks(realPath); err != nil {
166		return "", fmt.Errorf("failed to canonicalise path for %s: %s", path, err)
167	}
168	realPathInfo, err := os.Stat(realPath)
169	if err != nil {
170		return "", fmt.Errorf("failed to stat target '%s' of '%s': %s", realPath, path, err)
171	}
172	if !realPathInfo.Mode().IsDir() {
173		return "", fmt.Errorf("canonical path points to a file '%s'", realPath)
174	}
175	return realPath, nil
176}
177
178// CreateIfNotExists creates a file or a directory only if it does not already exist.
179func CreateIfNotExists(path string, isDir bool) error {
180	if _, err := os.Stat(path); err != nil {
181		if os.IsNotExist(err) {
182			if isDir {
183				return os.MkdirAll(path, 0755)
184			}
185			if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
186				return err
187			}
188			f, err := os.OpenFile(path, os.O_CREATE, 0755)
189			if err != nil {
190				return err
191			}
192			f.Close()
193		}
194	}
195	return nil
196}
197