diff --git a/src/restic/backend/layout.go b/src/restic/backend/layout.go index 43fa223b7..c8b795348 100644 --- a/src/restic/backend/layout.go +++ b/src/restic/backend/layout.go @@ -1,7 +1,13 @@ package backend import ( + "fmt" + "os" + "path/filepath" + "regexp" "restic" + "restic/errors" + "restic/fs" ) // Layout computes paths for file name storage. @@ -10,3 +16,135 @@ type Layout interface { Dirname(restic.Handle) string Paths() []string } + +// Filesystem is the abstraction of a file system used for a backend. +type Filesystem interface { + Join(...string) string + ReadDir(string) ([]os.FileInfo, error) +} + +// ensure statically that *LocalFilesystem implements Filesystem. +var _ Filesystem = &LocalFilesystem{} + +// LocalFilesystem implements Filesystem in a local path. +type LocalFilesystem struct { +} + +// ReadDir returns all entries of a directory. +func (l *LocalFilesystem) ReadDir(dir string) ([]os.FileInfo, error) { + f, err := fs.Open(dir) + if err != nil { + return nil, err + } + + entries, err := f.Readdir(-1) + if err != nil { + return nil, err + } + + err = f.Close() + if err != nil { + return nil, err + } + + return entries, nil +} + +// Join combines several path components to one. +func (l *LocalFilesystem) Join(paths ...string) string { + return filepath.Join(paths...) +} + +var backendFilenameLength = len(restic.ID{}) * 2 +var backendFilename = regexp.MustCompile(fmt.Sprintf("^[a-fA-F0-9]{%d}$", backendFilenameLength)) + +func hasBackendFile(fs Filesystem, dir string) (bool, error) { + entries, err := fs.ReadDir(dir) + if err != nil && os.IsNotExist(errors.Cause(err)) { + return false, nil + } + + if err != nil { + return false, errors.Wrap(err, "ReadDir") + } + + for _, e := range entries { + if backendFilename.MatchString(e.Name()) { + return true, nil + } + } + + return false, nil +} + +var dataSubdirName = regexp.MustCompile("^[a-fA-F0-9]{2}$") + +func hasSubdirBackendFile(fs Filesystem, dir string) (bool, error) { + entries, err := fs.ReadDir(dir) + if err != nil && os.IsNotExist(errors.Cause(err)) { + return false, nil + } + + if err != nil { + return false, errors.Wrap(err, "ReadDir") + } + + for _, subdir := range entries { + if !dataSubdirName.MatchString(subdir.Name()) { + continue + } + + present, err := hasBackendFile(fs, fs.Join(dir, subdir.Name())) + if err != nil { + return false, err + } + + if present { + return true, nil + } + } + + return false, nil +} + +// DetectLayout tries to find out which layout is used in a local (or sftp) +// filesystem at the given path. +func DetectLayout(repo Filesystem, dir string) (Layout, error) { + // key file in the "keys" dir (DefaultLayout or CloudLayout) + foundKeysFile, err := hasBackendFile(repo, repo.Join(dir, defaultLayoutPaths[restic.KeyFile])) + if err != nil { + return nil, err + } + + // key file in the "key" dir (S3Layout) + foundKeyFile, err := hasBackendFile(repo, repo.Join(dir, s3LayoutPaths[restic.KeyFile])) + if err != nil { + return nil, err + } + + // data file in "data" directory (S3Layout or CloudLayout) + foundDataFile, err := hasBackendFile(repo, repo.Join(dir, s3LayoutPaths[restic.DataFile])) + if err != nil { + return nil, err + } + + // data file in subdir of "data" directory (DefaultLayout) + foundDataSubdirFile, err := hasSubdirBackendFile(repo, repo.Join(dir, s3LayoutPaths[restic.DataFile])) + if err != nil { + return nil, err + } + + if foundKeysFile && foundDataFile && !foundKeyFile && !foundDataSubdirFile { + return &CloudLayout{}, nil + } + + if foundKeysFile && foundDataSubdirFile && !foundKeyFile && !foundDataFile { + return &DefaultLayout{}, nil + } + + if foundKeyFile && foundDataFile && !foundKeysFile && !foundDataSubdirFile { + return &S3Layout{}, nil + } + + return nil, errors.New("auto-detecting the filesystem layout failed") +} diff --git a/src/restic/backend/layout_test.go b/src/restic/backend/layout_test.go index 806329b71..c08829ea4 100644 --- a/src/restic/backend/layout_test.go +++ b/src/restic/backend/layout_test.go @@ -5,13 +5,13 @@ import ( "path/filepath" "reflect" "restic" - "restic/test" + . "restic/test" "sort" "testing" ) func TestDefaultLayout(t *testing.T) { - path, cleanup := test.TempDir(t) + path, cleanup := TempDir(t) defer cleanup() var tests = []struct { @@ -79,7 +79,7 @@ func TestDefaultLayout(t *testing.T) { } func TestCloudLayout(t *testing.T) { - path, cleanup := test.TempDir(t) + path, cleanup := TempDir(t) defer cleanup() var tests = []struct { @@ -147,7 +147,7 @@ func TestCloudLayout(t *testing.T) { } func TestS3Layout(t *testing.T) { - path, cleanup := test.TempDir(t) + path, cleanup := TempDir(t) defer cleanup() var tests = []struct { @@ -213,3 +213,40 @@ func TestS3Layout(t *testing.T) { }) } } + +func TestDetectLayout(t *testing.T) { + path, cleanup := TempDir(t) + defer cleanup() + + var tests = []struct { + filename string + want string + }{ + {"repo-layout-local.tar.gz", "*backend.DefaultLayout"}, + {"repo-layout-cloud.tar.gz", "*backend.CloudLayout"}, + {"repo-layout-s3-old.tar.gz", "*backend.S3Layout"}, + } + + var fs = &LocalFilesystem{} + for _, test := range tests { + t.Run(test.filename, func(t *testing.T) { + SetupTarTestFixture(t, path, filepath.Join("testdata", test.filename)) + + layout, err := DetectLayout(fs, filepath.Join(path, "repo")) + if err != nil { + t.Fatal(err) + } + + if layout == nil { + t.Fatal("wanted some layout, but detect returned nil") + } + + layoutName := fmt.Sprintf("%T", layout) + if layoutName != test.want { + t.Fatalf("want layout %v, got %v", test.want, layoutName) + } + + RemoveAll(t, filepath.Join(path, "repo")) + }) + } +} diff --git a/src/restic/backend/testdata/repo-layout-cloud.tar.gz b/src/restic/backend/testdata/repo-layout-cloud.tar.gz new file mode 100644 index 000000000..189832589 Binary files /dev/null and b/src/restic/backend/testdata/repo-layout-cloud.tar.gz differ diff --git a/src/restic/backend/testdata/repo-layout-local.tar.gz b/src/restic/backend/testdata/repo-layout-local.tar.gz new file mode 100644 index 000000000..e38deb54b Binary files /dev/null and b/src/restic/backend/testdata/repo-layout-local.tar.gz differ diff --git a/src/restic/backend/testdata/repo-layout-s3-old.tar.gz b/src/restic/backend/testdata/repo-layout-s3-old.tar.gz new file mode 100644 index 000000000..2b7d852cc Binary files /dev/null and b/src/restic/backend/testdata/repo-layout-s3-old.tar.gz differ