github.com/mika/distribution@v2.2.2-0.20160108133430-a75790e3d8e0+incompatible/registry/storage/driver/azure/azure.go (about)

     1  // Package azure provides a storagedriver.StorageDriver implementation to
     2  // store blobs in Microsoft Azure Blob Storage Service.
     3  package azure
     4  
     5  import (
     6  	"bytes"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/docker/distribution/context"
    15  	storagedriver "github.com/docker/distribution/registry/storage/driver"
    16  	"github.com/docker/distribution/registry/storage/driver/base"
    17  	"github.com/docker/distribution/registry/storage/driver/factory"
    18  
    19  	azure "github.com/Azure/azure-sdk-for-go/storage"
    20  )
    21  
    22  const driverName = "azure"
    23  
    24  const (
    25  	paramAccountName = "accountname"
    26  	paramAccountKey  = "accountkey"
    27  	paramContainer   = "container"
    28  	paramRealm       = "realm"
    29  )
    30  
    31  type driver struct {
    32  	client    azure.BlobStorageClient
    33  	container string
    34  }
    35  
    36  type baseEmbed struct{ base.Base }
    37  
    38  // Driver is a storagedriver.StorageDriver implementation backed by
    39  // Microsoft Azure Blob Storage Service.
    40  type Driver struct{ baseEmbed }
    41  
    42  func init() {
    43  	factory.Register(driverName, &azureDriverFactory{})
    44  }
    45  
    46  type azureDriverFactory struct{}
    47  
    48  func (factory *azureDriverFactory) Create(parameters map[string]interface{}) (storagedriver.StorageDriver, error) {
    49  	return FromParameters(parameters)
    50  }
    51  
    52  // FromParameters constructs a new Driver with a given parameters map.
    53  func FromParameters(parameters map[string]interface{}) (*Driver, error) {
    54  	accountName, ok := parameters[paramAccountName]
    55  	if !ok || fmt.Sprint(accountName) == "" {
    56  		return nil, fmt.Errorf("No %s parameter provided", paramAccountName)
    57  	}
    58  
    59  	accountKey, ok := parameters[paramAccountKey]
    60  	if !ok || fmt.Sprint(accountKey) == "" {
    61  		return nil, fmt.Errorf("No %s parameter provided", paramAccountKey)
    62  	}
    63  
    64  	container, ok := parameters[paramContainer]
    65  	if !ok || fmt.Sprint(container) == "" {
    66  		return nil, fmt.Errorf("No %s parameter provided", paramContainer)
    67  	}
    68  
    69  	realm, ok := parameters[paramRealm]
    70  	if !ok || fmt.Sprint(realm) == "" {
    71  		realm = azure.DefaultBaseURL
    72  	}
    73  
    74  	return New(fmt.Sprint(accountName), fmt.Sprint(accountKey), fmt.Sprint(container), fmt.Sprint(realm))
    75  }
    76  
    77  // New constructs a new Driver with the given Azure Storage Account credentials
    78  func New(accountName, accountKey, container, realm string) (*Driver, error) {
    79  	api, err := azure.NewClient(accountName, accountKey, realm, azure.DefaultAPIVersion, true)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  
    84  	blobClient := api.GetBlobService()
    85  
    86  	// Create registry container
    87  	if _, err = blobClient.CreateContainerIfNotExists(container, azure.ContainerAccessTypePrivate); err != nil {
    88  		return nil, err
    89  	}
    90  
    91  	d := &driver{
    92  		client:    blobClient,
    93  		container: container}
    94  	return &Driver{baseEmbed: baseEmbed{Base: base.Base{StorageDriver: d}}}, nil
    95  }
    96  
    97  // Implement the storagedriver.StorageDriver interface.
    98  func (d *driver) Name() string {
    99  	return driverName
   100  }
   101  
   102  // GetContent retrieves the content stored at "path" as a []byte.
   103  func (d *driver) GetContent(ctx context.Context, path string) ([]byte, error) {
   104  	blob, err := d.client.GetBlob(d.container, path)
   105  	if err != nil {
   106  		if is404(err) {
   107  			return nil, storagedriver.PathNotFoundError{Path: path}
   108  		}
   109  		return nil, err
   110  	}
   111  
   112  	return ioutil.ReadAll(blob)
   113  }
   114  
   115  // PutContent stores the []byte content at a location designated by "path".
   116  func (d *driver) PutContent(ctx context.Context, path string, contents []byte) error {
   117  	if _, err := d.client.DeleteBlobIfExists(d.container, path); err != nil {
   118  		return err
   119  	}
   120  	if err := d.client.CreateBlockBlob(d.container, path); err != nil {
   121  		return err
   122  	}
   123  	bs := newAzureBlockStorage(d.client)
   124  	bw := newRandomBlobWriter(&bs, azure.MaxBlobBlockSize)
   125  	_, err := bw.WriteBlobAt(d.container, path, 0, bytes.NewReader(contents))
   126  	return err
   127  }
   128  
   129  // ReadStream retrieves an io.ReadCloser for the content stored at "path" with a
   130  // given byte offset.
   131  func (d *driver) ReadStream(ctx context.Context, path string, offset int64) (io.ReadCloser, error) {
   132  	if ok, err := d.client.BlobExists(d.container, path); err != nil {
   133  		return nil, err
   134  	} else if !ok {
   135  		return nil, storagedriver.PathNotFoundError{Path: path}
   136  	}
   137  
   138  	info, err := d.client.GetBlobProperties(d.container, path)
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  
   143  	size := int64(info.ContentLength)
   144  	if offset >= size {
   145  		return ioutil.NopCloser(bytes.NewReader(nil)), nil
   146  	}
   147  
   148  	bytesRange := fmt.Sprintf("%v-", offset)
   149  	resp, err := d.client.GetBlobRange(d.container, path, bytesRange)
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  	return resp, nil
   154  }
   155  
   156  // WriteStream stores the contents of the provided io.ReadCloser at a location
   157  // designated by the given path.
   158  func (d *driver) WriteStream(ctx context.Context, path string, offset int64, reader io.Reader) (int64, error) {
   159  	if blobExists, err := d.client.BlobExists(d.container, path); err != nil {
   160  		return 0, err
   161  	} else if !blobExists {
   162  		err := d.client.CreateBlockBlob(d.container, path)
   163  		if err != nil {
   164  			return 0, err
   165  		}
   166  	}
   167  	if offset < 0 {
   168  		return 0, storagedriver.InvalidOffsetError{Path: path, Offset: offset}
   169  	}
   170  
   171  	bs := newAzureBlockStorage(d.client)
   172  	bw := newRandomBlobWriter(&bs, azure.MaxBlobBlockSize)
   173  	zw := newZeroFillWriter(&bw)
   174  	return zw.Write(d.container, path, offset, reader)
   175  }
   176  
   177  // Stat retrieves the FileInfo for the given path, including the current size
   178  // in bytes and the creation time.
   179  func (d *driver) Stat(ctx context.Context, path string) (storagedriver.FileInfo, error) {
   180  	// Check if the path is a blob
   181  	if ok, err := d.client.BlobExists(d.container, path); err != nil {
   182  		return nil, err
   183  	} else if ok {
   184  		blob, err := d.client.GetBlobProperties(d.container, path)
   185  		if err != nil {
   186  			return nil, err
   187  		}
   188  
   189  		mtim, err := time.Parse(http.TimeFormat, blob.LastModified)
   190  		if err != nil {
   191  			return nil, err
   192  		}
   193  
   194  		return storagedriver.FileInfoInternal{FileInfoFields: storagedriver.FileInfoFields{
   195  			Path:    path,
   196  			Size:    int64(blob.ContentLength),
   197  			ModTime: mtim,
   198  			IsDir:   false,
   199  		}}, nil
   200  	}
   201  
   202  	// Check if path is a virtual container
   203  	virtContainerPath := path
   204  	if !strings.HasSuffix(virtContainerPath, "/") {
   205  		virtContainerPath += "/"
   206  	}
   207  	blobs, err := d.client.ListBlobs(d.container, azure.ListBlobsParameters{
   208  		Prefix:     virtContainerPath,
   209  		MaxResults: 1,
   210  	})
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  	if len(blobs.Blobs) > 0 {
   215  		// path is a virtual container
   216  		return storagedriver.FileInfoInternal{FileInfoFields: storagedriver.FileInfoFields{
   217  			Path:  path,
   218  			IsDir: true,
   219  		}}, nil
   220  	}
   221  
   222  	// path is not a blob or virtual container
   223  	return nil, storagedriver.PathNotFoundError{Path: path}
   224  }
   225  
   226  // List returns a list of the objects that are direct descendants of the given
   227  // path.
   228  func (d *driver) List(ctx context.Context, path string) ([]string, error) {
   229  	if path == "/" {
   230  		path = ""
   231  	}
   232  
   233  	blobs, err := d.listBlobs(d.container, path)
   234  	if err != nil {
   235  		return blobs, err
   236  	}
   237  
   238  	list := directDescendants(blobs, path)
   239  	return list, nil
   240  }
   241  
   242  // Move moves an object stored at sourcePath to destPath, removing the original
   243  // object.
   244  func (d *driver) Move(ctx context.Context, sourcePath string, destPath string) error {
   245  	sourceBlobURL := d.client.GetBlobURL(d.container, sourcePath)
   246  	err := d.client.CopyBlob(d.container, destPath, sourceBlobURL)
   247  	if err != nil {
   248  		if is404(err) {
   249  			return storagedriver.PathNotFoundError{Path: sourcePath}
   250  		}
   251  		return err
   252  	}
   253  
   254  	return d.client.DeleteBlob(d.container, sourcePath)
   255  }
   256  
   257  // Delete recursively deletes all objects stored at "path" and its subpaths.
   258  func (d *driver) Delete(ctx context.Context, path string) error {
   259  	ok, err := d.client.DeleteBlobIfExists(d.container, path)
   260  	if err != nil {
   261  		return err
   262  	}
   263  	if ok {
   264  		return nil // was a blob and deleted, return
   265  	}
   266  
   267  	// Not a blob, see if path is a virtual container with blobs
   268  	blobs, err := d.listBlobs(d.container, path)
   269  	if err != nil {
   270  		return err
   271  	}
   272  
   273  	for _, b := range blobs {
   274  		if err = d.client.DeleteBlob(d.container, b); err != nil {
   275  			return err
   276  		}
   277  	}
   278  
   279  	if len(blobs) == 0 {
   280  		return storagedriver.PathNotFoundError{Path: path}
   281  	}
   282  	return nil
   283  }
   284  
   285  // URLFor returns a publicly accessible URL for the blob stored at given path
   286  // for specified duration by making use of Azure Storage Shared Access Signatures (SAS).
   287  // See https://msdn.microsoft.com/en-us/library/azure/ee395415.aspx for more info.
   288  func (d *driver) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) {
   289  	expiresTime := time.Now().UTC().Add(20 * time.Minute) // default expiration
   290  	expires, ok := options["expiry"]
   291  	if ok {
   292  		t, ok := expires.(time.Time)
   293  		if ok {
   294  			expiresTime = t
   295  		}
   296  	}
   297  	return d.client.GetBlobSASURI(d.container, path, expiresTime, "r")
   298  }
   299  
   300  // directDescendants will find direct descendants (blobs or virtual containers)
   301  // of from list of blob paths and will return their full paths. Elements in blobs
   302  // list must be prefixed with a "/" and
   303  //
   304  // Example: direct descendants of "/" in {"/foo", "/bar/1", "/bar/2"} is
   305  // {"/foo", "/bar"} and direct descendants of "bar" is {"/bar/1", "/bar/2"}
   306  func directDescendants(blobs []string, prefix string) []string {
   307  	if !strings.HasPrefix(prefix, "/") { // add trailing '/'
   308  		prefix = "/" + prefix
   309  	}
   310  	if !strings.HasSuffix(prefix, "/") { // containerify the path
   311  		prefix += "/"
   312  	}
   313  
   314  	out := make(map[string]bool)
   315  	for _, b := range blobs {
   316  		if strings.HasPrefix(b, prefix) {
   317  			rel := b[len(prefix):]
   318  			c := strings.Count(rel, "/")
   319  			if c == 0 {
   320  				out[b] = true
   321  			} else {
   322  				out[prefix+rel[:strings.Index(rel, "/")]] = true
   323  			}
   324  		}
   325  	}
   326  
   327  	var keys []string
   328  	for k := range out {
   329  		keys = append(keys, k)
   330  	}
   331  	return keys
   332  }
   333  
   334  func (d *driver) listBlobs(container, virtPath string) ([]string, error) {
   335  	if virtPath != "" && !strings.HasSuffix(virtPath, "/") { // containerify the path
   336  		virtPath += "/"
   337  	}
   338  
   339  	out := []string{}
   340  	marker := ""
   341  	for {
   342  		resp, err := d.client.ListBlobs(d.container, azure.ListBlobsParameters{
   343  			Marker: marker,
   344  			Prefix: virtPath,
   345  		})
   346  
   347  		if err != nil {
   348  			return out, err
   349  		}
   350  
   351  		for _, b := range resp.Blobs {
   352  			out = append(out, b.Name)
   353  		}
   354  
   355  		if len(resp.Blobs) == 0 || resp.NextMarker == "" {
   356  			break
   357  		}
   358  		marker = resp.NextMarker
   359  	}
   360  	return out, nil
   361  }
   362  
   363  func is404(err error) bool {
   364  	e, ok := err.(azure.AzureStorageServiceError)
   365  	return ok && e.StatusCode == http.StatusNotFound
   366  }