diff --git a/storage/authorization_test.go b/storage/authorization_test.go index c7b0e3c24cdb..df85f073adc0 100644 --- a/storage/authorization_test.go +++ b/storage/authorization_test.go @@ -205,8 +205,8 @@ func (a *AuthorizationSuite) Test_allSharedKeys(c *chk.C) { c.Assert(tableCli.auth, chk.Equals, sharedKeyForTable) table1 := tableCli.GetTableReference(randTable()) c.Assert(table1.tsc.auth, chk.Equals, sharedKeyForTable) - c.Assert(table1.Create(EmptyPayload, 30), chk.IsNil) - c.Assert(table1.Delete(30), chk.IsNil) + c.Assert(table1.Create(30, EmptyPayload, nil), chk.IsNil) + c.Assert(table1.Delete(30, nil), chk.IsNil) // Change to Lite cli.UseSharedKeyLite = true @@ -223,6 +223,6 @@ func (a *AuthorizationSuite) Test_allSharedKeys(c *chk.C) { c.Assert(tableCli.auth, chk.Equals, sharedKeyLiteForTable) table2 := tableCli.GetTableReference(randTable()) c.Assert(table2.tsc.auth, chk.Equals, sharedKeyLiteForTable) - c.Assert(table2.Create(EmptyPayload, 30), chk.IsNil) - c.Assert(table2.Delete(30), chk.IsNil) + c.Assert(table2.Create(30, EmptyPayload, nil), chk.IsNil) + c.Assert(table2.Delete(30, nil), chk.IsNil) } diff --git a/storage/entity.go b/storage/entity.go index f13c89acc5d2..86dd995ad8c7 100644 --- a/storage/entity.go +++ b/storage/entity.go @@ -3,9 +3,11 @@ package storage import ( "bytes" "encoding/json" + "errors" "fmt" "io/ioutil" "net/http" + "net/url" "strconv" "strings" "time" @@ -21,6 +23,12 @@ const ( 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 @@ -45,24 +53,30 @@ func (t *Table) GetEntityReference(partitionKey, rowKey string) Entity { } } +// EntityOptions includes options for entity operations. +type EntityOptions struct { + Timeout uint + RequestID string `header:"x-ms-client-request-id"` +} + // 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) error { - uri := e.Table.tsc.client.getEndpoint(tableServiceName, e.Table.buildPath(), nil) +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 := e.Table.tsc.client.getStandardHeaders() 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 @@ -94,8 +108,8 @@ func (e *Entity) Insert(ml MetadataLevel) error { // 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) error { - return e.updateMerge(force, http.MethodPut) +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 @@ -103,21 +117,22 @@ func (e *Entity) Update(force bool) error { // 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) error { - return e.updateMerge(force, "MERGE") +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) error { - uri := e.Table.tsc.client.getEndpoint(tableServiceName, e.buildPath(), nil) +func (e *Entity) Delete(force bool, options *EntityOptions) error { + query, headers := options.getParameters() + headers = mergeHeaders(headers, e.Table.tsc.client.getStandardHeaders()) - 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 { @@ -136,14 +151,14 @@ func (e *Entity) Delete(force bool) error { // 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() error { - return e.insertOr(http.MethodPut) +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() error { - return e.insertOr("MERGE") +func (e *Entity) InsertOrMerge(options *EntityOptions) error { + return e.insertOr("MERGE", options) } func (e *Entity) buildPath() string { @@ -290,18 +305,18 @@ func (e *Entity) updateTimestamp(headers http.Header) error { return nil } -func (e *Entity) insertOr(verb string) error { - uri := e.Table.tsc.client.getEndpoint(tableServiceName, e.buildPath(), 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 := e.Table.tsc.client.getStandardHeaders() 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 @@ -315,19 +330,19 @@ func (e *Entity) insertOr(verb string) error { return e.updateEtagAndTimestamp(resp.headers) } -func (e *Entity) updateMerge(force bool, verb string) error { - uri := e.Table.tsc.client.getEndpoint(tableServiceName, e.buildPath(), nil) +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 := e.Table.tsc.client.getStandardHeaders() 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 { @@ -351,3 +366,13 @@ func stringFromMap(props map[string]interface{}, key string) 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 +} diff --git a/storage/entity_test.go b/storage/entity_test.go index e405800bf39f..932c390c583e 100644 --- a/storage/entity_test.go +++ b/storage/entity_test.go @@ -3,7 +3,6 @@ package storage import ( "encoding/json" "fmt" - "net/url" "time" "github.com/satori/uuid" @@ -18,9 +17,9 @@ func (s *StorageEntitySuite) TestInsert(c *chk.C) { cli := getBasicClient(c).GetTableService() table := cli.GetTableReference(randTable()) - err := table.Create(EmptyPayload, 30) + err := table.Create(30, EmptyPayload, nil) c.Assert(err, chk.IsNil) - defer table.Delete(30) + defer table.Delete(30, nil) entity := table.GetEntityReference("mypartitionkey", "myrowkey") @@ -32,7 +31,7 @@ func (s *StorageEntitySuite) TestInsert(c *chk.C) { "NumberOfOrders": int64(255), } entity.Properties = props - err = entity.Insert(EmptyPayload) + err = entity.Insert(EmptyPayload, nil) c.Assert(err, chk.IsNil) // Did not update c.Assert(entity.TimeStamp, chk.Equals, time.Time{}) @@ -45,7 +44,7 @@ func (s *StorageEntitySuite) TestInsert(c *chk.C) { // Update entity.PartitionKey = "mypartitionkey2" entity.RowKey = "myrowkey2" - err = entity.Insert(FullMetadata) + err = entity.Insert(FullMetadata, nil) c.Assert(err, chk.IsNil) // Check everything was updated... c.Assert(entity.TimeStamp, chk.NotNil) @@ -60,9 +59,9 @@ func (s *StorageEntitySuite) TestUpdate(c *chk.C) { cli := getBasicClient(c).GetTableService() table := cli.GetTableReference(randTable()) - err := table.Create(EmptyPayload, 30) + err := table.Create(30, EmptyPayload, nil) c.Assert(err, chk.IsNil) - defer table.Delete(30) + defer table.Delete(30, nil) entity := table.GetEntityReference("mypartitionkey", "myrowkey") entity.Properties = map[string]interface{}{ @@ -73,7 +72,7 @@ func (s *StorageEntitySuite) TestUpdate(c *chk.C) { "NumberOfOrders": int64(255), } // Force update - err = entity.Insert(FullMetadata) + err = entity.Insert(FullMetadata, nil) c.Assert(err, chk.IsNil) etag := entity.OdataEtag @@ -86,7 +85,7 @@ func (s *StorageEntitySuite) TestUpdate(c *chk.C) { } entity.Properties = props // Update providing etag - err = entity.Update(false) + err = entity.Update(false, nil) c.Assert(err, chk.IsNil) c.Assert(entity.Properties, chk.DeepEquals, props) @@ -95,7 +94,7 @@ func (s *StorageEntitySuite) TestUpdate(c *chk.C) { // Try to update with old etag entity.OdataEtag = etag - err = entity.Update(false) + err = entity.Update(false, nil) c.Assert(err, chk.NotNil) c.Assert(err, chk.ErrorMatches, "Etag didn't match: .*") @@ -106,7 +105,7 @@ func (s *StorageEntitySuite) TestUpdate(c *chk.C) { "HasAwesomeDress": true, } entity.Properties = props - err = entity.Update(true) + err = entity.Update(true, nil) c.Assert(err, chk.IsNil) c.Assert(entity.Properties, chk.DeepEquals, props) } @@ -115,16 +114,16 @@ func (s *StorageEntitySuite) TestMerge(c *chk.C) { cli := getBasicClient(c).GetTableService() table := cli.GetTableReference(randTable()) - err := table.Create(EmptyPayload, 30) + err := table.Create(30, EmptyPayload, nil) c.Assert(err, chk.IsNil) - defer table.Delete(30) + defer table.Delete(30, nil) entity := table.GetEntityReference("mypartitionkey", "myrowkey") entity.Properties = map[string]interface{}{ "Country": "Mexico", "MalePoet": "Nezahualcoyotl", } - c.Assert(entity.Insert(FullMetadata), chk.IsNil) + c.Assert(entity.Insert(FullMetadata, nil), chk.IsNil) etag := entity.OdataEtag timestamp := entity.TimeStamp @@ -133,14 +132,14 @@ func (s *StorageEntitySuite) TestMerge(c *chk.C) { "FemalePoet": "Sor Juana Ines de la Cruz", } // Merge providing etag - err = entity.Merge(false) + err = entity.Merge(false, nil) c.Assert(err, chk.IsNil) c.Assert(entity.OdataEtag, chk.Not(chk.Equals), etag) c.Assert(entity.TimeStamp, chk.Not(chk.Equals), timestamp) // Try to merge with old etag entity.OdataEtag = etag - err = entity.Merge(false) + err = entity.Merge(false, nil) c.Assert(err, chk.NotNil) c.Assert(err, chk.ErrorMatches, "Etag didn't match: .*") @@ -149,7 +148,7 @@ func (s *StorageEntitySuite) TestMerge(c *chk.C) { "MalePainter": "Diego Rivera", "FemalePainter": "Frida Kahlo", } - err = entity.Merge(true) + err = entity.Merge(true, nil) c.Assert(err, chk.IsNil) } @@ -157,27 +156,27 @@ func (s *StorageEntitySuite) TestDelete(c *chk.C) { cli := getBasicClient(c).GetTableService() table := cli.GetTableReference(randTable()) - err := table.Create(EmptyPayload, 30) + err := table.Create(30, EmptyPayload, nil) c.Assert(err, chk.IsNil) - defer table.Delete(30) + defer table.Delete(30, nil) // Delete providing etag entity1 := table.GetEntityReference("mypartitionkey", "myrowkey") - c.Assert(entity1.Insert(FullMetadata), chk.IsNil) + c.Assert(entity1.Insert(FullMetadata, nil), chk.IsNil) - err = entity1.Delete(false) + err = entity1.Delete(false, nil) c.Assert(err, chk.IsNil) // Try to delete with incorrect etag entity2 := table.GetEntityReference("mypartitionkey", "myrowkey") - c.Assert(entity2.Insert(EmptyPayload), chk.IsNil) + c.Assert(entity2.Insert(EmptyPayload, nil), chk.IsNil) entity2.OdataEtag = "GolangRocksOnAzure" - err = entity1.Delete(false) + err = entity1.Delete(false, nil) c.Assert(err, chk.NotNil) // Force delete - err = entity2.Delete(true) + err = entity2.Delete(true, nil) c.Assert(err, chk.IsNil) } @@ -185,9 +184,9 @@ func (s *StorageEntitySuite) TestInsertOrReplace(c *chk.C) { cli := getBasicClient(c).GetTableService() table := cli.GetTableReference(randTable()) - err := table.Create(EmptyPayload, 30) + err := table.Create(30, EmptyPayload, nil) c.Assert(err, chk.IsNil) - defer table.Delete(30) + defer table.Delete(30, nil) entity := table.GetEntityReference("mypartitionkey", "myrowkey") entity.Properties = map[string]interface{}{ @@ -196,7 +195,7 @@ func (s *StorageEntitySuite) TestInsertOrReplace(c *chk.C) { "HasEpicTheme": true, } - err = entity.InsertOrReplace() + err = entity.InsertOrReplace(nil) c.Assert(err, chk.IsNil) entity.Properties = map[string]interface{}{ @@ -204,7 +203,7 @@ func (s *StorageEntitySuite) TestInsertOrReplace(c *chk.C) { "FamilyName": "Organa", "HasAwesomeDress": true, } - err = entity.InsertOrReplace() + err = entity.InsertOrReplace(nil) c.Assert(err, chk.IsNil) } @@ -212,9 +211,9 @@ func (s *StorageEntitySuite) TestInsertOrMerge(c *chk.C) { cli := getBasicClient(c).GetTableService() table := cli.GetTableReference(randTable()) - err := table.Create(EmptyPayload, 30) + err := table.Create(30, EmptyPayload, nil) c.Assert(err, chk.IsNil) - defer table.Delete(30) + defer table.Delete(30, nil) entity := table.GetEntityReference("mypartitionkey", "myrowkey") entity.Properties = map[string]interface{}{ @@ -222,14 +221,14 @@ func (s *StorageEntitySuite) TestInsertOrMerge(c *chk.C) { "FamilyName": "Skywalker", } - err = entity.InsertOrMerge() + err = entity.InsertOrMerge(nil) c.Assert(err, chk.IsNil) entity.Properties = map[string]interface{}{ "Father": "Anakin", "Mentor": "Yoda", } - err = entity.InsertOrMerge() + err = entity.InsertOrMerge(nil) c.Assert(err, chk.IsNil) } @@ -237,9 +236,9 @@ func (s *StorageEntitySuite) Test_InsertAndGetEntities(c *chk.C) { cli := getBasicClient(c).GetTableService() table := cli.GetTableReference(randTable()) - err := table.Create(EmptyPayload, 30) + err := table.Create(30, EmptyPayload, nil) c.Assert(err, chk.IsNil) - defer table.Delete(30) + defer table.Delete(30, nil) entity := table.GetEntityReference("mypartitionkey", "100") entity.Properties = map[string]interface{}{ @@ -247,12 +246,12 @@ func (s *StorageEntitySuite) Test_InsertAndGetEntities(c *chk.C) { "FamilyName": "Skywalker", "HasCoolWeapon": true, } - c.Assert(entity.Insert(EmptyPayload), chk.IsNil) + c.Assert(entity.Insert(EmptyPayload, nil), chk.IsNil) entity.RowKey = "200" - c.Assert(entity.Insert(FullMetadata), chk.IsNil) + c.Assert(entity.Insert(FullMetadata, nil), chk.IsNil) - entities, err := table.ExecuteQuery(nil, 30) + entities, err := table.QueryEntities(30, FullMetadata, nil) c.Assert(err, chk.IsNil) c.Assert(entities.Entities, chk.HasLen, 2) @@ -265,9 +264,9 @@ func (s *StorageEntitySuite) Test_InsertAndExecuteQuery(c *chk.C) { cli := getBasicClient(c).GetTableService() table := cli.GetTableReference(randTable()) - err := table.Create(EmptyPayload, 30) + err := table.Create(30, EmptyPayload, nil) c.Assert(err, chk.IsNil) - defer table.Delete(30) + defer table.Delete(30, nil) entity := table.GetEntityReference("mypartitionkey", "100") entity.Properties = map[string]interface{}{ @@ -275,12 +274,16 @@ func (s *StorageEntitySuite) Test_InsertAndExecuteQuery(c *chk.C) { "FamilyName": "Skywalker", "HasCoolWeapon": true, } - c.Assert(entity.Insert(EmptyPayload), chk.IsNil) + c.Assert(entity.Insert(EmptyPayload, nil), chk.IsNil) entity.RowKey = "200" - c.Assert(entity.Insert(EmptyPayload), chk.IsNil) + c.Assert(entity.Insert(EmptyPayload, nil), chk.IsNil) - entities, err := table.ExecuteQuery(url.Values{"filter": {"RowKey eq '200'"}}, 30) + queryOptions := QueryOptions{ + Filter: "RowKey eq '200'", + } + + entities, err := table.QueryEntities(30, FullMetadata, &queryOptions) c.Assert(err, chk.IsNil) c.Assert(entities.Entities, chk.HasLen, 1) @@ -291,9 +294,9 @@ func (s *StorageEntitySuite) Test_InsertAndDeleteEntities(c *chk.C) { cli := getBasicClient(c).GetTableService() table := cli.GetTableReference(randTable()) - err := table.Create(EmptyPayload, 30) + err := table.Create(30, EmptyPayload, nil) c.Assert(err, chk.IsNil) - defer table.Delete(30) + defer table.Delete(30, nil) entity := table.GetEntityReference("mypartitionkey", "100") entity.Properties = map[string]interface{}{ @@ -301,21 +304,25 @@ func (s *StorageEntitySuite) Test_InsertAndDeleteEntities(c *chk.C) { "Name": "Luke", "Number": 3, } - c.Assert(entity.Insert(EmptyPayload), chk.IsNil) + c.Assert(entity.Insert(EmptyPayload, nil), chk.IsNil) entity.Properties["Number"] = 1 entity.RowKey = "200" - c.Assert(entity.Insert(FullMetadata), chk.IsNil) + c.Assert(entity.Insert(FullMetadata, nil), chk.IsNil) - result, err := table.ExecuteQuery(url.Values{OdataFilter: {"Number eq 1"}}, 30) + options := QueryOptions{ + Filter: "Number eq 1", + } + + result, err := table.QueryEntities(30, FullMetadata, &options) c.Assert(err, chk.IsNil) c.Assert(result.Entities, chk.HasLen, 1) compareEntities(result.Entities[0], entity, c) - err = result.Entities[0].Delete(true) + err = result.Entities[0].Delete(true, nil) c.Assert(err, chk.IsNil) - result, err = table.ExecuteQuery(nil, 30) + result, err = table.QueryEntities(30, FullMetadata, nil) c.Assert(err, chk.IsNil) // only 1 entry must be present @@ -326,36 +333,39 @@ func (s *StorageEntitySuite) TestExecuteQueryNextResults(c *chk.C) { cli := getBasicClient(c).GetTableService() table := cli.GetTableReference(randTable()) - err := table.Create(EmptyPayload, 30) + err := table.Create(30, EmptyPayload, nil) c.Assert(err, chk.IsNil) - defer table.Delete(30) + defer table.Delete(30, nil) var entityList []Entity for i := 0; i < 5; i++ { entity := table.GetEntityReference("pkey", fmt.Sprintf("r%d", i)) - err := entity.Insert(FullMetadata) + err := entity.Insert(FullMetadata, nil) c.Assert(err, chk.IsNil) entityList = append(entityList, entity) } // retrieve using top = 2. Should return 2 entries, 2 entries and finally // 1 entry - results, err := table.ExecuteQuery(url.Values{OdataTop: {"2"}}, 30) + options := QueryOptions{ + Top: 2, + } + results, err := table.QueryEntities(30, FullMetadata, &options) c.Assert(err, chk.IsNil) c.Assert(results.Entities, chk.HasLen, 2) c.Assert(results.NextLink, chk.NotNil) compareEntities(results.Entities[0], entityList[0], c) compareEntities(results.Entities[1], entityList[1], c) - results, err = table.ExecuteQueryNextResults(results) + results, err = results.NextResults(nil) c.Assert(err, chk.IsNil) c.Assert(results.Entities, chk.HasLen, 2) c.Assert(results.NextLink, chk.NotNil) compareEntities(results.Entities[0], entityList[2], c) compareEntities(results.Entities[1], entityList[3], c) - results, err = table.ExecuteQueryNextResults(results) + results, err = results.NextResults(nil) c.Assert(err, chk.IsNil) c.Assert(results.Entities, chk.HasLen, 1) c.Assert(results.NextLink, chk.IsNil) diff --git a/storage/odata.go b/storage/odata.go index 34c799431041..41d832e2be11 100644 --- a/storage/odata.go +++ b/storage/odata.go @@ -1,11 +1,5 @@ package storage -import ( - "fmt" - "net/url" - "strings" -) - // MetadataLevel determines if operations should return a paylod, // and it level of detail. type MetadataLevel string @@ -37,17 +31,3 @@ const ( MinimalMetadata MetadataLevel = "application/json;odata=minimalmetadata" FullMetadata MetadataLevel = "application/json;odata=fullmetadata" ) - -func fixOdataQuery(odataQuery url.Values) url.Values { - if odataQuery == nil { - return url.Values{} - } - for k, v := range odataQuery { - if !strings.HasPrefix(k, "$") { - newkey := fmt.Sprintf("$%v", k) - odataQuery[newkey] = v - odataQuery.Del(k) - } - } - return odataQuery -} diff --git a/storage/table.go b/storage/table.go index d3f545b89e92..201690f8038e 100644 --- a/storage/table.go +++ b/storage/table.go @@ -3,13 +3,13 @@ package storage import ( "bytes" "encoding/json" - "errors" "fmt" "io" "io/ioutil" "net/http" "net/url" "strconv" + "strings" "time" ) @@ -48,7 +48,8 @@ type Table struct { type EntityQueryResult struct { OdataMetadata string `json:"odata.metadata"` Entities []Entity `json:"value"` - NextLink *string + QueryNextLink + table *Table } type continuationToken struct { @@ -66,7 +67,7 @@ func (t *Table) buildPath() string { // 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/create-table -func (t *Table) Create(ml MetadataLevel, timeout uint) error { +func (t *Table) Create(timeout uint, ml MetadataLevel, options *TableOptions) error { uri := t.tsc.client.getEndpoint(tableServiceName, tablesURIPath, url.Values{ "timeout": {strconv.FormatUint(uint64(timeout), 10)}, }) @@ -83,6 +84,7 @@ func (t *Table) Create(ml MetadataLevel, timeout uint) error { headers := t.tsc.client.getStandardHeaders() headers = addReturnContentHeaders(headers, ml) headers = addBodyRelatedHeaders(headers, buf.Len()) + headers = options.addToHeaders(headers) resp, err := t.tsc.client.exec(http.MethodPost, uri, headers, buf, t.tsc.auth) if err != nil { @@ -118,7 +120,7 @@ func (t *Table) Create(ml MetadataLevel, timeout uint) error { // This function fails if the table is not present. // Be advised: Delete deletes all the entries that may be present. // See https://docs.microsoft.com/rest/api/storageservices/fileservices/delete-table -func (t *Table) Delete(timeout uint) error { +func (t *Table) Delete(timeout uint, options *TableOptions) error { path := bytes.NewBufferString(tablesURIPath) path.WriteString("('") path.WriteString(t.Name) @@ -130,6 +132,7 @@ func (t *Table) Delete(timeout uint) error { headers := t.tsc.client.getStandardHeaders() headers = addReturnContentHeaders(headers, EmptyPayload) + headers = options.addToHeaders(headers) resp, err := t.tsc.client.exec(http.MethodDelete, uri, headers, nil, t.tsc.auth) if err != nil { @@ -144,38 +147,73 @@ func (t *Table) Delete(timeout uint) error { return nil } -// ExecuteQuery returns the entities in the table. +// QueryOptions includes options for a query entities operation. +// Top, filter and select are OData query options. +type QueryOptions struct { + Top uint + Filter string + Select []string + RequestID string +} + +func (options *QueryOptions) getParameters() (url.Values, map[string]string) { + query := url.Values{} + headers := map[string]string{} + if options != nil { + if options.Top > 0 { + query.Add(OdataTop, strconv.FormatUint(uint64(options.Top), 10)) + } + if options.Filter != "" { + query.Add(OdataFilter, options.Filter) + } + if len(options.Select) > 0 { + query.Add(OdataSelect, strings.Join(options.Select, ",")) + } + headers = addToHeaders(headers, "x-ms-client-request-id", options.RequestID) + } + return query, headers +} + +// QueryEntities returns the entities in the table. // You can use query options defined by the OData Protocol specification. // // See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/query-entities -func (t *Table) ExecuteQuery(odataQuery url.Values, timeout uint) (*EntityQueryResult, error) { - query := fixOdataQuery(odataQuery) - query.Add("timeout", strconv.Itoa(int(timeout))) - uri := t.tsc.client.getEndpoint(tableServiceName, t.buildPath(), fixOdataQuery(odataQuery)) - return t.executeQuery(uri) +func (t *Table) QueryEntities(timeout uint, ml MetadataLevel, options *QueryOptions) (*EntityQueryResult, error) { + if ml == EmptyPayload { + return nil, errEmptyPayload + } + query, headers := options.getParameters() + query = addTimeout(query, timeout) + uri := t.tsc.client.getEndpoint(tableServiceName, t.buildPath(), query) + return t.queryEntities(uri, headers, ml) } -// ExecuteQueryNextResults returns the next page of results -// from a ExecuteQuery or ExecuteQueryNextResults operation. +// NextResults returns the next page of results +// from a QueryEntities or NextResults operation. // // See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/query-entities // See https://docs.microsoft.com/rest/api/storageservices/fileservices/query-timeout-and-pagination -func (t *Table) ExecuteQueryNextResults(results *EntityQueryResult) (*EntityQueryResult, error) { - if results.NextLink == nil { - return nil, errors.New("There are no more pages in this query results") +func (eqr *EntityQueryResult) NextResults(options *TableOptions) (*EntityQueryResult, error) { + if eqr == nil { + return nil, errNilPreviousResult + } + if eqr.NextLink == nil { + return nil, errNilNextLink } - return t.executeQuery(*results.NextLink) + headers := options.addToHeaders(map[string]string{}) + return eqr.table.queryEntities(*eqr.NextLink, headers, eqr.ml) } // SetPermissions sets up table ACL permissions // See https://docs.microsoft.com/rest/api/storageservices/fileservices/Set-Table-ACL -func (t *Table) SetPermissions(tap []TableAccessPolicy, timeout uint) error { +func (t *Table) SetPermissions(tap []TableAccessPolicy, timeout uint, options *TableOptions) error { params := url.Values{"comp": {"acl"}, "timeout": {strconv.Itoa(int(timeout))}, } uri := t.tsc.client.getEndpoint(tableServiceName, t.Name, params) headers := t.tsc.client.getStandardHeaders() + headers = options.addToHeaders(headers) body, length, err := generateTableACLPayload(tap) if err != nil { @@ -209,13 +247,14 @@ func generateTableACLPayload(policies []TableAccessPolicy) (io.Reader, int, erro // GetPermissions gets the table ACL permissions // See https://docs.microsoft.com/rest/api/storageservices/fileservices/get-table-acl -func (t *Table) GetPermissions(timeout int) ([]TableAccessPolicy, error) { +func (t *Table) GetPermissions(timeout int, options *TableOptions) ([]TableAccessPolicy, error) { params := url.Values{"comp": {"acl"}, "timeout": {strconv.Itoa(int(timeout))}, } uri := t.tsc.client.getEndpoint(tableServiceName, t.Name, params) headers := t.tsc.client.getStandardHeaders() + headers = options.addToHeaders(headers) resp, err := t.tsc.client.exec(http.MethodGet, uri, headers, nil, t.tsc.auth) if err != nil { @@ -235,9 +274,11 @@ func (t *Table) GetPermissions(timeout int) ([]TableAccessPolicy, error) { return updateTableAccessPolicy(ap), nil } -func (t *Table) executeQuery(uri string) (*EntityQueryResult, error) { - headers := t.tsc.client.getStandardHeaders() - headers[headerAccept] = "application/json;odata=fullmetadata" +func (t *Table) queryEntities(uri string, headers map[string]string, ml MetadataLevel) (*EntityQueryResult, error) { + headers = mergeHeaders(headers, t.tsc.client.getStandardHeaders()) + if ml != EmptyPayload { + headers[headerAccept] = string(ml) + } resp, err := t.tsc.client.exec(http.MethodGet, uri, headers, nil, t.tsc.auth) if err != nil { @@ -262,6 +303,7 @@ func (t *Table) executeQuery(uri string) (*EntityQueryResult, error) { for i := range entities.Entities { entities.Entities[i].Table = t } + entities.table = t contToken := extractContinuationTokenFromHeaders(resp.headers) if contToken == nil { @@ -276,6 +318,7 @@ func (t *Table) executeQuery(uri string) (*EntityQueryResult, error) { v.Set(nextRowKeyQueryParameter, contToken.NextRowKey) newURI := t.tsc.client.getEndpoint(tableServiceName, t.buildPath(), v) entities.NextLink = &newURI + entities.ml = ml } return &entities, nil diff --git a/storage/table_test.go b/storage/table_test.go index 1fbddecdb67e..d513f66e285d 100644 --- a/storage/table_test.go +++ b/storage/table_test.go @@ -2,7 +2,6 @@ package storage import ( "crypto/rand" - "net/url" "time" chk "gopkg.in/check.v1" @@ -18,11 +17,11 @@ func getTableClient(c *chk.C) TableServiceClient { func deleteAllTables(c *chk.C) { cli := getBasicClient(c).GetTableService() - result, err := cli.QueryTables(url.Values{}) + result, err := cli.QueryTables(MinimalMetadata, nil) c.Assert(err, chk.IsNil) for _, t := range result.Tables { - err := t.Delete(30) + err := t.Delete(30, nil) c.Assert(err, chk.IsNil) } } @@ -31,12 +30,12 @@ func (s *StorageTableSuite) Test_CreateAndDeleteTable(c *chk.C) { cli := getBasicClient(c).GetTableService() table := cli.GetTableReference(randTable()) - err := table.Create(EmptyPayload, 30) + err := table.Create(30, EmptyPayload, nil) c.Assert(err, chk.IsNil) // update table metadata table2 := cli.GetTableReference(randTable()) - err = table2.Create(FullMetadata, 30) - defer table2.Delete(30) + err = table2.Create(30, FullMetadata, nil) + defer table2.Delete(30, nil) c.Assert(err, chk.IsNil) // Check not empty values c.Assert(table2.OdataEditLink, chk.Not(chk.Equals), "") @@ -44,7 +43,7 @@ func (s *StorageTableSuite) Test_CreateAndDeleteTable(c *chk.C) { c.Assert(table2.OdataMetadata, chk.Not(chk.Equals), "") c.Assert(table2.OdataType, chk.Not(chk.Equals), "") - err = table.Delete(30) + err = table.Delete(30, nil) c.Assert(err, chk.IsNil) } @@ -59,8 +58,8 @@ func (s *StorageTableSuite) Test_CreateTableWithAllResponsePayloadLeves(c *chk.C func createAndDeleteTable(cli TableServiceClient, ml MetadataLevel, c *chk.C) { table := cli.GetTableReference(randTable()) - c.Assert(table.Create(ml, 30), chk.IsNil) - c.Assert(table.Delete(30), chk.IsNil) + c.Assert(table.Create(30, ml, nil), chk.IsNil) + c.Assert(table.Delete(30, nil), chk.IsNil) } func (s *StorageTableSuite) TestQueryTablesNextResults(c *chk.C) { @@ -69,22 +68,25 @@ func (s *StorageTableSuite) TestQueryTablesNextResults(c *chk.C) { for i := 0; i < 3; i++ { table := cli.GetTableReference(randTable()) - err := table.Create(EmptyPayload, 30) + err := table.Create(30, EmptyPayload, nil) c.Assert(err, chk.IsNil) - defer table.Delete(30) + defer table.Delete(30, nil) } - result, err := cli.QueryTables(url.Values{"top": {"2"}}) + options := QueryTablesOptions{ + Top: 2, + } + result, err := cli.QueryTables(MinimalMetadata, &options) c.Assert(err, chk.IsNil) c.Assert(result.Tables, chk.HasLen, 2) c.Assert(result.NextLink, chk.NotNil) - result, err = cli.QueryTablesNextResults(result) + result, err = result.NextResults(nil) c.Assert(err, chk.IsNil) c.Assert(result.Tables, chk.HasLen, 1) c.Assert(result.NextLink, chk.IsNil) - result, err = cli.QueryTablesNextResults(result) + result, err = result.NextResults(nil) c.Assert(result, chk.IsNil) c.Assert(err, chk.NotNil) } @@ -119,13 +121,13 @@ func appendTablePermission(policies []TableAccessPolicy, ID string, func (s *StorageTableSuite) TestSetPermissionsSuccessfully(c *chk.C) { cli := getTableClient(c) table := cli.GetTableReference(randTable()) - c.Assert(table.Create(EmptyPayload, 30), chk.IsNil) - defer table.Delete(30) + c.Assert(table.Create(30, EmptyPayload, nil), chk.IsNil) + defer table.Delete(30, nil) policies := []TableAccessPolicy{} policies = appendTablePermission(policies, "GolangRocksOnAzure", true, true, true, true, now, now.Add(10*time.Hour)) - err := table.SetPermissions(policies, 30) + err := table.SetPermissions(policies, 30, nil) c.Assert(err, chk.IsNil) } @@ -136,24 +138,24 @@ func (s *StorageTableSuite) TestSetPermissionsUnsuccessfully(c *chk.C) { policies := []TableAccessPolicy{} policies = appendTablePermission(policies, "GolangRocksOnAzure", true, true, true, true, now, now.Add(10*time.Hour)) - err := table.SetPermissions(policies, 30) + err := table.SetPermissions(policies, 30, nil) c.Assert(err, chk.NotNil) } func (s *StorageTableSuite) TestSetThenGetPermissionsSuccessfully(c *chk.C) { cli := getTableClient(c) table := cli.GetTableReference(randTable()) - c.Assert(table.Create(EmptyPayload, 30), chk.IsNil) - defer table.Delete(30) + c.Assert(table.Create(30, EmptyPayload, nil), chk.IsNil) + defer table.Delete(30, nil) policies := []TableAccessPolicy{} policies = appendTablePermission(policies, "GolangRocksOnAzure", true, true, true, true, now, now.Add(10*time.Hour)) policies = appendTablePermission(policies, "AutoRestIsSuperCool", true, true, false, true, now.Add(20*time.Hour), now.Add(30*time.Hour)) - err := table.SetPermissions(policies, 30) + err := table.SetPermissions(policies, 30, nil) c.Assert(err, chk.IsNil) - newPolicies, err := table.GetPermissions(30) + newPolicies, err := table.GetPermissions(30, nil) c.Assert(err, chk.IsNil) // now check policy set. diff --git a/storage/tableserviceclient.go b/storage/tableserviceclient.go index 1460d1eec861..86e7cc2e7001 100644 --- a/storage/tableserviceclient.go +++ b/storage/tableserviceclient.go @@ -2,11 +2,11 @@ package storage import ( "encoding/json" - "errors" "fmt" "io/ioutil" "net/http" "net/url" + "strconv" ) const ( @@ -23,12 +23,23 @@ type TableServiceClient struct { auth authentication } -// TableQueryResult contains the response from -// QueryTables and QueryTablesNextResults functions. -type TableQueryResult struct { - OdataMetadata string `json:"odata.metadata"` - Tables []Table `json:"value"` - NextLink *string +// TableOptions includes options for some table operations +type TableOptions struct { + RequestID string +} + +func (options *TableOptions) addToHeaders(h map[string]string) map[string]string { + if options != nil { + h = addToHeaders(h, "x-ms-client-request-id", options.RequestID) + } + return h +} + +// QueryNextLink includes information for getting the next page of +// results in query operations +type QueryNextLink struct { + NextLink *string + ml MetadataLevel } // GetServiceProperties gets the properties of your storage account's table service. @@ -51,31 +62,71 @@ func (t *TableServiceClient) GetTableReference(name string) Table { } } +// QueryTablesOptions includes options for some table operations +type QueryTablesOptions struct { + Top uint + Filter string + RequestID string +} + +func (options *QueryTablesOptions) getParameters() (url.Values, map[string]string) { + query := url.Values{} + headers := map[string]string{} + if options != nil { + if options.Top > 0 { + query.Add(OdataTop, strconv.FormatUint(uint64(options.Top), 10)) + } + if options.Filter != "" { + query.Add(OdataFilter, options.Filter) + } + headers = addToHeaders(headers, "x-ms-client-request-id", options.RequestID) + } + return query, headers +} + // QueryTables returns the tables in the storage account. // You can use query options defined by the OData Protocol specification. // // See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/query-tables -func (t *TableServiceClient) QueryTables(odataQuery url.Values) (*TableQueryResult, error) { - uri := t.client.getEndpoint(tableServiceName, tablesURIPath, fixOdataQuery(odataQuery)) - return t.queryTables(uri) +func (t *TableServiceClient) QueryTables(ml MetadataLevel, options *QueryTablesOptions) (*TableQueryResult, error) { + query, headers := options.getParameters() + uri := t.client.getEndpoint(tableServiceName, tablesURIPath, query) + return t.queryTables(uri, headers, ml) } -// QueryTablesNextResults returns the next page of results -// from a QueryTables or a QueryTablesNextResults operation. +// NextResults returns the next page of results +// from a QueryTables or a NextResults operation. // // See https://docs.microsoft.com/rest/api/storageservices/fileservices/query-tables // See https://docs.microsoft.com/rest/api/storageservices/fileservices/query-timeout-and-pagination -func (t *TableServiceClient) QueryTablesNextResults(results *TableQueryResult) (*TableQueryResult, error) { - if results.NextLink == nil { - return nil, errors.New("There are no more pages in this query results") +func (tqr *TableQueryResult) NextResults(options *TableOptions) (*TableQueryResult, error) { + if tqr == nil { + return nil, errNilPreviousResult } - return t.queryTables(*results.NextLink) + if tqr.NextLink == nil { + return nil, errNilNextLink + } + headers := options.addToHeaders(map[string]string{}) + + return tqr.tsc.queryTables(*tqr.NextLink, headers, tqr.ml) +} + +// TableQueryResult contains the response from +// QueryTables and QueryTablesNextResults functions. +type TableQueryResult struct { + OdataMetadata string `json:"odata.metadata"` + Tables []Table `json:"value"` + QueryNextLink + tsc *TableServiceClient } -func (t *TableServiceClient) queryTables(uri string) (*TableQueryResult, error) { - headers := t.client.getStandardHeaders() - headers[headerAccept] = "application/json;odata=fullmetadata" +func (t *TableServiceClient) queryTables(uri string, headers map[string]string, ml MetadataLevel) (*TableQueryResult, error) { + if ml == EmptyPayload { + return nil, errEmptyPayload + } + headers = mergeHeaders(headers, t.client.getStandardHeaders()) + headers[headerAccept] = string(ml) resp, err := t.client.exec(http.MethodGet, uri, headers, nil, t.auth) if err != nil { @@ -100,6 +151,7 @@ func (t *TableServiceClient) queryTables(uri string) (*TableQueryResult, error) for i := range out.Tables { out.Tables[i].tsc = t } + out.tsc = t nextLink := resp.headers.Get(http.CanonicalHeaderKey(headerXmsContinuation)) if nextLink == "" { @@ -113,6 +165,7 @@ func (t *TableServiceClient) queryTables(uri string) (*TableQueryResult, error) v.Set(nextTableQueryParameter, nextLink) newURI := t.client.getEndpoint(tableServiceName, tablesURIPath, v) out.NextLink = &newURI + out.ml = ml } return &out, nil