1package storage 2 3// Copyright 2017 Microsoft Corporation 4// 5// Licensed under the Apache License, Version 2.0 (the "License"); 6// you may not use this file except in compliance with the License. 7// You may obtain a copy of the License at 8// 9// http://www.apache.org/licenses/LICENSE-2.0 10// 11// Unless required by applicable law or agreed to in writing, software 12// distributed under the License is distributed on an "AS IS" BASIS, 13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14// See the License for the specific language governing permissions and 15// limitations under the License. 16 17import ( 18 "encoding/xml" 19 "fmt" 20 "io" 21 "net/http" 22 "net/url" 23 "strconv" 24 "strings" 25 "time" 26) 27 28// Container represents an Azure container. 29type Container struct { 30 bsc *BlobStorageClient 31 Name string `xml:"Name"` 32 Properties ContainerProperties `xml:"Properties"` 33 Metadata map[string]string 34 sasuri url.URL 35} 36 37// Client returns the HTTP client used by the Container reference. 38func (c *Container) Client() *Client { 39 return &c.bsc.client 40} 41 42func (c *Container) buildPath() string { 43 return fmt.Sprintf("/%s", c.Name) 44} 45 46// GetURL gets the canonical URL to the container. 47// This method does not create a publicly accessible URL if the container 48// is private and this method does not check if the blob exists. 49func (c *Container) GetURL() string { 50 container := c.Name 51 if container == "" { 52 container = "$root" 53 } 54 return c.bsc.client.getEndpoint(blobServiceName, pathForResource(container, ""), nil) 55} 56 57// ContainerSASOptions are options to construct a container SAS 58// URI. 59// See https://docs.microsoft.com/en-us/rest/api/storageservices/constructing-a-service-sas 60type ContainerSASOptions struct { 61 ContainerSASPermissions 62 OverrideHeaders 63 SASOptions 64} 65 66// ContainerSASPermissions includes the available permissions for 67// a container SAS URI. 68type ContainerSASPermissions struct { 69 BlobServiceSASPermissions 70 List bool 71} 72 73// GetSASURI creates an URL to the container which contains the Shared 74// Access Signature with the specified options. 75// 76// See https://docs.microsoft.com/en-us/rest/api/storageservices/constructing-a-service-sas 77func (c *Container) GetSASURI(options ContainerSASOptions) (string, error) { 78 uri := c.GetURL() 79 signedResource := "c" 80 canonicalizedResource, err := c.bsc.client.buildCanonicalizedResource(uri, c.bsc.auth, true) 81 if err != nil { 82 return "", err 83 } 84 85 // build permissions string 86 permissions := options.BlobServiceSASPermissions.buildString() 87 if options.List { 88 permissions += "l" 89 } 90 91 return c.bsc.client.blobAndFileSASURI(options.SASOptions, uri, permissions, canonicalizedResource, signedResource, options.OverrideHeaders) 92} 93 94// ContainerProperties contains various properties of a container returned from 95// various endpoints like ListContainers. 96type ContainerProperties struct { 97 LastModified string `xml:"Last-Modified"` 98 Etag string `xml:"Etag"` 99 LeaseStatus string `xml:"LeaseStatus"` 100 LeaseState string `xml:"LeaseState"` 101 LeaseDuration string `xml:"LeaseDuration"` 102 PublicAccess ContainerAccessType `xml:"PublicAccess"` 103} 104 105// ContainerListResponse contains the response fields from 106// ListContainers call. 107// 108// See https://msdn.microsoft.com/en-us/library/azure/dd179352.aspx 109type ContainerListResponse struct { 110 XMLName xml.Name `xml:"EnumerationResults"` 111 Xmlns string `xml:"xmlns,attr"` 112 Prefix string `xml:"Prefix"` 113 Marker string `xml:"Marker"` 114 NextMarker string `xml:"NextMarker"` 115 MaxResults int64 `xml:"MaxResults"` 116 Containers []Container `xml:"Containers>Container"` 117} 118 119// BlobListResponse contains the response fields from ListBlobs call. 120// 121// See https://msdn.microsoft.com/en-us/library/azure/dd135734.aspx 122type BlobListResponse struct { 123 XMLName xml.Name `xml:"EnumerationResults"` 124 Xmlns string `xml:"xmlns,attr"` 125 Prefix string `xml:"Prefix"` 126 Marker string `xml:"Marker"` 127 NextMarker string `xml:"NextMarker"` 128 MaxResults int64 `xml:"MaxResults"` 129 Blobs []Blob `xml:"Blobs>Blob"` 130 131 // BlobPrefix is used to traverse blobs as if it were a file system. 132 // It is returned if ListBlobsParameters.Delimiter is specified. 133 // The list here can be thought of as "folders" that may contain 134 // other folders or blobs. 135 BlobPrefixes []string `xml:"Blobs>BlobPrefix>Name"` 136 137 // Delimiter is used to traverse blobs as if it were a file system. 138 // It is returned if ListBlobsParameters.Delimiter is specified. 139 Delimiter string `xml:"Delimiter"` 140} 141 142// IncludeBlobDataset has options to include in a list blobs operation 143type IncludeBlobDataset struct { 144 Snapshots bool 145 Metadata bool 146 UncommittedBlobs bool 147 Copy bool 148} 149 150// ListBlobsParameters defines the set of customizable 151// parameters to make a List Blobs call. 152// 153// See https://msdn.microsoft.com/en-us/library/azure/dd135734.aspx 154type ListBlobsParameters struct { 155 Prefix string 156 Delimiter string 157 Marker string 158 Include *IncludeBlobDataset 159 MaxResults uint 160 Timeout uint 161 RequestID string 162} 163 164func (p ListBlobsParameters) getParameters() url.Values { 165 out := url.Values{} 166 167 if p.Prefix != "" { 168 out.Set("prefix", p.Prefix) 169 } 170 if p.Delimiter != "" { 171 out.Set("delimiter", p.Delimiter) 172 } 173 if p.Marker != "" { 174 out.Set("marker", p.Marker) 175 } 176 if p.Include != nil { 177 include := []string{} 178 include = addString(include, p.Include.Snapshots, "snapshots") 179 include = addString(include, p.Include.Metadata, "metadata") 180 include = addString(include, p.Include.UncommittedBlobs, "uncommittedblobs") 181 include = addString(include, p.Include.Copy, "copy") 182 fullInclude := strings.Join(include, ",") 183 out.Set("include", fullInclude) 184 } 185 if p.MaxResults != 0 { 186 out.Set("maxresults", strconv.FormatUint(uint64(p.MaxResults), 10)) 187 } 188 if p.Timeout != 0 { 189 out.Set("timeout", strconv.FormatUint(uint64(p.Timeout), 10)) 190 } 191 192 return out 193} 194 195func addString(datasets []string, include bool, text string) []string { 196 if include { 197 datasets = append(datasets, text) 198 } 199 return datasets 200} 201 202// ContainerAccessType defines the access level to the container from a public 203// request. 204// 205// See https://msdn.microsoft.com/en-us/library/azure/dd179468.aspx and "x-ms- 206// blob-public-access" header. 207type ContainerAccessType string 208 209// Access options for containers 210const ( 211 ContainerAccessTypePrivate ContainerAccessType = "" 212 ContainerAccessTypeBlob ContainerAccessType = "blob" 213 ContainerAccessTypeContainer ContainerAccessType = "container" 214) 215 216// ContainerAccessPolicy represents each access policy in the container ACL. 217type ContainerAccessPolicy struct { 218 ID string 219 StartTime time.Time 220 ExpiryTime time.Time 221 CanRead bool 222 CanWrite bool 223 CanDelete bool 224} 225 226// ContainerPermissions represents the container ACLs. 227type ContainerPermissions struct { 228 AccessType ContainerAccessType 229 AccessPolicies []ContainerAccessPolicy 230} 231 232// ContainerAccessHeader references header used when setting/getting container ACL 233const ( 234 ContainerAccessHeader string = "x-ms-blob-public-access" 235) 236 237// GetBlobReference returns a Blob object for the specified blob name. 238func (c *Container) GetBlobReference(name string) *Blob { 239 return &Blob{ 240 Container: c, 241 Name: name, 242 } 243} 244 245// CreateContainerOptions includes the options for a create container operation 246type CreateContainerOptions struct { 247 Timeout uint 248 Access ContainerAccessType `header:"x-ms-blob-public-access"` 249 RequestID string `header:"x-ms-client-request-id"` 250} 251 252// Create creates a blob container within the storage account 253// with given name and access level. Returns error if container already exists. 254// 255// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Create-Container 256func (c *Container) Create(options *CreateContainerOptions) error { 257 resp, err := c.create(options) 258 if err != nil { 259 return err 260 } 261 defer drainRespBody(resp) 262 return checkRespCode(resp, []int{http.StatusCreated}) 263} 264 265// CreateIfNotExists creates a blob container if it does not exist. Returns 266// true if container is newly created or false if container already exists. 267func (c *Container) CreateIfNotExists(options *CreateContainerOptions) (bool, error) { 268 resp, err := c.create(options) 269 if resp != nil { 270 defer drainRespBody(resp) 271 if resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusConflict { 272 return resp.StatusCode == http.StatusCreated, nil 273 } 274 } 275 return false, err 276} 277 278func (c *Container) create(options *CreateContainerOptions) (*http.Response, error) { 279 query := url.Values{"restype": {"container"}} 280 headers := c.bsc.client.getStandardHeaders() 281 headers = c.bsc.client.addMetadataToHeaders(headers, c.Metadata) 282 283 if options != nil { 284 query = addTimeout(query, options.Timeout) 285 headers = mergeHeaders(headers, headersFromStruct(*options)) 286 } 287 uri := c.bsc.client.getEndpoint(blobServiceName, c.buildPath(), query) 288 289 return c.bsc.client.exec(http.MethodPut, uri, headers, nil, c.bsc.auth) 290} 291 292// Exists returns true if a container with given name exists 293// on the storage account, otherwise returns false. 294func (c *Container) Exists() (bool, error) { 295 q := url.Values{"restype": {"container"}} 296 var uri string 297 if c.bsc.client.isServiceSASClient() { 298 q = mergeParams(q, c.sasuri.Query()) 299 newURI := c.sasuri 300 newURI.RawQuery = q.Encode() 301 uri = newURI.String() 302 303 } else { 304 uri = c.bsc.client.getEndpoint(blobServiceName, c.buildPath(), q) 305 } 306 headers := c.bsc.client.getStandardHeaders() 307 308 resp, err := c.bsc.client.exec(http.MethodHead, uri, headers, nil, c.bsc.auth) 309 if resp != nil { 310 defer drainRespBody(resp) 311 if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound { 312 return resp.StatusCode == http.StatusOK, nil 313 } 314 } 315 return false, err 316} 317 318// SetContainerPermissionOptions includes options for a set container permissions operation 319type SetContainerPermissionOptions struct { 320 Timeout uint 321 LeaseID string `header:"x-ms-lease-id"` 322 IfModifiedSince *time.Time `header:"If-Modified-Since"` 323 IfUnmodifiedSince *time.Time `header:"If-Unmodified-Since"` 324 RequestID string `header:"x-ms-client-request-id"` 325} 326 327// SetPermissions sets up container permissions 328// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Set-Container-ACL 329func (c *Container) SetPermissions(permissions ContainerPermissions, options *SetContainerPermissionOptions) error { 330 body, length, err := generateContainerACLpayload(permissions.AccessPolicies) 331 if err != nil { 332 return err 333 } 334 params := url.Values{ 335 "restype": {"container"}, 336 "comp": {"acl"}, 337 } 338 headers := c.bsc.client.getStandardHeaders() 339 headers = addToHeaders(headers, ContainerAccessHeader, string(permissions.AccessType)) 340 headers["Content-Length"] = strconv.Itoa(length) 341 342 if options != nil { 343 params = addTimeout(params, options.Timeout) 344 headers = mergeHeaders(headers, headersFromStruct(*options)) 345 } 346 uri := c.bsc.client.getEndpoint(blobServiceName, c.buildPath(), params) 347 348 resp, err := c.bsc.client.exec(http.MethodPut, uri, headers, body, c.bsc.auth) 349 if err != nil { 350 return err 351 } 352 defer drainRespBody(resp) 353 return checkRespCode(resp, []int{http.StatusOK}) 354} 355 356// GetContainerPermissionOptions includes options for a get container permissions operation 357type GetContainerPermissionOptions struct { 358 Timeout uint 359 LeaseID string `header:"x-ms-lease-id"` 360 RequestID string `header:"x-ms-client-request-id"` 361} 362 363// GetPermissions gets the container permissions as per https://msdn.microsoft.com/en-us/library/azure/dd179469.aspx 364// If timeout is 0 then it will not be passed to Azure 365// leaseID will only be passed to Azure if populated 366func (c *Container) GetPermissions(options *GetContainerPermissionOptions) (*ContainerPermissions, error) { 367 params := url.Values{ 368 "restype": {"container"}, 369 "comp": {"acl"}, 370 } 371 headers := c.bsc.client.getStandardHeaders() 372 373 if options != nil { 374 params = addTimeout(params, options.Timeout) 375 headers = mergeHeaders(headers, headersFromStruct(*options)) 376 } 377 uri := c.bsc.client.getEndpoint(blobServiceName, c.buildPath(), params) 378 379 resp, err := c.bsc.client.exec(http.MethodGet, uri, headers, nil, c.bsc.auth) 380 if err != nil { 381 return nil, err 382 } 383 defer resp.Body.Close() 384 385 var ap AccessPolicy 386 err = xmlUnmarshal(resp.Body, &ap.SignedIdentifiersList) 387 if err != nil { 388 return nil, err 389 } 390 return buildAccessPolicy(ap, &resp.Header), nil 391} 392 393func buildAccessPolicy(ap AccessPolicy, headers *http.Header) *ContainerPermissions { 394 // containerAccess. Blob, Container, empty 395 containerAccess := headers.Get(http.CanonicalHeaderKey(ContainerAccessHeader)) 396 permissions := ContainerPermissions{ 397 AccessType: ContainerAccessType(containerAccess), 398 AccessPolicies: []ContainerAccessPolicy{}, 399 } 400 401 for _, policy := range ap.SignedIdentifiersList.SignedIdentifiers { 402 capd := ContainerAccessPolicy{ 403 ID: policy.ID, 404 StartTime: policy.AccessPolicy.StartTime, 405 ExpiryTime: policy.AccessPolicy.ExpiryTime, 406 } 407 capd.CanRead = updatePermissions(policy.AccessPolicy.Permission, "r") 408 capd.CanWrite = updatePermissions(policy.AccessPolicy.Permission, "w") 409 capd.CanDelete = updatePermissions(policy.AccessPolicy.Permission, "d") 410 411 permissions.AccessPolicies = append(permissions.AccessPolicies, capd) 412 } 413 return &permissions 414} 415 416// DeleteContainerOptions includes options for a delete container operation 417type DeleteContainerOptions struct { 418 Timeout uint 419 LeaseID string `header:"x-ms-lease-id"` 420 IfModifiedSince *time.Time `header:"If-Modified-Since"` 421 IfUnmodifiedSince *time.Time `header:"If-Unmodified-Since"` 422 RequestID string `header:"x-ms-client-request-id"` 423} 424 425// Delete deletes the container with given name on the storage 426// account. If the container does not exist returns error. 427// 428// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/delete-container 429func (c *Container) Delete(options *DeleteContainerOptions) error { 430 resp, err := c.delete(options) 431 if err != nil { 432 return err 433 } 434 defer drainRespBody(resp) 435 return checkRespCode(resp, []int{http.StatusAccepted}) 436} 437 438// DeleteIfExists deletes the container with given name on the storage 439// account if it exists. Returns true if container is deleted with this call, or 440// false if the container did not exist at the time of the Delete Container 441// operation. 442// 443// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/delete-container 444func (c *Container) DeleteIfExists(options *DeleteContainerOptions) (bool, error) { 445 resp, err := c.delete(options) 446 if resp != nil { 447 defer drainRespBody(resp) 448 if resp.StatusCode == http.StatusAccepted || resp.StatusCode == http.StatusNotFound { 449 return resp.StatusCode == http.StatusAccepted, nil 450 } 451 } 452 return false, err 453} 454 455func (c *Container) delete(options *DeleteContainerOptions) (*http.Response, error) { 456 query := url.Values{"restype": {"container"}} 457 headers := c.bsc.client.getStandardHeaders() 458 459 if options != nil { 460 query = addTimeout(query, options.Timeout) 461 headers = mergeHeaders(headers, headersFromStruct(*options)) 462 } 463 uri := c.bsc.client.getEndpoint(blobServiceName, c.buildPath(), query) 464 465 return c.bsc.client.exec(http.MethodDelete, uri, headers, nil, c.bsc.auth) 466} 467 468// ListBlobs returns an object that contains list of blobs in the container, 469// pagination token and other information in the response of List Blobs call. 470// 471// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/List-Blobs 472func (c *Container) ListBlobs(params ListBlobsParameters) (BlobListResponse, error) { 473 q := mergeParams(params.getParameters(), url.Values{ 474 "restype": {"container"}, 475 "comp": {"list"}, 476 }) 477 var uri string 478 if c.bsc.client.isServiceSASClient() { 479 q = mergeParams(q, c.sasuri.Query()) 480 newURI := c.sasuri 481 newURI.RawQuery = q.Encode() 482 uri = newURI.String() 483 } else { 484 uri = c.bsc.client.getEndpoint(blobServiceName, c.buildPath(), q) 485 } 486 487 headers := c.bsc.client.getStandardHeaders() 488 headers = addToHeaders(headers, "x-ms-client-request-id", params.RequestID) 489 490 var out BlobListResponse 491 resp, err := c.bsc.client.exec(http.MethodGet, uri, headers, nil, c.bsc.auth) 492 if err != nil { 493 return out, err 494 } 495 defer resp.Body.Close() 496 497 err = xmlUnmarshal(resp.Body, &out) 498 for i := range out.Blobs { 499 out.Blobs[i].Container = c 500 } 501 return out, err 502} 503 504// ContainerMetadataOptions includes options for container metadata operations 505type ContainerMetadataOptions struct { 506 Timeout uint 507 LeaseID string `header:"x-ms-lease-id"` 508 RequestID string `header:"x-ms-client-request-id"` 509} 510 511// SetMetadata replaces the metadata for the specified container. 512// 513// Some keys may be converted to Camel-Case before sending. All keys 514// are returned in lower case by GetBlobMetadata. HTTP header names 515// are case-insensitive so case munging should not matter to other 516// applications either. 517// 518// See https://docs.microsoft.com/en-us/rest/api/storageservices/set-container-metadata 519func (c *Container) SetMetadata(options *ContainerMetadataOptions) error { 520 params := url.Values{ 521 "comp": {"metadata"}, 522 "restype": {"container"}, 523 } 524 headers := c.bsc.client.getStandardHeaders() 525 headers = c.bsc.client.addMetadataToHeaders(headers, c.Metadata) 526 527 if options != nil { 528 params = addTimeout(params, options.Timeout) 529 headers = mergeHeaders(headers, headersFromStruct(*options)) 530 } 531 532 uri := c.bsc.client.getEndpoint(blobServiceName, c.buildPath(), params) 533 534 resp, err := c.bsc.client.exec(http.MethodPut, uri, headers, nil, c.bsc.auth) 535 if err != nil { 536 return err 537 } 538 defer drainRespBody(resp) 539 return checkRespCode(resp, []int{http.StatusOK}) 540} 541 542// GetMetadata returns all user-defined metadata for the specified container. 543// 544// All metadata keys will be returned in lower case. (HTTP header 545// names are case-insensitive.) 546// 547// See https://docs.microsoft.com/en-us/rest/api/storageservices/get-container-metadata 548func (c *Container) GetMetadata(options *ContainerMetadataOptions) error { 549 params := url.Values{ 550 "comp": {"metadata"}, 551 "restype": {"container"}, 552 } 553 headers := c.bsc.client.getStandardHeaders() 554 555 if options != nil { 556 params = addTimeout(params, options.Timeout) 557 headers = mergeHeaders(headers, headersFromStruct(*options)) 558 } 559 560 uri := c.bsc.client.getEndpoint(blobServiceName, c.buildPath(), params) 561 562 resp, err := c.bsc.client.exec(http.MethodGet, uri, headers, nil, c.bsc.auth) 563 if err != nil { 564 return err 565 } 566 defer drainRespBody(resp) 567 if err := checkRespCode(resp, []int{http.StatusOK}); err != nil { 568 return err 569 } 570 571 c.writeMetadata(resp.Header) 572 return nil 573} 574 575func (c *Container) writeMetadata(h http.Header) { 576 c.Metadata = writeMetadata(h) 577} 578 579func generateContainerACLpayload(policies []ContainerAccessPolicy) (io.Reader, int, error) { 580 sil := SignedIdentifiers{ 581 SignedIdentifiers: []SignedIdentifier{}, 582 } 583 for _, capd := range policies { 584 permission := capd.generateContainerPermissions() 585 signedIdentifier := convertAccessPolicyToXMLStructs(capd.ID, capd.StartTime, capd.ExpiryTime, permission) 586 sil.SignedIdentifiers = append(sil.SignedIdentifiers, signedIdentifier) 587 } 588 return xmlMarshal(sil) 589} 590 591func (capd *ContainerAccessPolicy) generateContainerPermissions() (permissions string) { 592 // generate the permissions string (rwd). 593 // still want the end user API to have bool flags. 594 permissions = "" 595 596 if capd.CanRead { 597 permissions += "r" 598 } 599 600 if capd.CanWrite { 601 permissions += "w" 602 } 603 604 if capd.CanDelete { 605 permissions += "d" 606 } 607 608 return permissions 609} 610 611// GetProperties updated the properties of the container. 612// 613// See https://docs.microsoft.com/en-us/rest/api/storageservices/get-container-properties 614func (c *Container) GetProperties() error { 615 params := url.Values{ 616 "restype": {"container"}, 617 } 618 headers := c.bsc.client.getStandardHeaders() 619 620 uri := c.bsc.client.getEndpoint(blobServiceName, c.buildPath(), params) 621 622 resp, err := c.bsc.client.exec(http.MethodGet, uri, headers, nil, c.bsc.auth) 623 if err != nil { 624 return err 625 } 626 defer resp.Body.Close() 627 if err := checkRespCode(resp, []int{http.StatusOK}); err != nil { 628 return err 629 } 630 631 // update properties 632 c.Properties.Etag = resp.Header.Get(headerEtag) 633 c.Properties.LeaseStatus = resp.Header.Get("x-ms-lease-status") 634 c.Properties.LeaseState = resp.Header.Get("x-ms-lease-state") 635 c.Properties.LeaseDuration = resp.Header.Get("x-ms-lease-duration") 636 c.Properties.LastModified = resp.Header.Get("Last-Modified") 637 c.Properties.PublicAccess = ContainerAccessType(resp.Header.Get(ContainerAccessHeader)) 638 639 return nil 640} 641