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

     1  package azure
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"net/http"
     7  	"os"
     8  	"path"
     9  	"strings"
    10  
    11  	"github.com/Azure/azure-sdk-for-go/storage"
    12  	"github.com/restic/restic/internal/backend"
    13  	"github.com/restic/restic/internal/debug"
    14  	"github.com/restic/restic/internal/errors"
    15  	"github.com/restic/restic/internal/restic"
    16  )
    17  
    18  // Backend stores data on an azure endpoint.
    19  type Backend struct {
    20  	accountName  string
    21  	container    *storage.Container
    22  	sem          *backend.Semaphore
    23  	prefix       string
    24  	listMaxItems int
    25  	backend.Layout
    26  }
    27  
    28  const defaultListMaxItems = 5000
    29  
    30  // make sure that *Backend implements backend.Backend
    31  var _ restic.Backend = &Backend{}
    32  
    33  func open(cfg Config, rt http.RoundTripper) (*Backend, error) {
    34  	debug.Log("open, config %#v", cfg)
    35  
    36  	client, err := storage.NewBasicClient(cfg.AccountName, cfg.AccountKey)
    37  	if err != nil {
    38  		return nil, errors.Wrap(err, "NewBasicClient")
    39  	}
    40  
    41  	client.HTTPClient = &http.Client{Transport: rt}
    42  
    43  	service := client.GetBlobService()
    44  
    45  	sem, err := backend.NewSemaphore(cfg.Connections)
    46  	if err != nil {
    47  		return nil, err
    48  	}
    49  
    50  	be := &Backend{
    51  		container:   service.GetContainerReference(cfg.Container),
    52  		accountName: cfg.AccountName,
    53  		sem:         sem,
    54  		prefix:      cfg.Prefix,
    55  		Layout: &backend.DefaultLayout{
    56  			Path: cfg.Prefix,
    57  			Join: path.Join,
    58  		},
    59  		listMaxItems: defaultListMaxItems,
    60  	}
    61  
    62  	return be, nil
    63  }
    64  
    65  // Open opens the Azure backend at specified container.
    66  func Open(cfg Config, rt http.RoundTripper) (restic.Backend, error) {
    67  	return open(cfg, rt)
    68  }
    69  
    70  // Create opens the Azure backend at specified container and creates the container if
    71  // it does not exist yet.
    72  func Create(cfg Config, rt http.RoundTripper) (restic.Backend, error) {
    73  	be, err := open(cfg, rt)
    74  
    75  	if err != nil {
    76  		return nil, errors.Wrap(err, "open")
    77  	}
    78  
    79  	options := storage.CreateContainerOptions{
    80  		Access: storage.ContainerAccessTypePrivate,
    81  	}
    82  
    83  	_, err = be.container.CreateIfNotExists(&options)
    84  	if err != nil {
    85  		return nil, errors.Wrap(err, "container.CreateIfNotExists")
    86  	}
    87  
    88  	return be, nil
    89  }
    90  
    91  // SetListMaxItems sets the number of list items to load per request.
    92  func (be *Backend) SetListMaxItems(i int) {
    93  	be.listMaxItems = i
    94  }
    95  
    96  // IsNotExist returns true if the error is caused by a not existing file.
    97  func (be *Backend) IsNotExist(err error) bool {
    98  	debug.Log("IsNotExist(%T, %#v)", err, err)
    99  	return os.IsNotExist(err)
   100  }
   101  
   102  // Join combines path components with slashes.
   103  func (be *Backend) Join(p ...string) string {
   104  	return path.Join(p...)
   105  }
   106  
   107  // Location returns this backend's location (the container name).
   108  func (be *Backend) Location() string {
   109  	return be.Join(be.container.Name, be.prefix)
   110  }
   111  
   112  // Path returns the path in the bucket that is used for this backend.
   113  func (be *Backend) Path() string {
   114  	return be.prefix
   115  }
   116  
   117  // preventCloser wraps an io.Reader to run a function instead of the original Close() function.
   118  type preventCloser struct {
   119  	io.Reader
   120  	f func()
   121  }
   122  
   123  func (wr preventCloser) Close() error {
   124  	wr.f()
   125  	return nil
   126  }
   127  
   128  // Save stores data in the backend at the handle.
   129  func (be *Backend) Save(ctx context.Context, h restic.Handle, rd io.Reader) (err error) {
   130  	if err := h.Valid(); err != nil {
   131  		return err
   132  	}
   133  
   134  	objName := be.Filename(h)
   135  
   136  	debug.Log("Save %v at %v", h, objName)
   137  
   138  	be.sem.GetToken()
   139  
   140  	// wrap the reader so that net/http client cannot close the reader, return
   141  	// the token instead.
   142  	rd = preventCloser{
   143  		Reader: rd,
   144  		f: func() {
   145  			debug.Log("Close()")
   146  		},
   147  	}
   148  
   149  	debug.Log("InsertObject(%v, %v)", be.container.Name, objName)
   150  
   151  	err = be.container.GetBlobReference(objName).CreateBlockBlobFromReader(rd, nil)
   152  
   153  	be.sem.ReleaseToken()
   154  	debug.Log("%v, err %#v", objName, err)
   155  
   156  	return errors.Wrap(err, "CreateBlockBlobFromReader")
   157  }
   158  
   159  // wrapReader wraps an io.ReadCloser to run an additional function on Close.
   160  type wrapReader struct {
   161  	io.ReadCloser
   162  	f func()
   163  }
   164  
   165  func (wr wrapReader) Close() error {
   166  	err := wr.ReadCloser.Close()
   167  	wr.f()
   168  	return err
   169  }
   170  
   171  // Load runs fn with a reader that yields the contents of the file at h at the
   172  // given offset.
   173  func (be *Backend) Load(ctx context.Context, h restic.Handle, length int, offset int64, fn func(rd io.Reader) error) error {
   174  	return backend.DefaultLoad(ctx, h, length, offset, be.openReader, fn)
   175  }
   176  
   177  func (be *Backend) openReader(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
   178  	debug.Log("Load %v, length %v, offset %v from %v", h, length, offset, be.Filename(h))
   179  	if err := h.Valid(); err != nil {
   180  		return nil, err
   181  	}
   182  
   183  	if offset < 0 {
   184  		return nil, errors.New("offset is negative")
   185  	}
   186  
   187  	if length < 0 {
   188  		return nil, errors.Errorf("invalid length %d", length)
   189  	}
   190  
   191  	objName := be.Filename(h)
   192  	blob := be.container.GetBlobReference(objName)
   193  
   194  	start := uint64(offset)
   195  	var end uint64
   196  
   197  	if length > 0 {
   198  		end = uint64(offset + int64(length) - 1)
   199  	} else {
   200  		end = 0
   201  	}
   202  
   203  	be.sem.GetToken()
   204  
   205  	rd, err := blob.GetRange(&storage.GetBlobRangeOptions{Range: &storage.BlobRange{Start: start, End: end}})
   206  	if err != nil {
   207  		be.sem.ReleaseToken()
   208  		return nil, err
   209  	}
   210  
   211  	closeRd := wrapReader{
   212  		ReadCloser: rd,
   213  		f: func() {
   214  			debug.Log("Close()")
   215  			be.sem.ReleaseToken()
   216  		},
   217  	}
   218  
   219  	return closeRd, err
   220  }
   221  
   222  // Stat returns information about a blob.
   223  func (be *Backend) Stat(ctx context.Context, h restic.Handle) (restic.FileInfo, error) {
   224  	debug.Log("%v", h)
   225  
   226  	objName := be.Filename(h)
   227  	blob := be.container.GetBlobReference(objName)
   228  
   229  	be.sem.GetToken()
   230  	err := blob.GetProperties(nil)
   231  	be.sem.ReleaseToken()
   232  
   233  	if err != nil {
   234  		debug.Log("blob.GetProperties err %v", err)
   235  		return restic.FileInfo{}, errors.Wrap(err, "blob.GetProperties")
   236  	}
   237  
   238  	fi := restic.FileInfo{
   239  		Size: int64(blob.Properties.ContentLength),
   240  		Name: h.Name,
   241  	}
   242  	return fi, nil
   243  }
   244  
   245  // Test returns true if a blob of the given type and name exists in the backend.
   246  func (be *Backend) Test(ctx context.Context, h restic.Handle) (bool, error) {
   247  	objName := be.Filename(h)
   248  
   249  	be.sem.GetToken()
   250  	found, err := be.container.GetBlobReference(objName).Exists()
   251  	be.sem.ReleaseToken()
   252  
   253  	if err != nil {
   254  		return false, err
   255  	}
   256  	return found, nil
   257  }
   258  
   259  // Remove removes the blob with the given name and type.
   260  func (be *Backend) Remove(ctx context.Context, h restic.Handle) error {
   261  	objName := be.Filename(h)
   262  
   263  	be.sem.GetToken()
   264  	_, err := be.container.GetBlobReference(objName).DeleteIfExists(nil)
   265  	be.sem.ReleaseToken()
   266  
   267  	debug.Log("Remove(%v) at %v -> err %v", h, objName, err)
   268  	return errors.Wrap(err, "client.RemoveObject")
   269  }
   270  
   271  // List runs fn for each file in the backend which has the type t. When an
   272  // error occurs (or fn returns an error), List stops and returns it.
   273  func (be *Backend) List(ctx context.Context, t restic.FileType, fn func(restic.FileInfo) error) error {
   274  	debug.Log("listing %v", t)
   275  
   276  	prefix, _ := be.Basedir(t)
   277  
   278  	// make sure prefix ends with a slash
   279  	if !strings.HasSuffix(prefix, "/") {
   280  		prefix += "/"
   281  	}
   282  
   283  	params := storage.ListBlobsParameters{
   284  		MaxResults: uint(be.listMaxItems),
   285  		Prefix:     prefix,
   286  	}
   287  
   288  	for {
   289  		be.sem.GetToken()
   290  		obj, err := be.container.ListBlobs(params)
   291  		be.sem.ReleaseToken()
   292  
   293  		if err != nil {
   294  			return err
   295  		}
   296  
   297  		debug.Log("got %v objects", len(obj.Blobs))
   298  
   299  		for _, item := range obj.Blobs {
   300  			m := strings.TrimPrefix(item.Name, prefix)
   301  			if m == "" {
   302  				continue
   303  			}
   304  
   305  			fi := restic.FileInfo{
   306  				Name: path.Base(m),
   307  				Size: item.Properties.ContentLength,
   308  			}
   309  
   310  			if ctx.Err() != nil {
   311  				return ctx.Err()
   312  			}
   313  
   314  			err := fn(fi)
   315  			if err != nil {
   316  				return err
   317  			}
   318  
   319  			if ctx.Err() != nil {
   320  				return ctx.Err()
   321  			}
   322  
   323  		}
   324  
   325  		if obj.NextMarker == "" {
   326  			break
   327  		}
   328  		params.Marker = obj.NextMarker
   329  	}
   330  
   331  	return ctx.Err()
   332  }
   333  
   334  // Remove keys for a specified backend type.
   335  func (be *Backend) removeKeys(ctx context.Context, t restic.FileType) error {
   336  	return be.List(ctx, t, func(fi restic.FileInfo) error {
   337  		return be.Remove(ctx, restic.Handle{Type: t, Name: fi.Name})
   338  	})
   339  }
   340  
   341  // Delete removes all restic keys in the bucket. It will not remove the bucket itself.
   342  func (be *Backend) Delete(ctx context.Context) error {
   343  	alltypes := []restic.FileType{
   344  		restic.DataFile,
   345  		restic.KeyFile,
   346  		restic.LockFile,
   347  		restic.SnapshotFile,
   348  		restic.IndexFile}
   349  
   350  	for _, t := range alltypes {
   351  		err := be.removeKeys(ctx, t)
   352  		if err != nil {
   353  			return nil
   354  		}
   355  	}
   356  
   357  	return be.Remove(ctx, restic.Handle{Type: restic.ConfigFile})
   358  }
   359  
   360  // Close does nothing
   361  func (be *Backend) Close() error { return nil }