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