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  }