...

Source file src/github.com/Azure/azure-sdk-for-go/storage/file.go

Documentation: github.com/Azure/azure-sdk-for-go/storage

     1  package storage
     2  
     3  // Copyright (c) Microsoft Corporation. All rights reserved.
     4  // Licensed under the MIT License. See License.txt in the project root for license information.
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	"net/http"
    12  	"net/url"
    13  	"strconv"
    14  	"sync"
    15  )
    16  
    17  const fourMB = uint64(4194304)
    18  const oneTB = uint64(1099511627776)
    19  
    20  // Export maximum range and file sizes
    21  
    22  // MaxRangeSize defines the maximum size in bytes for a file range.
    23  const MaxRangeSize = fourMB
    24  
    25  // MaxFileSize defines the maximum size in bytes for a file.
    26  const MaxFileSize = oneTB
    27  
    28  // File represents a file on a share.
    29  type File struct {
    30  	fsc                *FileServiceClient
    31  	Metadata           map[string]string
    32  	Name               string `xml:"Name"`
    33  	parent             *Directory
    34  	Properties         FileProperties `xml:"Properties"`
    35  	share              *Share
    36  	FileCopyProperties FileCopyState
    37  	mutex              *sync.Mutex
    38  }
    39  
    40  // FileProperties contains various properties of a file.
    41  type FileProperties struct {
    42  	CacheControl string `header:"x-ms-cache-control"`
    43  	Disposition  string `header:"x-ms-content-disposition"`
    44  	Encoding     string `header:"x-ms-content-encoding"`
    45  	Etag         string
    46  	Language     string `header:"x-ms-content-language"`
    47  	LastModified string
    48  	Length       uint64 `xml:"Content-Length" header:"x-ms-content-length"`
    49  	MD5          string `header:"x-ms-content-md5"`
    50  	Type         string `header:"x-ms-content-type"`
    51  }
    52  
    53  // FileCopyState contains various properties of a file copy operation.
    54  type FileCopyState struct {
    55  	CompletionTime string
    56  	ID             string `header:"x-ms-copy-id"`
    57  	Progress       string
    58  	Source         string
    59  	Status         string `header:"x-ms-copy-status"`
    60  	StatusDesc     string
    61  }
    62  
    63  // FileStream contains file data returned from a call to GetFile.
    64  type FileStream struct {
    65  	Body       io.ReadCloser
    66  	ContentMD5 string
    67  }
    68  
    69  // FileRequestOptions will be passed to misc file operations.
    70  // Currently just Timeout (in seconds) but could expand.
    71  type FileRequestOptions struct {
    72  	Timeout uint // timeout duration in seconds.
    73  }
    74  
    75  func prepareOptions(options *FileRequestOptions) url.Values {
    76  	params := url.Values{}
    77  	if options != nil {
    78  		params = addTimeout(params, options.Timeout)
    79  	}
    80  	return params
    81  }
    82  
    83  // FileRanges contains a list of file range information for a file.
    84  //
    85  // See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/List-Ranges
    86  type FileRanges struct {
    87  	ContentLength uint64
    88  	LastModified  string
    89  	ETag          string
    90  	FileRanges    []FileRange `xml:"Range"`
    91  }
    92  
    93  // FileRange contains range information for a file.
    94  //
    95  // See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/List-Ranges
    96  type FileRange struct {
    97  	Start uint64 `xml:"Start"`
    98  	End   uint64 `xml:"End"`
    99  }
   100  
   101  func (fr FileRange) String() string {
   102  	return fmt.Sprintf("bytes=%d-%d", fr.Start, fr.End)
   103  }
   104  
   105  // builds the complete file path for this file object
   106  func (f *File) buildPath() string {
   107  	return f.parent.buildPath() + "/" + f.Name
   108  }
   109  
   110  // ClearRange releases the specified range of space in a file.
   111  //
   112  // See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Put-Range
   113  func (f *File) ClearRange(fileRange FileRange, options *FileRequestOptions) error {
   114  	var timeout *uint
   115  	if options != nil {
   116  		timeout = &options.Timeout
   117  	}
   118  	headers, err := f.modifyRange(nil, fileRange, timeout, nil)
   119  	if err != nil {
   120  		return err
   121  	}
   122  
   123  	f.updateEtagAndLastModified(headers)
   124  	return nil
   125  }
   126  
   127  // Create creates a new file or replaces an existing one.
   128  //
   129  // See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Create-File
   130  func (f *File) Create(maxSize uint64, options *FileRequestOptions) error {
   131  	if maxSize > oneTB {
   132  		return fmt.Errorf("max file size is 1TB")
   133  	}
   134  	params := prepareOptions(options)
   135  	headers := headersFromStruct(f.Properties)
   136  	headers["x-ms-content-length"] = strconv.FormatUint(maxSize, 10)
   137  	headers["x-ms-type"] = "file"
   138  
   139  	outputHeaders, err := f.fsc.createResource(f.buildPath(), resourceFile, params, mergeMDIntoExtraHeaders(f.Metadata, headers), []int{http.StatusCreated})
   140  	if err != nil {
   141  		return err
   142  	}
   143  
   144  	f.Properties.Length = maxSize
   145  	f.updateEtagAndLastModified(outputHeaders)
   146  	return nil
   147  }
   148  
   149  // CopyFile operation copied a file/blob from the sourceURL to the path provided.
   150  //
   151  // See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/copy-file
   152  func (f *File) CopyFile(sourceURL string, options *FileRequestOptions) error {
   153  	extraHeaders := map[string]string{
   154  		"x-ms-type":        "file",
   155  		"x-ms-copy-source": sourceURL,
   156  	}
   157  	params := prepareOptions(options)
   158  
   159  	headers, err := f.fsc.createResource(f.buildPath(), resourceFile, params, mergeMDIntoExtraHeaders(f.Metadata, extraHeaders), []int{http.StatusAccepted})
   160  	if err != nil {
   161  		return err
   162  	}
   163  
   164  	f.updateEtagAndLastModified(headers)
   165  	f.FileCopyProperties.ID = headers.Get("X-Ms-Copy-Id")
   166  	f.FileCopyProperties.Status = headers.Get("X-Ms-Copy-Status")
   167  	return nil
   168  }
   169  
   170  // Delete immediately removes this file from the storage account.
   171  //
   172  // See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Delete-File2
   173  func (f *File) Delete(options *FileRequestOptions) error {
   174  	return f.fsc.deleteResource(f.buildPath(), resourceFile, options)
   175  }
   176  
   177  // DeleteIfExists removes this file if it exists.
   178  //
   179  // See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Delete-File2
   180  func (f *File) DeleteIfExists(options *FileRequestOptions) (bool, error) {
   181  	resp, err := f.fsc.deleteResourceNoClose(f.buildPath(), resourceFile, options)
   182  	if resp != nil {
   183  		defer drainRespBody(resp)
   184  		if resp.StatusCode == http.StatusAccepted || resp.StatusCode == http.StatusNotFound {
   185  			return resp.StatusCode == http.StatusAccepted, nil
   186  		}
   187  	}
   188  	return false, err
   189  }
   190  
   191  // GetFileOptions includes options for a get file operation
   192  type GetFileOptions struct {
   193  	Timeout       uint
   194  	GetContentMD5 bool
   195  }
   196  
   197  // DownloadToStream operation downloads the file.
   198  //
   199  // See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/get-file
   200  func (f *File) DownloadToStream(options *FileRequestOptions) (io.ReadCloser, error) {
   201  	params := prepareOptions(options)
   202  	resp, err := f.fsc.getResourceNoClose(f.buildPath(), compNone, resourceFile, params, http.MethodGet, nil)
   203  	if err != nil {
   204  		return nil, err
   205  	}
   206  
   207  	if err = checkRespCode(resp, []int{http.StatusOK}); err != nil {
   208  		drainRespBody(resp)
   209  		return nil, err
   210  	}
   211  	return resp.Body, nil
   212  }
   213  
   214  // DownloadRangeToStream operation downloads the specified range of this file with optional MD5 hash.
   215  //
   216  // See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/get-file
   217  func (f *File) DownloadRangeToStream(fileRange FileRange, options *GetFileOptions) (fs FileStream, err error) {
   218  	extraHeaders := map[string]string{
   219  		"Range": fileRange.String(),
   220  	}
   221  	params := url.Values{}
   222  	if options != nil {
   223  		if options.GetContentMD5 {
   224  			if isRangeTooBig(fileRange) {
   225  				return fs, fmt.Errorf("must specify a range less than or equal to 4MB when getContentMD5 is true")
   226  			}
   227  			extraHeaders["x-ms-range-get-content-md5"] = "true"
   228  		}
   229  		params = addTimeout(params, options.Timeout)
   230  	}
   231  
   232  	resp, err := f.fsc.getResourceNoClose(f.buildPath(), compNone, resourceFile, params, http.MethodGet, extraHeaders)
   233  	if err != nil {
   234  		return fs, err
   235  	}
   236  
   237  	if err = checkRespCode(resp, []int{http.StatusOK, http.StatusPartialContent}); err != nil {
   238  		drainRespBody(resp)
   239  		return fs, err
   240  	}
   241  
   242  	fs.Body = resp.Body
   243  	if options != nil && options.GetContentMD5 {
   244  		fs.ContentMD5 = resp.Header.Get("Content-MD5")
   245  	}
   246  	return fs, nil
   247  }
   248  
   249  // Exists returns true if this file exists.
   250  func (f *File) Exists() (bool, error) {
   251  	exists, headers, err := f.fsc.resourceExists(f.buildPath(), resourceFile)
   252  	if exists {
   253  		f.updateEtagAndLastModified(headers)
   254  		f.updateProperties(headers)
   255  	}
   256  	return exists, err
   257  }
   258  
   259  // FetchAttributes updates metadata and properties for this file.
   260  // See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/get-file-properties
   261  func (f *File) FetchAttributes(options *FileRequestOptions) error {
   262  	params := prepareOptions(options)
   263  	headers, err := f.fsc.getResourceHeaders(f.buildPath(), compNone, resourceFile, params, http.MethodHead)
   264  	if err != nil {
   265  		return err
   266  	}
   267  
   268  	f.updateEtagAndLastModified(headers)
   269  	f.updateProperties(headers)
   270  	f.Metadata = getMetadataFromHeaders(headers)
   271  	return nil
   272  }
   273  
   274  // returns true if the range is larger than 4MB
   275  func isRangeTooBig(fileRange FileRange) bool {
   276  	if fileRange.End-fileRange.Start > fourMB {
   277  		return true
   278  	}
   279  
   280  	return false
   281  }
   282  
   283  // ListRangesOptions includes options for a list file ranges operation
   284  type ListRangesOptions struct {
   285  	Timeout   uint
   286  	ListRange *FileRange
   287  }
   288  
   289  // ListRanges returns the list of valid ranges for this file.
   290  //
   291  // See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/List-Ranges
   292  func (f *File) ListRanges(options *ListRangesOptions) (*FileRanges, error) {
   293  	params := url.Values{"comp": {"rangelist"}}
   294  
   295  	// add optional range to list
   296  	var headers map[string]string
   297  	if options != nil {
   298  		params = addTimeout(params, options.Timeout)
   299  		if options.ListRange != nil {
   300  			headers = make(map[string]string)
   301  			headers["Range"] = options.ListRange.String()
   302  		}
   303  	}
   304  
   305  	resp, err := f.fsc.listContent(f.buildPath(), params, headers)
   306  	if err != nil {
   307  		return nil, err
   308  	}
   309  
   310  	defer resp.Body.Close()
   311  	var cl uint64
   312  	cl, err = strconv.ParseUint(resp.Header.Get("x-ms-content-length"), 10, 64)
   313  	if err != nil {
   314  		ioutil.ReadAll(resp.Body)
   315  		return nil, err
   316  	}
   317  
   318  	var out FileRanges
   319  	out.ContentLength = cl
   320  	out.ETag = resp.Header.Get("ETag")
   321  	out.LastModified = resp.Header.Get("Last-Modified")
   322  
   323  	err = xmlUnmarshal(resp.Body, &out)
   324  	return &out, err
   325  }
   326  
   327  // modifies a range of bytes in this file
   328  func (f *File) modifyRange(bytes io.Reader, fileRange FileRange, timeout *uint, contentMD5 *string) (http.Header, error) {
   329  	if err := f.fsc.checkForStorageEmulator(); err != nil {
   330  		return nil, err
   331  	}
   332  	if fileRange.End < fileRange.Start {
   333  		return nil, errors.New("the value for rangeEnd must be greater than or equal to rangeStart")
   334  	}
   335  	if bytes != nil && isRangeTooBig(fileRange) {
   336  		return nil, errors.New("range cannot exceed 4MB in size")
   337  	}
   338  
   339  	params := url.Values{"comp": {"range"}}
   340  	if timeout != nil {
   341  		params = addTimeout(params, *timeout)
   342  	}
   343  
   344  	uri := f.fsc.client.getEndpoint(fileServiceName, f.buildPath(), params)
   345  
   346  	// default to clear
   347  	write := "clear"
   348  	cl := uint64(0)
   349  
   350  	// if bytes is not nil then this is an update operation
   351  	if bytes != nil {
   352  		write = "update"
   353  		cl = (fileRange.End - fileRange.Start) + 1
   354  	}
   355  
   356  	extraHeaders := map[string]string{
   357  		"Content-Length": strconv.FormatUint(cl, 10),
   358  		"Range":          fileRange.String(),
   359  		"x-ms-write":     write,
   360  	}
   361  
   362  	if contentMD5 != nil {
   363  		extraHeaders["Content-MD5"] = *contentMD5
   364  	}
   365  
   366  	headers := mergeHeaders(f.fsc.client.getStandardHeaders(), extraHeaders)
   367  	resp, err := f.fsc.client.exec(http.MethodPut, uri, headers, bytes, f.fsc.auth)
   368  	if err != nil {
   369  		return nil, err
   370  	}
   371  	defer drainRespBody(resp)
   372  	return resp.Header, checkRespCode(resp, []int{http.StatusCreated})
   373  }
   374  
   375  // SetMetadata replaces the metadata for this file.
   376  //
   377  // Some keys may be converted to Camel-Case before sending. All keys
   378  // are returned in lower case by GetFileMetadata. HTTP header names
   379  // are case-insensitive so case munging should not matter to other
   380  // applications either.
   381  //
   382  // See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Set-File-Metadata
   383  func (f *File) SetMetadata(options *FileRequestOptions) error {
   384  	headers, err := f.fsc.setResourceHeaders(f.buildPath(), compMetadata, resourceFile, mergeMDIntoExtraHeaders(f.Metadata, nil), options)
   385  	if err != nil {
   386  		return err
   387  	}
   388  
   389  	f.updateEtagAndLastModified(headers)
   390  	return nil
   391  }
   392  
   393  // SetProperties sets system properties on this file.
   394  //
   395  // Some keys may be converted to Camel-Case before sending. All keys
   396  // are returned in lower case by SetFileProperties. HTTP header names
   397  // are case-insensitive so case munging should not matter to other
   398  // applications either.
   399  //
   400  // See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Set-File-Properties
   401  func (f *File) SetProperties(options *FileRequestOptions) error {
   402  	headers, err := f.fsc.setResourceHeaders(f.buildPath(), compProperties, resourceFile, headersFromStruct(f.Properties), options)
   403  	if err != nil {
   404  		return err
   405  	}
   406  
   407  	f.updateEtagAndLastModified(headers)
   408  	return nil
   409  }
   410  
   411  // updates Etag and last modified date
   412  func (f *File) updateEtagAndLastModified(headers http.Header) {
   413  	f.Properties.Etag = headers.Get("Etag")
   414  	f.Properties.LastModified = headers.Get("Last-Modified")
   415  }
   416  
   417  // updates file properties from the specified HTTP header
   418  func (f *File) updateProperties(header http.Header) {
   419  	size, err := strconv.ParseUint(header.Get("Content-Length"), 10, 64)
   420  	if err == nil {
   421  		f.Properties.Length = size
   422  	}
   423  
   424  	f.updateEtagAndLastModified(header)
   425  	f.Properties.CacheControl = header.Get("Cache-Control")
   426  	f.Properties.Disposition = header.Get("Content-Disposition")
   427  	f.Properties.Encoding = header.Get("Content-Encoding")
   428  	f.Properties.Language = header.Get("Content-Language")
   429  	f.Properties.MD5 = header.Get("Content-MD5")
   430  	f.Properties.Type = header.Get("Content-Type")
   431  }
   432  
   433  // URL gets the canonical URL to this file.
   434  // This method does not create a publicly accessible URL if the file
   435  // is private and this method does not check if the file exists.
   436  func (f *File) URL() string {
   437  	return f.fsc.client.getEndpoint(fileServiceName, f.buildPath(), nil)
   438  }
   439  
   440  // WriteRangeOptions includes options for a write file range operation
   441  type WriteRangeOptions struct {
   442  	Timeout    uint
   443  	ContentMD5 string
   444  }
   445  
   446  // WriteRange writes a range of bytes to this file with an optional MD5 hash of the content (inside
   447  // options parameter). Note that the length of bytes must match (rangeEnd - rangeStart) + 1 with
   448  // a maximum size of 4MB.
   449  //
   450  // See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Put-Range
   451  func (f *File) WriteRange(bytes io.Reader, fileRange FileRange, options *WriteRangeOptions) error {
   452  	if bytes == nil {
   453  		return errors.New("bytes cannot be nil")
   454  	}
   455  	var timeout *uint
   456  	var md5 *string
   457  	if options != nil {
   458  		timeout = &options.Timeout
   459  		md5 = &options.ContentMD5
   460  	}
   461  
   462  	headers, err := f.modifyRange(bytes, fileRange, timeout, md5)
   463  	if err != nil {
   464  		return err
   465  	}
   466  	// it's perfectly legal for multiple go routines to call WriteRange
   467  	// on the same *File (e.g. concurrently writing non-overlapping ranges)
   468  	// so we must take the file mutex before updating our properties.
   469  	f.mutex.Lock()
   470  	f.updateEtagAndLastModified(headers)
   471  	f.mutex.Unlock()
   472  	return nil
   473  }
   474  

View as plain text