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 }