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