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