From 60245970280c6e6442cc57777bd570cb49f66448 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Mon, 26 Aug 2024 20:28:39 +0200 Subject: [PATCH] drop support for s3legacy layout --- cmd/restic/cmd_check.go | 3 - cmd/restic/cmd_restore_integration_test.go | 36 +--- doc/design.rst | 11 +- internal/backend/layout/layout.go | 167 --------------- internal/backend/layout/layout_default.go | 7 + internal/backend/layout/layout_s3legacy.go | 79 ------- internal/backend/layout/layout_test.go | 200 ------------------ internal/backend/local/config.go | 3 +- internal/backend/local/layout_test.go | 11 +- internal/backend/local/local.go | 11 +- internal/backend/s3/s3.go | 10 +- internal/backend/sftp/config.go | 1 - internal/backend/sftp/layout_test.go | 11 +- internal/backend/sftp/sftp.go | 28 +-- .../testdata/repo-layout-s3legacy.tar.gz | Bin 38096 -> 0 bytes internal/checker/checker.go | 17 -- internal/feature/registry.go | 2 - internal/migrations/s3_layout.go | 123 ----------- internal/repository/s3_backend.go | 12 -- 19 files changed, 34 insertions(+), 698 deletions(-) delete mode 100644 internal/backend/layout/layout_s3legacy.go delete mode 100644 internal/backend/testdata/repo-layout-s3legacy.tar.gz delete mode 100644 internal/migrations/s3_layout.go delete mode 100644 internal/repository/s3_backend.go diff --git a/cmd/restic/cmd_check.go b/cmd/restic/cmd_check.go index e8596ae33..fc460e39e 100644 --- a/cmd/restic/cmd_check.go +++ b/cmd/restic/cmd_check.go @@ -296,9 +296,6 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts GlobalOptions, args errorsFound = true printer.E("%v\n", err) } - } else if err == checker.ErrLegacyLayout { - errorsFound = true - printer.E("error: repository still uses the S3 legacy layout\nYou must run `restic migrate s3legacy` to correct this.\n") } else { errorsFound = true printer.E("%v\n", err) diff --git a/cmd/restic/cmd_restore_integration_test.go b/cmd/restic/cmd_restore_integration_test.go index b0543850b..42cd1f87d 100644 --- a/cmd/restic/cmd_restore_integration_test.go +++ b/cmd/restic/cmd_restore_integration_test.go @@ -12,7 +12,6 @@ import ( "testing" "time" - "github.com/restic/restic/internal/feature" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" "github.com/restic/restic/internal/ui/termstatus" @@ -403,36 +402,21 @@ func TestRestoreNoMetadataOnIgnoredIntermediateDirs(t *testing.T) { "meta data of intermediate directory hasn't been restore") } -func TestRestoreLocalLayout(t *testing.T) { - defer feature.TestSetFlag(t, feature.Flag, feature.DeprecateS3LegacyLayout, false)() +func TestRestoreDefaultLayout(t *testing.T) { env, cleanup := withTestEnvironment(t) defer cleanup() - var tests = []struct { - filename string - layout string - }{ - {"repo-layout-default.tar.gz", ""}, - {"repo-layout-s3legacy.tar.gz", ""}, - {"repo-layout-default.tar.gz", "default"}, - {"repo-layout-s3legacy.tar.gz", "s3legacy"}, - } + datafile := filepath.Join("..", "..", "internal", "backend", "testdata", "repo-layout-default.tar.gz") - for _, test := range tests { - datafile := filepath.Join("..", "..", "internal", "backend", "testdata", test.filename) + rtest.SetupTarTestFixture(t, env.base, datafile) - rtest.SetupTarTestFixture(t, env.base, datafile) + // check the repo + testRunCheck(t, env.gopts) - env.gopts.extended["local.layout"] = test.layout + // restore latest snapshot + target := filepath.Join(env.base, "restore") + testRunRestoreLatest(t, env.gopts, target, nil, nil) - // check the repo - testRunCheck(t, env.gopts) - - // restore latest snapshot - target := filepath.Join(env.base, "restore") - testRunRestoreLatest(t, env.gopts, target, nil, nil) - - rtest.RemoveAll(t, filepath.Join(env.base, "repo")) - rtest.RemoveAll(t, target) - } + rtest.RemoveAll(t, filepath.Join(env.base, "repo")) + rtest.RemoveAll(t, target) } diff --git a/doc/design.rst b/doc/design.rst index c974e997a..62b7e9bf9 100644 --- a/doc/design.rst +++ b/doc/design.rst @@ -119,16 +119,11 @@ A local repository can be initialized with the ``restic init`` command, e.g.: $ restic -r /tmp/restic-repo init -The local and sftp backends will auto-detect and accept all layouts described -in the following sections, so that remote repositories mounted locally e.g. via -fuse can be accessed. The layout auto-detection can be overridden by specifying -the option ``-o local.layout=default``, valid values are ``default`` and -``s3legacy``. The option for the sftp backend is named ``sftp.layout``, for the -s3 backend ``s3.layout``. - S3 Legacy Layout (deprecated) ----------------------------- +Restic 0.17 is the last version that supports the legacy layout. + Unfortunately during development the Amazon S3 backend uses slightly different paths (directory names use singular instead of plural for ``key``, ``lock``, and ``snapshot`` files), and the pack files are stored directly below @@ -152,8 +147,6 @@ the ``data`` directory. The S3 Legacy repository layout looks like this: /snapshot └── 22a5af1bdc6e616f8a29579458c49627e01b32210d09adb288d1ecda7c5711ec -Restic 0.17 is the last version that supports the legacy layout. - Pack Format =========== diff --git a/internal/backend/layout/layout.go b/internal/backend/layout/layout.go index 052fd66ca..cd69efc34 100644 --- a/internal/backend/layout/layout.go +++ b/internal/backend/layout/layout.go @@ -1,18 +1,7 @@ package layout import ( - "context" - "fmt" - "os" - "path/filepath" - "regexp" - "github.com/restic/restic/internal/backend" - "github.com/restic/restic/internal/debug" - "github.com/restic/restic/internal/errors" - "github.com/restic/restic/internal/feature" - "github.com/restic/restic/internal/fs" - "github.com/restic/restic/internal/restic" ) // Layout computes paths for file name storage. @@ -23,159 +12,3 @@ type Layout interface { Paths() []string Name() string } - -// Filesystem is the abstraction of a file system used for a backend. -type Filesystem interface { - Join(...string) string - ReadDir(context.Context, string) ([]os.FileInfo, error) - IsNotExist(error) bool -} - -// 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(_ context.Context, 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, errors.Wrap(err, "Readdir") - } - - err = f.Close() - if err != nil { - return nil, errors.Wrap(err, "Close") - } - - return entries, nil -} - -// Join combines several path components to one. -func (l *LocalFilesystem) Join(paths ...string) string { - return filepath.Join(paths...) -} - -// IsNotExist returns true for errors that are caused by not existing files. -func (l *LocalFilesystem) IsNotExist(err error) bool { - return os.IsNotExist(err) -} - -var backendFilenameLength = len(restic.ID{}) * 2 -var backendFilename = regexp.MustCompile(fmt.Sprintf("^[a-fA-F0-9]{%d}$", backendFilenameLength)) - -func hasBackendFile(ctx context.Context, fs Filesystem, dir string) (bool, error) { - entries, err := fs.ReadDir(ctx, dir) - if err != nil && fs.IsNotExist(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 -} - -// ErrLayoutDetectionFailed is returned by DetectLayout() when the layout -// cannot be detected automatically. -var ErrLayoutDetectionFailed = errors.New("auto-detecting the filesystem layout failed") - -var ErrLegacyLayoutFound = errors.New("detected legacy S3 layout. Use `RESTIC_FEATURES=deprecate-s3-legacy-layout=false restic migrate s3_layout` to migrate your repository") - -// DetectLayout tries to find out which layout is used in a local (or sftp) -// filesystem at the given path. If repo is nil, an instance of LocalFilesystem -// is used. -func DetectLayout(ctx context.Context, repo Filesystem, dir string) (Layout, error) { - debug.Log("detect layout at %v", dir) - if repo == nil { - repo = &LocalFilesystem{} - } - - // key file in the "keys" dir (DefaultLayout) - foundKeysFile, err := hasBackendFile(ctx, repo, repo.Join(dir, defaultLayoutPaths[backend.KeyFile])) - if err != nil { - return nil, err - } - - // key file in the "key" dir (S3LegacyLayout) - foundKeyFile, err := hasBackendFile(ctx, repo, repo.Join(dir, s3LayoutPaths[backend.KeyFile])) - if err != nil { - return nil, err - } - - if foundKeysFile && !foundKeyFile { - debug.Log("found default layout at %v", dir) - return &DefaultLayout{ - Path: dir, - Join: repo.Join, - }, nil - } - - if foundKeyFile && !foundKeysFile { - if feature.Flag.Enabled(feature.DeprecateS3LegacyLayout) { - return nil, ErrLegacyLayoutFound - } - - debug.Log("found s3 layout at %v", dir) - return &S3LegacyLayout{ - Path: dir, - Join: repo.Join, - }, nil - } - - debug.Log("layout detection failed") - return nil, ErrLayoutDetectionFailed -} - -// ParseLayout parses the config string and returns a Layout. When layout is -// the empty string, DetectLayout is used. If that fails, defaultLayout is used. -func ParseLayout(ctx context.Context, repo Filesystem, layout, defaultLayout, path string) (l Layout, err error) { - debug.Log("parse layout string %q for backend at %v", layout, path) - switch layout { - case "default": - l = &DefaultLayout{ - Path: path, - Join: repo.Join, - } - case "s3legacy": - if feature.Flag.Enabled(feature.DeprecateS3LegacyLayout) { - return nil, ErrLegacyLayoutFound - } - - l = &S3LegacyLayout{ - Path: path, - Join: repo.Join, - } - case "": - l, err = DetectLayout(ctx, repo, path) - - // use the default layout if auto detection failed - if errors.Is(err, ErrLayoutDetectionFailed) && defaultLayout != "" { - debug.Log("error: %v, use default layout %v", err, defaultLayout) - return ParseLayout(ctx, repo, defaultLayout, "", path) - } - - if err != nil { - return nil, err - } - debug.Log("layout detected: %v", l) - default: - return nil, errors.Errorf("unknown backend layout string %q, may be one of: default, s3legacy", layout) - } - - return l, nil -} diff --git a/internal/backend/layout/layout_default.go b/internal/backend/layout/layout_default.go index 9a8419f10..3f73a941d 100644 --- a/internal/backend/layout/layout_default.go +++ b/internal/backend/layout/layout_default.go @@ -23,6 +23,13 @@ var defaultLayoutPaths = map[backend.FileType]string{ backend.KeyFile: "keys", } +func NewDefaultLayout(path string, join func(...string) string) *DefaultLayout { + return &DefaultLayout{ + Path: path, + Join: join, + } +} + func (l *DefaultLayout) String() string { return "" } diff --git a/internal/backend/layout/layout_s3legacy.go b/internal/backend/layout/layout_s3legacy.go deleted file mode 100644 index 8b90789d8..000000000 --- a/internal/backend/layout/layout_s3legacy.go +++ /dev/null @@ -1,79 +0,0 @@ -package layout - -import ( - "github.com/restic/restic/internal/backend" -) - -// S3LegacyLayout implements the old layout used for s3 cloud storage backends, as -// described in the Design document. -type S3LegacyLayout struct { - URL string - Path string - Join func(...string) string -} - -var s3LayoutPaths = map[backend.FileType]string{ - backend.PackFile: "data", - backend.SnapshotFile: "snapshot", - backend.IndexFile: "index", - backend.LockFile: "lock", - backend.KeyFile: "key", -} - -func (l *S3LegacyLayout) String() string { - return "" -} - -// Name returns the name for this layout. -func (l *S3LegacyLayout) Name() string { - return "s3legacy" -} - -// join calls Join with the first empty elements removed. -func (l *S3LegacyLayout) join(url string, items ...string) string { - for len(items) > 0 && items[0] == "" { - items = items[1:] - } - - path := l.Join(items...) - if path == "" || path[0] != '/' { - if url != "" && url[len(url)-1] != '/' { - url += "/" - } - } - - return url + path -} - -// Dirname returns the directory path for a given file type and name. -func (l *S3LegacyLayout) Dirname(h backend.Handle) string { - if h.Type == backend.ConfigFile { - return l.URL + l.Join(l.Path, "/") - } - - return l.join(l.URL, l.Path, s3LayoutPaths[h.Type]) + "/" -} - -// Filename returns a path to a file, including its name. -func (l *S3LegacyLayout) Filename(h backend.Handle) string { - name := h.Name - - if h.Type == backend.ConfigFile { - name = "config" - } - - return l.join(l.URL, l.Path, s3LayoutPaths[h.Type], name) -} - -// Paths returns all directory names -func (l *S3LegacyLayout) Paths() (dirs []string) { - for _, p := range s3LayoutPaths { - dirs = append(dirs, l.Join(l.Path, p)) - } - return dirs -} - -// Basedir returns the base dir name for type t. -func (l *S3LegacyLayout) Basedir(t backend.FileType) (dirname string, subdirs bool) { - return l.Join(l.Path, s3LayoutPaths[t]), false -} diff --git a/internal/backend/layout/layout_test.go b/internal/backend/layout/layout_test.go index 55a0749c9..de5ae7d69 100644 --- a/internal/backend/layout/layout_test.go +++ b/internal/backend/layout/layout_test.go @@ -1,7 +1,6 @@ package layout import ( - "context" "fmt" "path" "path/filepath" @@ -10,7 +9,6 @@ import ( "testing" "github.com/restic/restic/internal/backend" - "github.com/restic/restic/internal/feature" rtest "github.com/restic/restic/internal/test" ) @@ -232,42 +230,6 @@ func TestRESTLayoutURLs(t *testing.T) { "https://hostname.foo:1234/prefix/repo/config", "https://hostname.foo:1234/prefix/repo/", }, - { - &S3LegacyLayout{URL: "https://hostname.foo", Path: "/", Join: path.Join}, - backend.Handle{Type: backend.PackFile, Name: "foobar"}, - "https://hostname.foo/data/foobar", - "https://hostname.foo/data/", - }, - { - &S3LegacyLayout{URL: "https://hostname.foo:1234/prefix/repo", Path: "", Join: path.Join}, - backend.Handle{Type: backend.LockFile, Name: "foobar"}, - "https://hostname.foo:1234/prefix/repo/lock/foobar", - "https://hostname.foo:1234/prefix/repo/lock/", - }, - { - &S3LegacyLayout{URL: "https://hostname.foo:1234/prefix/repo", Path: "/", Join: path.Join}, - backend.Handle{Type: backend.ConfigFile, Name: "foobar"}, - "https://hostname.foo:1234/prefix/repo/config", - "https://hostname.foo:1234/prefix/repo/", - }, - { - &S3LegacyLayout{URL: "", Path: "", Join: path.Join}, - backend.Handle{Type: backend.PackFile, Name: "foobar"}, - "data/foobar", - "data/", - }, - { - &S3LegacyLayout{URL: "", Path: "", Join: path.Join}, - backend.Handle{Type: backend.LockFile, Name: "foobar"}, - "lock/foobar", - "lock/", - }, - { - &S3LegacyLayout{URL: "", Path: "/", Join: path.Join}, - backend.Handle{Type: backend.ConfigFile, Name: "foobar"}, - "/config", - "/", - }, } for _, test := range tests { @@ -284,165 +246,3 @@ func TestRESTLayoutURLs(t *testing.T) { }) } } - -func TestS3LegacyLayout(t *testing.T) { - path := rtest.TempDir(t) - - var tests = []struct { - backend.Handle - filename string - }{ - { - backend.Handle{Type: backend.PackFile, Name: "0123456"}, - filepath.Join(path, "data", "0123456"), - }, - { - backend.Handle{Type: backend.ConfigFile, Name: "CFG"}, - filepath.Join(path, "config"), - }, - { - backend.Handle{Type: backend.SnapshotFile, Name: "123456"}, - filepath.Join(path, "snapshot", "123456"), - }, - { - backend.Handle{Type: backend.IndexFile, Name: "123456"}, - filepath.Join(path, "index", "123456"), - }, - { - backend.Handle{Type: backend.LockFile, Name: "123456"}, - filepath.Join(path, "lock", "123456"), - }, - { - backend.Handle{Type: backend.KeyFile, Name: "123456"}, - filepath.Join(path, "key", "123456"), - }, - } - - l := &S3LegacyLayout{ - Path: path, - Join: filepath.Join, - } - - t.Run("Paths", func(t *testing.T) { - dirs := l.Paths() - - want := []string{ - filepath.Join(path, "data"), - filepath.Join(path, "snapshot"), - filepath.Join(path, "index"), - filepath.Join(path, "lock"), - filepath.Join(path, "key"), - } - - sort.Strings(want) - sort.Strings(dirs) - - if !reflect.DeepEqual(dirs, want) { - t.Fatalf("wrong paths returned, want:\n %v\ngot:\n %v", want, dirs) - } - }) - - for _, test := range tests { - t.Run(fmt.Sprintf("%v/%v", test.Type, test.Handle.Name), func(t *testing.T) { - filename := l.Filename(test.Handle) - if filename != test.filename { - t.Fatalf("wrong filename, want %v, got %v", test.filename, filename) - } - }) - } -} - -func TestDetectLayout(t *testing.T) { - defer feature.TestSetFlag(t, feature.Flag, feature.DeprecateS3LegacyLayout, false)() - path := rtest.TempDir(t) - - var tests = []struct { - filename string - want string - }{ - {"repo-layout-default.tar.gz", "*layout.DefaultLayout"}, - {"repo-layout-s3legacy.tar.gz", "*layout.S3LegacyLayout"}, - } - - var fs = &LocalFilesystem{} - for _, test := range tests { - for _, fs := range []Filesystem{fs, nil} { - t.Run(fmt.Sprintf("%v/fs-%T", test.filename, fs), func(t *testing.T) { - rtest.SetupTarTestFixture(t, path, filepath.Join("../testdata", test.filename)) - - layout, err := DetectLayout(context.TODO(), 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) - } - - rtest.RemoveAll(t, filepath.Join(path, "repo")) - }) - } - } -} - -func TestParseLayout(t *testing.T) { - defer feature.TestSetFlag(t, feature.Flag, feature.DeprecateS3LegacyLayout, false)() - path := rtest.TempDir(t) - - var tests = []struct { - layoutName string - defaultLayoutName string - want string - }{ - {"default", "", "*layout.DefaultLayout"}, - {"s3legacy", "", "*layout.S3LegacyLayout"}, - {"", "", "*layout.DefaultLayout"}, - } - - rtest.SetupTarTestFixture(t, path, filepath.Join("..", "testdata", "repo-layout-default.tar.gz")) - - for _, test := range tests { - t.Run(test.layoutName, func(t *testing.T) { - layout, err := ParseLayout(context.TODO(), &LocalFilesystem{}, test.layoutName, test.defaultLayoutName, filepath.Join(path, "repo")) - if err != nil { - t.Fatal(err) - } - - if layout == nil { - t.Fatal("wanted some layout, but detect returned nil") - } - - // test that the functions work (and don't panic) - _ = layout.Dirname(backend.Handle{Type: backend.PackFile}) - _ = layout.Filename(backend.Handle{Type: backend.PackFile, Name: "1234"}) - _ = layout.Paths() - - layoutName := fmt.Sprintf("%T", layout) - if layoutName != test.want { - t.Fatalf("want layout %v, got %v", test.want, layoutName) - } - }) - } -} - -func TestParseLayoutInvalid(t *testing.T) { - path := rtest.TempDir(t) - - var invalidNames = []string{ - "foo", "bar", "local", - } - - for _, name := range invalidNames { - t.Run(name, func(t *testing.T) { - layout, err := ParseLayout(context.TODO(), nil, name, "", path) - if err == nil { - t.Fatalf("expected error not found for layout name %v, layout is %v", name, layout) - } - }) - } -} diff --git a/internal/backend/local/config.go b/internal/backend/local/config.go index e08f05550..782f132d0 100644 --- a/internal/backend/local/config.go +++ b/internal/backend/local/config.go @@ -9,8 +9,7 @@ import ( // Config holds all information needed to open a local repository. type Config struct { - Path string - Layout string `option:"layout" help:"use this backend directory layout (default: auto-detect) (deprecated)"` + Path string Connections uint `option:"connections" help:"set a limit for the number of concurrent operations (default: 2)"` } diff --git a/internal/backend/local/layout_test.go b/internal/backend/local/layout_test.go index 00c91376a..cac89e552 100644 --- a/internal/backend/local/layout_test.go +++ b/internal/backend/local/layout_test.go @@ -6,30 +6,22 @@ import ( "testing" "github.com/restic/restic/internal/backend" - "github.com/restic/restic/internal/feature" rtest "github.com/restic/restic/internal/test" ) func TestLayout(t *testing.T) { - defer feature.TestSetFlag(t, feature.Flag, feature.DeprecateS3LegacyLayout, false)() path := rtest.TempDir(t) var tests = []struct { filename string - layout string failureExpected bool packfiles map[string]bool }{ - {"repo-layout-default.tar.gz", "", false, map[string]bool{ + {"repo-layout-default.tar.gz", false, map[string]bool{ "aa464e9fd598fe4202492ee317ffa728e82fa83a1de1a61996e5bd2d6651646c": false, "fc919a3b421850f6fa66ad22ebcf91e433e79ffef25becf8aef7c7b1eca91683": false, "c089d62788da14f8b7cbf77188305c0874906f0b73d3fce5a8869050e8d0c0e1": false, }}, - {"repo-layout-s3legacy.tar.gz", "", false, map[string]bool{ - "fc919a3b421850f6fa66ad22ebcf91e433e79ffef25becf8aef7c7b1eca91683": false, - "c089d62788da14f8b7cbf77188305c0874906f0b73d3fce5a8869050e8d0c0e1": false, - "aa464e9fd598fe4202492ee317ffa728e82fa83a1de1a61996e5bd2d6651646c": false, - }}, } for _, test := range tests { @@ -39,7 +31,6 @@ func TestLayout(t *testing.T) { repo := filepath.Join(path, "repo") be, err := Open(context.TODO(), Config{ Path: repo, - Layout: test.layout, Connections: 2, }) if err != nil { diff --git a/internal/backend/local/local.go b/internal/backend/local/local.go index f041d608a..ff7e3d35d 100644 --- a/internal/backend/local/local.go +++ b/internal/backend/local/local.go @@ -37,13 +37,8 @@ func NewFactory() location.Factory { return location.NewLimitedBackendFactory("local", ParseConfig, location.NoPassword, limiter.WrapBackendConstructor(Create), limiter.WrapBackendConstructor(Open)) } -const defaultLayout = "default" - func open(ctx context.Context, cfg Config) (*Local, error) { - l, err := layout.ParseLayout(ctx, &layout.LocalFilesystem{}, cfg.Layout, defaultLayout, cfg.Path) - if err != nil { - return nil, err - } + l := layout.NewDefaultLayout(cfg.Path, filepath.Join) fi, err := fs.Stat(l.Filename(backend.Handle{Type: backend.ConfigFile})) m := util.DeriveModesFromFileInfo(fi, err) @@ -58,14 +53,14 @@ func open(ctx context.Context, cfg Config) (*Local, error) { // Open opens the local backend as specified by config. func Open(ctx context.Context, cfg Config) (*Local, error) { - debug.Log("open local backend at %v (layout %q)", cfg.Path, cfg.Layout) + debug.Log("open local backend at %v", cfg.Path) return open(ctx, cfg) } // Create creates all the necessary files and directories for a new local // backend at dir. Afterwards a new config blob should be created. func Create(ctx context.Context, cfg Config) (*Local, error) { - debug.Log("create local backend at %v (layout %q)", cfg.Path, cfg.Layout) + debug.Log("create local backend at %v", cfg.Path) be, err := open(ctx, cfg) if err != nil { diff --git a/internal/backend/s3/s3.go b/internal/backend/s3/s3.go index 019f8471b..5ef952891 100644 --- a/internal/backend/s3/s3.go +++ b/internal/backend/s3/s3.go @@ -37,8 +37,6 @@ func NewFactory() location.Factory { return location.NewHTTPBackendFactory("s3", ParseConfig, location.NoPassword, Create, Open) } -const defaultLayout = "default" - func open(ctx context.Context, cfg Config, rt http.RoundTripper) (*Backend, error) { debug.Log("open, config %#v", cfg) @@ -83,15 +81,9 @@ func open(ctx context.Context, cfg Config, rt http.RoundTripper) (*Backend, erro be := &Backend{ client: client, cfg: cfg, + Layout: layout.NewDefaultLayout(cfg.Prefix, path.Join), } - l, err := layout.ParseLayout(ctx, be, cfg.Layout, defaultLayout, cfg.Prefix) - if err != nil { - return nil, err - } - - be.Layout = l - return be, nil } diff --git a/internal/backend/sftp/config.go b/internal/backend/sftp/config.go index aa8ac7bff..daefbf441 100644 --- a/internal/backend/sftp/config.go +++ b/internal/backend/sftp/config.go @@ -13,7 +13,6 @@ import ( type Config struct { User, Host, Port, Path string - Layout string `option:"layout" help:"use this backend directory layout (default: auto-detect) (deprecated)"` Command string `option:"command" help:"specify command to create sftp connection"` Args string `option:"args" help:"specify arguments for ssh"` diff --git a/internal/backend/sftp/layout_test.go b/internal/backend/sftp/layout_test.go index 8bb7eac01..9e143d4fd 100644 --- a/internal/backend/sftp/layout_test.go +++ b/internal/backend/sftp/layout_test.go @@ -8,7 +8,6 @@ import ( "github.com/restic/restic/internal/backend" "github.com/restic/restic/internal/backend/sftp" - "github.com/restic/restic/internal/feature" rtest "github.com/restic/restic/internal/test" ) @@ -17,25 +16,18 @@ func TestLayout(t *testing.T) { t.Skip("sftp server binary not available") } - defer feature.TestSetFlag(t, feature.Flag, feature.DeprecateS3LegacyLayout, false)() path := rtest.TempDir(t) var tests = []struct { filename string - layout string failureExpected bool packfiles map[string]bool }{ - {"repo-layout-default.tar.gz", "", false, map[string]bool{ + {"repo-layout-default.tar.gz", false, map[string]bool{ "aa464e9fd598fe4202492ee317ffa728e82fa83a1de1a61996e5bd2d6651646c": false, "fc919a3b421850f6fa66ad22ebcf91e433e79ffef25becf8aef7c7b1eca91683": false, "c089d62788da14f8b7cbf77188305c0874906f0b73d3fce5a8869050e8d0c0e1": false, }}, - {"repo-layout-s3legacy.tar.gz", "", false, map[string]bool{ - "fc919a3b421850f6fa66ad22ebcf91e433e79ffef25becf8aef7c7b1eca91683": false, - "c089d62788da14f8b7cbf77188305c0874906f0b73d3fce5a8869050e8d0c0e1": false, - "aa464e9fd598fe4202492ee317ffa728e82fa83a1de1a61996e5bd2d6651646c": false, - }}, } for _, test := range tests { @@ -46,7 +38,6 @@ func TestLayout(t *testing.T) { be, err := sftp.Open(context.TODO(), sftp.Config{ Command: fmt.Sprintf("%q -e", sftpServer), Path: repo, - Layout: test.layout, Connections: 5, }) if err != nil { diff --git a/internal/backend/sftp/sftp.go b/internal/backend/sftp/sftp.go index d766591b7..8ac6781e9 100644 --- a/internal/backend/sftp/sftp.go +++ b/internal/backend/sftp/sftp.go @@ -121,7 +121,13 @@ func startClient(cfg Config) (*SFTP, error) { } _, posixRename := client.HasExtension("posix-rename@openssh.com") - return &SFTP{c: client, cmd: cmd, result: ch, posixRename: posixRename}, nil + return &SFTP{ + c: client, + cmd: cmd, + result: ch, + posixRename: posixRename, + Layout: layout.NewDefaultLayout(cfg.Path, path.Join), + }, nil } // clientError returns an error if the client has exited. Otherwise, nil is @@ -152,14 +158,6 @@ func Open(ctx context.Context, cfg Config) (*SFTP, error) { } func open(ctx context.Context, sftp *SFTP, cfg Config) (*SFTP, error) { - var err error - sftp.Layout, err = layout.ParseLayout(ctx, sftp, cfg.Layout, defaultLayout, cfg.Path) - if err != nil { - return nil, err - } - - debug.Log("layout: %v\n", sftp.Layout) - fi, err := sftp.c.Stat(sftp.Layout.Filename(backend.Handle{Type: backend.ConfigFile})) m := util.DeriveModesFromFileInfo(fi, err) debug.Log("using (%03O file, %03O dir) permissions", m.File, m.Dir) @@ -195,11 +193,6 @@ func (r *SFTP) mkdirAllDataSubdirs(ctx context.Context, nconn uint) error { return g.Wait() } -// Join combines path components with slashes (according to the sftp spec). -func (r *SFTP) Join(p ...string) string { - return path.Join(p...) -} - // ReadDir returns the entries for a directory. func (r *SFTP) ReadDir(_ context.Context, dir string) ([]os.FileInfo, error) { fi, err := r.c.ReadDir(dir) @@ -266,11 +259,6 @@ func Create(ctx context.Context, cfg Config) (*SFTP, error) { return nil, err } - sftp.Layout, err = layout.ParseLayout(ctx, sftp, cfg.Layout, defaultLayout, cfg.Path) - if err != nil { - return nil, err - } - sftp.Modes = util.DefaultModes // test if config file already exists @@ -582,7 +570,7 @@ func (r *SFTP) deleteRecursive(ctx context.Context, name string) error { return ctx.Err() } - itemName := r.Join(name, fi.Name()) + itemName := path.Join(name, fi.Name()) if fi.IsDir() { err := r.deleteRecursive(ctx, itemName) if err != nil { diff --git a/internal/backend/testdata/repo-layout-s3legacy.tar.gz b/internal/backend/testdata/repo-layout-s3legacy.tar.gz deleted file mode 100644 index 2b7d852cc9ab4cf8da442461bf9b8a0ded688855..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38096 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

yhwo;cMz${J@-o@KYqq( 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 A4FCWD diff --git a/internal/checker/checker.go b/internal/checker/checker.go index e0c1766d7..76bb15f63 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -8,8 +8,6 @@ import ( "sync" "github.com/klauspost/compress/zstd" - "github.com/restic/restic/internal/backend" - "github.com/restic/restic/internal/backend/s3" "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/repository" @@ -53,9 +51,6 @@ func New(repo restic.Repository, trackUnused bool) *Checker { return c } -// ErrLegacyLayout is returned when the repository uses the S3 legacy layout. -var ErrLegacyLayout = errors.New("repository uses S3 legacy layout") - // ErrDuplicatePacks is returned when a pack is found in more than one index. type ErrDuplicatePacks struct { PackID restic.ID @@ -177,23 +172,11 @@ func (e *PackError) Error() string { return "pack " + e.ID.String() + ": " + e.Err.Error() } -func isS3Legacy(b backend.Backend) bool { - be := backend.AsBackend[*s3.Backend](b) - return be != nil && be.Layout.Name() == "s3legacy" -} - // Packs checks that all packs referenced in the index are still available and // there are no packs that aren't in an index. errChan is closed after all // packs have been checked. func (c *Checker) Packs(ctx context.Context, errChan chan<- error) { defer close(errChan) - - if r, ok := c.repo.(*repository.Repository); ok { - if isS3Legacy(repository.AsS3Backend(r)) { - errChan <- ErrLegacyLayout - } - } - debug.Log("checking for %d packs", len(c.packs)) debug.Log("listing repository packs") diff --git a/internal/feature/registry.go b/internal/feature/registry.go index 8bdb5480e..7fe7da965 100644 --- a/internal/feature/registry.go +++ b/internal/feature/registry.go @@ -6,7 +6,6 @@ var Flag = New() // flag names are written in kebab-case const ( BackendErrorRedesign FlagName = "backend-error-redesign" - DeprecateS3LegacyLayout FlagName = "deprecate-s3-legacy-layout" DeviceIDForHardlinks FlagName = "device-id-for-hardlinks" ExplicitS3AnonymousAuth FlagName = "explicit-s3-anonymous-auth" SafeForgetKeepTags FlagName = "safe-forget-keep-tags" @@ -15,7 +14,6 @@ const ( func init() { Flag.SetFlags(map[FlagName]FlagDesc{ BackendErrorRedesign: {Type: Beta, Description: "enforce timeouts for stuck HTTP requests and use new backend error handling design."}, - DeprecateS3LegacyLayout: {Type: Beta, Description: "disable support for S3 legacy layout used up to restic 0.7.0. Use `RESTIC_FEATURES=deprecate-s3-legacy-layout=false restic migrate s3_layout` to migrate your S3 repository if necessary."}, DeviceIDForHardlinks: {Type: Alpha, Description: "store deviceID only for hardlinks to reduce metadata changes for example when using btrfs subvolumes. Will be removed in a future restic version after repository format 3 is available"}, ExplicitS3AnonymousAuth: {Type: Beta, Description: "forbid anonymous S3 authentication unless `-o s3.unsafe-anonymous-auth=true` is set"}, SafeForgetKeepTags: {Type: Beta, Description: "prevent deleting all snapshots if the tag passed to `forget --keep-tags tagname` does not exist"}, diff --git a/internal/migrations/s3_layout.go b/internal/migrations/s3_layout.go deleted file mode 100644 index 8b994b8fc..000000000 --- a/internal/migrations/s3_layout.go +++ /dev/null @@ -1,123 +0,0 @@ -package migrations - -import ( - "context" - "fmt" - "os" - "path" - - "github.com/restic/restic/internal/backend" - "github.com/restic/restic/internal/backend/layout" - "github.com/restic/restic/internal/backend/s3" - "github.com/restic/restic/internal/debug" - "github.com/restic/restic/internal/errors" - "github.com/restic/restic/internal/repository" - "github.com/restic/restic/internal/restic" -) - -func init() { - register(&S3Layout{}) -} - -// S3Layout migrates a repository on an S3 backend from the "s3legacy" to the -// "default" layout. -type S3Layout struct{} - -// Check tests whether the migration can be applied. -func (m *S3Layout) Check(_ context.Context, repo restic.Repository) (bool, string, error) { - be := repository.AsS3Backend(repo.(*repository.Repository)) - if be == nil { - debug.Log("backend is not s3") - return false, "backend is not s3", nil - } - - if be.Layout.Name() != "s3legacy" { - debug.Log("layout is not s3legacy") - return false, "not using the legacy s3 layout", nil - } - - return true, "", nil -} - -func (m *S3Layout) RepoCheck() bool { - return false -} - -func retry(max int, fail func(err error), f func() error) error { - var err error - for i := 0; i < max; i++ { - err = f() - if err == nil { - return nil - } - if fail != nil { - fail(err) - } - } - return err -} - -// maxErrors for retrying renames on s3. -const maxErrors = 20 - -func (m *S3Layout) moveFiles(ctx context.Context, be *s3.Backend, l layout.Layout, t restic.FileType) error { - printErr := func(err error) { - fmt.Fprintf(os.Stderr, "renaming file returned error: %v\n", err) - } - - return be.List(ctx, t, func(fi backend.FileInfo) error { - h := backend.Handle{Type: t, Name: fi.Name} - debug.Log("move %v", h) - - return retry(maxErrors, printErr, func() error { - return be.Rename(ctx, h, l) - }) - }) -} - -// Apply runs the migration. -func (m *S3Layout) Apply(ctx context.Context, repo restic.Repository) error { - be := repository.AsS3Backend(repo.(*repository.Repository)) - if be == nil { - debug.Log("backend is not s3") - return errors.New("backend is not s3") - } - - oldLayout := &layout.S3LegacyLayout{ - Path: be.Path(), - Join: path.Join, - } - - newLayout := &layout.DefaultLayout{ - Path: be.Path(), - Join: path.Join, - } - - be.Layout = oldLayout - - for _, t := range []restic.FileType{ - restic.SnapshotFile, - restic.PackFile, - restic.KeyFile, - restic.LockFile, - } { - err := m.moveFiles(ctx, be, newLayout, t) - if err != nil { - return err - } - } - - be.Layout = newLayout - - return nil -} - -// Name returns the name for this migration. -func (m *S3Layout) Name() string { - return "s3_layout" -} - -// Desc returns a short description what the migration does. -func (m *S3Layout) Desc() string { - return "move files from 's3legacy' to the 'default' repository layout" -} diff --git a/internal/repository/s3_backend.go b/internal/repository/s3_backend.go deleted file mode 100644 index 4c77c69a2..000000000 --- a/internal/repository/s3_backend.go +++ /dev/null @@ -1,12 +0,0 @@ -package repository - -import ( - "github.com/restic/restic/internal/backend" - "github.com/restic/restic/internal/backend/s3" -) - -// AsS3Backend extracts the S3 backend from a repository -// TODO remove me once restic 0.17 was released -func AsS3Backend(repo *Repository) *s3.Backend { - return backend.AsBackend[*s3.Backend](repo.be) -}