github.com/fawick/restic@v0.1.1-0.20171126184616-c02923fbfc79/internal/backend/s3/s3.go (about)

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