github.com/mckael/restic@v0.8.3/internal/backend/gs/gs.go (about)

     1  // Package gs provides a restic backend for Google Cloud Storage.
     2  package gs
     3  
     4  import (
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"os"
    10  	"path"
    11  	"strings"
    12  
    13  	"github.com/pkg/errors"
    14  	"github.com/restic/restic/internal/backend"
    15  	"github.com/restic/restic/internal/debug"
    16  	"github.com/restic/restic/internal/restic"
    17  
    18  	"io/ioutil"
    19  
    20  	"golang.org/x/oauth2"
    21  	"golang.org/x/oauth2/google"
    22  	"google.golang.org/api/googleapi"
    23  	storage "google.golang.org/api/storage/v1"
    24  )
    25  
    26  // Backend stores data in a GCS bucket.
    27  //
    28  // The service account used to access the bucket must have these permissions:
    29  //  * storage.objects.create
    30  //  * storage.objects.delete
    31  //  * storage.objects.get
    32  //  * storage.objects.list
    33  type Backend struct {
    34  	service      *storage.Service
    35  	projectID    string
    36  	sem          *backend.Semaphore
    37  	bucketName   string
    38  	prefix       string
    39  	listMaxItems int
    40  	backend.Layout
    41  }
    42  
    43  // Ensure that *Backend implements restic.Backend.
    44  var _ restic.Backend = &Backend{}
    45  
    46  func getStorageService(jsonKeyPath string, rt http.RoundTripper) (*storage.Service, error) {
    47  
    48  	raw, err := ioutil.ReadFile(jsonKeyPath)
    49  	if err != nil {
    50  		return nil, errors.Wrap(err, "ReadFile")
    51  	}
    52  
    53  	conf, err := google.JWTConfigFromJSON(raw, storage.DevstorageReadWriteScope)
    54  	if err != nil {
    55  		return nil, err
    56  	}
    57  
    58  	// create a new HTTP client
    59  	httpClient := &http.Client{
    60  		Transport: rt,
    61  	}
    62  
    63  	// create a now context with the HTTP client stored at the oauth2.HTTPClient key
    64  	ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient)
    65  
    66  	// then pass this context to Client(), which returns a new HTTP client
    67  	client := conf.Client(ctx)
    68  
    69  	// that we can then pass to New()
    70  	service, err := storage.New(client)
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  
    75  	return service, nil
    76  }
    77  
    78  const defaultListMaxItems = 1000
    79  
    80  func open(cfg Config, rt http.RoundTripper) (*Backend, error) {
    81  	debug.Log("open, config %#v", cfg)
    82  
    83  	service, err := getStorageService(cfg.JSONKeyPath, rt)
    84  	if err != nil {
    85  		return nil, errors.Wrap(err, "getStorageService")
    86  	}
    87  
    88  	sem, err := backend.NewSemaphore(cfg.Connections)
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  
    93  	be := &Backend{
    94  		service:    service,
    95  		projectID:  cfg.ProjectID,
    96  		sem:        sem,
    97  		bucketName: cfg.Bucket,
    98  		prefix:     cfg.Prefix,
    99  		Layout: &backend.DefaultLayout{
   100  			Path: cfg.Prefix,
   101  			Join: path.Join,
   102  		},
   103  		listMaxItems: defaultListMaxItems,
   104  	}
   105  
   106  	return be, nil
   107  }
   108  
   109  // Open opens the gs backend at the specified bucket.
   110  func Open(cfg Config, rt http.RoundTripper) (restic.Backend, error) {
   111  	return open(cfg, rt)
   112  }
   113  
   114  // Create opens the gs backend at the specified bucket and attempts to creates
   115  // the bucket if it does not exist yet.
   116  //
   117  // The service account must have the "storage.buckets.create" permission to
   118  // create a bucket the does not yet exist.
   119  func Create(cfg Config, rt http.RoundTripper) (restic.Backend, error) {
   120  	be, err := open(cfg, rt)
   121  	if err != nil {
   122  		return nil, errors.Wrap(err, "open")
   123  	}
   124  
   125  	// Try to determine if the bucket exists. If it does not, try to create it.
   126  	//
   127  	// A Get call has three typical error cases:
   128  	//
   129  	// * nil: Bucket exists and we have access to the metadata (returned).
   130  	//
   131  	// * 403: Bucket exists and we do not have access to the metadata. We
   132  	// don't have storage.buckets.get permission to the bucket, but we may
   133  	// still be able to access objects in the bucket.
   134  	//
   135  	// * 404: Bucket doesn't exist.
   136  	//
   137  	// Determining if the bucket is accessible is best-effort because the
   138  	// 403 case is ambiguous.
   139  	if _, err := be.service.Buckets.Get(be.bucketName).Do(); err != nil {
   140  		gerr, ok := err.(*googleapi.Error)
   141  		if !ok {
   142  			// Don't know what to do with this error.
   143  			return nil, errors.Wrap(err, "service.Buckets.Get")
   144  		}
   145  
   146  		switch gerr.Code {
   147  		case 403:
   148  			// Bucket exists, but we don't know if it is
   149  			// accessible. Optimistically assume it is; if not,
   150  			// future Backend calls will fail.
   151  			debug.Log("Unable to determine if bucket %s is accessible (err %v). Continuing as if it is.", be.bucketName, err)
   152  		case 404:
   153  			// Bucket doesn't exist, try to create it.
   154  			bucket := &storage.Bucket{
   155  				Name: be.bucketName,
   156  			}
   157  
   158  			if _, err := be.service.Buckets.Insert(be.projectID, bucket).Do(); err != nil {
   159  				// Always an error, as the bucket definitely
   160  				// doesn't exist.
   161  				return nil, errors.Wrap(err, "service.Buckets.Insert")
   162  			}
   163  		default:
   164  			// Don't know what to do with this error.
   165  			return nil, errors.Wrap(err, "service.Buckets.Get")
   166  		}
   167  	}
   168  
   169  	return be, nil
   170  }
   171  
   172  // SetListMaxItems sets the number of list items to load per request.
   173  func (be *Backend) SetListMaxItems(i int) {
   174  	be.listMaxItems = i
   175  }
   176  
   177  // IsNotExist returns true if the error is caused by a not existing file.
   178  func (be *Backend) IsNotExist(err error) bool {
   179  	debug.Log("IsNotExist(%T, %#v)", err, err)
   180  
   181  	if os.IsNotExist(err) {
   182  		return true
   183  	}
   184  
   185  	if er, ok := err.(*googleapi.Error); ok {
   186  		if er.Code == 404 {
   187  			return true
   188  		}
   189  	}
   190  
   191  	return false
   192  }
   193  
   194  // Join combines path components with slashes.
   195  func (be *Backend) Join(p ...string) string {
   196  	return path.Join(p...)
   197  }
   198  
   199  // Location returns this backend's location (the bucket name).
   200  func (be *Backend) Location() string {
   201  	return be.Join(be.bucketName, be.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.prefix
   207  }
   208  
   209  // Save stores data in the backend at the handle.
   210  func (be *Backend) Save(ctx context.Context, h restic.Handle, rd io.Reader) (err error) {
   211  	if err := h.Valid(); err != nil {
   212  		return err
   213  	}
   214  
   215  	objName := be.Filename(h)
   216  
   217  	debug.Log("Save %v at %v", h, objName)
   218  
   219  	be.sem.GetToken()
   220  
   221  	debug.Log("InsertObject(%v, %v)", be.bucketName, objName)
   222  
   223  	// Set chunk size to zero to disable resumable uploads.
   224  	//
   225  	// With a non-zero chunk size (the default is
   226  	// googleapi.DefaultUploadChunkSize, 8MB), Insert will buffer data from
   227  	// rd in chunks of this size so it can upload these chunks in
   228  	// individual requests.
   229  	//
   230  	// This chunking allows the library to automatically handle network
   231  	// interruptions and re-upload only the last chunk rather than the full
   232  	// file.
   233  	//
   234  	// Unfortunately, this buffering doesn't play nicely with
   235  	// --limit-upload, which applies a rate limit to rd. This rate limit
   236  	// ends up only limiting the read from rd into the buffer rather than
   237  	// the network traffic itself. This results in poor network rate limit
   238  	// behavior, where individual chunks are written to the network at full
   239  	// bandwidth for several seconds, followed by several seconds of no
   240  	// network traffic as the next chunk is read through the rate limiter.
   241  	//
   242  	// By disabling chunking, rd is passed further down the request stack,
   243  	// where there is less (but some) buffering, which ultimately results
   244  	// in better rate limiting behavior.
   245  	//
   246  	// restic typically writes small blobs (4MB-30MB), so the resumable
   247  	// uploads are not providing significant benefit anyways.
   248  	cs := googleapi.ChunkSize(0)
   249  
   250  	info, err := be.service.Objects.Insert(be.bucketName,
   251  		&storage.Object{
   252  			Name: objName,
   253  		}).Media(rd, cs).Do()
   254  
   255  	be.sem.ReleaseToken()
   256  
   257  	if err != nil {
   258  		debug.Log("%v: err %#v: %v", objName, err, err)
   259  		return errors.Wrap(err, "service.Objects.Insert")
   260  	}
   261  
   262  	debug.Log("%v -> %v bytes", objName, info.Size)
   263  	return nil
   264  }
   265  
   266  // wrapReader wraps an io.ReadCloser to run an additional function on Close.
   267  type wrapReader struct {
   268  	io.ReadCloser
   269  	f func()
   270  }
   271  
   272  func (wr wrapReader) Close() error {
   273  	err := wr.ReadCloser.Close()
   274  	wr.f()
   275  	return err
   276  }
   277  
   278  // Load runs fn with a reader that yields the contents of the file at h at the
   279  // given offset.
   280  func (be *Backend) Load(ctx context.Context, h restic.Handle, length int, offset int64, fn func(rd io.Reader) error) error {
   281  	return backend.DefaultLoad(ctx, h, length, offset, be.openReader, fn)
   282  }
   283  
   284  func (be *Backend) openReader(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
   285  	debug.Log("Load %v, length %v, offset %v from %v", h, length, offset, be.Filename(h))
   286  	if err := h.Valid(); err != nil {
   287  		return nil, err
   288  	}
   289  
   290  	if offset < 0 {
   291  		return nil, errors.New("offset is negative")
   292  	}
   293  
   294  	if length < 0 {
   295  		return nil, errors.Errorf("invalid length %d", length)
   296  	}
   297  
   298  	objName := be.Filename(h)
   299  
   300  	be.sem.GetToken()
   301  
   302  	var byteRange string
   303  	if length > 0 {
   304  		byteRange = fmt.Sprintf("bytes=%d-%d", offset, offset+int64(length-1))
   305  	} else {
   306  		byteRange = fmt.Sprintf("bytes=%d-", offset)
   307  	}
   308  
   309  	req := be.service.Objects.Get(be.bucketName, objName)
   310  	// https://cloud.google.com/storage/docs/json_api/v1/parameters#range
   311  	req.Header().Set("Range", byteRange)
   312  	res, err := req.Download()
   313  	if err != nil {
   314  		be.sem.ReleaseToken()
   315  		return nil, err
   316  	}
   317  
   318  	closeRd := wrapReader{
   319  		ReadCloser: res.Body,
   320  		f: func() {
   321  			debug.Log("Close()")
   322  			be.sem.ReleaseToken()
   323  		},
   324  	}
   325  
   326  	return closeRd, err
   327  }
   328  
   329  // Stat returns information about a blob.
   330  func (be *Backend) Stat(ctx context.Context, h restic.Handle) (bi restic.FileInfo, err error) {
   331  	debug.Log("%v", h)
   332  
   333  	objName := be.Filename(h)
   334  
   335  	be.sem.GetToken()
   336  	obj, err := be.service.Objects.Get(be.bucketName, objName).Do()
   337  	be.sem.ReleaseToken()
   338  
   339  	if err != nil {
   340  		debug.Log("GetObject() err %v", err)
   341  		return restic.FileInfo{}, errors.Wrap(err, "service.Objects.Get")
   342  	}
   343  
   344  	return restic.FileInfo{Size: int64(obj.Size), Name: h.Name}, nil
   345  }
   346  
   347  // Test returns true if a blob of the given type and name exists in the backend.
   348  func (be *Backend) Test(ctx context.Context, h restic.Handle) (bool, error) {
   349  	found := false
   350  	objName := be.Filename(h)
   351  
   352  	be.sem.GetToken()
   353  	_, err := be.service.Objects.Get(be.bucketName, objName).Do()
   354  	be.sem.ReleaseToken()
   355  
   356  	if err == nil {
   357  		found = true
   358  	}
   359  	// If error, then not found
   360  	return found, nil
   361  }
   362  
   363  // Remove removes the blob with the given name and type.
   364  func (be *Backend) Remove(ctx context.Context, h restic.Handle) error {
   365  	objName := be.Filename(h)
   366  
   367  	be.sem.GetToken()
   368  	err := be.service.Objects.Delete(be.bucketName, objName).Do()
   369  	be.sem.ReleaseToken()
   370  
   371  	if er, ok := err.(*googleapi.Error); ok {
   372  		if er.Code == 404 {
   373  			err = nil
   374  		}
   375  	}
   376  
   377  	debug.Log("Remove(%v) at %v -> err %v", h, objName, err)
   378  	return errors.Wrap(err, "client.RemoveObject")
   379  }
   380  
   381  // List runs fn for each file in the backend which has the type t. When an
   382  // error occurs (or fn returns an error), List stops and returns it.
   383  func (be *Backend) List(ctx context.Context, t restic.FileType, fn func(restic.FileInfo) error) error {
   384  	debug.Log("listing %v", t)
   385  
   386  	prefix, _ := be.Basedir(t)
   387  
   388  	// make sure prefix ends with a slash
   389  	if !strings.HasSuffix(prefix, "/") {
   390  		prefix += "/"
   391  	}
   392  
   393  	ctx, cancel := context.WithCancel(ctx)
   394  	defer cancel()
   395  
   396  	listReq := be.service.Objects.List(be.bucketName).Context(ctx).Prefix(prefix).MaxResults(int64(be.listMaxItems))
   397  	for {
   398  		be.sem.GetToken()
   399  		obj, err := listReq.Do()
   400  		be.sem.ReleaseToken()
   401  
   402  		if err != nil {
   403  			return err
   404  		}
   405  
   406  		debug.Log("returned %v items", len(obj.Items))
   407  
   408  		for _, item := range obj.Items {
   409  			m := strings.TrimPrefix(item.Name, prefix)
   410  			if m == "" {
   411  				continue
   412  			}
   413  
   414  			if ctx.Err() != nil {
   415  				return ctx.Err()
   416  			}
   417  
   418  			fi := restic.FileInfo{
   419  				Name: path.Base(m),
   420  				Size: int64(item.Size),
   421  			}
   422  
   423  			err := fn(fi)
   424  			if err != nil {
   425  				return err
   426  			}
   427  
   428  			if ctx.Err() != nil {
   429  				return ctx.Err()
   430  			}
   431  		}
   432  
   433  		if obj.NextPageToken == "" {
   434  			break
   435  		}
   436  		listReq.PageToken(obj.NextPageToken)
   437  	}
   438  
   439  	return ctx.Err()
   440  }
   441  
   442  // Remove keys for a specified backend type.
   443  func (be *Backend) removeKeys(ctx context.Context, t restic.FileType) error {
   444  	return be.List(ctx, t, func(fi restic.FileInfo) error {
   445  		return be.Remove(ctx, restic.Handle{Type: t, Name: fi.Name})
   446  	})
   447  }
   448  
   449  // Delete removes all restic keys in the bucket. It will not remove the bucket itself.
   450  func (be *Backend) Delete(ctx context.Context) error {
   451  	alltypes := []restic.FileType{
   452  		restic.DataFile,
   453  		restic.KeyFile,
   454  		restic.LockFile,
   455  		restic.SnapshotFile,
   456  		restic.IndexFile}
   457  
   458  	for _, t := range alltypes {
   459  		err := be.removeKeys(ctx, t)
   460  		if err != nil {
   461  			return nil
   462  		}
   463  	}
   464  
   465  	return be.Remove(ctx, restic.Handle{Type: restic.ConfigFile})
   466  }
   467  
   468  // Close does nothing.
   469  func (be *Backend) Close() error { return nil }