github.com/mckael/restic@v0.8.3/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  	var size int64 = -1
   239  
   240  	type lenner interface {
   241  		Len() int
   242  	}
   243  
   244  	// find size for reader
   245  	if f, ok := rd.(*os.File); ok {
   246  		size, err = lenForFile(f)
   247  		if err != nil {
   248  			return err
   249  		}
   250  	} else if l, ok := rd.(lenner); ok {
   251  		size = int64(l.Len())
   252  	}
   253  
   254  	opts := minio.PutObjectOptions{}
   255  	opts.ContentType = "application/octet-stream"
   256  
   257  	debug.Log("PutObject(%v, %v, %v)", be.cfg.Bucket, objName, size)
   258  	n, err := be.client.PutObjectWithContext(ctx, be.cfg.Bucket, objName, ioutil.NopCloser(rd), size, opts)
   259  
   260  	debug.Log("%v -> %v bytes, err %#v: %v", objName, n, err, err)
   261  
   262  	return errors.Wrap(err, "client.PutObject")
   263  }
   264  
   265  // wrapReader wraps an io.ReadCloser to run an additional function on Close.
   266  type wrapReader struct {
   267  	io.ReadCloser
   268  	f func()
   269  }
   270  
   271  func (wr wrapReader) Close() error {
   272  	err := wr.ReadCloser.Close()
   273  	wr.f()
   274  	return err
   275  }
   276  
   277  // Load runs fn with a reader that yields the contents of the file at h at the
   278  // given offset.
   279  func (be *Backend) Load(ctx context.Context, h restic.Handle, length int, offset int64, fn func(rd io.Reader) error) error {
   280  	return backend.DefaultLoad(ctx, h, length, offset, be.openReader, fn)
   281  }
   282  
   283  func (be *Backend) openReader(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
   284  	debug.Log("Load %v, length %v, offset %v from %v", h, length, offset, be.Filename(h))
   285  	if err := h.Valid(); err != nil {
   286  		return nil, err
   287  	}
   288  
   289  	if offset < 0 {
   290  		return nil, errors.New("offset is negative")
   291  	}
   292  
   293  	if length < 0 {
   294  		return nil, errors.Errorf("invalid length %d", length)
   295  	}
   296  
   297  	objName := be.Filename(h)
   298  	opts := minio.GetObjectOptions{}
   299  
   300  	var err error
   301  	if length > 0 {
   302  		debug.Log("range: %v-%v", offset, offset+int64(length)-1)
   303  		err = opts.SetRange(offset, offset+int64(length)-1)
   304  	} else if offset > 0 {
   305  		debug.Log("range: %v-", offset)
   306  		err = opts.SetRange(offset, 0)
   307  	}
   308  
   309  	if err != nil {
   310  		return nil, errors.Wrap(err, "SetRange")
   311  	}
   312  
   313  	be.sem.GetToken()
   314  	coreClient := minio.Core{Client: be.client}
   315  	rd, err := coreClient.GetObjectWithContext(ctx, be.cfg.Bucket, objName, opts)
   316  	if err != nil {
   317  		be.sem.ReleaseToken()
   318  		return nil, err
   319  	}
   320  
   321  	closeRd := wrapReader{
   322  		ReadCloser: rd,
   323  		f: func() {
   324  			debug.Log("Close()")
   325  			be.sem.ReleaseToken()
   326  		},
   327  	}
   328  
   329  	return closeRd, err
   330  }
   331  
   332  // Stat returns information about a blob.
   333  func (be *Backend) Stat(ctx context.Context, h restic.Handle) (bi restic.FileInfo, err error) {
   334  	debug.Log("%v", h)
   335  
   336  	objName := be.Filename(h)
   337  	var obj *minio.Object
   338  
   339  	opts := minio.GetObjectOptions{}
   340  
   341  	be.sem.GetToken()
   342  	obj, err = be.client.GetObjectWithContext(ctx, be.cfg.Bucket, objName, opts)
   343  	if err != nil {
   344  		debug.Log("GetObject() err %v", err)
   345  		be.sem.ReleaseToken()
   346  		return restic.FileInfo{}, errors.Wrap(err, "client.GetObject")
   347  	}
   348  
   349  	// make sure that the object is closed properly.
   350  	defer func() {
   351  		e := obj.Close()
   352  		be.sem.ReleaseToken()
   353  		if err == nil {
   354  			err = errors.Wrap(e, "Close")
   355  		}
   356  	}()
   357  
   358  	fi, err := obj.Stat()
   359  	if err != nil {
   360  		debug.Log("Stat() err %v", err)
   361  		return restic.FileInfo{}, errors.Wrap(err, "Stat")
   362  	}
   363  
   364  	return restic.FileInfo{Size: fi.Size, Name: h.Name}, nil
   365  }
   366  
   367  // Test returns true if a blob of the given type and name exists in the backend.
   368  func (be *Backend) Test(ctx context.Context, h restic.Handle) (bool, error) {
   369  	found := false
   370  	objName := be.Filename(h)
   371  
   372  	be.sem.GetToken()
   373  	_, err := be.client.StatObject(be.cfg.Bucket, objName, minio.StatObjectOptions{})
   374  	be.sem.ReleaseToken()
   375  
   376  	if err == nil {
   377  		found = true
   378  	}
   379  
   380  	// If error, then not found
   381  	return found, nil
   382  }
   383  
   384  // Remove removes the blob with the given name and type.
   385  func (be *Backend) Remove(ctx context.Context, h restic.Handle) error {
   386  	objName := be.Filename(h)
   387  
   388  	be.sem.GetToken()
   389  	err := be.client.RemoveObject(be.cfg.Bucket, objName)
   390  	be.sem.ReleaseToken()
   391  
   392  	debug.Log("Remove(%v) at %v -> err %v", h, objName, err)
   393  
   394  	if be.IsNotExist(err) {
   395  		err = nil
   396  	}
   397  
   398  	return errors.Wrap(err, "client.RemoveObject")
   399  }
   400  
   401  // List runs fn for each file in the backend which has the type t. When an
   402  // error occurs (or fn returns an error), List stops and returns it.
   403  func (be *Backend) List(ctx context.Context, t restic.FileType, fn func(restic.FileInfo) error) error {
   404  	debug.Log("listing %v", t)
   405  
   406  	prefix, recursive := be.Basedir(t)
   407  
   408  	// make sure prefix ends with a slash
   409  	if !strings.HasSuffix(prefix, "/") {
   410  		prefix += "/"
   411  	}
   412  
   413  	ctx, cancel := context.WithCancel(ctx)
   414  	defer cancel()
   415  
   416  	// NB: unfortunately we can't protect this with be.sem.GetToken() here.
   417  	// Doing so would enable a deadlock situation (gh-1399), as ListObjects()
   418  	// starts its own goroutine and returns results via a channel.
   419  	listresp := be.client.ListObjects(be.cfg.Bucket, prefix, recursive, ctx.Done())
   420  
   421  	for obj := range listresp {
   422  		m := strings.TrimPrefix(obj.Key, prefix)
   423  		if m == "" {
   424  			continue
   425  		}
   426  
   427  		fi := restic.FileInfo{
   428  			Name: path.Base(m),
   429  			Size: obj.Size,
   430  		}
   431  
   432  		if ctx.Err() != nil {
   433  			return ctx.Err()
   434  		}
   435  
   436  		err := fn(fi)
   437  		if err != nil {
   438  			return err
   439  		}
   440  
   441  		if ctx.Err() != nil {
   442  			return ctx.Err()
   443  		}
   444  	}
   445  
   446  	return ctx.Err()
   447  }
   448  
   449  // Remove keys for a specified backend type.
   450  func (be *Backend) removeKeys(ctx context.Context, t restic.FileType) error {
   451  	return be.List(ctx, restic.DataFile, func(fi restic.FileInfo) error {
   452  		return be.Remove(ctx, restic.Handle{Type: t, Name: fi.Name})
   453  	})
   454  }
   455  
   456  // Delete removes all restic keys in the bucket. It will not remove the bucket itself.
   457  func (be *Backend) Delete(ctx context.Context) error {
   458  	alltypes := []restic.FileType{
   459  		restic.DataFile,
   460  		restic.KeyFile,
   461  		restic.LockFile,
   462  		restic.SnapshotFile,
   463  		restic.IndexFile}
   464  
   465  	for _, t := range alltypes {
   466  		err := be.removeKeys(ctx, t)
   467  		if err != nil {
   468  			return nil
   469  		}
   470  	}
   471  
   472  	return be.Remove(ctx, restic.Handle{Type: restic.ConfigFile})
   473  }
   474  
   475  // Close does nothing
   476  func (be *Backend) Close() error { return nil }
   477  
   478  // Rename moves a file based on the new layout l.
   479  func (be *Backend) Rename(h restic.Handle, l backend.Layout) error {
   480  	debug.Log("Rename %v to %v", h, l)
   481  	oldname := be.Filename(h)
   482  	newname := l.Filename(h)
   483  
   484  	if oldname == newname {
   485  		debug.Log("  %v is already renamed", newname)
   486  		return nil
   487  	}
   488  
   489  	debug.Log("  %v -> %v", oldname, newname)
   490  
   491  	src := minio.NewSourceInfo(be.cfg.Bucket, oldname, nil)
   492  
   493  	dst, err := minio.NewDestinationInfo(be.cfg.Bucket, newname, nil, nil)
   494  	if err != nil {
   495  		return errors.Wrap(err, "NewDestinationInfo")
   496  	}
   497  
   498  	err = be.client.CopyObject(dst, src)
   499  	if err != nil && be.IsNotExist(err) {
   500  		debug.Log("copy failed: %v, seems to already have been renamed", err)
   501  		return nil
   502  	}
   503  
   504  	if err != nil {
   505  		debug.Log("copy failed: %v", err)
   506  		return err
   507  	}
   508  
   509  	return be.client.RemoveObject(be.cfg.Bucket, oldname)
   510  }