forked from cerc-io/plugeth
353 lines
11 KiB
Go
353 lines
11 KiB
Go
|
package storage
|
||
|
|
||
|
import (
|
||
|
"encoding/xml"
|
||
|
"fmt"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
// FileServiceClient contains operations for Microsoft Azure File Service.
|
||
|
type FileServiceClient struct {
|
||
|
client Client
|
||
|
}
|
||
|
|
||
|
// A Share is an entry in ShareListResponse.
|
||
|
type Share struct {
|
||
|
Name string `xml:"Name"`
|
||
|
Properties ShareProperties `xml:"Properties"`
|
||
|
}
|
||
|
|
||
|
// ShareProperties contains various properties of a share returned from
|
||
|
// various endpoints like ListShares.
|
||
|
type ShareProperties struct {
|
||
|
LastModified string `xml:"Last-Modified"`
|
||
|
Etag string `xml:"Etag"`
|
||
|
Quota string `xml:"Quota"`
|
||
|
}
|
||
|
|
||
|
// ShareListResponse contains the response fields from
|
||
|
// ListShares call.
|
||
|
//
|
||
|
// See https://msdn.microsoft.com/en-us/library/azure/dn167009.aspx
|
||
|
type ShareListResponse 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"`
|
||
|
Shares []Share `xml:"Shares>Share"`
|
||
|
}
|
||
|
|
||
|
// ListSharesParameters defines the set of customizable parameters to make a
|
||
|
// List Shares call.
|
||
|
//
|
||
|
// See https://msdn.microsoft.com/en-us/library/azure/dn167009.aspx
|
||
|
type ListSharesParameters struct {
|
||
|
Prefix string
|
||
|
Marker string
|
||
|
Include string
|
||
|
MaxResults uint
|
||
|
Timeout uint
|
||
|
}
|
||
|
|
||
|
// ShareHeaders contains various properties of a file and is an entry
|
||
|
// in SetShareProperties
|
||
|
type ShareHeaders struct {
|
||
|
Quota string `header:"x-ms-share-quota"`
|
||
|
}
|
||
|
|
||
|
func (p ListSharesParameters) getParameters() url.Values {
|
||
|
out := url.Values{}
|
||
|
|
||
|
if p.Prefix != "" {
|
||
|
out.Set("prefix", p.Prefix)
|
||
|
}
|
||
|
if p.Marker != "" {
|
||
|
out.Set("marker", p.Marker)
|
||
|
}
|
||
|
if p.Include != "" {
|
||
|
out.Set("include", p.Include)
|
||
|
}
|
||
|
if p.MaxResults != 0 {
|
||
|
out.Set("maxresults", fmt.Sprintf("%v", p.MaxResults))
|
||
|
}
|
||
|
if p.Timeout != 0 {
|
||
|
out.Set("timeout", fmt.Sprintf("%v", p.Timeout))
|
||
|
}
|
||
|
|
||
|
return out
|
||
|
}
|
||
|
|
||
|
// pathForFileShare returns the URL path segment for a File Share resource
|
||
|
func pathForFileShare(name string) string {
|
||
|
return fmt.Sprintf("/%s", name)
|
||
|
}
|
||
|
|
||
|
// ListShares returns the list of shares in a storage account along with
|
||
|
// pagination token and other response details.
|
||
|
//
|
||
|
// See https://msdn.microsoft.com/en-us/library/azure/dd179352.aspx
|
||
|
func (f FileServiceClient) ListShares(params ListSharesParameters) (ShareListResponse, error) {
|
||
|
q := mergeParams(params.getParameters(), url.Values{"comp": {"list"}})
|
||
|
uri := f.client.getEndpoint(fileServiceName, "", q)
|
||
|
headers := f.client.getStandardHeaders()
|
||
|
|
||
|
var out ShareListResponse
|
||
|
resp, err := f.client.exec("GET", uri, headers, nil)
|
||
|
if err != nil {
|
||
|
return out, err
|
||
|
}
|
||
|
defer resp.body.Close()
|
||
|
|
||
|
err = xmlUnmarshal(resp.body, &out)
|
||
|
return out, err
|
||
|
}
|
||
|
|
||
|
// CreateShare operation creates a new share under the specified account. If the
|
||
|
// share with the same name already exists, the operation fails.
|
||
|
//
|
||
|
// See https://msdn.microsoft.com/en-us/library/azure/dn167008.aspx
|
||
|
func (f FileServiceClient) CreateShare(name string) error {
|
||
|
resp, err := f.createShare(name)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer resp.body.Close()
|
||
|
return checkRespCode(resp.statusCode, []int{http.StatusCreated})
|
||
|
}
|
||
|
|
||
|
// ShareExists returns true if a share with given name exists
|
||
|
// on the storage account, otherwise returns false.
|
||
|
func (f FileServiceClient) ShareExists(name string) (bool, error) {
|
||
|
uri := f.client.getEndpoint(fileServiceName, pathForFileShare(name), url.Values{"restype": {"share"}})
|
||
|
headers := f.client.getStandardHeaders()
|
||
|
|
||
|
resp, err := f.client.exec("HEAD", uri, headers, nil)
|
||
|
if resp != nil {
|
||
|
defer resp.body.Close()
|
||
|
if resp.statusCode == http.StatusOK || resp.statusCode == http.StatusNotFound {
|
||
|
return resp.statusCode == http.StatusOK, nil
|
||
|
}
|
||
|
}
|
||
|
return false, err
|
||
|
}
|
||
|
|
||
|
// GetShareURL gets the canonical URL to the share with the specified name in the
|
||
|
// specified container. This method does not create a publicly accessible URL if
|
||
|
// the file is private and this method does not check if the file
|
||
|
// exists.
|
||
|
func (f FileServiceClient) GetShareURL(name string) string {
|
||
|
return f.client.getEndpoint(fileServiceName, pathForFileShare(name), url.Values{})
|
||
|
}
|
||
|
|
||
|
// CreateShareIfNotExists creates a new share under the specified account if
|
||
|
// it does not exist. Returns true if container is newly created or false if
|
||
|
// container already exists.
|
||
|
//
|
||
|
// See https://msdn.microsoft.com/en-us/library/azure/dn167008.aspx
|
||
|
func (f FileServiceClient) CreateShareIfNotExists(name string) (bool, error) {
|
||
|
resp, err := f.createShare(name)
|
||
|
if resp != nil {
|
||
|
defer resp.body.Close()
|
||
|
if resp.statusCode == http.StatusCreated || resp.statusCode == http.StatusConflict {
|
||
|
return resp.statusCode == http.StatusCreated, nil
|
||
|
}
|
||
|
}
|
||
|
return false, err
|
||
|
}
|
||
|
|
||
|
// CreateShare creates a Azure File Share and returns its response
|
||
|
func (f FileServiceClient) createShare(name string) (*storageResponse, error) {
|
||
|
if err := f.checkForStorageEmulator(); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
uri := f.client.getEndpoint(fileServiceName, pathForFileShare(name), url.Values{"restype": {"share"}})
|
||
|
headers := f.client.getStandardHeaders()
|
||
|
return f.client.exec("PUT", uri, headers, nil)
|
||
|
}
|
||
|
|
||
|
// GetShareProperties provides various information about the specified
|
||
|
// file. See https://msdn.microsoft.com/en-us/library/azure/dn689099.aspx
|
||
|
func (f FileServiceClient) GetShareProperties(name string) (*ShareProperties, error) {
|
||
|
uri := f.client.getEndpoint(fileServiceName, pathForFileShare(name), url.Values{"restype": {"share"}})
|
||
|
|
||
|
headers := f.client.getStandardHeaders()
|
||
|
resp, err := f.client.exec("HEAD", uri, headers, nil)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer resp.body.Close()
|
||
|
|
||
|
if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return &ShareProperties{
|
||
|
LastModified: resp.headers.Get("Last-Modified"),
|
||
|
Etag: resp.headers.Get("Etag"),
|
||
|
Quota: resp.headers.Get("x-ms-share-quota"),
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
// SetShareProperties replaces the ShareHeaders for the specified file.
|
||
|
//
|
||
|
// Some keys may be converted to Camel-Case before sending. All keys
|
||
|
// are returned in lower case by SetShareProperties. HTTP header names
|
||
|
// are case-insensitive so case munging should not matter to other
|
||
|
// applications either.
|
||
|
//
|
||
|
// See https://msdn.microsoft.com/en-us/library/azure/mt427368.aspx
|
||
|
func (f FileServiceClient) SetShareProperties(name string, shareHeaders ShareHeaders) error {
|
||
|
params := url.Values{}
|
||
|
params.Set("restype", "share")
|
||
|
params.Set("comp", "properties")
|
||
|
|
||
|
uri := f.client.getEndpoint(fileServiceName, pathForFileShare(name), params)
|
||
|
headers := f.client.getStandardHeaders()
|
||
|
|
||
|
extraHeaders := headersFromStruct(shareHeaders)
|
||
|
|
||
|
for k, v := range extraHeaders {
|
||
|
headers[k] = v
|
||
|
}
|
||
|
|
||
|
resp, err := f.client.exec("PUT", uri, headers, nil)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer resp.body.Close()
|
||
|
|
||
|
return checkRespCode(resp.statusCode, []int{http.StatusOK})
|
||
|
}
|
||
|
|
||
|
// DeleteShare operation marks the specified share for deletion. The share
|
||
|
// and any files contained within it are later deleted during garbage
|
||
|
// collection.
|
||
|
//
|
||
|
// See https://msdn.microsoft.com/en-us/library/azure/dn689090.aspx
|
||
|
func (f FileServiceClient) DeleteShare(name string) error {
|
||
|
resp, err := f.deleteShare(name)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer resp.body.Close()
|
||
|
return checkRespCode(resp.statusCode, []int{http.StatusAccepted})
|
||
|
}
|
||
|
|
||
|
// DeleteShareIfExists operation marks the specified share for deletion if it
|
||
|
// exists. The share and any files contained within it are later deleted during
|
||
|
// garbage collection. Returns true if share existed and deleted with this call,
|
||
|
// false otherwise.
|
||
|
//
|
||
|
// See https://msdn.microsoft.com/en-us/library/azure/dn689090.aspx
|
||
|
func (f FileServiceClient) DeleteShareIfExists(name string) (bool, error) {
|
||
|
resp, err := f.deleteShare(name)
|
||
|
if resp != nil {
|
||
|
defer resp.body.Close()
|
||
|
if resp.statusCode == http.StatusAccepted || resp.statusCode == http.StatusNotFound {
|
||
|
return resp.statusCode == http.StatusAccepted, nil
|
||
|
}
|
||
|
}
|
||
|
return false, err
|
||
|
}
|
||
|
|
||
|
// deleteShare makes the call to Delete Share operation endpoint and returns
|
||
|
// the response
|
||
|
func (f FileServiceClient) deleteShare(name string) (*storageResponse, error) {
|
||
|
if err := f.checkForStorageEmulator(); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
uri := f.client.getEndpoint(fileServiceName, pathForFileShare(name), url.Values{"restype": {"share"}})
|
||
|
return f.client.exec("DELETE", uri, f.client.getStandardHeaders(), nil)
|
||
|
}
|
||
|
|
||
|
// SetShareMetadata replaces the metadata for the specified Share.
|
||
|
//
|
||
|
// Some keys may be converted to Camel-Case before sending. All keys
|
||
|
// are returned in lower case by GetShareMetadata. HTTP header names
|
||
|
// are case-insensitive so case munging should not matter to other
|
||
|
// applications either.
|
||
|
//
|
||
|
// See https://msdn.microsoft.com/en-us/library/azure/dd179414.aspx
|
||
|
func (f FileServiceClient) SetShareMetadata(name string, metadata map[string]string, extraHeaders map[string]string) error {
|
||
|
params := url.Values{}
|
||
|
params.Set("restype", "share")
|
||
|
params.Set("comp", "metadata")
|
||
|
|
||
|
uri := f.client.getEndpoint(fileServiceName, pathForFileShare(name), params)
|
||
|
headers := f.client.getStandardHeaders()
|
||
|
for k, v := range metadata {
|
||
|
headers[userDefinedMetadataHeaderPrefix+k] = v
|
||
|
}
|
||
|
|
||
|
for k, v := range extraHeaders {
|
||
|
headers[k] = v
|
||
|
}
|
||
|
|
||
|
resp, err := f.client.exec("PUT", uri, headers, nil)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer resp.body.Close()
|
||
|
|
||
|
return checkRespCode(resp.statusCode, []int{http.StatusOK})
|
||
|
}
|
||
|
|
||
|
// GetShareMetadata returns all user-defined metadata for the specified share.
|
||
|
//
|
||
|
// All metadata keys will be returned in lower case. (HTTP header
|
||
|
// names are case-insensitive.)
|
||
|
//
|
||
|
// See https://msdn.microsoft.com/en-us/library/azure/dd179414.aspx
|
||
|
func (f FileServiceClient) GetShareMetadata(name string) (map[string]string, error) {
|
||
|
params := url.Values{}
|
||
|
params.Set("restype", "share")
|
||
|
params.Set("comp", "metadata")
|
||
|
|
||
|
uri := f.client.getEndpoint(fileServiceName, pathForFileShare(name), params)
|
||
|
headers := f.client.getStandardHeaders()
|
||
|
|
||
|
resp, err := f.client.exec("GET", uri, headers, nil)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer resp.body.Close()
|
||
|
|
||
|
if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
metadata := make(map[string]string)
|
||
|
for k, v := range resp.headers {
|
||
|
// Can't trust CanonicalHeaderKey() to munge case
|
||
|
// reliably. "_" is allowed in identifiers:
|
||
|
// https://msdn.microsoft.com/en-us/library/azure/dd179414.aspx
|
||
|
// https://msdn.microsoft.com/library/aa664670(VS.71).aspx
|
||
|
// http://tools.ietf.org/html/rfc7230#section-3.2
|
||
|
// ...but "_" is considered invalid by
|
||
|
// CanonicalMIMEHeaderKey in
|
||
|
// https://golang.org/src/net/textproto/reader.go?s=14615:14659#L542
|
||
|
// so k can be "X-Ms-Meta-Foo" or "x-ms-meta-foo_bar".
|
||
|
k = strings.ToLower(k)
|
||
|
if len(v) == 0 || !strings.HasPrefix(k, strings.ToLower(userDefinedMetadataHeaderPrefix)) {
|
||
|
continue
|
||
|
}
|
||
|
// metadata["foo"] = content of the last X-Ms-Meta-Foo header
|
||
|
k = k[len(userDefinedMetadataHeaderPrefix):]
|
||
|
metadata[k] = v[len(v)-1]
|
||
|
}
|
||
|
return metadata, nil
|
||
|
}
|
||
|
|
||
|
//checkForStorageEmulator determines if the client is setup for use with
|
||
|
//Azure Storage Emulator, and returns a relevant error
|
||
|
func (f FileServiceClient) checkForStorageEmulator() error {
|
||
|
if f.client.accountName == StorageEmulatorAccountName {
|
||
|
return fmt.Errorf("Error: File service is not currently supported by Azure Storage Emulator")
|
||
|
}
|
||
|
return nil
|
||
|
}
|