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	"bytes"
19	"encoding/base64"
20	"encoding/json"
21	"errors"
22	"fmt"
23	"io/ioutil"
24	"net/http"
25	"net/url"
26	"strconv"
27	"strings"
28	"time"
29
30	"github.com/satori/go.uuid"
31)
32
33// Annotating as secure for gas scanning
34/* #nosec */
35const (
36	partitionKeyNode  = "PartitionKey"
37	rowKeyNode        = "RowKey"
38	etagErrorTemplate = "Etag didn't match: %v"
39)
40
41var (
42	errEmptyPayload      = errors.New("Empty payload is not a valid metadata level for this operation")
43	errNilPreviousResult = errors.New("The previous results page is nil")
44	errNilNextLink       = errors.New("There are no more pages in this query results")
45)
46
47// Entity represents an entity inside an Azure table.
48type Entity struct {
49	Table         *Table
50	PartitionKey  string
51	RowKey        string
52	TimeStamp     time.Time
53	OdataMetadata string
54	OdataType     string
55	OdataID       string
56	OdataEtag     string
57	OdataEditLink string
58	Properties    map[string]interface{}
59}
60
61// GetEntityReference returns an Entity object with the specified
62// partition key and row key.
63func (t *Table) GetEntityReference(partitionKey, rowKey string) *Entity {
64	return &Entity{
65		PartitionKey: partitionKey,
66		RowKey:       rowKey,
67		Table:        t,
68	}
69}
70
71// EntityOptions includes options for entity operations.
72type EntityOptions struct {
73	Timeout   uint
74	RequestID string `header:"x-ms-client-request-id"`
75}
76
77// GetEntityOptions includes options for a get entity operation
78type GetEntityOptions struct {
79	Select    []string
80	RequestID string `header:"x-ms-client-request-id"`
81}
82
83// Get gets the referenced entity. Which properties to get can be
84// specified using the select option.
85// See:
86// https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/query-entities
87// https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/querying-tables-and-entities
88func (e *Entity) Get(timeout uint, ml MetadataLevel, options *GetEntityOptions) error {
89	if ml == EmptyPayload {
90		return errEmptyPayload
91	}
92	// RowKey and PartitionKey could be lost if not included in the query
93	// As those are the entity identifiers, it is best if they are not lost
94	rk := e.RowKey
95	pk := e.PartitionKey
96
97	query := url.Values{
98		"timeout": {strconv.FormatUint(uint64(timeout), 10)},
99	}
100	headers := e.Table.tsc.client.getStandardHeaders()
101	headers[headerAccept] = string(ml)
102
103	if options != nil {
104		if len(options.Select) > 0 {
105			query.Add("$select", strings.Join(options.Select, ","))
106		}
107		headers = mergeHeaders(headers, headersFromStruct(*options))
108	}
109
110	uri := e.Table.tsc.client.getEndpoint(tableServiceName, e.buildPath(), query)
111	resp, err := e.Table.tsc.client.exec(http.MethodGet, uri, headers, nil, e.Table.tsc.auth)
112	if err != nil {
113		return err
114	}
115	defer drainRespBody(resp)
116
117	if err = checkRespCode(resp, []int{http.StatusOK}); err != nil {
118		return err
119	}
120
121	respBody, err := ioutil.ReadAll(resp.Body)
122	if err != nil {
123		return err
124	}
125	err = json.Unmarshal(respBody, e)
126	if err != nil {
127		return err
128	}
129	e.PartitionKey = pk
130	e.RowKey = rk
131
132	return nil
133}
134
135// Insert inserts the referenced entity in its table.
136// The function fails if there is an entity with the same
137// PartitionKey and RowKey in the table.
138// ml determines the level of detail of metadata in the operation response,
139// or no data at all.
140// See: https://docs.microsoft.com/rest/api/storageservices/fileservices/insert-entity
141func (e *Entity) Insert(ml MetadataLevel, options *EntityOptions) error {
142	query, headers := options.getParameters()
143	headers = mergeHeaders(headers, e.Table.tsc.client.getStandardHeaders())
144
145	body, err := json.Marshal(e)
146	if err != nil {
147		return err
148	}
149	headers = addBodyRelatedHeaders(headers, len(body))
150	headers = addReturnContentHeaders(headers, ml)
151
152	uri := e.Table.tsc.client.getEndpoint(tableServiceName, e.Table.buildPath(), query)
153	resp, err := e.Table.tsc.client.exec(http.MethodPost, uri, headers, bytes.NewReader(body), e.Table.tsc.auth)
154	if err != nil {
155		return err
156	}
157	defer drainRespBody(resp)
158
159	if ml != EmptyPayload {
160		if err = checkRespCode(resp, []int{http.StatusCreated}); err != nil {
161			return err
162		}
163		data, err := ioutil.ReadAll(resp.Body)
164		if err != nil {
165			return err
166		}
167		if err = e.UnmarshalJSON(data); err != nil {
168			return err
169		}
170	} else {
171		if err = checkRespCode(resp, []int{http.StatusNoContent}); err != nil {
172			return err
173		}
174	}
175
176	return nil
177}
178
179// Update updates the contents of an entity. The function fails if there is no entity
180// with the same PartitionKey and RowKey in the table or if the ETag is different
181// than the one in Azure.
182// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/update-entity2
183func (e *Entity) Update(force bool, options *EntityOptions) error {
184	return e.updateMerge(force, http.MethodPut, options)
185}
186
187// Merge merges the contents of entity specified with PartitionKey and RowKey
188// with the content specified in Properties.
189// The function fails if there is no entity with the same PartitionKey and
190// RowKey in the table or if the ETag is different than the one in Azure.
191// Read more: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/merge-entity
192func (e *Entity) Merge(force bool, options *EntityOptions) error {
193	return e.updateMerge(force, "MERGE", options)
194}
195
196// Delete deletes the entity.
197// The function fails if there is no entity with the same PartitionKey and
198// RowKey in the table or if the ETag is different than the one in Azure.
199// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/delete-entity1
200func (e *Entity) Delete(force bool, options *EntityOptions) error {
201	query, headers := options.getParameters()
202	headers = mergeHeaders(headers, e.Table.tsc.client.getStandardHeaders())
203
204	headers = addIfMatchHeader(headers, force, e.OdataEtag)
205	headers = addReturnContentHeaders(headers, EmptyPayload)
206
207	uri := e.Table.tsc.client.getEndpoint(tableServiceName, e.buildPath(), query)
208	resp, err := e.Table.tsc.client.exec(http.MethodDelete, uri, headers, nil, e.Table.tsc.auth)
209	if err != nil {
210		if resp.StatusCode == http.StatusPreconditionFailed {
211			return fmt.Errorf(etagErrorTemplate, err)
212		}
213		return err
214	}
215	defer drainRespBody(resp)
216
217	if err = checkRespCode(resp, []int{http.StatusNoContent}); err != nil {
218		return err
219	}
220
221	return e.updateTimestamp(resp.Header)
222}
223
224// InsertOrReplace inserts an entity or replaces the existing one.
225// Read more: https://docs.microsoft.com/rest/api/storageservices/fileservices/insert-or-replace-entity
226func (e *Entity) InsertOrReplace(options *EntityOptions) error {
227	return e.insertOr(http.MethodPut, options)
228}
229
230// InsertOrMerge inserts an entity or merges the existing one.
231// Read more: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/insert-or-merge-entity
232func (e *Entity) InsertOrMerge(options *EntityOptions) error {
233	return e.insertOr("MERGE", options)
234}
235
236func (e *Entity) buildPath() string {
237	return fmt.Sprintf("%s(PartitionKey='%s', RowKey='%s')", e.Table.buildPath(), e.PartitionKey, e.RowKey)
238}
239
240// MarshalJSON is a custom marshaller for entity
241func (e *Entity) MarshalJSON() ([]byte, error) {
242	completeMap := map[string]interface{}{}
243	completeMap[partitionKeyNode] = e.PartitionKey
244	completeMap[rowKeyNode] = e.RowKey
245	for k, v := range e.Properties {
246		typeKey := strings.Join([]string{k, OdataTypeSuffix}, "")
247		switch t := v.(type) {
248		case []byte:
249			completeMap[typeKey] = OdataBinary
250			completeMap[k] = t
251		case time.Time:
252			completeMap[typeKey] = OdataDateTime
253			completeMap[k] = t.Format(time.RFC3339Nano)
254		case uuid.UUID:
255			completeMap[typeKey] = OdataGUID
256			completeMap[k] = t.String()
257		case int64:
258			completeMap[typeKey] = OdataInt64
259			completeMap[k] = fmt.Sprintf("%v", v)
260		default:
261			completeMap[k] = v
262		}
263		if strings.HasSuffix(k, OdataTypeSuffix) {
264			if !(completeMap[k] == OdataBinary ||
265				completeMap[k] == OdataDateTime ||
266				completeMap[k] == OdataGUID ||
267				completeMap[k] == OdataInt64) {
268				return nil, fmt.Errorf("Odata.type annotation %v value is not valid", k)
269			}
270			valueKey := strings.TrimSuffix(k, OdataTypeSuffix)
271			if _, ok := completeMap[valueKey]; !ok {
272				return nil, fmt.Errorf("Odata.type annotation %v defined without value defined", k)
273			}
274		}
275	}
276	return json.Marshal(completeMap)
277}
278
279// UnmarshalJSON is a custom unmarshaller for entities
280func (e *Entity) UnmarshalJSON(data []byte) error {
281	errorTemplate := "Deserializing error: %v"
282
283	props := map[string]interface{}{}
284	err := json.Unmarshal(data, &props)
285	if err != nil {
286		return err
287	}
288
289	// deselialize metadata
290	e.OdataMetadata = stringFromMap(props, "odata.metadata")
291	e.OdataType = stringFromMap(props, "odata.type")
292	e.OdataID = stringFromMap(props, "odata.id")
293	e.OdataEtag = stringFromMap(props, "odata.etag")
294	e.OdataEditLink = stringFromMap(props, "odata.editLink")
295	e.PartitionKey = stringFromMap(props, partitionKeyNode)
296	e.RowKey = stringFromMap(props, rowKeyNode)
297
298	// deserialize timestamp
299	timeStamp, ok := props["Timestamp"]
300	if ok {
301		str, ok := timeStamp.(string)
302		if !ok {
303			return fmt.Errorf(errorTemplate, "Timestamp casting error")
304		}
305		t, err := time.Parse(time.RFC3339Nano, str)
306		if err != nil {
307			return fmt.Errorf(errorTemplate, err)
308		}
309		e.TimeStamp = t
310	}
311	delete(props, "Timestamp")
312	delete(props, "Timestamp@odata.type")
313
314	// deserialize entity (user defined fields)
315	for k, v := range props {
316		if strings.HasSuffix(k, OdataTypeSuffix) {
317			valueKey := strings.TrimSuffix(k, OdataTypeSuffix)
318			str, ok := props[valueKey].(string)
319			if !ok {
320				return fmt.Errorf(errorTemplate, fmt.Sprintf("%v casting error", v))
321			}
322			switch v {
323			case OdataBinary:
324				props[valueKey], err = base64.StdEncoding.DecodeString(str)
325				if err != nil {
326					return fmt.Errorf(errorTemplate, err)
327				}
328			case OdataDateTime:
329				t, err := time.Parse("2006-01-02T15:04:05Z", str)
330				if err != nil {
331					return fmt.Errorf(errorTemplate, err)
332				}
333				props[valueKey] = t
334			case OdataGUID:
335				props[valueKey] = uuid.FromStringOrNil(str)
336			case OdataInt64:
337				i, err := strconv.ParseInt(str, 10, 64)
338				if err != nil {
339					return fmt.Errorf(errorTemplate, err)
340				}
341				props[valueKey] = i
342			default:
343				return fmt.Errorf(errorTemplate, fmt.Sprintf("%v is not supported", v))
344			}
345			delete(props, k)
346		}
347	}
348
349	e.Properties = props
350	return nil
351}
352
353func getAndDelete(props map[string]interface{}, key string) interface{} {
354	if value, ok := props[key]; ok {
355		delete(props, key)
356		return value
357	}
358	return nil
359}
360
361func addIfMatchHeader(h map[string]string, force bool, etag string) map[string]string {
362	if force {
363		h[headerIfMatch] = "*"
364	} else {
365		h[headerIfMatch] = etag
366	}
367	return h
368}
369
370// updates Etag and timestamp
371func (e *Entity) updateEtagAndTimestamp(headers http.Header) error {
372	e.OdataEtag = headers.Get(headerEtag)
373	return e.updateTimestamp(headers)
374}
375
376func (e *Entity) updateTimestamp(headers http.Header) error {
377	str := headers.Get(headerDate)
378	t, err := time.Parse(time.RFC1123, str)
379	if err != nil {
380		return fmt.Errorf("Update timestamp error: %v", err)
381	}
382	e.TimeStamp = t
383	return nil
384}
385
386func (e *Entity) insertOr(verb string, options *EntityOptions) error {
387	query, headers := options.getParameters()
388	headers = mergeHeaders(headers, e.Table.tsc.client.getStandardHeaders())
389
390	body, err := json.Marshal(e)
391	if err != nil {
392		return err
393	}
394	headers = addBodyRelatedHeaders(headers, len(body))
395	headers = addReturnContentHeaders(headers, EmptyPayload)
396
397	uri := e.Table.tsc.client.getEndpoint(tableServiceName, e.buildPath(), query)
398	resp, err := e.Table.tsc.client.exec(verb, uri, headers, bytes.NewReader(body), e.Table.tsc.auth)
399	if err != nil {
400		return err
401	}
402	defer drainRespBody(resp)
403
404	if err = checkRespCode(resp, []int{http.StatusNoContent}); err != nil {
405		return err
406	}
407
408	return e.updateEtagAndTimestamp(resp.Header)
409}
410
411func (e *Entity) updateMerge(force bool, verb string, options *EntityOptions) error {
412	query, headers := options.getParameters()
413	headers = mergeHeaders(headers, e.Table.tsc.client.getStandardHeaders())
414
415	body, err := json.Marshal(e)
416	if err != nil {
417		return err
418	}
419	headers = addBodyRelatedHeaders(headers, len(body))
420	headers = addIfMatchHeader(headers, force, e.OdataEtag)
421	headers = addReturnContentHeaders(headers, EmptyPayload)
422
423	uri := e.Table.tsc.client.getEndpoint(tableServiceName, e.buildPath(), query)
424	resp, err := e.Table.tsc.client.exec(verb, uri, headers, bytes.NewReader(body), e.Table.tsc.auth)
425	if err != nil {
426		if resp.StatusCode == http.StatusPreconditionFailed {
427			return fmt.Errorf(etagErrorTemplate, err)
428		}
429		return err
430	}
431	defer drainRespBody(resp)
432
433	if err = checkRespCode(resp, []int{http.StatusNoContent}); err != nil {
434		return err
435	}
436
437	return e.updateEtagAndTimestamp(resp.Header)
438}
439
440func stringFromMap(props map[string]interface{}, key string) string {
441	value := getAndDelete(props, key)
442	if value != nil {
443		return value.(string)
444	}
445	return ""
446}
447
448func (options *EntityOptions) getParameters() (url.Values, map[string]string) {
449	query := url.Values{}
450	headers := map[string]string{}
451	if options != nil {
452		query = addTimeout(query, options.Timeout)
453		headers = headersFromStruct(*options)
454	}
455	return query, headers
456}
457