github.com/advanderveer/restic@v0.8.1-0.20171209104529-42a8c19aaea6/cmd/restic/exclude.go (about) 1 package main 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "strings" 10 "sync" 11 12 "github.com/restic/restic/internal/debug" 13 "github.com/restic/restic/internal/errors" 14 "github.com/restic/restic/internal/filter" 15 "github.com/restic/restic/internal/fs" 16 "github.com/restic/restic/internal/repository" 17 ) 18 19 type rejectionCache struct { 20 m map[string]bool 21 mtx sync.Mutex 22 } 23 24 // Lock locks the mutex in rc. 25 func (rc *rejectionCache) Lock() { 26 if rc != nil { 27 rc.mtx.Lock() 28 } 29 } 30 31 // Unlock unlocks the mutex in rc. 32 func (rc *rejectionCache) Unlock() { 33 if rc != nil { 34 rc.mtx.Unlock() 35 } 36 } 37 38 // Get returns the last stored value for dir and a second boolean that 39 // indicates whether that value was actually written to the cache. It is the 40 // callers responsibility to call rc.Lock and rc.Unlock before using this 41 // method, otherwise data races may occur. 42 func (rc *rejectionCache) Get(dir string) (bool, bool) { 43 if rc == nil || rc.m == nil { 44 return false, false 45 } 46 v, ok := rc.m[dir] 47 return v, ok 48 } 49 50 // Store stores a new value for dir. It is the callers responsibility to call 51 // rc.Lock and rc.Unlock before using this method, otherwise data races may 52 // occur. 53 func (rc *rejectionCache) Store(dir string, rejected bool) { 54 if rc == nil { 55 return 56 } 57 if rc.m == nil { 58 rc.m = make(map[string]bool) 59 } 60 rc.m[dir] = rejected 61 } 62 63 // RejectFunc is a function that takes a filename and os.FileInfo of a 64 // file that would be included in the backup. The function returns true if it 65 // should be excluded (rejected) from the backup. 66 type RejectFunc func(path string, fi os.FileInfo) bool 67 68 // rejectByPattern returns a RejectFunc which rejects files that match 69 // one of the patterns. 70 func rejectByPattern(patterns []string) RejectFunc { 71 return func(item string, fi os.FileInfo) bool { 72 matched, _, err := filter.List(patterns, item) 73 if err != nil { 74 Warnf("error for exclude pattern: %v", err) 75 } 76 77 if matched { 78 debug.Log("path %q excluded by an exclude pattern", item) 79 return true 80 } 81 82 return false 83 } 84 } 85 86 // rejectIfPresent returns a RejectFunc which itself returns whether a path 87 // should be excluded. The RejectFunc considers a file to be excluded when 88 // it resides in a directory with an exclusion file, that is specified by 89 // excludeFileSpec in the form "filename[:content]". The returned error is 90 // non-nil if the filename component of excludeFileSpec is empty. If rc is 91 // non-nil, it is going to be used in the RejectFunc to expedite the evaluation 92 // of a directory based on previous visits. 93 func rejectIfPresent(excludeFileSpec string) (RejectFunc, error) { 94 if excludeFileSpec == "" { 95 return nil, errors.New("name for exclusion tagfile is empty") 96 } 97 colon := strings.Index(excludeFileSpec, ":") 98 if colon == 0 { 99 return nil, fmt.Errorf("no name for exclusion tagfile provided") 100 } 101 tf, tc := "", "" 102 if colon > 0 { 103 tf = excludeFileSpec[:colon] 104 tc = excludeFileSpec[colon+1:] 105 } else { 106 tf = excludeFileSpec 107 } 108 debug.Log("using %q as exclusion tagfile", tf) 109 rc := &rejectionCache{} 110 fn := func(filename string, _ os.FileInfo) bool { 111 return isExcludedByFile(filename, tf, tc, rc) 112 } 113 return fn, nil 114 } 115 116 // isExcludedByFile interprets filename as a path and returns true if that file 117 // is in a excluded directory. A directory is identified as excluded if it contains a 118 // tagfile which bears the name specified in tagFilename and starts with 119 // header. If rc is non-nil, it is used to expedite the evaluation of a 120 // directory based on previous visits. 121 func isExcludedByFile(filename, tagFilename, header string, rc *rejectionCache) bool { 122 if tagFilename == "" { 123 return false 124 } 125 dir, base := filepath.Split(filename) 126 if base == tagFilename { 127 return false // do not exclude the tagfile itself 128 } 129 rc.Lock() 130 defer rc.Unlock() 131 132 rejected, visited := rc.Get(dir) 133 if visited { 134 return rejected 135 } 136 rejected = isDirExcludedByFile(dir, tagFilename, header) 137 rc.Store(dir, rejected) 138 return rejected 139 } 140 141 func isDirExcludedByFile(dir, tagFilename, header string) bool { 142 tf := filepath.Join(dir, tagFilename) 143 _, err := fs.Lstat(tf) 144 if os.IsNotExist(err) { 145 return false 146 } 147 if err != nil { 148 Warnf("could not access exclusion tagfile: %v", err) 149 return false 150 } 151 // when no signature is given, the mere presence of tf is enough reason 152 // to exclude filename 153 if len(header) == 0 { 154 return true 155 } 156 // From this stage, errors mean tagFilename exists but it is malformed. 157 // Warnings will be generated so that the user is informed that the 158 // indented ignore-action is not performed. 159 f, err := os.Open(tf) 160 if err != nil { 161 Warnf("could not open exclusion tagfile: %v", err) 162 return false 163 } 164 defer f.Close() 165 buf := make([]byte, len(header)) 166 _, err = io.ReadFull(f, buf) 167 // EOF is handled with a dedicated message, otherwise the warning were too cryptic 168 if err == io.EOF { 169 Warnf("invalid (too short) signature in exclusion tagfile %q\n", tf) 170 return false 171 } 172 if err != nil { 173 Warnf("could not read signature from exclusion tagfile %q: %v\n", tf, err) 174 return false 175 } 176 if bytes.Compare(buf, []byte(header)) != 0 { 177 Warnf("invalid signature in exclusion tagfile %q\n", tf) 178 return false 179 } 180 return true 181 } 182 183 // gatherDevices returns the set of unique device ids of the files and/or 184 // directory paths listed in "items". 185 func gatherDevices(items []string) (deviceMap map[string]uint64, err error) { 186 deviceMap = make(map[string]uint64) 187 for _, item := range items { 188 fi, err := fs.Lstat(item) 189 if err != nil { 190 return nil, err 191 } 192 id, err := fs.DeviceID(fi) 193 if err != nil { 194 return nil, err 195 } 196 deviceMap[item] = id 197 } 198 if len(deviceMap) == 0 { 199 return nil, errors.New("zero allowed devices") 200 } 201 return deviceMap, nil 202 } 203 204 // rejectByDevice returns a RejectFunc that rejects files which are on a 205 // different file systems than the files/dirs in samples. 206 func rejectByDevice(samples []string) (RejectFunc, error) { 207 allowed, err := gatherDevices(samples) 208 if err != nil { 209 return nil, err 210 } 211 debug.Log("allowed devices: %v\n", allowed) 212 213 return func(item string, fi os.FileInfo) bool { 214 if fi == nil { 215 return false 216 } 217 218 id, err := fs.DeviceID(fi) 219 if err != nil { 220 // This should never happen because gatherDevices() would have 221 // errored out earlier. If it still does that's a reason to panic. 222 panic(err) 223 } 224 225 for dir := item; dir != ""; dir = filepath.Dir(dir) { 226 debug.Log("item %v, test dir %v", item, dir) 227 228 allowedID, ok := allowed[dir] 229 if !ok { 230 continue 231 } 232 233 if allowedID != id { 234 debug.Log("path %q on disallowed device %d", item, id) 235 return true 236 } 237 238 return false 239 } 240 241 panic(fmt.Sprintf("item %v, device id %v not found, allowedDevs: %v", item, id, allowed)) 242 }, nil 243 } 244 245 // rejectResticCache returns a RejectFunc that rejects the restic cache 246 // directory (if set). 247 func rejectResticCache(repo *repository.Repository) (RejectFunc, error) { 248 if repo.Cache == nil { 249 return func(string, os.FileInfo) bool { 250 return false 251 }, nil 252 } 253 cacheBase := repo.Cache.BaseDir() 254 255 if cacheBase == "" { 256 return nil, errors.New("cacheBase is empty string") 257 } 258 259 return func(item string, _ os.FileInfo) bool { 260 if fs.HasPathPrefix(cacheBase, item) { 261 debug.Log("rejecting restic cache directory %v", item) 262 return true 263 } 264 265 return false 266 }, nil 267 }