file.go 15 KB

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