1 package storage
2
3
4
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
21
22
23 const MaxRangeSize = fourMB
24
25
26 const MaxFileSize = oneTB
27
28
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
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
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
64 type FileStream struct {
65 Body io.ReadCloser
66 ContentMD5 string
67 }
68
69
70
71 type FileRequestOptions struct {
72 Timeout uint
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
84
85
86 type FileRanges struct {
87 ContentLength uint64
88 LastModified string
89 ETag string
90 FileRanges []FileRange `xml:"Range"`
91 }
92
93
94
95
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
106 func (f *File) buildPath() string {
107 return f.parent.buildPath() + "/" + f.Name
108 }
109
110
111
112
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
128
129
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
150
151
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
171
172
173 func (f *File) Delete(options *FileRequestOptions) error {
174 return f.fsc.deleteResource(f.buildPath(), resourceFile, options)
175 }
176
177
178
179
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
192 type GetFileOptions struct {
193 Timeout uint
194 GetContentMD5 bool
195 }
196
197
198
199
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
215
216
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
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
260
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
275 func isRangeTooBig(fileRange FileRange) bool {
276 if fileRange.End-fileRange.Start > fourMB {
277 return true
278 }
279
280 return false
281 }
282
283
284 type ListRangesOptions struct {
285 Timeout uint
286 ListRange *FileRange
287 }
288
289
290
291
292 func (f *File) ListRanges(options *ListRangesOptions) (*FileRanges, error) {
293 params := url.Values{"comp": {"rangelist"}}
294
295
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
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
347 write := "clear"
348 cl := uint64(0)
349
350
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
376
377
378
379
380
381
382
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
394
395
396
397
398
399
400
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
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
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
434
435
436 func (f *File) URL() string {
437 return f.fsc.client.getEndpoint(fileServiceName, f.buildPath(), nil)
438 }
439
440
441 type WriteRangeOptions struct {
442 Timeout uint
443 ContentMD5 string
444 }
445
446
447
448
449
450
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
467
468
469 f.mutex.Lock()
470 f.updateEtagAndLastModified(headers)
471 f.mutex.Unlock()
472 return nil
473 }
474
View as plain text