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 }