github.com/mckael/restic@v0.8.3/internal/backend/layout.go (about)

     1  package backend
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"regexp"
     8  
     9  	"github.com/restic/restic/internal/debug"
    10  	"github.com/restic/restic/internal/errors"
    11  	"github.com/restic/restic/internal/fs"
    12  	"github.com/restic/restic/internal/restic"
    13  )
    14  
    15  // Layout computes paths for file name storage.
    16  type Layout interface {
    17  	Filename(restic.Handle) string
    18  	Dirname(restic.Handle) string
    19  	Basedir(restic.FileType) (dir string, subdirs bool)
    20  	Paths() []string
    21  	Name() string
    22  }
    23  
    24  // Filesystem is the abstraction of a file system used for a backend.
    25  type Filesystem interface {
    26  	Join(...string) string
    27  	ReadDir(string) ([]os.FileInfo, error)
    28  	IsNotExist(error) bool
    29  }
    30  
    31  // ensure statically that *LocalFilesystem implements Filesystem.
    32  var _ Filesystem = &LocalFilesystem{}
    33  
    34  // LocalFilesystem implements Filesystem in a local path.
    35  type LocalFilesystem struct {
    36  }
    37  
    38  // ReadDir returns all entries of a directory.
    39  func (l *LocalFilesystem) ReadDir(dir string) ([]os.FileInfo, error) {
    40  	f, err := fs.Open(dir)
    41  	if err != nil {
    42  		return nil, err
    43  	}
    44  
    45  	entries, err := f.Readdir(-1)
    46  	if err != nil {
    47  		return nil, errors.Wrap(err, "Readdir")
    48  	}
    49  
    50  	err = f.Close()
    51  	if err != nil {
    52  		return nil, errors.Wrap(err, "Close")
    53  	}
    54  
    55  	return entries, nil
    56  }
    57  
    58  // Join combines several path components to one.
    59  func (l *LocalFilesystem) Join(paths ...string) string {
    60  	return filepath.Join(paths...)
    61  }
    62  
    63  // IsNotExist returns true for errors that are caused by not existing files.
    64  func (l *LocalFilesystem) IsNotExist(err error) bool {
    65  	return os.IsNotExist(err)
    66  }
    67  
    68  var backendFilenameLength = len(restic.ID{}) * 2
    69  var backendFilename = regexp.MustCompile(fmt.Sprintf("^[a-fA-F0-9]{%d}$", backendFilenameLength))
    70  
    71  func hasBackendFile(fs Filesystem, dir string) (bool, error) {
    72  	entries, err := fs.ReadDir(dir)
    73  	if err != nil && fs.IsNotExist(errors.Cause(err)) {
    74  		return false, nil
    75  	}
    76  
    77  	if err != nil {
    78  		return false, errors.Wrap(err, "ReadDir")
    79  	}
    80  
    81  	for _, e := range entries {
    82  		if backendFilename.MatchString(e.Name()) {
    83  			return true, nil
    84  		}
    85  	}
    86  
    87  	return false, nil
    88  }
    89  
    90  // ErrLayoutDetectionFailed is returned by DetectLayout() when the layout
    91  // cannot be detected automatically.
    92  var ErrLayoutDetectionFailed = errors.New("auto-detecting the filesystem layout failed")
    93  
    94  // DetectLayout tries to find out which layout is used in a local (or sftp)
    95  // filesystem at the given path. If repo is nil, an instance of LocalFilesystem
    96  // is used.
    97  func DetectLayout(repo Filesystem, dir string) (Layout, error) {
    98  	debug.Log("detect layout at %v", dir)
    99  	if repo == nil {
   100  		repo = &LocalFilesystem{}
   101  	}
   102  
   103  	// key file in the "keys" dir (DefaultLayout)
   104  	foundKeysFile, err := hasBackendFile(repo, repo.Join(dir, defaultLayoutPaths[restic.KeyFile]))
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  
   109  	// key file in the "key" dir (S3LegacyLayout)
   110  	foundKeyFile, err := hasBackendFile(repo, repo.Join(dir, s3LayoutPaths[restic.KeyFile]))
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  
   115  	if foundKeysFile && !foundKeyFile {
   116  		debug.Log("found default layout at %v", dir)
   117  		return &DefaultLayout{
   118  			Path: dir,
   119  			Join: repo.Join,
   120  		}, nil
   121  	}
   122  
   123  	if foundKeyFile && !foundKeysFile {
   124  		debug.Log("found s3 layout at %v", dir)
   125  		return &S3LegacyLayout{
   126  			Path: dir,
   127  			Join: repo.Join,
   128  		}, nil
   129  	}
   130  
   131  	debug.Log("layout detection failed")
   132  	return nil, ErrLayoutDetectionFailed
   133  }
   134  
   135  // ParseLayout parses the config string and returns a Layout. When layout is
   136  // the empty string, DetectLayout is used. If that fails, defaultLayout is used.
   137  func ParseLayout(repo Filesystem, layout, defaultLayout, path string) (l Layout, err error) {
   138  	debug.Log("parse layout string %q for backend at %v", layout, path)
   139  	switch layout {
   140  	case "default":
   141  		l = &DefaultLayout{
   142  			Path: path,
   143  			Join: repo.Join,
   144  		}
   145  	case "s3legacy":
   146  		l = &S3LegacyLayout{
   147  			Path: path,
   148  			Join: repo.Join,
   149  		}
   150  	case "":
   151  		l, err = DetectLayout(repo, path)
   152  
   153  		// use the default layout if auto detection failed
   154  		if errors.Cause(err) == ErrLayoutDetectionFailed && defaultLayout != "" {
   155  			debug.Log("error: %v, use default layout %v", err, defaultLayout)
   156  			return ParseLayout(repo, defaultLayout, "", path)
   157  		}
   158  
   159  		if err != nil {
   160  			return nil, err
   161  		}
   162  		debug.Log("layout detected: %v", l)
   163  	default:
   164  		return nil, errors.Errorf("unknown backend layout string %q, may be one of: default, s3legacy", layout)
   165  	}
   166  
   167  	return l, nil
   168  }