file.go 15 KB

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