diff --git a/backend/s3/s3.go b/backend/s3/s3.go new file mode 100644 index 000000000..47ac21871 --- /dev/null +++ b/backend/s3/s3.go @@ -0,0 +1,200 @@ +package s3 + +import ( + "bytes" + "errors" + "io" + "strings" + "sync" + + "github.com/mitchellh/goamz/aws" + as3 "github.com/mitchellh/goamz/s3" + + "github.com/restic/restic/backend" +) + +var ErrWrongData = errors.New("wrong data returned by backend, checksum does not match") + +func s3path(t backend.Type, name string) string { + if t == backend.Config { + return string(t) + } + return string(t) + "/" + name +} + +type S3DB struct { + bucket *as3.Bucket + mput sync.Mutex + path string +} + +// Open opens the local backend at bucket and region. +func Open(regionname, bucketname string) (*S3DB, error) { + auth, err := aws.EnvAuth() + if err != nil { + return nil, err + } + + client := as3.New(auth, aws.Regions[regionname]) + + return &S3DB{bucket: client.Bucket(bucketname), path: bucketname}, nil +} + +// Location returns this backend's location (the directory name). +func (b *S3DB) Location() string { + return b.path +} + +type s3Blob struct { + b *S3DB + buf *bytes.Buffer + final bool +} + +func (bb *s3Blob) Write(p []byte) (int, error) { + if bb.final { + return 0, errors.New("blob already closed") + } + + n, err := bb.buf.Write(p) + return n, err +} + +func (bb *s3Blob) Read(p []byte) (int, error) { + return bb.buf.Read(p) +} + +func (bb *s3Blob) Close() error { + bb.final = true + bb.buf.Reset() + return nil +} + +func (bb *s3Blob) Size() uint { + return uint(bb.buf.Len()) +} + +func (bb *s3Blob) Finalize(t backend.Type, name string) error { + if bb.final { + return errors.New("Already finalized") + } + + bb.final = true + + path := s3path(t, name) + bb.b.mput.Lock() + err := bb.b.bucket.Put(path, bb.buf.Bytes(), "binary/octet-stream", "private") + bb.b.mput.Unlock() + bb.buf.Reset() + return err +} + +// Create creates a new Blob. The data is available only after Finalize() +// has been called on the returned Blob. +func (b *S3DB) Create() (backend.Blob, error) { + blob := s3Blob{ + b: b, + buf: &bytes.Buffer{}, + } + + return &blob, nil +} + +func (b *S3DB) get(t backend.Type, name string) (*s3Blob, error) { + blob := &s3Blob{ + b: b, + } + + path := s3path(t, name) + data, err := b.bucket.Get(path) + blob.buf = bytes.NewBuffer(data) + return blob, err +} + +// Get returns a reader that yields the content stored under the given +// name. The reader should be closed after draining it. +func (b *S3DB) Get(t backend.Type, name string) (io.ReadCloser, error) { + return b.get(t, name) +} + +// GetReader returns an io.ReadCloser for the Blob with the given name of +// type t at offset and length. If length is 0, the reader reads until EOF. +func (b *S3DB) GetReader(t backend.Type, name string, offset, length uint) (io.ReadCloser, error) { + blob, err := b.get(t, name) + if err != nil { + return nil, err + } + + blob.buf.Next(int(offset)) + + if length == 0 { + return blob, nil + } + + return backend.LimitReadCloser(blob, int64(length)), nil +} + +// Test returns true if a blob of the given type and name exists in the backend. +func (b *S3DB) Test(t backend.Type, name string) (bool, error) { + found := false + path := s3path(t, name) + key, err := b.bucket.GetKey(path) + if err == nil && key.Key == path { + found = true + } + + // If error, then not found + return found, nil +} + +// Remove removes the blob with the given name and type. +func (b *S3DB) Remove(t backend.Type, name string) error { + path := s3path(t, name) + return b.bucket.Del(path) +} + +// List returns a channel that yields all names of blobs of type t. A +// goroutine is started for this. If the channel done is closed, sending +// stops. +func (b *S3DB) List(t backend.Type, done <-chan struct{}) <-chan string { + ch := make(chan string) + + prefix := string(t) + "/" + + listresp, err := b.bucket.List(prefix, "/", "", 10000) + + if err != nil { + close(ch) + return ch + } + + matches := make([]string, len(listresp.Contents)) + for idx, key := range listresp.Contents { + matches[idx] = strings.TrimPrefix(key.Key, prefix) + } + + go func() { + defer close(ch) + for _, m := range matches { + if m == "" { + continue + } + + select { + case ch <- m: + case <-done: + return + } + } + }() + + return ch +} + +// Delete removes the repository and all files. +func (b *S3DB) Delete() error { + return b.bucket.DelBucket() +} + +// Close does nothing +func (b *S3DB) Close() error { return nil } diff --git a/cmd/restic/main.go b/cmd/restic/main.go index b34755c84..86baa34ab 100644 --- a/cmd/restic/main.go +++ b/cmd/restic/main.go @@ -12,6 +12,7 @@ import ( "github.com/jessevdk/go-flags" "github.com/restic/restic/backend" "github.com/restic/restic/backend/local" + "github.com/restic/restic/backend/s3" "github.com/restic/restic/backend/sftp" "github.com/restic/restic/debug" "github.com/restic/restic/repository" @@ -119,6 +120,7 @@ func (cmd CmdInit) Execute(args []string) error { // Open the backend specified by URI. // Valid formats are: // * /foo/bar -> local repository at /foo/bar +// * s3://region/bucket -> amazon s3 bucket // * sftp://user@host/foo/bar -> remote sftp repository on host for user at path foo/bar // * sftp://host//tmp/backup -> remote sftp repository on host at path /tmp/backup func open(u string) (backend.Backend, error) { @@ -129,6 +131,8 @@ func open(u string) (backend.Backend, error) { if url.Scheme == "" { return local.Open(url.Path) + } else if url.Scheme == "s3" { + return s3.Open(url.Host, url.Path[1:]) } args := []string{url.Host} @@ -150,6 +154,8 @@ func create(u string) (backend.Backend, error) { if url.Scheme == "" { return local.Create(url.Path) + } else if url.Scheme == "s3" { + return s3.Open(url.Host, url.Path[1:]) } args := []string{url.Host}