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  }