forked from TrueCloudLab/restic
453 lines
14 KiB
Go
453 lines
14 KiB
Go
package storage
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Container represents an Azure container.
|
|
type Container struct {
|
|
bsc *BlobStorageClient
|
|
Name string `xml:"Name"`
|
|
Properties ContainerProperties `xml:"Properties"`
|
|
Metadata map[string]string
|
|
}
|
|
|
|
func (c *Container) buildPath() string {
|
|
return fmt.Sprintf("/%s", c.Name)
|
|
}
|
|
|
|
// ContainerProperties contains various properties of a container returned from
|
|
// various endpoints like ListContainers.
|
|
type ContainerProperties struct {
|
|
LastModified string `xml:"Last-Modified"`
|
|
Etag string `xml:"Etag"`
|
|
LeaseStatus string `xml:"LeaseStatus"`
|
|
LeaseState string `xml:"LeaseState"`
|
|
LeaseDuration string `xml:"LeaseDuration"`
|
|
}
|
|
|
|
// ContainerListResponse contains the response fields from
|
|
// ListContainers call.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd179352.aspx
|
|
type ContainerListResponse struct {
|
|
XMLName xml.Name `xml:"EnumerationResults"`
|
|
Xmlns string `xml:"xmlns,attr"`
|
|
Prefix string `xml:"Prefix"`
|
|
Marker string `xml:"Marker"`
|
|
NextMarker string `xml:"NextMarker"`
|
|
MaxResults int64 `xml:"MaxResults"`
|
|
Containers []Container `xml:"Containers>Container"`
|
|
}
|
|
|
|
// BlobListResponse contains the response fields from ListBlobs call.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd135734.aspx
|
|
type BlobListResponse struct {
|
|
XMLName xml.Name `xml:"EnumerationResults"`
|
|
Xmlns string `xml:"xmlns,attr"`
|
|
Prefix string `xml:"Prefix"`
|
|
Marker string `xml:"Marker"`
|
|
NextMarker string `xml:"NextMarker"`
|
|
MaxResults int64 `xml:"MaxResults"`
|
|
Blobs []Blob `xml:"Blobs>Blob"`
|
|
|
|
// BlobPrefix is used to traverse blobs as if it were a file system.
|
|
// It is returned if ListBlobsParameters.Delimiter is specified.
|
|
// The list here can be thought of as "folders" that may contain
|
|
// other folders or blobs.
|
|
BlobPrefixes []string `xml:"Blobs>BlobPrefix>Name"`
|
|
|
|
// Delimiter is used to traverse blobs as if it were a file system.
|
|
// It is returned if ListBlobsParameters.Delimiter is specified.
|
|
Delimiter string `xml:"Delimiter"`
|
|
}
|
|
|
|
// IncludeBlobDataset has options to include in a list blobs operation
|
|
type IncludeBlobDataset struct {
|
|
Snapshots bool
|
|
Metadata bool
|
|
UncommittedBlobs bool
|
|
Copy bool
|
|
}
|
|
|
|
// ListBlobsParameters defines the set of customizable
|
|
// parameters to make a List Blobs call.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd135734.aspx
|
|
type ListBlobsParameters struct {
|
|
Prefix string
|
|
Delimiter string
|
|
Marker string
|
|
Include *IncludeBlobDataset
|
|
MaxResults uint
|
|
Timeout uint
|
|
RequestID string
|
|
}
|
|
|
|
func (p ListBlobsParameters) getParameters() url.Values {
|
|
out := url.Values{}
|
|
|
|
if p.Prefix != "" {
|
|
out.Set("prefix", p.Prefix)
|
|
}
|
|
if p.Delimiter != "" {
|
|
out.Set("delimiter", p.Delimiter)
|
|
}
|
|
if p.Marker != "" {
|
|
out.Set("marker", p.Marker)
|
|
}
|
|
if p.Include != nil {
|
|
include := []string{}
|
|
include = addString(include, p.Include.Snapshots, "snapshots")
|
|
include = addString(include, p.Include.Metadata, "metadata")
|
|
include = addString(include, p.Include.UncommittedBlobs, "uncommittedblobs")
|
|
include = addString(include, p.Include.Copy, "copy")
|
|
fullInclude := strings.Join(include, ",")
|
|
out.Set("include", fullInclude)
|
|
}
|
|
if p.MaxResults != 0 {
|
|
out.Set("maxresults", strconv.FormatUint(uint64(p.MaxResults), 10))
|
|
}
|
|
if p.Timeout != 0 {
|
|
out.Set("timeout", strconv.FormatUint(uint64(p.Timeout), 10))
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
func addString(datasets []string, include bool, text string) []string {
|
|
if include {
|
|
datasets = append(datasets, text)
|
|
}
|
|
return datasets
|
|
}
|
|
|
|
// ContainerAccessType defines the access level to the container from a public
|
|
// request.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd179468.aspx and "x-ms-
|
|
// blob-public-access" header.
|
|
type ContainerAccessType string
|
|
|
|
// Access options for containers
|
|
const (
|
|
ContainerAccessTypePrivate ContainerAccessType = ""
|
|
ContainerAccessTypeBlob ContainerAccessType = "blob"
|
|
ContainerAccessTypeContainer ContainerAccessType = "container"
|
|
)
|
|
|
|
// ContainerAccessPolicy represents each access policy in the container ACL.
|
|
type ContainerAccessPolicy struct {
|
|
ID string
|
|
StartTime time.Time
|
|
ExpiryTime time.Time
|
|
CanRead bool
|
|
CanWrite bool
|
|
CanDelete bool
|
|
}
|
|
|
|
// ContainerPermissions represents the container ACLs.
|
|
type ContainerPermissions struct {
|
|
AccessType ContainerAccessType
|
|
AccessPolicies []ContainerAccessPolicy
|
|
}
|
|
|
|
// ContainerAccessHeader references header used when setting/getting container ACL
|
|
const (
|
|
ContainerAccessHeader string = "x-ms-blob-public-access"
|
|
)
|
|
|
|
// GetBlobReference returns a Blob object for the specified blob name.
|
|
func (c *Container) GetBlobReference(name string) *Blob {
|
|
return &Blob{
|
|
Container: c,
|
|
Name: name,
|
|
}
|
|
}
|
|
|
|
// CreateContainerOptions includes the options for a create container operation
|
|
type CreateContainerOptions struct {
|
|
Timeout uint
|
|
Access ContainerAccessType `header:"x-ms-blob-public-access"`
|
|
RequestID string `header:"x-ms-client-request-id"`
|
|
}
|
|
|
|
// Create creates a blob container within the storage account
|
|
// with given name and access level. Returns error if container already exists.
|
|
//
|
|
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Create-Container
|
|
func (c *Container) Create(options *CreateContainerOptions) error {
|
|
resp, err := c.create(options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
readAndCloseBody(resp.body)
|
|
return checkRespCode(resp.statusCode, []int{http.StatusCreated})
|
|
}
|
|
|
|
// CreateIfNotExists creates a blob container if it does not exist. Returns
|
|
// true if container is newly created or false if container already exists.
|
|
func (c *Container) CreateIfNotExists(options *CreateContainerOptions) (bool, error) {
|
|
resp, err := c.create(options)
|
|
if resp != nil {
|
|
defer readAndCloseBody(resp.body)
|
|
if resp.statusCode == http.StatusCreated || resp.statusCode == http.StatusConflict {
|
|
return resp.statusCode == http.StatusCreated, nil
|
|
}
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
func (c *Container) create(options *CreateContainerOptions) (*storageResponse, error) {
|
|
query := url.Values{"restype": {"container"}}
|
|
headers := c.bsc.client.getStandardHeaders()
|
|
headers = c.bsc.client.addMetadataToHeaders(headers, c.Metadata)
|
|
|
|
if options != nil {
|
|
query = addTimeout(query, options.Timeout)
|
|
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
}
|
|
uri := c.bsc.client.getEndpoint(blobServiceName, c.buildPath(), query)
|
|
|
|
return c.bsc.client.exec(http.MethodPut, uri, headers, nil, c.bsc.auth)
|
|
}
|
|
|
|
// Exists returns true if a container with given name exists
|
|
// on the storage account, otherwise returns false.
|
|
func (c *Container) Exists() (bool, error) {
|
|
uri := c.bsc.client.getEndpoint(blobServiceName, c.buildPath(), url.Values{"restype": {"container"}})
|
|
headers := c.bsc.client.getStandardHeaders()
|
|
|
|
resp, err := c.bsc.client.exec(http.MethodHead, uri, headers, nil, c.bsc.auth)
|
|
if resp != nil {
|
|
defer readAndCloseBody(resp.body)
|
|
if resp.statusCode == http.StatusOK || resp.statusCode == http.StatusNotFound {
|
|
return resp.statusCode == http.StatusOK, nil
|
|
}
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
// SetContainerPermissionOptions includes options for a set container permissions operation
|
|
type SetContainerPermissionOptions struct {
|
|
Timeout uint
|
|
LeaseID string `header:"x-ms-lease-id"`
|
|
IfModifiedSince *time.Time `header:"If-Modified-Since"`
|
|
IfUnmodifiedSince *time.Time `header:"If-Unmodified-Since"`
|
|
RequestID string `header:"x-ms-client-request-id"`
|
|
}
|
|
|
|
// SetPermissions sets up container permissions
|
|
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Set-Container-ACL
|
|
func (c *Container) SetPermissions(permissions ContainerPermissions, options *SetContainerPermissionOptions) error {
|
|
body, length, err := generateContainerACLpayload(permissions.AccessPolicies)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
params := url.Values{
|
|
"restype": {"container"},
|
|
"comp": {"acl"},
|
|
}
|
|
headers := c.bsc.client.getStandardHeaders()
|
|
headers = addToHeaders(headers, ContainerAccessHeader, string(permissions.AccessType))
|
|
headers["Content-Length"] = strconv.Itoa(length)
|
|
|
|
if options != nil {
|
|
params = addTimeout(params, options.Timeout)
|
|
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
}
|
|
uri := c.bsc.client.getEndpoint(blobServiceName, c.buildPath(), params)
|
|
|
|
resp, err := c.bsc.client.exec(http.MethodPut, uri, headers, body, c.bsc.auth)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer readAndCloseBody(resp.body)
|
|
|
|
if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
|
return errors.New("Unable to set permissions")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetContainerPermissionOptions includes options for a get container permissions operation
|
|
type GetContainerPermissionOptions struct {
|
|
Timeout uint
|
|
LeaseID string `header:"x-ms-lease-id"`
|
|
RequestID string `header:"x-ms-client-request-id"`
|
|
}
|
|
|
|
// GetPermissions gets the container permissions as per https://msdn.microsoft.com/en-us/library/azure/dd179469.aspx
|
|
// If timeout is 0 then it will not be passed to Azure
|
|
// leaseID will only be passed to Azure if populated
|
|
func (c *Container) GetPermissions(options *GetContainerPermissionOptions) (*ContainerPermissions, error) {
|
|
params := url.Values{
|
|
"restype": {"container"},
|
|
"comp": {"acl"},
|
|
}
|
|
headers := c.bsc.client.getStandardHeaders()
|
|
|
|
if options != nil {
|
|
params = addTimeout(params, options.Timeout)
|
|
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
}
|
|
uri := c.bsc.client.getEndpoint(blobServiceName, c.buildPath(), params)
|
|
|
|
resp, err := c.bsc.client.exec(http.MethodGet, uri, headers, nil, c.bsc.auth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.body.Close()
|
|
|
|
var ap AccessPolicy
|
|
err = xmlUnmarshal(resp.body, &ap.SignedIdentifiersList)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return buildAccessPolicy(ap, &resp.headers), nil
|
|
}
|
|
|
|
func buildAccessPolicy(ap AccessPolicy, headers *http.Header) *ContainerPermissions {
|
|
// containerAccess. Blob, Container, empty
|
|
containerAccess := headers.Get(http.CanonicalHeaderKey(ContainerAccessHeader))
|
|
permissions := ContainerPermissions{
|
|
AccessType: ContainerAccessType(containerAccess),
|
|
AccessPolicies: []ContainerAccessPolicy{},
|
|
}
|
|
|
|
for _, policy := range ap.SignedIdentifiersList.SignedIdentifiers {
|
|
capd := ContainerAccessPolicy{
|
|
ID: policy.ID,
|
|
StartTime: policy.AccessPolicy.StartTime,
|
|
ExpiryTime: policy.AccessPolicy.ExpiryTime,
|
|
}
|
|
capd.CanRead = updatePermissions(policy.AccessPolicy.Permission, "r")
|
|
capd.CanWrite = updatePermissions(policy.AccessPolicy.Permission, "w")
|
|
capd.CanDelete = updatePermissions(policy.AccessPolicy.Permission, "d")
|
|
|
|
permissions.AccessPolicies = append(permissions.AccessPolicies, capd)
|
|
}
|
|
return &permissions
|
|
}
|
|
|
|
// DeleteContainerOptions includes options for a delete container operation
|
|
type DeleteContainerOptions struct {
|
|
Timeout uint
|
|
LeaseID string `header:"x-ms-lease-id"`
|
|
IfModifiedSince *time.Time `header:"If-Modified-Since"`
|
|
IfUnmodifiedSince *time.Time `header:"If-Unmodified-Since"`
|
|
RequestID string `header:"x-ms-client-request-id"`
|
|
}
|
|
|
|
// Delete deletes the container with given name on the storage
|
|
// account. If the container does not exist returns error.
|
|
//
|
|
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/delete-container
|
|
func (c *Container) Delete(options *DeleteContainerOptions) error {
|
|
resp, err := c.delete(options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
readAndCloseBody(resp.body)
|
|
return checkRespCode(resp.statusCode, []int{http.StatusAccepted})
|
|
}
|
|
|
|
// DeleteIfExists deletes the container with given name on the storage
|
|
// account if it exists. Returns true if container is deleted with this call, or
|
|
// false if the container did not exist at the time of the Delete Container
|
|
// operation.
|
|
//
|
|
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/delete-container
|
|
func (c *Container) DeleteIfExists(options *DeleteContainerOptions) (bool, error) {
|
|
resp, err := c.delete(options)
|
|
if resp != nil {
|
|
defer readAndCloseBody(resp.body)
|
|
if resp.statusCode == http.StatusAccepted || resp.statusCode == http.StatusNotFound {
|
|
return resp.statusCode == http.StatusAccepted, nil
|
|
}
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
func (c *Container) delete(options *DeleteContainerOptions) (*storageResponse, error) {
|
|
query := url.Values{"restype": {"container"}}
|
|
headers := c.bsc.client.getStandardHeaders()
|
|
|
|
if options != nil {
|
|
query = addTimeout(query, options.Timeout)
|
|
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
}
|
|
uri := c.bsc.client.getEndpoint(blobServiceName, c.buildPath(), query)
|
|
|
|
return c.bsc.client.exec(http.MethodDelete, uri, headers, nil, c.bsc.auth)
|
|
}
|
|
|
|
// ListBlobs returns an object that contains list of blobs in the container,
|
|
// pagination token and other information in the response of List Blobs call.
|
|
//
|
|
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/List-Blobs
|
|
func (c *Container) ListBlobs(params ListBlobsParameters) (BlobListResponse, error) {
|
|
q := mergeParams(params.getParameters(), url.Values{
|
|
"restype": {"container"},
|
|
"comp": {"list"}},
|
|
)
|
|
uri := c.bsc.client.getEndpoint(blobServiceName, c.buildPath(), q)
|
|
|
|
headers := c.bsc.client.getStandardHeaders()
|
|
headers = addToHeaders(headers, "x-ms-client-request-id", params.RequestID)
|
|
|
|
var out BlobListResponse
|
|
resp, err := c.bsc.client.exec(http.MethodGet, uri, headers, nil, c.bsc.auth)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
defer resp.body.Close()
|
|
|
|
err = xmlUnmarshal(resp.body, &out)
|
|
for i := range out.Blobs {
|
|
out.Blobs[i].Container = c
|
|
}
|
|
return out, err
|
|
}
|
|
|
|
func generateContainerACLpayload(policies []ContainerAccessPolicy) (io.Reader, int, error) {
|
|
sil := SignedIdentifiers{
|
|
SignedIdentifiers: []SignedIdentifier{},
|
|
}
|
|
for _, capd := range policies {
|
|
permission := capd.generateContainerPermissions()
|
|
signedIdentifier := convertAccessPolicyToXMLStructs(capd.ID, capd.StartTime, capd.ExpiryTime, permission)
|
|
sil.SignedIdentifiers = append(sil.SignedIdentifiers, signedIdentifier)
|
|
}
|
|
return xmlMarshal(sil)
|
|
}
|
|
|
|
func (capd *ContainerAccessPolicy) generateContainerPermissions() (permissions string) {
|
|
// generate the permissions string (rwd).
|
|
// still want the end user API to have bool flags.
|
|
permissions = ""
|
|
|
|
if capd.CanRead {
|
|
permissions += "r"
|
|
}
|
|
|
|
if capd.CanWrite {
|
|
permissions += "w"
|
|
}
|
|
|
|
if capd.CanDelete {
|
|
permissions += "d"
|
|
}
|
|
|
|
return permissions
|
|
}
|