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 }