aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/github.com/Azure/azure-storage-go/table_entities.go
blob: 36413a0cffd182fc7c1bc7af137bde191fc6589d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
package storage

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "reflect"
)

// Annotating as secure for gas scanning
/* #nosec */
const (
    partitionKeyNode                    = "PartitionKey"
    rowKeyNode                          = "RowKey"
    tag                                 = "table"
    tagIgnore                           = "-"
    continuationTokenPartitionKeyHeader = "X-Ms-Continuation-Nextpartitionkey"
    continuationTokenRowHeader          = "X-Ms-Continuation-Nextrowkey"
    maxTopParameter                     = 1000
)

type queryTablesResponse struct {
    TableName []struct {
        TableName string `json:"TableName"`
    } `json:"value"`
}

const (
    tableOperationTypeInsert          = iota
    tableOperationTypeUpdate          = iota
    tableOperationTypeMerge           = iota
    tableOperationTypeInsertOrReplace = iota
    tableOperationTypeInsertOrMerge   = iota
)

type tableOperation int

// TableEntity interface specifies
// the functions needed to support
// marshaling and unmarshaling into
// Azure Tables. The struct must only contain
// simple types because Azure Tables do not
// support hierarchy.
type TableEntity interface {
    PartitionKey() string
    RowKey() string
    SetPartitionKey(string) error
    SetRowKey(string) error
}

// ContinuationToken is an opaque (ie not useful to inspect)
// struct that Get... methods can return if there are more
// entries to be returned than the ones already
// returned. Just pass it to the same function to continue
// receiving the remaining entries.
type ContinuationToken struct {
    NextPartitionKey string
    NextRowKey       string
}

type getTableEntriesResponse struct {
    Elements []map[string]interface{} `json:"value"`
}

// QueryTableEntities queries the specified table and returns the unmarshaled
// entities of type retType.
// top parameter limits the returned entries up to top. Maximum top
// allowed by Azure API is 1000. In case there are more than top entries to be
// returned the function will return a non nil *ContinuationToken. You can call the
// same function again passing the received ContinuationToken as previousContToken
// parameter in order to get the following entries. The query parameter
// is the odata query. To retrieve all the entries pass the empty string.
// The function returns a pointer to a TableEntity slice, the *ContinuationToken
// if there are more entries to be returned and an error in case something went
// wrong.
//
// Example:
//      entities, cToken, err = tSvc.QueryTableEntities("table", cToken, reflect.TypeOf(entity), 20, "")
func (c *TableServiceClient) QueryTableEntities(tableName AzureTable, previousContToken *ContinuationToken, retType reflect.Type, top int, query string) ([]TableEntity, *ContinuationToken, error) {
    if top > maxTopParameter {
        return nil, nil, fmt.Errorf("top accepts at maximum %d elements. Requested %d instead", maxTopParameter, top)
    }

    uri := c.client.getEndpoint(tableServiceName, pathForTable(tableName), url.Values{})
    uri += fmt.Sprintf("?$top=%d", top)
    if query != "" {
        uri += fmt.Sprintf("&$filter=%s", url.QueryEscape(query))
    }

    if previousContToken != nil {
        uri += fmt.Sprintf("&NextPartitionKey=%s&NextRowKey=%s", previousContToken.NextPartitionKey, previousContToken.NextRowKey)
    }

    headers := c.getStandardHeaders()

    headers["Content-Length"] = "0"

    resp, err := c.client.execInternalJSON(http.MethodGet, uri, headers, nil, c.auth)

    if err != nil {
        return nil, nil, err
    }

    contToken := extractContinuationTokenFromHeaders(resp.headers)

    defer resp.body.Close()

    if err = checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
        return nil, contToken, err
    }

    retEntries, err := deserializeEntity(retType, resp.body)
    if err != nil {
        return nil, contToken, err
    }

    return retEntries, contToken, nil
}

// InsertEntity inserts an entity in the specified table.
// The function fails if there is an entity with the same
// PartitionKey and RowKey in the table.
func (c *TableServiceClient) InsertEntity(table AzureTable, entity TableEntity) error {
    sc, err := c.execTable(table, entity, false, http.MethodPost)
    if err != nil {
        return err
    }

    return checkRespCode(sc, []int{http.StatusCreated})
}

func (c *TableServiceClient) execTable(table AzureTable, entity TableEntity, specifyKeysInURL bool, method string) (int, error) {
    uri := c.client.getEndpoint(tableServiceName, pathForTable(table), url.Values{})
    if specifyKeysInURL {
        uri += fmt.Sprintf("(PartitionKey='%s',RowKey='%s')", url.QueryEscape(entity.PartitionKey()), url.QueryEscape(entity.RowKey()))
    }

    headers := c.getStandardHeaders()

    var buf bytes.Buffer

    if err := injectPartitionAndRowKeys(entity, &buf); err != nil {
        return 0, err
    }

    headers["Content-Length"] = fmt.Sprintf("%d", buf.Len())

    resp, err := c.client.execInternalJSON(method, uri, headers, &buf, c.auth)

    if err != nil {
        return 0, err
    }

    defer resp.body.Close()

    return resp.statusCode, nil
}

// UpdateEntity updates the contents of an entity with the
// one passed as parameter. The function fails if there is no entity
// with the same PartitionKey and RowKey in the table.
func (c *TableServiceClient) UpdateEntity(table AzureTable, entity TableEntity) error {
    sc, err := c.execTable(table, entity, true, http.MethodPut)
    if err != nil {
        return err
    }

    return checkRespCode(sc, []int{http.StatusNoContent})
}

// MergeEntity merges the contents of an entity with the
// one passed as parameter.
// The function fails if there is no entity
// with the same PartitionKey and RowKey in the table.
func (c *TableServiceClient) MergeEntity(table AzureTable, entity TableEntity) error {
    sc, err := c.execTable(table, entity, true, "MERGE")
    if err != nil {
        return err
    }

    return checkRespCode(sc, []int{http.StatusNoContent})
}

// DeleteEntityWithoutCheck deletes the entity matching by
// PartitionKey and RowKey. There is no check on IfMatch
// parameter so the entity is always deleted.
// The function fails if there is no entity
// with the same PartitionKey and RowKey in the table.
func (c *TableServiceClient) DeleteEntityWithoutCheck(table AzureTable, entity TableEntity) error {
    return c.DeleteEntity(table, entity, "*")
}

// DeleteEntity deletes the entity matching by
// PartitionKey, RowKey and ifMatch field.
// The function fails if there is no entity
// with the same PartitionKey and RowKey in the table or
// the ifMatch is different.
func (c *TableServiceClient) DeleteEntity(table AzureTable, entity TableEntity, ifMatch string) error {
    uri := c.client.getEndpoint(tableServiceName, pathForTable(table), url.Values{})
    uri += fmt.Sprintf("(PartitionKey='%s',RowKey='%s')", url.QueryEscape(entity.PartitionKey()), url.QueryEscape(entity.RowKey()))

    headers := c.getStandardHeaders()

    headers["Content-Length"] = "0"
    headers["If-Match"] = ifMatch

    resp, err := c.client.execInternalJSON(http.MethodDelete, uri, headers, nil, c.auth)

    if err != nil {
        return err
    }
    defer resp.body.Close()

    if err := checkRespCode(resp.statusCode, []int{http.StatusNoContent}); err != nil {
        return err
    }

    return nil
}

// InsertOrReplaceEntity inserts an entity in the specified table
// or replaced the existing one.
func (c *TableServiceClient) InsertOrReplaceEntity(table AzureTable, entity TableEntity) error {
    sc, err := c.execTable(table, entity, true, http.MethodPut)
    if err != nil {
        return err
    }

    return checkRespCode(sc, []int{http.StatusNoContent})
}

// InsertOrMergeEntity inserts an entity in the specified table
// or merges the existing one.
func (c *TableServiceClient) InsertOrMergeEntity(table AzureTable, entity TableEntity) error {
    sc, err := c.execTable(table, entity, true, "MERGE")
    if err != nil {
        return err
    }

    return checkRespCode(sc, []int{http.StatusNoContent})
}

func injectPartitionAndRowKeys(entity TableEntity, buf *bytes.Buffer) error {
    if err := json.NewEncoder(buf).Encode(entity); err != nil {
        return err
    }

    dec := make(map[string]interface{})
    if err := json.NewDecoder(buf).Decode(&dec); err != nil {
        return err
    }

    // Inject PartitionKey and RowKey
    dec[partitionKeyNode] = entity.PartitionKey()
    dec[rowKeyNode] = entity.RowKey()

    // Remove tagged fields
    // The tag is defined in the const section
    // This is useful to avoid storing the PartitionKey and RowKey twice.
    numFields := reflect.ValueOf(entity).Elem().NumField()
    for i := 0; i < numFields; i++ {
        f := reflect.ValueOf(entity).Elem().Type().Field(i)

        if f.Tag.Get(tag) == tagIgnore {
            // we must look for its JSON name in the dictionary
            // as the user can rename it using a tag
            jsonName := f.Name
            if f.Tag.Get("json") != "" {
                jsonName = f.Tag.Get("json")
            }
            delete(dec, jsonName)
        }
    }

    buf.Reset()

    if err := json.NewEncoder(buf).Encode(&dec); err != nil {
        return err
    }

    return nil
}

func deserializeEntity(retType reflect.Type, reader io.Reader) ([]TableEntity, error) {
    buf := new(bytes.Buffer)

    var ret getTableEntriesResponse
    if err := json.NewDecoder(reader).Decode(&ret); err != nil {
        return nil, err
    }

    tEntries := make([]TableEntity, len(ret.Elements))

    for i, entry := range ret.Elements {

        buf.Reset()
        if err := json.NewEncoder(buf).Encode(entry); err != nil {
            return nil, err
        }

        dec := make(map[string]interface{})
        if err := json.NewDecoder(buf).Decode(&dec); err != nil {
            return nil, err
        }

        var pKey, rKey string
        // strip pk and rk
        for key, val := range dec {
            switch key {
            case partitionKeyNode:
                pKey = val.(string)
            case rowKeyNode:
                rKey = val.(string)
            }
        }

        delete(dec, partitionKeyNode)
        delete(dec, rowKeyNode)

        buf.Reset()
        if err := json.NewEncoder(buf).Encode(dec); err != nil {
            return nil, err
        }

        // Create a empty retType instance
        tEntries[i] = reflect.New(retType.Elem()).Interface().(TableEntity)
        // Popolate it with the values
        if err := json.NewDecoder(buf).Decode(&tEntries[i]); err != nil {
            return nil, err
        }

        // Reset PartitionKey and RowKey
        if err := tEntries[i].SetPartitionKey(pKey); err != nil {
            return nil, err
        }
        if err := tEntries[i].SetRowKey(rKey); err != nil {
            return nil, err
        }
    }

    return tEntries, nil
}

func extractContinuationTokenFromHeaders(h http.Header) *ContinuationToken {
    ct := ContinuationToken{h.Get(continuationTokenPartitionKeyHeader), h.Get(continuationTokenRowHeader)}

    if ct.NextPartitionKey != "" && ct.NextRowKey != "" {
        return &ct
    }
    return nil
}