github.com/advanderveer/restic@v0.8.1-0.20171209104529-42a8c19aaea6/internal/backend/s3/s3.go (about)

     1  package s3
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"os"
     9  	"path"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/restic/restic/internal/backend"
    14  	"github.com/restic/restic/internal/errors"
    15  	"github.com/restic/restic/internal/restic"
    16  
    17  	"github.com/minio/minio-go"
    18  	"github.com/minio/minio-go/pkg/credentials"
    19  
    20  	"github.com/restic/restic/internal/debug"
    21  )
    22  
    23  // Backend stores data on an S3 endpoint.
    24  type Backend struct {
    25  	client *minio.Client
    26  	sem    *backend.Semaphore
    27  	cfg    Config
    28  	backend.Layout
    29  }
    30  
    31  // make sure that *Backend implements backend.Backend
    32  var _ restic.Backend = &Backend{}
    33  
    34  const defaultLayout = "default"
    35  
    36  func open(cfg Config, rt http.RoundTripper) (*Backend, error) {
    37  	debug.Log("open, config %#v", cfg)
    38  
    39  	if cfg.MaxRetries > 0 {
    40  		minio.MaxRetry = int(cfg.MaxRetries)
    41  	}
    42  
    43  	// Chains all credential types, starting with
    44  	// Static credentials provided by user.
    45  	// IAM profile based credentials. (performs an HTTP
    46  	// call to a pre-defined endpoint, only valid inside
    47  	// configured ec2 instances)
    48  	// AWS env variables such as AWS_ACCESS_KEY_ID
    49  	// Minio env variables such as MINIO_ACCESS_KEY
    50  	creds := credentials.NewChainCredentials([]credentials.Provider{
    51  		&credentials.Static{
    52  			Value: credentials.Value{
    53  				AccessKeyID:     cfg.KeyID,
    54  				SecretAccessKey: cfg.Secret,
    55  			},
    56  		},
    57  		&credentials.IAM{
    58  			Client: &http.Client{
    59  				Transport: http.DefaultTransport,
    60  			},
    61  		},
    62  		&credentials.EnvAWS{},
    63  		&credentials.EnvMinio{},
    64  	})
    65  	client, err := minio.NewWithCredentials(cfg.Endpoint, creds, !cfg.UseHTTP, "")
    66  	if err != nil {
    67  		return nil, errors.Wrap(err, "minio.NewWithCredentials")
    68  	}
    69  
    70  	sem, err := backend.NewSemaphore(cfg.Connections)
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  
    75  	be := &Backend{
    76  		client: client,
    77  		sem:    sem,
    78  		cfg:    cfg,
    79  	}
    80  
    81  	client.SetCustomTransport(rt)
    82  
    83  	l, err := backend.ParseLayout(be, cfg.Layout, defaultLayout, cfg.Prefix)
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  
    88  	be.Layout = l
    89  
    90  	return be, nil
    91  }
    92  
    93  // Open opens the S3 backend at bucket and region. The bucket is created if it
    94  // does not exist yet.
    95  func Open(cfg Config, rt http.RoundTripper) (restic.Backend, error) {
    96  	return open(cfg, rt)
    97  }
    98  
    99  // Create opens the S3 backend at bucket and region and creates the bucket if
   100  // it does not exist yet.
   101  func Create(cfg Config, rt http.RoundTripper) (restic.Backend, error) {
   102  	be, err := open(cfg, rt)
   103  	if err != nil {
   104  		return nil, errors.Wrap(err, "open")
   105  	}
   106  	found, err := be.client.BucketExists(cfg.Bucket)
   107  	if err != nil {
   108  		debug.Log("BucketExists(%v) returned err %v", cfg.Bucket, err)
   109  		return nil, errors.Wrap(err, "client.BucketExists")
   110  	}
   111  
   112  	if !found {
   113  		// create new bucket with default ACL in default region
   114  		err = be.client.MakeBucket(cfg.Bucket, "")
   115  		if err != nil {
   116  			return nil, errors.Wrap(err, "client.MakeBucket")
   117  		}
   118  	}
   119  
   120  	return be, nil
   121  }
   122  
   123  // IsNotExist returns true if the error is caused by a not existing file.
   124  func (be *Backend) IsNotExist(err error) bool {
   125  	debug.Log("IsNotExist(%T, %#v)", err, err)
   126  	if os.IsNotExist(errors.Cause(err)) {
   127  		return true
   128  	}
   129  
   130  	if e, ok := errors.Cause(err).(minio.ErrorResponse); ok && e.Code == "NoSuchKey" {
   131  		return true
   132  	}
   133  
   134  	return false
   135  }
   136  
   137  // Join combines path components with slashes.
   138  func (be *Backend) Join(p ...string) string {
   139  	return path.Join(p...)
   140  }
   141  
   142  type fileInfo struct {
   143  	name    string
   144  	size    int64
   145  	mode    os.FileMode
   146  	modTime time.Time
   147  	isDir   bool
   148  }
   149  
   150  func (fi fileInfo) Name() string       { return fi.name }    // base name of the file
   151  func (fi fileInfo) Size() int64        { return fi.size }    // length in bytes for regular files; system-dependent for others
   152  func (fi fileInfo) Mode() os.FileMode  { return fi.mode }    // file mode bits
   153  func (fi fileInfo) ModTime() time.Time { return fi.modTime } // modification time
   154  func (fi fileInfo) IsDir() bool        { return fi.isDir }   // abbreviation for Mode().IsDir()
   155  func (fi fileInfo) Sys() interface{}   { return nil }        // underlying data source (can return nil)
   156  
   157  // ReadDir returns the entries for a directory.
   158  func (be *Backend) ReadDir(dir string) (list []os.FileInfo, err error) {
   159  	debug.Log("ReadDir(%v)", dir)
   160  
   161  	// make sure dir ends with a slash
   162  	if dir[len(dir)-1] != '/' {
   163  		dir += "/"
   164  	}
   165  
   166  	done := make(chan struct{})
   167  	defer close(done)
   168  
   169  	for obj := range be.client.ListObjects(be.cfg.Bucket, dir, false, done) {
   170  		if obj.Key == "" {
   171  			continue
   172  		}
   173  
   174  		name := strings.TrimPrefix(obj.Key, dir)
   175  		// Sometimes s3 returns an entry for the dir itself. Ignore it.
   176  		if name == "" {
   177  			continue
   178  		}
   179  		entry := fileInfo{
   180  			name:    name,
   181  			size:    obj.Size,
   182  			modTime: obj.LastModified,
   183  		}
   184  
   185  		if name[len(name)-1] == '/' {
   186  			entry.isDir = true
   187  			entry.mode = os.ModeDir | 0755
   188  			entry.name = name[:len(name)-1]
   189  		} else {
   190  			entry.mode = 0644
   191  		}
   192  
   193  		list = append(list, entry)
   194  	}
   195  
   196  	return list, nil
   197  }
   198  
   199  // Location returns this backend's location (the bucket name).
   200  func (be *Backend) Location() string {
   201  	return be.Join(be.cfg.Bucket, be.cfg.Prefix)
   202  }
   203  
   204  // Path returns the path in the bucket that is used for this backend.
   205  func (be *Backend) Path() string {
   206  	return be.cfg.Prefix
   207  }
   208  
   209  // lenForFile returns the length of the file.
   210  func lenForFile(f *os.File) (int64, error) {
   211  	fi, err := f.Stat()
   212  	if err != nil {
   213  		return 0, errors.Wrap(err, "Stat")
   214  	}
   215  
   216  	pos, err := f.Seek(0, io.SeekCurrent)
   217  	if err != nil {
   218  		return 0, errors.Wrap(err, "Seek")
   219  	}
   220  
   221  	size := fi.Size() - pos
   222  	return size, nil
   223  }
   224  
   225  // Save stores data in the backend at the handle.
   226  func (be *Backend) Save(ctx context.Context, h restic.Handle, rd io.Reader) (err error) {
   227  	debug.Log("Save %v", h)
   228  
   229  	if err := h.Valid(); err != nil {
   230  		return err
   231  	}
   232  
   233  	objName := be.Filename(h)
   234  
   235  	be.sem.GetToken()
   236  	defer be.sem.ReleaseToken()
   237  
   238  	// Check key does not already exist
   239  	_, err = be.client.StatObject(be.cfg.Bucket, objName, minio.StatObjectOptions{})
   240  	if err == nil {
   241  		debug.Log("%v already exists", h)
   242  		return errors.New("key already exists")
   243  	}
   244  
   245  	var size int64 = -1
   246  
   247  	type lenner interface {
   248  		Len() int
   249  	}
   250  
   251  	// find size for reader
   252  	if f, ok := rd.(*os.File); ok {
   253  		size, err = lenForFile(f)
   254  		if err != nil {
   255  			return err
   256  		}
   257  	} else if l, ok := rd.(lenner); ok {
   258  		size = int64(l.Len())
   259  	}
   260  
   261  	opts := minio.PutObjectOptions{}
   262  	opts.ContentType = "application/octet-stream"
   263  
   264  	debug.Log("PutObject(%v, %v, %v)", be.cfg.Bucket, objName, size)
   265  	n, err := be.client.PutObjectWithContext(ctx, be.cfg.Bucket, objName, ioutil.NopCloser(rd), size, opts)
   266  
   267  	debug.Log("%v -> %v bytes, err %#v: %v", objName, n, err, err)
   268  
   269  	return errors.Wrap(err, "client.PutObject")
   270  }
   271  
   272  // wrapReader wraps an io.ReadCloser to run an additional function on Close.
   273  type wrapReader struct {
   274  	io.ReadCloser
   275  	f func()
   276  }
   277  
   278  func (wr wrapReader) Close() error {
   279  	err := wr.ReadCloser.Close()
   280  	wr.f()
   281  	return err
   282  }
   283  
   284  // Load returns a reader that yields the contents of the file at h at the
   285  // given offset. If length is nonzero, only a portion of the file is
   286  // returned. rd must be closed after use.
   287  func (be *Backend) Load(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
   288  	debug.Log("Load %v, length %v, offset %v from %v", h, length, offset, be.Filename(h))
   289  	if err := h.Valid(); err != nil {
   290  		return nil, err
   291  	}
   292  
   293  	if offset < 0 {
   294  		return nil, errors.New("offset is negative")
   295  	}
   296  
   297  	if length < 0 {
   298  		return nil, errors.Errorf("invalid length %d", length)
   299  	}
   300  
   301  	objName := be.Filename(h)
   302  	opts := minio.GetObjectOptions{}
   303  
   304  	var err error
   305  	if length > 0 {
   306  		debug.Log("range: %v-%v", offset, offset+int64(length)-1)
   307  		err = opts.SetRange(offset, offset+int64(length)-1)
   308  	} else if offset > 0 {
   309  		debug.Log("range: %v-", offset)
   310  		err = opts.SetRange(offset, 0)
   311  	}
   312  
   313  	if err != nil {
   314  		return nil, errors.Wrap(err, "SetRange")
   315  	}
   316  
   317  	be.sem.GetToken()
   318  	coreClient := minio.Core{Client: be.client}
   319  	rd, err := coreClient.GetObjectWithContext(ctx, be.cfg.Bucket, objName, opts)
   320  	if err != nil {
   321  		be.sem.ReleaseToken()
   322  		return nil, err
   323  	}
   324  
   325  	closeRd := wrapReader{
   326  		ReadCloser: rd,
   327  		f: func() {
   328  			debug.Log("Close()")
   329  			be.sem.ReleaseToken()
   330  		},
   331  	}
   332  
   333  	return closeRd, err
   334  }
   335  
   336  // Stat returns information about a blob.
   337  func (be *Backend) Stat(ctx context.Context, h restic.Handle) (bi restic.FileInfo, err error) {
   338  	debug.Log("%v", h)
   339  
   340  	objName := be.Filename(h)
   341  	var obj *minio.Object
   342  
   343  	opts := minio.GetObjectOptions{}
   344  
   345  	be.sem.GetToken()
   346  	obj, err = be.client.GetObjectWithContext(ctx, be.cfg.Bucket, objName, opts)
   347  	if err != nil {
   348  		debug.Log("GetObject() err %v", err)
   349  		be.sem.ReleaseToken()
   350  		return restic.FileInfo{}, errors.Wrap(err, "client.GetObject")
   351  	}
   352  
   353  	// make sure that the object is closed properly.
   354  	defer func() {
   355  		e := obj.Close()
   356  		be.sem.ReleaseToken()
   357  		if err == nil {
   358  			err = errors.Wrap(e, "Close")
   359  		}
   360  	}()
   361  
   362  	fi, err := obj.Stat()
   363  	if err != nil {
   364  		debug.Log("Stat() err %v", err)
   365  		return restic.FileInfo{}, errors.Wrap(err, "Stat")
   366  	}
   367  
   368  	return restic.FileInfo{Size: fi.Size}, nil
   369  }
   370  
   371  // Test returns true if a blob of the given type and name exists in the backend.
   372  func (be *Backend) Test(ctx context.Context, h restic.Handle) (bool, error) {
   373  	found := false
   374  	objName := be.Filename(h)
   375  
   376  	be.sem.GetToken()
   377  	_, err := be.client.StatObject(be.cfg.Bucket, objName, minio.StatObjectOptions{})
   378  	be.sem.ReleaseToken()
   379  
   380  	if err == nil {
   381  		found = true
   382  	}
   383  
   384  	// If error, then not found
   385  	return found, nil
   386  }
   387  
   388  // Remove removes the blob with the given name and type.
   389  func (be *Backend) Remove(ctx context.Context, h restic.Handle) error {
   390  	objName := be.Filename(h)
   391  
   392  	be.sem.GetToken()
   393  	err := be.client.RemoveObject(be.cfg.Bucket, objName)
   394  	be.sem.ReleaseToken()
   395  
   396  	debug.Log("Remove(%v) at %v -> err %v", h, objName, err)
   397  
   398  	if be.IsNotExist(err) {
   399  		err = nil
   400  	}
   401  
   402  	return errors.Wrap(err, "client.RemoveObject")
   403  }
   404  
   405  // List returns a channel that yields all names of blobs of type t. A
   406  // goroutine is started for this. If the channel done is closed, sending
   407  // stops.
   408  func (be *Backend) List(ctx context.Context, t restic.FileType) <-chan string {
   409  	debug.Log("listing %v", t)
   410  	ch := make(chan string)
   411  
   412  	prefix := be.Dirname(restic.Handle{Type: t})
   413  
   414  	// make sure prefix ends with a slash
   415  	if prefix[len(prefix)-1] != '/' {
   416  		prefix += "/"
   417  	}
   418  
   419  	// NB: unfortunately we can't protect this with be.sem.GetToken() here.
   420  	// Doing so would enable a deadlock situation (gh-1399), as ListObjects()
   421  	// starts its own goroutine and returns results via a channel.
   422  	listresp := be.client.ListObjects(be.cfg.Bucket, prefix, true, ctx.Done())
   423  
   424  	go func() {
   425  		defer close(ch)
   426  		for obj := range listresp {
   427  			m := strings.TrimPrefix(obj.Key, prefix)
   428  			if m == "" {
   429  				continue
   430  			}
   431  
   432  			select {
   433  			case ch <- path.Base(m):
   434  			case <-ctx.Done():
   435  				return
   436  			}
   437  		}
   438  	}()
   439  
   440  	return ch
   441  }
   442  
   443  // Remove keys for a specified backend type.
   444  func (be *Backend) removeKeys(ctx context.Context, t restic.FileType) error {
   445  	for key := range be.List(ctx, restic.DataFile) {
   446  		err := be.Remove(ctx, restic.Handle{Type: restic.DataFile, Name: key})
   447  		if err != nil {
   448  			return err
   449  		}
   450  	}
   451  
   452  	return nil
   453  }
   454  
   455  // Delete removes all restic keys in the bucket. It will not remove the bucket itself.
   456  func (be *Backend) Delete(ctx context.Context) error {
   457  	alltypes := []restic.FileType{
   458  		restic.DataFile,
   459  		restic.KeyFile,
   460  		restic.LockFile,
   461  		restic.SnapshotFile,
   462  		restic.IndexFile}
   463  
   464  	for _, t := range alltypes {
   465  		err := be.removeKeys(ctx, t)
   466  		if err != nil {
   467  			return nil
   468  		}
   469  	}
   470  
   471  	return be.Remove(ctx, restic.Handle{Type: restic.ConfigFile})
   472  }
   473  
   474  // Close does nothing
   475  func (be *Backend) Close() error { return nil }
   476  
   477  // Rename moves a file based on the new layout l.
   478  func (be *Backend) Rename(h restic.Handle, l backend.Layout) error {
   479  	debug.Log("Rename %v to %v", h, l)
   480  	oldname := be.Filename(h)
   481  	newname := l.Filename(h)
   482  
   483  	if oldname == newname {
   484  		debug.Log("  %v is already renamed", newname)
   485  		return nil
   486  	}
   487  
   488  	debug.Log("  %v -> %v", oldname, newname)
   489  
   490  	src := minio.NewSourceInfo(be.cfg.Bucket, oldname, nil)
   491  
   492  	dst, err := minio.NewDestinationInfo(be.cfg.Bucket, newname, nil, nil)
   493  	if err != nil {
   494  		return errors.Wrap(err, "NewDestinationInfo")
   495  	}
   496  
   497  	err = be.client.CopyObject(dst, src)
   498  	if err != nil && be.IsNotExist(err) {
   499  		debug.Log("copy failed: %v, seems to already have been renamed", err)
   500  		return nil
   501  	}
   502  
   503  	if err != nil {
   504  		debug.Log("copy failed: %v", err)
   505  		return err
   506  	}
   507  
   508  	return be.client.RemoveObject(be.cfg.Bucket, oldname)
   509  }