2
2
mirror of https://github.com/octoleo/restic.git synced 2024-06-01 00:20:48 +00:00

Merge pull request #966 from restic/unify-repo-layout

WIP: Unify repository layout
This commit is contained in:
Alexander Neumann 2017-05-22 21:15:38 +02:00
commit d1cc87ba28
17 changed files with 200 additions and 201 deletions

View File

@ -76,8 +76,8 @@ identifies the repository, regardless if it is accessed via SFTP or
locally. The field ``chunker_polynomial`` contains a parameter that is locally. The field ``chunker_polynomial`` contains a parameter that is
used for splitting large files into smaller chunks (see below). used for splitting large files into smaller chunks (see below).
Filesystem-Based Repositories Repository Layout
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
The ``local`` and ``sftp`` backends are implemented using files and The ``local`` and ``sftp`` backends are implemented using files and
directories stored in a file system. The directory layout is the same directories stored in a file system. The directory layout is the same
@ -117,44 +117,20 @@ e.g.:
$ restic -r /tmp/restic-repo init $ restic -r /tmp/restic-repo init
The local and sftp backends will also accept the repository layout The local and sftp backends will auto-detect and accept all layouts described
described in the following section, so that remote repositories mounted in the following sections, so that remote repositories mounted locally e.g. via
locally e.g. via fuse can be accessed. The layout auto-detection can be fuse can be accessed. The layout auto-detection can be overridden by specifying
overridden by specifying the option ``-o local.layout=default``, valid the option ``-o local.layout=default``, valid values are ``default`` and
values are ``default``, ``cloud`` and ``s3``. The option for the sftp ``s3legacy``. The option for the sftp backend is named ``sftp.layout``, for the
backend is named ``sftp.layout``. s3 backend ``s3.layout``.
Object-Storage-Based Repositories S3 Legacy Layout
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
Repositories in a backend based on an object store (e.g. Amazon s3) have Unfortunately during development the AWS S3 backend uses slightly different
the same basic layout, with the exception that all data pack files are
directly saved in the ``data`` path, without the sub-directories listed
for the filesystem-based backends as listed in the previous section. The
layout looks like this:
::
/config
/data
├── 2159dd48f8a24f33c307b750592773f8b71ff8d11452132a7b2e2a6a01611be1
├── 32ea976bc30771cebad8285cd99120ac8786f9ffd42141d452458089985043a5
├── 59fe4bcde59bd6222eba87795e35a90d82cd2f138a27b6835032b7b58173a426
├── 73d04e6125cf3c28a299cc2f3cca3b78ceac396e4fcf9575e34536b26782413c
[...]
/index
├── c38f5fb68307c6a3e3aa945d556e325dc38f5fb68307c6a3e3aa945d556e325d
└── ca171b1b7394d90d330b265d90f506f9984043b342525f019788f97e745c71fd
/keys
└── b02de829beeb3c01a63e6b25cbd421a98fef144f03b9a02e46eff9e2ca3f0bd7
/locks
/snapshots
└── 22a5af1bdc6e616f8a29579458c49627e01b32210d09adb288d1ecda7c5711ec
Unfortunately during development the s3 backend uses slightly different
paths (directory names use singular instead of plural for ``key``, paths (directory names use singular instead of plural for ``key``,
``lock``, and ``snapshot`` files), for s3 the repository layout looks ``lock``, and ``snapshot`` files), and the data files are stored directly below
like this: the ``data`` directory. The S3 Legacy repository layout looks like this:
:: ::
@ -174,8 +150,8 @@ like this:
/snapshot /snapshot
└── 22a5af1bdc6e616f8a29579458c49627e01b32210d09adb288d1ecda7c5711ec └── 22a5af1bdc6e616f8a29579458c49627e01b32210d09adb288d1ecda7c5711ec
The s3 backend understands and accepts both forms, new backends are The S3 backend understands and accepts both forms, new backends are
always created with the former layout for compatibility reasons. always created with the default layout for compatibility reasons.
Pack Format Pack Format
----------- -----------

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"io/ioutil"
"testing" "testing"
) )
@ -13,6 +14,7 @@ func TestFlags(t *testing.T) {
for _, cmd := range cmdRoot.Commands() { for _, cmd := range cmdRoot.Commands() {
t.Run(cmd.Name(), func(t *testing.T) { t.Run(cmd.Name(), func(t *testing.T) {
cmd.Flags().SetOutput(ioutil.Discard)
err := cmd.ParseFlags([]string{"--help"}) err := cmd.ParseFlags([]string{"--help"})
if err.Error() == "pflag: help requested" { if err.Error() == "pflag: help requested" {
err = nil err = nil

View File

@ -12,12 +12,10 @@ func TestRestoreLocalLayout(t *testing.T) {
filename string filename string
layout string layout string
}{ }{
{"repo-layout-cloud.tar.gz", ""}, {"repo-layout-default.tar.gz", ""},
{"repo-layout-local.tar.gz", ""}, {"repo-layout-s3legacy.tar.gz", ""},
{"repo-layout-s3-old.tar.gz", ""}, {"repo-layout-default.tar.gz", "default"},
{"repo-layout-cloud.tar.gz", "cloud"}, {"repo-layout-s3legacy.tar.gz", "s3legacy"},
{"repo-layout-local.tar.gz", "default"},
{"repo-layout-s3-old.tar.gz", "s3"},
} }
for _, test := range tests { for _, test := range tests {

View File

@ -85,36 +85,6 @@ func hasBackendFile(fs Filesystem, dir string) (bool, error) {
return false, 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 && fs.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
}
// ErrLayoutDetectionFailed is returned by DetectLayout() when the layout // ErrLayoutDetectionFailed is returned by DetectLayout() when the layout
// cannot be detected automatically. // cannot be detected automatically.
var ErrLayoutDetectionFailed = errors.New("auto-detecting the filesystem layout failed") var ErrLayoutDetectionFailed = errors.New("auto-detecting the filesystem layout failed")
@ -128,39 +98,19 @@ func DetectLayout(repo Filesystem, dir string) (Layout, error) {
repo = &LocalFilesystem{} repo = &LocalFilesystem{}
} }
// key file in the "keys" dir (DefaultLayout or CloudLayout) // key file in the "keys" dir (DefaultLayout)
foundKeysFile, err := hasBackendFile(repo, repo.Join(dir, defaultLayoutPaths[restic.KeyFile])) foundKeysFile, err := hasBackendFile(repo, repo.Join(dir, defaultLayoutPaths[restic.KeyFile]))
if err != nil { if err != nil {
return nil, err return nil, err
} }
// key file in the "key" dir (S3Layout) // key file in the "key" dir (S3LegacyLayout)
foundKeyFile, err := hasBackendFile(repo, repo.Join(dir, s3LayoutPaths[restic.KeyFile])) foundKeyFile, err := hasBackendFile(repo, repo.Join(dir, s3LayoutPaths[restic.KeyFile]))
if err != nil { if err != nil {
return nil, err return nil, err
} }
// data file in "data" directory (S3Layout or CloudLayout) if foundKeysFile && !foundKeyFile {
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 {
debug.Log("found cloud layout at %v", dir)
return &CloudLayout{
Path: dir,
Join: repo.Join,
}, nil
}
if foundKeysFile && foundDataSubdirFile && !foundKeyFile && !foundDataFile {
debug.Log("found default layout at %v", dir) debug.Log("found default layout at %v", dir)
return &DefaultLayout{ return &DefaultLayout{
Path: dir, Path: dir,
@ -168,9 +118,9 @@ func DetectLayout(repo Filesystem, dir string) (Layout, error) {
}, nil }, nil
} }
if foundKeyFile && foundDataFile && !foundKeysFile && !foundDataSubdirFile { if foundKeyFile && !foundKeysFile {
debug.Log("found s3 layout at %v", dir) debug.Log("found s3 layout at %v", dir)
return &S3Layout{ return &S3LegacyLayout{
Path: dir, Path: dir,
Join: repo.Join, Join: repo.Join,
}, nil }, nil
@ -190,13 +140,8 @@ func ParseLayout(repo Filesystem, layout, defaultLayout, path string) (l Layout,
Path: path, Path: path,
Join: repo.Join, Join: repo.Join,
} }
case "cloud": case "s3legacy":
l = &CloudLayout{ l = &S3LegacyLayout{
Path: path,
Join: repo.Join,
}
case "s3":
l = &S3Layout{
Path: path, Path: path,
Join: repo.Join, Join: repo.Join,
} }
@ -205,7 +150,7 @@ func ParseLayout(repo Filesystem, layout, defaultLayout, path string) (l Layout,
// use the default layout if auto detection failed // use the default layout if auto detection failed
if errors.Cause(err) == ErrLayoutDetectionFailed && defaultLayout != "" { if errors.Cause(err) == ErrLayoutDetectionFailed && defaultLayout != "" {
debug.Log("error: %v, use default layout %v", defaultLayout) debug.Log("error: %v, use default layout %v", err, defaultLayout)
return ParseLayout(repo, defaultLayout, "", path) return ParseLayout(repo, defaultLayout, "", path)
} }
@ -214,7 +159,7 @@ func ParseLayout(repo Filesystem, layout, defaultLayout, path string) (l Layout,
} }
debug.Log("layout detected: %v", l) debug.Log("layout detected: %v", l)
default: default:
return nil, errors.Errorf("unknown backend layout string %q, may be one of: default, cloud, s3", layout) return nil, errors.Errorf("unknown backend layout string %q, may be one of: default, s3legacy", layout)
} }
return l, nil return l, nil

View File

@ -1,46 +0,0 @@
package backend
import "restic"
// CloudLayout implements the default layout for cloud storage backends, as
// described in the Design document.
type CloudLayout struct {
URL string
Path string
Join func(...string) string
}
var cloudLayoutPaths = defaultLayoutPaths
// Dirname returns the directory path for a given file type and name.
func (l *CloudLayout) Dirname(h restic.Handle) string {
if h.Type == restic.ConfigFile {
return l.URL + l.Join(l.Path, "/")
}
return l.URL + l.Join(l.Path, "/", cloudLayoutPaths[h.Type]) + "/"
}
// Filename returns a path to a file, including its name.
func (l *CloudLayout) Filename(h restic.Handle) string {
name := h.Name
if h.Type == restic.ConfigFile {
name = "config"
}
return l.URL + l.Join(l.Path, "/", cloudLayoutPaths[h.Type], name)
}
// Paths returns all directory names
func (l *CloudLayout) Paths() (dirs []string) {
for _, p := range cloudLayoutPaths {
dirs = append(dirs, l.URL+l.Join(l.Path, p))
}
return dirs
}
// Basedir returns the base dir name for files of type t.
func (l *CloudLayout) Basedir(t restic.FileType) string {
return l.URL + l.Join(l.Path, cloudLayoutPaths[t])
}

View File

@ -24,10 +24,10 @@ func (l *DefaultLayout) Dirname(h restic.Handle) string {
p := defaultLayoutPaths[h.Type] p := defaultLayoutPaths[h.Type]
if h.Type == restic.DataFile && len(h.Name) > 2 { if h.Type == restic.DataFile && len(h.Name) > 2 {
p = l.Join(p, h.Name[:2]) p = l.Join(p, h.Name[:2]) + "/"
} }
return l.Join(l.Path, p) return l.Join(l.Path, p) + "/"
} }
// Filename returns a path to a file, including its name. // Filename returns a path to a file, including its name.

View File

@ -0,0 +1,45 @@
package backend
import "restic"
// RESTLayout implements the default layout for the REST protocol.
type RESTLayout struct {
URL string
Path string
Join func(...string) string
}
var restLayoutPaths = defaultLayoutPaths
// Dirname returns the directory path for a given file type and name.
func (l *RESTLayout) Dirname(h restic.Handle) string {
if h.Type == restic.ConfigFile {
return l.URL + l.Join(l.Path, "/")
}
return l.URL + l.Join(l.Path, "/", restLayoutPaths[h.Type]) + "/"
}
// Filename returns a path to a file, including its name.
func (l *RESTLayout) Filename(h restic.Handle) string {
name := h.Name
if h.Type == restic.ConfigFile {
name = "config"
}
return l.URL + l.Join(l.Path, "/", restLayoutPaths[h.Type], name)
}
// Paths returns all directory names
func (l *RESTLayout) Paths() (dirs []string) {
for _, p := range restLayoutPaths {
dirs = append(dirs, l.URL+l.Join(l.Path, p))
}
return dirs
}
// Basedir returns the base dir name for files of type t.
func (l *RESTLayout) Basedir(t restic.FileType) string {
return l.URL + l.Join(l.Path, restLayoutPaths[t])
}

View File

@ -2,9 +2,9 @@ package backend
import "restic" import "restic"
// S3Layout implements the old layout used for s3 cloud storage backends, as // S3LegacyLayout implements the old layout used for s3 cloud storage backends, as
// described in the Design document. // described in the Design document.
type S3Layout struct { type S3LegacyLayout struct {
URL string URL string
Path string Path string
Join func(...string) string Join func(...string) string
@ -19,7 +19,7 @@ var s3LayoutPaths = map[restic.FileType]string{
} }
// join calls Join with the first empty elements removed. // join calls Join with the first empty elements removed.
func (l *S3Layout) join(url string, items ...string) string { func (l *S3LegacyLayout) join(url string, items ...string) string {
for len(items) > 0 && items[0] == "" { for len(items) > 0 && items[0] == "" {
items = items[1:] items = items[1:]
} }
@ -35,7 +35,7 @@ func (l *S3Layout) join(url string, items ...string) string {
} }
// Dirname returns the directory path for a given file type and name. // Dirname returns the directory path for a given file type and name.
func (l *S3Layout) Dirname(h restic.Handle) string { func (l *S3LegacyLayout) Dirname(h restic.Handle) string {
if h.Type == restic.ConfigFile { if h.Type == restic.ConfigFile {
return l.URL + l.Join(l.Path, "/") return l.URL + l.Join(l.Path, "/")
} }
@ -44,7 +44,7 @@ func (l *S3Layout) Dirname(h restic.Handle) string {
} }
// Filename returns a path to a file, including its name. // Filename returns a path to a file, including its name.
func (l *S3Layout) Filename(h restic.Handle) string { func (l *S3LegacyLayout) Filename(h restic.Handle) string {
name := h.Name name := h.Name
if h.Type == restic.ConfigFile { if h.Type == restic.ConfigFile {
@ -55,7 +55,7 @@ func (l *S3Layout) Filename(h restic.Handle) string {
} }
// Paths returns all directory names // Paths returns all directory names
func (l *S3Layout) Paths() (dirs []string) { func (l *S3LegacyLayout) Paths() (dirs []string) {
for _, p := range s3LayoutPaths { for _, p := range s3LayoutPaths {
dirs = append(dirs, l.Join(l.Path, p)) dirs = append(dirs, l.Join(l.Path, p))
} }
@ -63,6 +63,6 @@ func (l *S3Layout) Paths() (dirs []string) {
} }
// Basedir returns the base dir name for type t. // Basedir returns the base dir name for type t.
func (l *S3Layout) Basedir(t restic.FileType) string { func (l *S3LegacyLayout) Basedir(t restic.FileType) string {
return l.Join(l.Path, s3LayoutPaths[t]) return l.Join(l.Path, s3LayoutPaths[t])
} }

View File

@ -79,7 +79,7 @@ func TestDefaultLayout(t *testing.T) {
} }
} }
func TestCloudLayout(t *testing.T) { func TestRESTLayout(t *testing.T) {
path, cleanup := TempDir(t) path, cleanup := TempDir(t)
defer cleanup() defer cleanup()
@ -113,7 +113,7 @@ func TestCloudLayout(t *testing.T) {
}, },
} }
l := &CloudLayout{ l := &RESTLayout{
Path: path, Path: path,
Join: filepath.Join, Join: filepath.Join,
} }
@ -147,7 +147,7 @@ func TestCloudLayout(t *testing.T) {
} }
} }
func TestCloudLayoutURLs(t *testing.T) { func TestRESTLayoutURLs(t *testing.T) {
var tests = []struct { var tests = []struct {
l Layout l Layout
h restic.Handle h restic.Handle
@ -155,55 +155,55 @@ func TestCloudLayoutURLs(t *testing.T) {
dir string dir string
}{ }{
{ {
&CloudLayout{URL: "https://hostname.foo", Path: "", Join: path.Join}, &RESTLayout{URL: "https://hostname.foo", Path: "", Join: path.Join},
restic.Handle{Type: restic.DataFile, Name: "foobar"}, restic.Handle{Type: restic.DataFile, Name: "foobar"},
"https://hostname.foo/data/foobar", "https://hostname.foo/data/foobar",
"https://hostname.foo/data/", "https://hostname.foo/data/",
}, },
{ {
&CloudLayout{URL: "https://hostname.foo:1234/prefix/repo", Path: "/", Join: path.Join}, &RESTLayout{URL: "https://hostname.foo:1234/prefix/repo", Path: "/", Join: path.Join},
restic.Handle{Type: restic.LockFile, Name: "foobar"}, restic.Handle{Type: restic.LockFile, Name: "foobar"},
"https://hostname.foo:1234/prefix/repo/locks/foobar", "https://hostname.foo:1234/prefix/repo/locks/foobar",
"https://hostname.foo:1234/prefix/repo/locks/", "https://hostname.foo:1234/prefix/repo/locks/",
}, },
{ {
&CloudLayout{URL: "https://hostname.foo:1234/prefix/repo", Path: "/", Join: path.Join}, &RESTLayout{URL: "https://hostname.foo:1234/prefix/repo", Path: "/", Join: path.Join},
restic.Handle{Type: restic.ConfigFile, Name: "foobar"}, restic.Handle{Type: restic.ConfigFile, Name: "foobar"},
"https://hostname.foo:1234/prefix/repo/config", "https://hostname.foo:1234/prefix/repo/config",
"https://hostname.foo:1234/prefix/repo/", "https://hostname.foo:1234/prefix/repo/",
}, },
{ {
&S3Layout{URL: "https://hostname.foo", Path: "/", Join: path.Join}, &S3LegacyLayout{URL: "https://hostname.foo", Path: "/", Join: path.Join},
restic.Handle{Type: restic.DataFile, Name: "foobar"}, restic.Handle{Type: restic.DataFile, Name: "foobar"},
"https://hostname.foo/data/foobar", "https://hostname.foo/data/foobar",
"https://hostname.foo/data/", "https://hostname.foo/data/",
}, },
{ {
&S3Layout{URL: "https://hostname.foo:1234/prefix/repo", Path: "", Join: path.Join}, &S3LegacyLayout{URL: "https://hostname.foo:1234/prefix/repo", Path: "", Join: path.Join},
restic.Handle{Type: restic.LockFile, Name: "foobar"}, restic.Handle{Type: restic.LockFile, Name: "foobar"},
"https://hostname.foo:1234/prefix/repo/lock/foobar", "https://hostname.foo:1234/prefix/repo/lock/foobar",
"https://hostname.foo:1234/prefix/repo/lock/", "https://hostname.foo:1234/prefix/repo/lock/",
}, },
{ {
&S3Layout{URL: "https://hostname.foo:1234/prefix/repo", Path: "/", Join: path.Join}, &S3LegacyLayout{URL: "https://hostname.foo:1234/prefix/repo", Path: "/", Join: path.Join},
restic.Handle{Type: restic.ConfigFile, Name: "foobar"}, restic.Handle{Type: restic.ConfigFile, Name: "foobar"},
"https://hostname.foo:1234/prefix/repo/config", "https://hostname.foo:1234/prefix/repo/config",
"https://hostname.foo:1234/prefix/repo/", "https://hostname.foo:1234/prefix/repo/",
}, },
{ {
&S3Layout{URL: "", Path: "", Join: path.Join}, &S3LegacyLayout{URL: "", Path: "", Join: path.Join},
restic.Handle{Type: restic.DataFile, Name: "foobar"}, restic.Handle{Type: restic.DataFile, Name: "foobar"},
"data/foobar", "data/foobar",
"data/", "data/",
}, },
{ {
&S3Layout{URL: "", Path: "", Join: path.Join}, &S3LegacyLayout{URL: "", Path: "", Join: path.Join},
restic.Handle{Type: restic.LockFile, Name: "foobar"}, restic.Handle{Type: restic.LockFile, Name: "foobar"},
"lock/foobar", "lock/foobar",
"lock/", "lock/",
}, },
{ {
&S3Layout{URL: "", Path: "/", Join: path.Join}, &S3LegacyLayout{URL: "", Path: "/", Join: path.Join},
restic.Handle{Type: restic.ConfigFile, Name: "foobar"}, restic.Handle{Type: restic.ConfigFile, Name: "foobar"},
"/config", "/config",
"/", "/",
@ -225,7 +225,7 @@ func TestCloudLayoutURLs(t *testing.T) {
} }
} }
func TestS3Layout(t *testing.T) { func TestS3LegacyLayout(t *testing.T) {
path, cleanup := TempDir(t) path, cleanup := TempDir(t)
defer cleanup() defer cleanup()
@ -259,7 +259,7 @@ func TestS3Layout(t *testing.T) {
}, },
} }
l := &S3Layout{ l := &S3LegacyLayout{
Path: path, Path: path,
Join: filepath.Join, Join: filepath.Join,
} }
@ -301,9 +301,8 @@ func TestDetectLayout(t *testing.T) {
filename string filename string
want string want string
}{ }{
{"repo-layout-local.tar.gz", "*backend.DefaultLayout"}, {"repo-layout-default.tar.gz", "*backend.DefaultLayout"},
{"repo-layout-cloud.tar.gz", "*backend.CloudLayout"}, {"repo-layout-s3legacy.tar.gz", "*backend.S3LegacyLayout"},
{"repo-layout-s3-old.tar.gz", "*backend.S3Layout"},
} }
var fs = &LocalFilesystem{} var fs = &LocalFilesystem{}
@ -342,12 +341,11 @@ func TestParseLayout(t *testing.T) {
want string want string
}{ }{
{"default", "", "*backend.DefaultLayout"}, {"default", "", "*backend.DefaultLayout"},
{"cloud", "", "*backend.CloudLayout"}, {"s3legacy", "", "*backend.S3LegacyLayout"},
{"s3", "", "*backend.S3Layout"}, {"", "", "*backend.DefaultLayout"},
{"", "", "*backend.CloudLayout"},
} }
SetupTarTestFixture(t, path, filepath.Join("testdata", "repo-layout-cloud.tar.gz")) SetupTarTestFixture(t, path, filepath.Join("testdata", "repo-layout-default.tar.gz"))
for _, test := range tests { for _, test := range tests {
t.Run(test.layoutName, func(t *testing.T) { t.Run(test.layoutName, func(t *testing.T) {

View File

@ -17,17 +17,12 @@ func TestLayout(t *testing.T) {
failureExpected bool failureExpected bool
datafiles map[string]bool datafiles map[string]bool
}{ }{
{"repo-layout-local.tar.gz", "", false, map[string]bool{ {"repo-layout-default.tar.gz", "", false, map[string]bool{
"aa464e9fd598fe4202492ee317ffa728e82fa83a1de1a61996e5bd2d6651646c": false, "aa464e9fd598fe4202492ee317ffa728e82fa83a1de1a61996e5bd2d6651646c": false,
"fc919a3b421850f6fa66ad22ebcf91e433e79ffef25becf8aef7c7b1eca91683": false, "fc919a3b421850f6fa66ad22ebcf91e433e79ffef25becf8aef7c7b1eca91683": false,
"c089d62788da14f8b7cbf77188305c0874906f0b73d3fce5a8869050e8d0c0e1": false, "c089d62788da14f8b7cbf77188305c0874906f0b73d3fce5a8869050e8d0c0e1": false,
}}, }},
{"repo-layout-cloud.tar.gz", "", false, map[string]bool{ {"repo-layout-s3legacy.tar.gz", "", false, map[string]bool{
"fc919a3b421850f6fa66ad22ebcf91e433e79ffef25becf8aef7c7b1eca91683": false,
"c089d62788da14f8b7cbf77188305c0874906f0b73d3fce5a8869050e8d0c0e1": false,
"aa464e9fd598fe4202492ee317ffa728e82fa83a1de1a61996e5bd2d6651646c": false,
}},
{"repo-layout-s3-old.tar.gz", "", false, map[string]bool{
"fc919a3b421850f6fa66ad22ebcf91e433e79ffef25becf8aef7c7b1eca91683": false, "fc919a3b421850f6fa66ad22ebcf91e433e79ffef25becf8aef7c7b1eca91683": false,
"c089d62788da14f8b7cbf77188305c0874906f0b73d3fce5a8869050e8d0c0e1": false, "c089d62788da14f8b7cbf77188305c0874906f0b73d3fce5a8869050e8d0c0e1": false,
"aa464e9fd598fe4202492ee317ffa728e82fa83a1de1a61996e5bd2d6651646c": false, "aa464e9fd598fe4202492ee317ffa728e82fa83a1de1a61996e5bd2d6651646c": false,

View File

@ -48,7 +48,7 @@ func Open(cfg Config) (restic.Backend, error) {
url: cfg.URL, url: cfg.URL,
connChan: connChan, connChan: connChan,
client: client, client: client,
Layout: &backend.CloudLayout{URL: url, Join: path.Join}, Layout: &backend.RESTLayout{URL: url, Join: path.Join},
} }
return be, nil return be, nil

View File

@ -6,6 +6,7 @@ import (
"strings" "strings"
"restic/errors" "restic/errors"
"restic/options"
) )
// Config contains all configuration necessary to connect to an s3 compatible // Config contains all configuration necessary to connect to an s3 compatible
@ -16,6 +17,11 @@ type Config struct {
KeyID, Secret string KeyID, Secret string
Bucket string Bucket string
Prefix string Prefix string
Layout string `option:"layout" help:"use this backend layout (default: auto-detect)"`
}
func init() {
options.Register("s3", Config{})
} }
const defaultPrefix = "restic" const defaultPrefix = "restic"

View File

@ -8,6 +8,7 @@ import (
"restic" "restic"
"strings" "strings"
"sync" "sync"
"time"
"restic/backend" "restic/backend"
"restic/errors" "restic/errors"
@ -30,6 +31,8 @@ type s3 struct {
backend.Layout backend.Layout
} }
const defaultLayout = "s3legacy"
// Open opens the S3 backend at bucket and region. The bucket is created if it // Open opens the S3 backend at bucket and region. The bucket is created if it
// does not exist yet. // does not exist yet.
func Open(cfg Config) (restic.Backend, error) { func Open(cfg Config) (restic.Backend, error) {
@ -45,11 +48,17 @@ func Open(cfg Config) (restic.Backend, error) {
bucketname: cfg.Bucket, bucketname: cfg.Bucket,
prefix: cfg.Prefix, prefix: cfg.Prefix,
cacheObjSize: make(map[string]int64), cacheObjSize: make(map[string]int64),
Layout: &backend.S3Layout{Path: cfg.Prefix, Join: path.Join},
} }
client.SetCustomTransport(backend.Transport()) client.SetCustomTransport(backend.Transport())
l, err := backend.ParseLayout(be, cfg.Layout, defaultLayout, cfg.Prefix)
if err != nil {
return nil, err
}
be.Layout = l
be.createConnections() be.createConnections()
found, err := client.BucketExists(cfg.Bucket) found, err := client.BucketExists(cfg.Bucket)
@ -76,6 +85,77 @@ func (be *s3) createConnections() {
} }
} }
// IsNotExist returns true if the error is caused by a not existing file.
func (be *s3) IsNotExist(err error) bool {
debug.Log("IsNotExist(%T, %#v)", err, err)
if os.IsNotExist(err) {
return true
}
return false
}
// Join combines path components with slashes.
func (be *s3) Join(p ...string) string {
return path.Join(p...)
}
type fileInfo struct {
name string
size int64
mode os.FileMode
modTime time.Time
isDir bool
}
func (fi fileInfo) Name() string { return fi.name } // base name of the file
func (fi fileInfo) Size() int64 { return fi.size } // length in bytes for regular files; system-dependent for others
func (fi fileInfo) Mode() os.FileMode { return fi.mode } // file mode bits
func (fi fileInfo) ModTime() time.Time { return fi.modTime } // modification time
func (fi fileInfo) IsDir() bool { return fi.isDir } // abbreviation for Mode().IsDir()
func (fi fileInfo) Sys() interface{} { return nil } // underlying data source (can return nil)
// ReadDir returns the entries for a directory.
func (be *s3) ReadDir(dir string) (list []os.FileInfo, err error) {
debug.Log("ReadDir(%v)", dir)
// make sure dir ends with a slash
if dir[len(dir)-1] != '/' {
dir += "/"
}
done := make(chan struct{})
defer close(done)
for obj := range be.client.ListObjects(be.bucketname, dir, false, done) {
if obj.Key == "" {
continue
}
name := strings.TrimPrefix(obj.Key, dir)
if name == "" {
return nil, errors.Errorf("invalid key name %v, removing prefix %v yielded empty string", obj.Key, dir)
}
entry := fileInfo{
name: name,
size: obj.Size,
modTime: obj.LastModified,
}
if name[len(name)-1] == '/' {
entry.isDir = true
entry.mode = os.ModeDir | 0755
entry.name = name[:len(name)-1]
} else {
entry.mode = 0644
}
list = append(list, entry)
}
return list, nil
}
// Location returns this backend's location (the bucket name). // Location returns this backend's location (the bucket name).
func (be *s3) Location() string { func (be *s3) Location() string {
return be.bucketname return be.bucketname
@ -290,6 +370,11 @@ func (be *s3) List(t restic.FileType, done <-chan struct{}) <-chan string {
prefix := be.Dirname(restic.Handle{Type: t}) prefix := be.Dirname(restic.Handle{Type: t})
// make sure prefix ends with a slash
if prefix[len(prefix)-1] != '/' {
prefix += "/"
}
listresp := be.client.ListObjects(be.bucketname, prefix, true, done) listresp := be.client.ListObjects(be.bucketname, prefix, true, done)
go func() { go func() {

View File

@ -23,17 +23,12 @@ func TestLayout(t *testing.T) {
failureExpected bool failureExpected bool
datafiles map[string]bool datafiles map[string]bool
}{ }{
{"repo-layout-local.tar.gz", "", false, map[string]bool{ {"repo-layout-default.tar.gz", "", false, map[string]bool{
"aa464e9fd598fe4202492ee317ffa728e82fa83a1de1a61996e5bd2d6651646c": false, "aa464e9fd598fe4202492ee317ffa728e82fa83a1de1a61996e5bd2d6651646c": false,
"fc919a3b421850f6fa66ad22ebcf91e433e79ffef25becf8aef7c7b1eca91683": false, "fc919a3b421850f6fa66ad22ebcf91e433e79ffef25becf8aef7c7b1eca91683": false,
"c089d62788da14f8b7cbf77188305c0874906f0b73d3fce5a8869050e8d0c0e1": false, "c089d62788da14f8b7cbf77188305c0874906f0b73d3fce5a8869050e8d0c0e1": false,
}}, }},
{"repo-layout-cloud.tar.gz", "", false, map[string]bool{ {"repo-layout-s3legacy.tar.gz", "", false, map[string]bool{
"fc919a3b421850f6fa66ad22ebcf91e433e79ffef25becf8aef7c7b1eca91683": false,
"c089d62788da14f8b7cbf77188305c0874906f0b73d3fce5a8869050e8d0c0e1": false,
"aa464e9fd598fe4202492ee317ffa728e82fa83a1de1a61996e5bd2d6651646c": false,
}},
{"repo-layout-s3-old.tar.gz", "", false, map[string]bool{
"fc919a3b421850f6fa66ad22ebcf91e433e79ffef25becf8aef7c7b1eca91683": false, "fc919a3b421850f6fa66ad22ebcf91e433e79ffef25becf8aef7c7b1eca91683": false,
"c089d62788da14f8b7cbf77188305c0874906f0b73d3fce5a8869050e8d0c0e1": false, "c089d62788da14f8b7cbf77188305c0874906f0b73d3fce5a8869050e8d0c0e1": false,
"aa464e9fd598fe4202492ee317ffa728e82fa83a1de1a61996e5bd2d6651646c": false, "aa464e9fd598fe4202492ee317ffa728e82fa83a1de1a61996e5bd2d6651646c": false,