github.com/advanderveer/restic@v0.8.1-0.20171209104529-42a8c19aaea6/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  	// Check key does not already exist
   139  	found, err := be.container.GetBlobReference(objName).Exists()
   140  	if err != nil {
   141  		return errors.Wrap(err, "GetBlobReference().Exists()")
   142  	}
   143  	if found {
   144  		debug.Log("%v already exists", h)
   145  		return errors.New("key already exists")
   146  	}
   147  
   148  	be.sem.GetToken()
   149  
   150  	// wrap the reader so that net/http client cannot close the reader, return
   151  	// the token instead.
   152  	rd = preventCloser{
   153  		Reader: rd,
   154  		f: func() {
   155  			debug.Log("Close()")
   156  		},
   157  	}
   158  
   159  	debug.Log("InsertObject(%v, %v)", be.container.Name, objName)
   160  
   161  	err = be.container.GetBlobReference(objName).CreateBlockBlobFromReader(rd, nil)
   162  
   163  	be.sem.ReleaseToken()
   164  	debug.Log("%v, err %#v", objName, err)
   165  
   166  	return errors.Wrap(err, "CreateBlockBlobFromReader")
   167  }
   168  
   169  // wrapReader wraps an io.ReadCloser to run an additional function on Close.
   170  type wrapReader struct {
   171  	io.ReadCloser
   172  	f func()
   173  }
   174  
   175  func (wr wrapReader) Close() error {
   176  	err := wr.ReadCloser.Close()
   177  	wr.f()
   178  	return err
   179  }
   180  
   181  // Load returns a reader that yields the contents of the file at h at the
   182  // given offset. If length is nonzero, only a portion of the file is
   183  // returned. rd must be closed after use.
   184  func (be *Backend) Load(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
   185  	debug.Log("Load %v, length %v, offset %v from %v", h, length, offset, be.Filename(h))
   186  	if err := h.Valid(); err != nil {
   187  		return nil, err
   188  	}
   189  
   190  	if offset < 0 {
   191  		return nil, errors.New("offset is negative")
   192  	}
   193  
   194  	if length < 0 {
   195  		return nil, errors.Errorf("invalid length %d", length)
   196  	}
   197  
   198  	objName := be.Filename(h)
   199  	blob := be.container.GetBlobReference(objName)
   200  
   201  	start := uint64(offset)
   202  	var end uint64
   203  
   204  	if length > 0 {
   205  		end = uint64(offset + int64(length) - 1)
   206  	} else {
   207  		end = 0
   208  	}
   209  
   210  	be.sem.GetToken()
   211  
   212  	rd, err := blob.GetRange(&storage.GetBlobRangeOptions{Range: &storage.BlobRange{Start: start, End: end}})
   213  	if err != nil {
   214  		be.sem.ReleaseToken()
   215  		return nil, err
   216  	}
   217  
   218  	closeRd := wrapReader{
   219  		ReadCloser: rd,
   220  		f: func() {
   221  			debug.Log("Close()")
   222  			be.sem.ReleaseToken()
   223  		},
   224  	}
   225  
   226  	return closeRd, err
   227  }
   228  
   229  // Stat returns information about a blob.
   230  func (be *Backend) Stat(ctx context.Context, h restic.Handle) (restic.FileInfo, error) {
   231  	debug.Log("%v", h)
   232  
   233  	objName := be.Filename(h)
   234  	blob := be.container.GetBlobReference(objName)
   235  
   236  	be.sem.GetToken()
   237  	err := blob.GetProperties(nil)
   238  	be.sem.ReleaseToken()
   239  
   240  	if err != nil {
   241  		debug.Log("blob.GetProperties err %v", err)
   242  		return restic.FileInfo{}, errors.Wrap(err, "blob.GetProperties")
   243  	}
   244  
   245  	return restic.FileInfo{Size: int64(blob.Properties.ContentLength)}, nil
   246  }
   247  
   248  // Test returns true if a blob of the given type and name exists in the backend.
   249  func (be *Backend) Test(ctx context.Context, h restic.Handle) (bool, error) {
   250  	objName := be.Filename(h)
   251  
   252  	be.sem.GetToken()
   253  	found, err := be.container.GetBlobReference(objName).Exists()
   254  	be.sem.ReleaseToken()
   255  
   256  	if err != nil {
   257  		return false, err
   258  	}
   259  	return found, nil
   260  }
   261  
   262  // Remove removes the blob with the given name and type.
   263  func (be *Backend) Remove(ctx context.Context, h restic.Handle) error {
   264  	objName := be.Filename(h)
   265  
   266  	be.sem.GetToken()
   267  	_, err := be.container.GetBlobReference(objName).DeleteIfExists(nil)
   268  	be.sem.ReleaseToken()
   269  
   270  	debug.Log("Remove(%v) at %v -> err %v", h, objName, err)
   271  	return errors.Wrap(err, "client.RemoveObject")
   272  }
   273  
   274  // List returns a channel that yields all names of blobs of type t. A
   275  // goroutine is started for this. If the channel done is closed, sending
   276  // stops.
   277  func (be *Backend) List(ctx context.Context, t restic.FileType) <-chan string {
   278  	debug.Log("listing %v", t)
   279  	ch := make(chan string)
   280  
   281  	prefix := be.Dirname(restic.Handle{Type: t})
   282  
   283  	// make sure prefix ends with a slash
   284  	if prefix[len(prefix)-1] != '/' {
   285  		prefix += "/"
   286  	}
   287  
   288  	params := storage.ListBlobsParameters{
   289  		MaxResults: uint(be.listMaxItems),
   290  		Prefix:     prefix,
   291  	}
   292  
   293  	go func() {
   294  		defer close(ch)
   295  
   296  		for {
   297  			be.sem.GetToken()
   298  			obj, err := be.container.ListBlobs(params)
   299  			be.sem.ReleaseToken()
   300  
   301  			if err != nil {
   302  				return
   303  			}
   304  
   305  			debug.Log("got %v objects", len(obj.Blobs))
   306  
   307  			for _, item := range obj.Blobs {
   308  				m := strings.TrimPrefix(item.Name, prefix)
   309  				if m == "" {
   310  					continue
   311  				}
   312  
   313  				select {
   314  				case ch <- path.Base(m):
   315  				case <-ctx.Done():
   316  					return
   317  				}
   318  			}
   319  
   320  			if obj.NextMarker == "" {
   321  				break
   322  			}
   323  			params.Marker = obj.NextMarker
   324  		}
   325  	}()
   326  
   327  	return ch
   328  }
   329  
   330  // Remove keys for a specified backend type.
   331  func (be *Backend) removeKeys(ctx context.Context, t restic.FileType) error {
   332  	for key := range be.List(ctx, restic.DataFile) {
   333  		err := be.Remove(ctx, restic.Handle{Type: restic.DataFile, Name: key})
   334  		if err != nil {
   335  			return err
   336  		}
   337  	}
   338  
   339  	return nil
   340  }
   341  
   342  // Delete removes all restic keys in the bucket. It will not remove the bucket itself.
   343  func (be *Backend) Delete(ctx context.Context) error {
   344  	alltypes := []restic.FileType{
   345  		restic.DataFile,
   346  		restic.KeyFile,
   347  		restic.LockFile,
   348  		restic.SnapshotFile,
   349  		restic.IndexFile}
   350  
   351  	for _, t := range alltypes {
   352  		err := be.removeKeys(ctx, t)
   353  		if err != nil {
   354  			return nil
   355  		}
   356  	}
   357  
   358  	return be.Remove(ctx, restic.Handle{Type: restic.ConfigFile})
   359  }
   360  
   361  // Close does nothing
   362  func (be *Backend) Close() error { return nil }