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

     1  package b2
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"net/http"
     7  	"path"
     8  	"strings"
     9  
    10  	"github.com/restic/restic/internal/backend"
    11  	"github.com/restic/restic/internal/debug"
    12  	"github.com/restic/restic/internal/errors"
    13  	"github.com/restic/restic/internal/restic"
    14  
    15  	"github.com/kurin/blazer/b2"
    16  )
    17  
    18  // b2Backend is a backend which stores its data on Backblaze B2.
    19  type b2Backend struct {
    20  	client       *b2.Client
    21  	bucket       *b2.Bucket
    22  	cfg          Config
    23  	listMaxItems int
    24  	backend.Layout
    25  	sem *backend.Semaphore
    26  }
    27  
    28  const defaultListMaxItems = 1000
    29  
    30  // ensure statically that *b2Backend implements restic.Backend.
    31  var _ restic.Backend = &b2Backend{}
    32  
    33  func newClient(ctx context.Context, cfg Config, rt http.RoundTripper) (*b2.Client, error) {
    34  	opts := []b2.ClientOption{b2.Transport(rt)}
    35  
    36  	c, err := b2.NewClient(ctx, cfg.AccountID, cfg.Key, opts...)
    37  	if err != nil {
    38  		return nil, errors.Wrap(err, "b2.NewClient")
    39  	}
    40  	return c, nil
    41  }
    42  
    43  // Open opens a connection to the B2 service.
    44  func Open(ctx context.Context, cfg Config, rt http.RoundTripper) (restic.Backend, error) {
    45  	debug.Log("cfg %#v", cfg)
    46  
    47  	ctx, cancel := context.WithCancel(ctx)
    48  	defer cancel()
    49  
    50  	client, err := newClient(ctx, cfg, rt)
    51  	if err != nil {
    52  		return nil, err
    53  	}
    54  
    55  	bucket, err := client.Bucket(ctx, cfg.Bucket)
    56  	if err != nil {
    57  		return nil, errors.Wrap(err, "Bucket")
    58  	}
    59  
    60  	sem, err := backend.NewSemaphore(cfg.Connections)
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  
    65  	be := &b2Backend{
    66  		client: client,
    67  		bucket: bucket,
    68  		cfg:    cfg,
    69  		Layout: &backend.DefaultLayout{
    70  			Join: path.Join,
    71  			Path: cfg.Prefix,
    72  		},
    73  		listMaxItems: defaultListMaxItems,
    74  		sem:          sem,
    75  	}
    76  
    77  	return be, nil
    78  }
    79  
    80  // Create opens a connection to the B2 service. If the bucket does not exist yet,
    81  // it is created.
    82  func Create(ctx context.Context, cfg Config, rt http.RoundTripper) (restic.Backend, error) {
    83  	debug.Log("cfg %#v", cfg)
    84  
    85  	ctx, cancel := context.WithCancel(ctx)
    86  	defer cancel()
    87  
    88  	client, err := newClient(ctx, cfg, rt)
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  
    93  	attr := b2.BucketAttrs{
    94  		Type: b2.Private,
    95  	}
    96  	bucket, err := client.NewBucket(ctx, cfg.Bucket, &attr)
    97  	if err != nil {
    98  		return nil, errors.Wrap(err, "NewBucket")
    99  	}
   100  
   101  	sem, err := backend.NewSemaphore(cfg.Connections)
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  
   106  	be := &b2Backend{
   107  		client: client,
   108  		bucket: bucket,
   109  		cfg:    cfg,
   110  		Layout: &backend.DefaultLayout{
   111  			Join: path.Join,
   112  			Path: cfg.Prefix,
   113  		},
   114  		listMaxItems: defaultListMaxItems,
   115  		sem:          sem,
   116  	}
   117  
   118  	present, err := be.Test(ctx, restic.Handle{Type: restic.ConfigFile})
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  
   123  	if present {
   124  		return nil, errors.New("config already exists")
   125  	}
   126  
   127  	return be, nil
   128  }
   129  
   130  // SetListMaxItems sets the number of list items to load per request.
   131  func (be *b2Backend) SetListMaxItems(i int) {
   132  	be.listMaxItems = i
   133  }
   134  
   135  // Location returns the location for the backend.
   136  func (be *b2Backend) Location() string {
   137  	return be.cfg.Bucket
   138  }
   139  
   140  // IsNotExist returns true if the error is caused by a non-existing file.
   141  func (be *b2Backend) IsNotExist(err error) bool {
   142  	return b2.IsNotExist(errors.Cause(err))
   143  }
   144  
   145  // Load runs fn with a reader that yields the contents of the file at h at the
   146  // given offset.
   147  func (be *b2Backend) Load(ctx context.Context, h restic.Handle, length int, offset int64, fn func(rd io.Reader) error) error {
   148  	return backend.DefaultLoad(ctx, h, length, offset, be.openReader, fn)
   149  }
   150  
   151  func (be *b2Backend) openReader(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
   152  	debug.Log("Load %v, length %v, offset %v from %v", h, length, offset, be.Filename(h))
   153  	if err := h.Valid(); err != nil {
   154  		return nil, err
   155  	}
   156  
   157  	if offset < 0 {
   158  		return nil, errors.New("offset is negative")
   159  	}
   160  
   161  	if length < 0 {
   162  		return nil, errors.Errorf("invalid length %d", length)
   163  	}
   164  
   165  	ctx, cancel := context.WithCancel(ctx)
   166  
   167  	be.sem.GetToken()
   168  
   169  	name := be.Layout.Filename(h)
   170  	obj := be.bucket.Object(name)
   171  
   172  	if offset == 0 && length == 0 {
   173  		rd := obj.NewReader(ctx)
   174  		return be.sem.ReleaseTokenOnClose(rd, cancel), nil
   175  	}
   176  
   177  	// pass a negative length to NewRangeReader so that the remainder of the
   178  	// file is read.
   179  	if length == 0 {
   180  		length = -1
   181  	}
   182  
   183  	rd := obj.NewRangeReader(ctx, offset, int64(length))
   184  	return be.sem.ReleaseTokenOnClose(rd, cancel), nil
   185  }
   186  
   187  // Save stores data in the backend at the handle.
   188  func (be *b2Backend) Save(ctx context.Context, h restic.Handle, rd io.Reader) error {
   189  	ctx, cancel := context.WithCancel(ctx)
   190  	defer cancel()
   191  
   192  	if err := h.Valid(); err != nil {
   193  		return err
   194  	}
   195  
   196  	be.sem.GetToken()
   197  	defer be.sem.ReleaseToken()
   198  
   199  	name := be.Filename(h)
   200  	debug.Log("Save %v, name %v", h, name)
   201  	obj := be.bucket.Object(name)
   202  
   203  	w := obj.NewWriter(ctx)
   204  	n, err := io.Copy(w, rd)
   205  	debug.Log("  saved %d bytes, err %v", n, err)
   206  
   207  	if err != nil {
   208  		_ = w.Close()
   209  		return errors.Wrap(err, "Copy")
   210  	}
   211  
   212  	return errors.Wrap(w.Close(), "Close")
   213  }
   214  
   215  // Stat returns information about a blob.
   216  func (be *b2Backend) Stat(ctx context.Context, h restic.Handle) (bi restic.FileInfo, err error) {
   217  	debug.Log("Stat %v", h)
   218  
   219  	be.sem.GetToken()
   220  	defer be.sem.ReleaseToken()
   221  
   222  	name := be.Filename(h)
   223  	obj := be.bucket.Object(name)
   224  	info, err := obj.Attrs(ctx)
   225  	if err != nil {
   226  		debug.Log("Attrs() err %v", err)
   227  		return restic.FileInfo{}, errors.Wrap(err, "Stat")
   228  	}
   229  	return restic.FileInfo{Size: info.Size, Name: h.Name}, nil
   230  }
   231  
   232  // Test returns true if a blob of the given type and name exists in the backend.
   233  func (be *b2Backend) Test(ctx context.Context, h restic.Handle) (bool, error) {
   234  	debug.Log("Test %v", h)
   235  
   236  	be.sem.GetToken()
   237  	defer be.sem.ReleaseToken()
   238  
   239  	found := false
   240  	name := be.Filename(h)
   241  	obj := be.bucket.Object(name)
   242  	info, err := obj.Attrs(ctx)
   243  	if err == nil && info != nil && info.Status == b2.Uploaded {
   244  		found = true
   245  	}
   246  	return found, nil
   247  }
   248  
   249  // Remove removes the blob with the given name and type.
   250  func (be *b2Backend) Remove(ctx context.Context, h restic.Handle) error {
   251  	debug.Log("Remove %v", h)
   252  
   253  	be.sem.GetToken()
   254  	defer be.sem.ReleaseToken()
   255  
   256  	obj := be.bucket.Object(be.Filename(h))
   257  	return errors.Wrap(obj.Delete(ctx), "Delete")
   258  }
   259  
   260  // List returns a channel that yields all names of blobs of type t. A
   261  // goroutine is started for this. If the channel done is closed, sending
   262  // stops.
   263  func (be *b2Backend) List(ctx context.Context, t restic.FileType, fn func(restic.FileInfo) error) error {
   264  	debug.Log("List %v", t)
   265  
   266  	prefix, _ := be.Basedir(t)
   267  	cur := &b2.Cursor{Prefix: prefix}
   268  
   269  	ctx, cancel := context.WithCancel(ctx)
   270  	defer cancel()
   271  
   272  	for {
   273  		be.sem.GetToken()
   274  		objs, c, err := be.bucket.ListCurrentObjects(ctx, be.listMaxItems, cur)
   275  		be.sem.ReleaseToken()
   276  
   277  		if err != nil && err != io.EOF {
   278  			debug.Log("List: %v", err)
   279  			return err
   280  		}
   281  
   282  		debug.Log("returned %v items", len(objs))
   283  		for _, obj := range objs {
   284  			// Skip objects returned that do not have the specified prefix.
   285  			if !strings.HasPrefix(obj.Name(), prefix) {
   286  				continue
   287  			}
   288  
   289  			m := path.Base(obj.Name())
   290  			if m == "" {
   291  				continue
   292  			}
   293  
   294  			if ctx.Err() != nil {
   295  				return ctx.Err()
   296  			}
   297  
   298  			if ctx.Err() != nil {
   299  				return ctx.Err()
   300  			}
   301  
   302  			attrs, err := obj.Attrs(ctx)
   303  			if err != nil {
   304  				return err
   305  			}
   306  
   307  			fi := restic.FileInfo{
   308  				Name: m,
   309  				Size: attrs.Size,
   310  			}
   311  
   312  			err = fn(fi)
   313  			if err != nil {
   314  				return err
   315  			}
   316  
   317  			if ctx.Err() != nil {
   318  				return ctx.Err()
   319  			}
   320  		}
   321  
   322  		if err == io.EOF {
   323  			return ctx.Err()
   324  		}
   325  		cur = c
   326  	}
   327  
   328  	return ctx.Err()
   329  }
   330  
   331  // Remove keys for a specified backend type.
   332  func (be *b2Backend) removeKeys(ctx context.Context, t restic.FileType) error {
   333  	debug.Log("removeKeys %v", t)
   334  	return be.List(ctx, t, func(fi restic.FileInfo) error {
   335  		return be.Remove(ctx, restic.Handle{Type: t, Name: fi.Name})
   336  	})
   337  }
   338  
   339  // Delete removes all restic keys in the bucket. It will not remove the bucket itself.
   340  func (be *b2Backend) Delete(ctx context.Context) error {
   341  	alltypes := []restic.FileType{
   342  		restic.DataFile,
   343  		restic.KeyFile,
   344  		restic.LockFile,
   345  		restic.SnapshotFile,
   346  		restic.IndexFile}
   347  
   348  	for _, t := range alltypes {
   349  		err := be.removeKeys(ctx, t)
   350  		if err != nil {
   351  			return nil
   352  		}
   353  	}
   354  	err := be.Remove(ctx, restic.Handle{Type: restic.ConfigFile})
   355  	if err != nil && b2.IsNotExist(errors.Cause(err)) {
   356  		err = nil
   357  	}
   358  
   359  	return err
   360  }
   361  
   362  // Close does nothing
   363  func (be *b2Backend) Close() error { return nil }