package s3 import ( "bytes" "errors" "io" "os" "strings" "github.com/minio/minio-go" "github.com/restic/restic/backend" ) const maxKeysInList = 1000 const connLimit = 10 const backendPrefix = "restic" func s3path(t backend.Type, name string) string { if t == backend.Config { return backendPrefix + "/" + string(t) } return backendPrefix + "/" + string(t) + "/" + name } type S3Backend struct { s3api minio.API connChan chan struct{} bucketname string } // Open opens the S3 backend at bucket and region. func Open(regionname, bucketname string) (backend.Backend, error) { config := minio.Config{ AccessKeyID: os.Getenv("AWS_ACCESS_KEY_ID"), SecretAccessKey: os.Getenv("AWS_SECRET_ACCESS_KEY"), } if !strings.Contains(regionname, ".") { // Amazon region name switch regionname { case "us-east-1": config.Endpoint = "https://s3.amazonaws.com" default: config.Endpoint = "https://s3-" + regionname + ".amazonaws.com" } } else { // S3 compatible endpoint if strings.Contains(regionname, "localhost") || strings.Contains(regionname, "127.0.0.1") { config.Endpoint = "http://" + regionname } else { config.Endpoint = "https://" + regionname } } s3api, s3err := minio.New(config) if s3err != nil { return nil, s3err } connChan := make(chan struct{}, connLimit) for i := 0; i < connLimit; i++ { connChan <- struct{}{} } return &S3Backend{s3api: s3api, bucketname: bucketname, connChan: connChan}, nil } // Location returns this backend's location (the bucket name). func (be *S3Backend) Location() string { return be.bucketname } type s3Blob struct { b *S3Backend 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) // Check key does not already exist _, err := bb.b.s3api.StatObject(bb.b.bucketname, path) if err == nil { return errors.New("key already exists!") } <-bb.b.connChan err = bb.b.s3api.PutObject(bb.b.bucketname, path, "binary/octet-stream", int64(bb.buf.Len()), bb.buf) bb.b.connChan <- struct{}{} 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 (be *S3Backend) Create() (backend.Blob, error) { blob := s3Blob{ b: be, buf: &bytes.Buffer{}, } return &blob, nil } // Get returns a reader that yields the content stored under the given // name. The reader should be closed after draining it. func (be *S3Backend) Get(t backend.Type, name string) (io.ReadCloser, error) { path := s3path(t, name) rc, _, err := be.s3api.GetObject(be.bucketname, path) return rc, err } // 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 (be *S3Backend) GetReader(t backend.Type, name string, offset, length uint) (io.ReadCloser, error) { path := s3path(t, name) rc, _, err := be.s3api.GetPartialObject(be.bucketname, path, int64(offset), int64(length)) return rc, err } // Test returns true if a blob of the given type and name exists in the backend. func (be *S3Backend) Test(t backend.Type, name string) (bool, error) { found := false path := s3path(t, name) _, err := be.s3api.StatObject(be.bucketname, path) if err == nil { found = true } // If error, then not found return found, nil } // Remove removes the blob with the given name and type. func (be *S3Backend) Remove(t backend.Type, name string) error { path := s3path(t, name) return be.s3api.RemoveObject(be.bucketname, 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 (be *S3Backend) List(t backend.Type, done <-chan struct{}) <-chan string { ch := make(chan string) prefix := s3path(t, "") listresp := be.s3api.ListObjects(be.bucketname, prefix, true) go func() { defer close(ch) for obj := range listresp { m := strings.TrimPrefix(obj.Stat.Key, prefix) if m == "" { continue } select { case ch <- m: case <-done: return } } }() return ch } // Remove keys for a specified backend type func (be *S3Backend) removeKeys(t backend.Type) { doneChan := make(chan struct{}) for key := range be.List(backend.Data, doneChan) { be.Remove(backend.Data, key) } close(doneChan) } // Delete removes all restic keys func (be *S3Backend) Delete() error { be.removeKeys(backend.Data) be.removeKeys(backend.Key) be.removeKeys(backend.Lock) be.removeKeys(backend.Snapshot) be.removeKeys(backend.Index) be.removeKeys(backend.Config) return nil } // Close does nothing func (be *S3Backend) Close() error { return nil }