123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457 |
- package storage
- // Copyright 2017 Microsoft Corporation
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- import (
- "bytes"
- "encoding/base64"
- "encoding/json"
- "errors"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/url"
- "strconv"
- "strings"
- "time"
- "github.com/satori/go.uuid"
- )
- // Annotating as secure for gas scanning
- /* #nosec */
- const (
- partitionKeyNode = "PartitionKey"
- rowKeyNode = "RowKey"
- etagErrorTemplate = "Etag didn't match: %v"
- )
- var (
- errEmptyPayload = errors.New("Empty payload is not a valid metadata level for this operation")
- errNilPreviousResult = errors.New("The previous results page is nil")
- errNilNextLink = errors.New("There are no more pages in this query results")
- )
- // Entity represents an entity inside an Azure table.
- type Entity struct {
- Table *Table
- PartitionKey string
- RowKey string
- TimeStamp time.Time
- OdataMetadata string
- OdataType string
- OdataID string
- OdataEtag string
- OdataEditLink string
- Properties map[string]interface{}
- }
- // GetEntityReference returns an Entity object with the specified
- // partition key and row key.
- func (t *Table) GetEntityReference(partitionKey, rowKey string) *Entity {
- return &Entity{
- PartitionKey: partitionKey,
- RowKey: rowKey,
- Table: t,
- }
- }
- // EntityOptions includes options for entity operations.
- type EntityOptions struct {
- Timeout uint
- RequestID string `header:"x-ms-client-request-id"`
- }
- // GetEntityOptions includes options for a get entity operation
- type GetEntityOptions struct {
- Select []string
- RequestID string `header:"x-ms-client-request-id"`
- }
- // Get gets the referenced entity. Which properties to get can be
- // specified using the select option.
- // See:
- // https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/query-entities
- // https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/querying-tables-and-entities
- func (e *Entity) Get(timeout uint, ml MetadataLevel, options *GetEntityOptions) error {
- if ml == EmptyPayload {
- return errEmptyPayload
- }
- // RowKey and PartitionKey could be lost if not included in the query
- // As those are the entity identifiers, it is best if they are not lost
- rk := e.RowKey
- pk := e.PartitionKey
- query := url.Values{
- "timeout": {strconv.FormatUint(uint64(timeout), 10)},
- }
- headers := e.Table.tsc.client.getStandardHeaders()
- headers[headerAccept] = string(ml)
- if options != nil {
- if len(options.Select) > 0 {
- query.Add("$select", strings.Join(options.Select, ","))
- }
- headers = mergeHeaders(headers, headersFromStruct(*options))
- }
- uri := e.Table.tsc.client.getEndpoint(tableServiceName, e.buildPath(), query)
- resp, err := e.Table.tsc.client.exec(http.MethodGet, uri, headers, nil, e.Table.tsc.auth)
- if err != nil {
- return err
- }
- defer drainRespBody(resp)
- if err = checkRespCode(resp, []int{http.StatusOK}); err != nil {
- return err
- }
- respBody, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- return err
- }
- err = json.Unmarshal(respBody, e)
- if err != nil {
- return err
- }
- e.PartitionKey = pk
- e.RowKey = rk
- return nil
- }
- // Insert inserts the referenced entity in its table.
- // The function fails if there is an entity with the same
- // PartitionKey and RowKey in the table.
- // ml determines the level of detail of metadata in the operation response,
- // or no data at all.
- // See: https://docs.microsoft.com/rest/api/storageservices/fileservices/insert-entity
- func (e *Entity) Insert(ml MetadataLevel, options *EntityOptions) error {
- query, headers := options.getParameters()
- headers = mergeHeaders(headers, e.Table.tsc.client.getStandardHeaders())
- body, err := json.Marshal(e)
- if err != nil {
- return err
- }
- headers = addBodyRelatedHeaders(headers, len(body))
- headers = addReturnContentHeaders(headers, ml)
- uri := e.Table.tsc.client.getEndpoint(tableServiceName, e.Table.buildPath(), query)
- resp, err := e.Table.tsc.client.exec(http.MethodPost, uri, headers, bytes.NewReader(body), e.Table.tsc.auth)
- if err != nil {
- return err
- }
- defer drainRespBody(resp)
- if ml != EmptyPayload {
- if err = checkRespCode(resp, []int{http.StatusCreated}); err != nil {
- return err
- }
- data, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- return err
- }
- if err = e.UnmarshalJSON(data); err != nil {
- return err
- }
- } else {
- if err = checkRespCode(resp, []int{http.StatusNoContent}); err != nil {
- return err
- }
- }
- return nil
- }
- // Update updates the contents of an entity. The function fails if there is no entity
- // with the same PartitionKey and RowKey in the table or if the ETag is different
- // than the one in Azure.
- // See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/update-entity2
- func (e *Entity) Update(force bool, options *EntityOptions) error {
- return e.updateMerge(force, http.MethodPut, options)
- }
- // Merge merges the contents of entity specified with PartitionKey and RowKey
- // with the content specified in Properties.
- // The function fails if there is no entity with the same PartitionKey and
- // RowKey in the table or if the ETag is different than the one in Azure.
- // Read more: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/merge-entity
- func (e *Entity) Merge(force bool, options *EntityOptions) error {
- return e.updateMerge(force, "MERGE", options)
- }
- // Delete deletes the entity.
- // The function fails if there is no entity with the same PartitionKey and
- // RowKey in the table or if the ETag is different than the one in Azure.
- // See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/delete-entity1
- func (e *Entity) Delete(force bool, options *EntityOptions) error {
- query, headers := options.getParameters()
- headers = mergeHeaders(headers, e.Table.tsc.client.getStandardHeaders())
- headers = addIfMatchHeader(headers, force, e.OdataEtag)
- headers = addReturnContentHeaders(headers, EmptyPayload)
- uri := e.Table.tsc.client.getEndpoint(tableServiceName, e.buildPath(), query)
- resp, err := e.Table.tsc.client.exec(http.MethodDelete, uri, headers, nil, e.Table.tsc.auth)
- if err != nil {
- if resp.StatusCode == http.StatusPreconditionFailed {
- return fmt.Errorf(etagErrorTemplate, err)
- }
- return err
- }
- defer drainRespBody(resp)
- if err = checkRespCode(resp, []int{http.StatusNoContent}); err != nil {
- return err
- }
- return e.updateTimestamp(resp.Header)
- }
- // InsertOrReplace inserts an entity or replaces the existing one.
- // Read more: https://docs.microsoft.com/rest/api/storageservices/fileservices/insert-or-replace-entity
- func (e *Entity) InsertOrReplace(options *EntityOptions) error {
- return e.insertOr(http.MethodPut, options)
- }
- // InsertOrMerge inserts an entity or merges the existing one.
- // Read more: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/insert-or-merge-entity
- func (e *Entity) InsertOrMerge(options *EntityOptions) error {
- return e.insertOr("MERGE", options)
- }
- func (e *Entity) buildPath() string {
- return fmt.Sprintf("%s(PartitionKey='%s', RowKey='%s')", e.Table.buildPath(), e.PartitionKey, e.RowKey)
- }
- // MarshalJSON is a custom marshaller for entity
- func (e *Entity) MarshalJSON() ([]byte, error) {
- completeMap := map[string]interface{}{}
- completeMap[partitionKeyNode] = e.PartitionKey
- completeMap[rowKeyNode] = e.RowKey
- for k, v := range e.Properties {
- typeKey := strings.Join([]string{k, OdataTypeSuffix}, "")
- switch t := v.(type) {
- case []byte:
- completeMap[typeKey] = OdataBinary
- completeMap[k] = t
- case time.Time:
- completeMap[typeKey] = OdataDateTime
- completeMap[k] = t.Format(time.RFC3339Nano)
- case uuid.UUID:
- completeMap[typeKey] = OdataGUID
- completeMap[k] = t.String()
- case int64:
- completeMap[typeKey] = OdataInt64
- completeMap[k] = fmt.Sprintf("%v", v)
- default:
- completeMap[k] = v
- }
- if strings.HasSuffix(k, OdataTypeSuffix) {
- if !(completeMap[k] == OdataBinary ||
- completeMap[k] == OdataDateTime ||
- completeMap[k] == OdataGUID ||
- completeMap[k] == OdataInt64) {
- return nil, fmt.Errorf("Odata.type annotation %v value is not valid", k)
- }
- valueKey := strings.TrimSuffix(k, OdataTypeSuffix)
- if _, ok := completeMap[valueKey]; !ok {
- return nil, fmt.Errorf("Odata.type annotation %v defined without value defined", k)
- }
- }
- }
- return json.Marshal(completeMap)
- }
- // UnmarshalJSON is a custom unmarshaller for entities
- func (e *Entity) UnmarshalJSON(data []byte) error {
- errorTemplate := "Deserializing error: %v"
- props := map[string]interface{}{}
- err := json.Unmarshal(data, &props)
- if err != nil {
- return err
- }
- // deselialize metadata
- e.OdataMetadata = stringFromMap(props, "odata.metadata")
- e.OdataType = stringFromMap(props, "odata.type")
- e.OdataID = stringFromMap(props, "odata.id")
- e.OdataEtag = stringFromMap(props, "odata.etag")
- e.OdataEditLink = stringFromMap(props, "odata.editLink")
- e.PartitionKey = stringFromMap(props, partitionKeyNode)
- e.RowKey = stringFromMap(props, rowKeyNode)
- // deserialize timestamp
- timeStamp, ok := props["Timestamp"]
- if ok {
- str, ok := timeStamp.(string)
- if !ok {
- return fmt.Errorf(errorTemplate, "Timestamp casting error")
- }
- t, err := time.Parse(time.RFC3339Nano, str)
- if err != nil {
- return fmt.Errorf(errorTemplate, err)
- }
- e.TimeStamp = t
- }
- delete(props, "Timestamp")
- delete(props, "Timestamp@odata.type")
- // deserialize entity (user defined fields)
- for k, v := range props {
- if strings.HasSuffix(k, OdataTypeSuffix) {
- valueKey := strings.TrimSuffix(k, OdataTypeSuffix)
- str, ok := props[valueKey].(string)
- if !ok {
- return fmt.Errorf(errorTemplate, fmt.Sprintf("%v casting error", v))
- }
- switch v {
- case OdataBinary:
- props[valueKey], err = base64.StdEncoding.DecodeString(str)
- if err != nil {
- return fmt.Errorf(errorTemplate, err)
- }
- case OdataDateTime:
- t, err := time.Parse("2006-01-02T15:04:05Z", str)
- if err != nil {
- return fmt.Errorf(errorTemplate, err)
- }
- props[valueKey] = t
- case OdataGUID:
- props[valueKey] = uuid.FromStringOrNil(str)
- case OdataInt64:
- i, err := strconv.ParseInt(str, 10, 64)
- if err != nil {
- return fmt.Errorf(errorTemplate, err)
- }
- props[valueKey] = i
- default:
- return fmt.Errorf(errorTemplate, fmt.Sprintf("%v is not supported", v))
- }
- delete(props, k)
- }
- }
- e.Properties = props
- return nil
- }
- func getAndDelete(props map[string]interface{}, key string) interface{} {
- if value, ok := props[key]; ok {
- delete(props, key)
- return value
- }
- return nil
- }
- func addIfMatchHeader(h map[string]string, force bool, etag string) map[string]string {
- if force {
- h[headerIfMatch] = "*"
- } else {
- h[headerIfMatch] = etag
- }
- return h
- }
- // updates Etag and timestamp
- func (e *Entity) updateEtagAndTimestamp(headers http.Header) error {
- e.OdataEtag = headers.Get(headerEtag)
- return e.updateTimestamp(headers)
- }
- func (e *Entity) updateTimestamp(headers http.Header) error {
- str := headers.Get(headerDate)
- t, err := time.Parse(time.RFC1123, str)
- if err != nil {
- return fmt.Errorf("Update timestamp error: %v", err)
- }
- e.TimeStamp = t
- return nil
- }
- func (e *Entity) insertOr(verb string, options *EntityOptions) error {
- query, headers := options.getParameters()
- headers = mergeHeaders(headers, e.Table.tsc.client.getStandardHeaders())
- body, err := json.Marshal(e)
- if err != nil {
- return err
- }
- headers = addBodyRelatedHeaders(headers, len(body))
- headers = addReturnContentHeaders(headers, EmptyPayload)
- uri := e.Table.tsc.client.getEndpoint(tableServiceName, e.buildPath(), query)
- resp, err := e.Table.tsc.client.exec(verb, uri, headers, bytes.NewReader(body), e.Table.tsc.auth)
- if err != nil {
- return err
- }
- defer drainRespBody(resp)
- if err = checkRespCode(resp, []int{http.StatusNoContent}); err != nil {
- return err
- }
- return e.updateEtagAndTimestamp(resp.Header)
- }
- func (e *Entity) updateMerge(force bool, verb string, options *EntityOptions) error {
- query, headers := options.getParameters()
- headers = mergeHeaders(headers, e.Table.tsc.client.getStandardHeaders())
- body, err := json.Marshal(e)
- if err != nil {
- return err
- }
- headers = addBodyRelatedHeaders(headers, len(body))
- headers = addIfMatchHeader(headers, force, e.OdataEtag)
- headers = addReturnContentHeaders(headers, EmptyPayload)
- uri := e.Table.tsc.client.getEndpoint(tableServiceName, e.buildPath(), query)
- resp, err := e.Table.tsc.client.exec(verb, uri, headers, bytes.NewReader(body), e.Table.tsc.auth)
- if err != nil {
- if resp.StatusCode == http.StatusPreconditionFailed {
- return fmt.Errorf(etagErrorTemplate, err)
- }
- return err
- }
- defer drainRespBody(resp)
- if err = checkRespCode(resp, []int{http.StatusNoContent}); err != nil {
- return err
- }
- return e.updateEtagAndTimestamp(resp.Header)
- }
- func stringFromMap(props map[string]interface{}, key string) string {
- value := getAndDelete(props, key)
- if value != nil {
- return value.(string)
- }
- return ""
- }
- func (options *EntityOptions) getParameters() (url.Values, map[string]string) {
- query := url.Values{}
- headers := map[string]string{}
- if options != nil {
- query = addTimeout(query, options.Timeout)
- headers = headersFromStruct(*options)
- }
- return query, headers
- }
|