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