c76ad94492
This commit adds a build step to travis to auto-delete unstable archives older than 14 days (our regular release schedule) from Azure via ci.go purge. The commit also pulls in the latest Azure storage code, also switching over from the old import path (github.com/Azure/azure-sdk-for-go) to the new split one (github.com/Azure/azure-storage-go).
376 lines
11 KiB
Go
376 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
|
|
auth authentication
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
type compType string
|
|
|
|
const (
|
|
compNone compType = ""
|
|
compList compType = "list"
|
|
compMetadata compType = "metadata"
|
|
compProperties compType = "properties"
|
|
compRangeList compType = "rangelist"
|
|
)
|
|
|
|
func (ct compType) String() string {
|
|
return string(ct)
|
|
}
|
|
|
|
type resourceType string
|
|
|
|
const (
|
|
resourceDirectory resourceType = "directory"
|
|
resourceFile resourceType = ""
|
|
resourceShare resourceType = "share"
|
|
)
|
|
|
|
func (rt resourceType) String() string {
|
|
return string(rt)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (p ListDirsAndFilesParameters) getParameters() url.Values {
|
|
out := url.Values{}
|
|
|
|
if p.Marker != "" {
|
|
out.Set("marker", p.Marker)
|
|
}
|
|
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
|
|
}
|
|
|
|
// returns url.Values for the specified types
|
|
func getURLInitValues(comp compType, res resourceType) url.Values {
|
|
values := url.Values{}
|
|
if comp != compNone {
|
|
values.Set("comp", comp.String())
|
|
}
|
|
if res != resourceFile {
|
|
values.Set("restype", res.String())
|
|
}
|
|
return values
|
|
}
|
|
|
|
// GetShareReference returns a Share object for the specified share name.
|
|
func (f FileServiceClient) GetShareReference(name string) Share {
|
|
return Share{
|
|
fsc: &f,
|
|
Name: name,
|
|
Properties: ShareProperties{
|
|
Quota: -1,
|
|
},
|
|
}
|
|
}
|
|
|
|
// 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"}})
|
|
|
|
var out ShareListResponse
|
|
resp, err := f.listContent("", q, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.body.Close()
|
|
err = xmlUnmarshal(resp.body, &out)
|
|
|
|
// assign our client to the newly created Share objects
|
|
for i := range out.Shares {
|
|
out.Shares[i].fsc = &f
|
|
}
|
|
return &out, err
|
|
}
|
|
|
|
// GetServiceProperties gets the properties of your storage account's file service.
|
|
// File service does not support logging
|
|
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/get-file-service-properties
|
|
func (f *FileServiceClient) GetServiceProperties() (*ServiceProperties, error) {
|
|
return f.client.getServiceProperties(fileServiceName, f.auth)
|
|
}
|
|
|
|
// SetServiceProperties sets the properties of your storage account's file service.
|
|
// File service does not support logging
|
|
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/set-file-service-properties
|
|
func (f *FileServiceClient) SetServiceProperties(props ServiceProperties) error {
|
|
return f.client.setServiceProperties(props, fileServiceName, f.auth)
|
|
}
|
|
|
|
// retrieves directory or share content
|
|
func (f FileServiceClient) listContent(path string, params url.Values, extraHeaders map[string]string) (*storageResponse, error) {
|
|
if err := f.checkForStorageEmulator(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
uri := f.client.getEndpoint(fileServiceName, path, params)
|
|
extraHeaders = f.client.protectUserAgent(extraHeaders)
|
|
headers := mergeHeaders(f.client.getStandardHeaders(), extraHeaders)
|
|
|
|
resp, err := f.client.exec(http.MethodGet, uri, headers, nil, f.auth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err = checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
|
readAndCloseBody(resp.body)
|
|
return nil, err
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// returns true if the specified resource exists
|
|
func (f FileServiceClient) resourceExists(path string, res resourceType) (bool, http.Header, error) {
|
|
if err := f.checkForStorageEmulator(); err != nil {
|
|
return false, nil, err
|
|
}
|
|
|
|
uri := f.client.getEndpoint(fileServiceName, path, getURLInitValues(compNone, res))
|
|
headers := f.client.getStandardHeaders()
|
|
|
|
resp, err := f.client.exec(http.MethodHead, uri, headers, nil, f.auth)
|
|
if resp != nil {
|
|
defer readAndCloseBody(resp.body)
|
|
if resp.statusCode == http.StatusOK || resp.statusCode == http.StatusNotFound {
|
|
return resp.statusCode == http.StatusOK, resp.headers, nil
|
|
}
|
|
}
|
|
return false, nil, err
|
|
}
|
|
|
|
// creates a resource depending on the specified resource type
|
|
func (f FileServiceClient) createResource(path string, res resourceType, urlParams url.Values, extraHeaders map[string]string, expectedResponseCodes []int) (http.Header, error) {
|
|
resp, err := f.createResourceNoClose(path, res, urlParams, extraHeaders)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer readAndCloseBody(resp.body)
|
|
return resp.headers, checkRespCode(resp.statusCode, expectedResponseCodes)
|
|
}
|
|
|
|
// creates a resource depending on the specified resource type, doesn't close the response body
|
|
func (f FileServiceClient) createResourceNoClose(path string, res resourceType, urlParams url.Values, extraHeaders map[string]string) (*storageResponse, error) {
|
|
if err := f.checkForStorageEmulator(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
values := getURLInitValues(compNone, res)
|
|
combinedParams := mergeParams(values, urlParams)
|
|
uri := f.client.getEndpoint(fileServiceName, path, combinedParams)
|
|
extraHeaders = f.client.protectUserAgent(extraHeaders)
|
|
headers := mergeHeaders(f.client.getStandardHeaders(), extraHeaders)
|
|
|
|
return f.client.exec(http.MethodPut, uri, headers, nil, f.auth)
|
|
}
|
|
|
|
// returns HTTP header data for the specified directory or share
|
|
func (f FileServiceClient) getResourceHeaders(path string, comp compType, res resourceType, verb string) (http.Header, error) {
|
|
resp, err := f.getResourceNoClose(path, comp, res, verb, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer readAndCloseBody(resp.body)
|
|
|
|
if err = checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return resp.headers, nil
|
|
}
|
|
|
|
// gets the specified resource, doesn't close the response body
|
|
func (f FileServiceClient) getResourceNoClose(path string, comp compType, res resourceType, verb string, extraHeaders map[string]string) (*storageResponse, error) {
|
|
if err := f.checkForStorageEmulator(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
params := getURLInitValues(comp, res)
|
|
uri := f.client.getEndpoint(fileServiceName, path, params)
|
|
extraHeaders = f.client.protectUserAgent(extraHeaders)
|
|
headers := mergeHeaders(f.client.getStandardHeaders(), extraHeaders)
|
|
|
|
return f.client.exec(verb, uri, headers, nil, f.auth)
|
|
}
|
|
|
|
// deletes the resource and returns the response
|
|
func (f FileServiceClient) deleteResource(path string, res resourceType) error {
|
|
resp, err := f.deleteResourceNoClose(path, res)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer readAndCloseBody(resp.body)
|
|
return checkRespCode(resp.statusCode, []int{http.StatusAccepted})
|
|
}
|
|
|
|
// deletes the resource and returns the response, doesn't close the response body
|
|
func (f FileServiceClient) deleteResourceNoClose(path string, res resourceType) (*storageResponse, error) {
|
|
if err := f.checkForStorageEmulator(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
values := getURLInitValues(compNone, res)
|
|
uri := f.client.getEndpoint(fileServiceName, path, values)
|
|
return f.client.exec(http.MethodDelete, uri, f.client.getStandardHeaders(), nil, f.auth)
|
|
}
|
|
|
|
// merges metadata into extraHeaders and returns extraHeaders
|
|
func mergeMDIntoExtraHeaders(metadata, extraHeaders map[string]string) map[string]string {
|
|
if metadata == nil && extraHeaders == nil {
|
|
return nil
|
|
}
|
|
if extraHeaders == nil {
|
|
extraHeaders = make(map[string]string)
|
|
}
|
|
for k, v := range metadata {
|
|
extraHeaders[userDefinedMetadataHeaderPrefix+k] = v
|
|
}
|
|
return extraHeaders
|
|
}
|
|
|
|
// merges extraHeaders into headers and returns headers
|
|
func mergeHeaders(headers, extraHeaders map[string]string) map[string]string {
|
|
for k, v := range extraHeaders {
|
|
headers[k] = v
|
|
}
|
|
return headers
|
|
}
|
|
|
|
// sets extra header data for the specified resource
|
|
func (f FileServiceClient) setResourceHeaders(path string, comp compType, res resourceType, extraHeaders map[string]string) (http.Header, error) {
|
|
if err := f.checkForStorageEmulator(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
params := getURLInitValues(comp, res)
|
|
uri := f.client.getEndpoint(fileServiceName, path, params)
|
|
extraHeaders = f.client.protectUserAgent(extraHeaders)
|
|
headers := mergeHeaders(f.client.getStandardHeaders(), extraHeaders)
|
|
|
|
resp, err := f.client.exec(http.MethodPut, uri, headers, nil, f.auth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer readAndCloseBody(resp.body)
|
|
|
|
return resp.headers, checkRespCode(resp.statusCode, []int{http.StatusOK})
|
|
}
|
|
|
|
// gets metadata for the specified resource
|
|
func (f FileServiceClient) getMetadata(path string, res resourceType) (map[string]string, error) {
|
|
if err := f.checkForStorageEmulator(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
headers, err := f.getResourceHeaders(path, compMetadata, res, http.MethodGet)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return getMetadataFromHeaders(headers), nil
|
|
}
|
|
|
|
// returns a map of custom metadata values from the specified HTTP header
|
|
func getMetadataFromHeaders(header http.Header) map[string]string {
|
|
metadata := make(map[string]string)
|
|
for k, v := range header {
|
|
// 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]
|
|
}
|
|
|
|
if len(metadata) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return metadata
|
|
}
|
|
|
|
//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
|
|
}
|