github.com/fawick/restic@v0.1.1-0.20171126184616-c02923fbfc79/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, rc *rejectionCache) (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 fn := func(filename string, _ os.FileInfo) bool { 110 return isExcludedByFile(filename, tf, tc, rc) 111 } 112 return fn, nil 113 } 114 115 // isExcludedByFile interprets filename as a path and returns true if that file 116 // is in a excluded directory. A directory is identified as excluded if it contains a 117 // tagfile which bears the name specified in tagFilename and starts with 118 // header. If rc is non-nil, it is used to expedite the evaluation of a 119 // directory based on previous visits. 120 func isExcludedByFile(filename, tagFilename, header string, rc *rejectionCache) bool { 121 if tagFilename == "" { 122 return false 123 } 124 dir, base := filepath.Split(filename) 125 if base == tagFilename { 126 return false // do not exclude the tagfile itself 127 } 128 rc.Lock() 129 defer rc.Unlock() 130 131 rejected, visited := rc.Get(dir) 132 if visited { 133 return rejected 134 } 135 rejected = isDirExcludedByFile(dir, tagFilename, header) 136 rc.Store(dir, rejected) 137 return rejected 138 } 139 140 func isDirExcludedByFile(dir, tagFilename, header string) bool { 141 tf := filepath.Join(dir, tagFilename) 142 _, err := fs.Lstat(tf) 143 if os.IsNotExist(err) { 144 return false 145 } 146 if err != nil { 147 Warnf("could not access exclusion tagfile: %v", err) 148 return false 149 } 150 // when no signature is given, the mere presence of tf is enough reason 151 // to exclude filename 152 if len(header) == 0 { 153 return true 154 } 155 // From this stage, errors mean tagFilename exists but it is malformed. 156 // Warnings will be generated so that the user is informed that the 157 // indented ignore-action is not performed. 158 f, err := os.Open(tf) 159 if err != nil { 160 Warnf("could not open exclusion tagfile: %v", err) 161 return false 162 } 163 defer f.Close() 164 buf := make([]byte, len(header)) 165 _, err = io.ReadFull(f, buf) 166 // EOF is handled with a dedicated message, otherwise the warning were too cryptic 167 if err == io.EOF { 168 Warnf("invalid (too short) signature in exclusion tagfile %q\n", tf) 169 return false 170 } 171 if err != nil { 172 Warnf("could not read signature from exclusion tagfile %q: %v\n", tf, err) 173 return false 174 } 175 if bytes.Compare(buf, []byte(header)) != 0 { 176 Warnf("invalid signature in exclusion tagfile %q\n", tf) 177 return false 178 } 179 return true 180 } 181 182 // gatherDevices returns the set of unique device ids of the files and/or 183 // directory paths listed in "items". 184 func gatherDevices(items []string) (deviceMap map[string]uint64, err error) { 185 deviceMap = make(map[string]uint64) 186 for _, item := range items { 187 fi, err := fs.Lstat(item) 188 if err != nil { 189 return nil, err 190 } 191 id, err := fs.DeviceID(fi) 192 if err != nil { 193 return nil, err 194 } 195 deviceMap[item] = id 196 } 197 if len(deviceMap) == 0 { 198 return nil, errors.New("zero allowed devices") 199 } 200 return deviceMap, nil 201 } 202 203 // rejectByDevice returns a RejectFunc that rejects files which are on a 204 // different file systems than the files/dirs in samples. 205 func rejectByDevice(samples []string) (RejectFunc, error) { 206 allowed, err := gatherDevices(samples) 207 if err != nil { 208 return nil, err 209 } 210 debug.Log("allowed devices: %v\n", allowed) 211 212 return func(item string, fi os.FileInfo) bool { 213 if fi == nil { 214 return false 215 } 216 217 id, err := fs.DeviceID(fi) 218 if err != nil { 219 // This should never happen because gatherDevices() would have 220 // errored out earlier. If it still does that's a reason to panic. 221 panic(err) 222 } 223 224 for dir := item; dir != ""; dir = filepath.Dir(dir) { 225 debug.Log("item %v, test dir %v", item, dir) 226 227 allowedID, ok := allowed[dir] 228 if !ok { 229 continue 230 } 231 232 if allowedID != id { 233 debug.Log("path %q on disallowed device %d", item, id) 234 return true 235 } 236 237 return false 238 } 239 240 panic(fmt.Sprintf("item %v, device id %v not found, allowedDevs: %v", item, id, allowed)) 241 }, nil 242 } 243 244 // rejectResticCache returns a RejectFunc that rejects the restic cache 245 // directory (if set). 246 func rejectResticCache(repo *repository.Repository) (RejectFunc, error) { 247 if repo.Cache == nil { 248 return func(string, os.FileInfo) bool { 249 return false 250 }, nil 251 } 252 cacheBase := repo.Cache.BaseDir() 253 254 if cacheBase == "" { 255 return nil, errors.New("cacheBase is empty string") 256 } 257 258 return func(item string, _ os.FileInfo) bool { 259 if fs.HasPathPrefix(cacheBase, item) { 260 debug.Log("rejecting restic cache directory %v", item) 261 return true 262 } 263 264 return false 265 }, nil 266 }