diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..787012d --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +vendor/ +.idea/ +example/example +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b8c3989 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8776246 --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# Giovanni +An alternative Azure Storage SDK for Go + +--- + +This repository is an alternative Azure Storage SDK for Go; which supports for: + +- The [Blob Storage API's](https://docs.microsoft.com/en-us/rest/api/storageservices/blob-service-rest-api) +- The [File Storage API's](https://docs.microsoft.com/en-us/rest/api/storageservices/file-service-rest-api) +- The [Queue Storage API's](https://docs.microsoft.com/en-us/rest/api/storageservices/queue-service-rest-api) +- The [Table Storage API's](https://docs.microsoft.com/en-us/rest/api/storageservices/table-service-rest-api) + +At this time we support the following API Versions: + +* `2018-11-09` (`./storage/2018-11-09`) +* `2018-03-28` (`./storage/2018-03-28`) +* `2017-07-29` (`./storage/2017-07-29`) + +We're also open to supporting other versions of the Azure Storage SDK as necessary. + +Documentation for how to use each SDK can be found within the README for that SDK version - for example [here's the README for 2018-11-09](storage/2018-11-09/README.md). + +Each Package also contains Unit and Acceptance tests to ensure that the functionality works; instructions on how to run the tests can be found below. + +## Mission Statement + +Fundamentally: developers should be able to pick which version of the Azure API they target using this SDK. + +As such, there's two main goals here: + +* New API Versions will be added additively to the `storage` folder. + +* Any supported API Versions will continue to exist in the `storage` folder until they're EOL'd/stop working. + +To ensure that each of these scenarios is possible - we have Acceptance and Unit Tests to confirm that the functionality in these versions works - and will use SemVer as appropriate. + +## Future Enhancements + +At this time this SDK is mostly feature complete, with a couple of notable additions (since we didn't need them). + +Whilst it's possible to create Snapshots (for example, of a Container) - at this time most SDK calls don't support specifying the optional query-string value for `snapshot`. + +In addition, we also don't support the `timeout` querystring on every API call; this is because instead all SDK methods take a `context` object, which allows a timeout to be set (albeit on the Client rather than the Remote API Call). + +In both instances this is because we didn't need this functionality for our use-cases - but feel free to send a PR if you need this. + +## Licence + +Apache 2.0 + +## Technical Implementation + +This SDK makes use of the standard Preparer-Sender-Responder pattern found in [Azure/go-autorest](https://github.com/Azure/go-autorest) - which means that this SDK should be familiar and compatible with [the Azure SDK for Go](https://github.com/Azure/azure-sdk-for-go). + +Depending on the API Version / API being used - different authentication mechanisms are possible (see the README within the specific SDK for more info ([example](XXX)). In all cases one of the following Authorizers will be required: + +* An Authorizer for Azure Active Directory +* A SharedKeyLite Authorizer (for Blob, Queue and Table Storage) +* A SharedKeyLite Authorizer (for Table Storage) + +Examples for all of these can be found below in [the Examples Directory](examples/). + +## Running the Tests + +Each package contains both Unit and Acceptance Tests which provision a real Storage Account on Azure, and then run tests against that. + +To run those, the following Environment Variables need to be set: + +* `ARM_SUBSCRIPTION_ID` - The ID of the Subscription where tests should be run, such as `00000000-0000-0000-0000-000000000000`. +* `ARM_CLIENT_ID` - The ID of the AzureAD Application (also known as a Client ID), such as `00000000-0000-0000-0000-000000000000`. +* `ARM_CLIENT_SECRET` - The Client Secret/Password for a Service Principal where tests should be run. +* `ARM_ENVIRONMENT` - The name of the Azure Environment where the tests should be run, such as `Public` or `Germany`. +* `ARM_TEST_LOCATION` - The name of the Azure Region where resources provisioned by the tests should be created, such as `West Europe`. + +Once those Environment Variables are set - you should be able to run: + +```bash +$ go test -v ./storage/... +``` + +You can also run them for a specific API version by running: + +```bash +$ go test -v ./storage/2018-11-09/... +``` + +## Debugging + +You can see the Requests/Responses from this SDK by setting the Environment Variable `TEST_LOG` to any value. diff --git a/example/azuread-auth/README.md b/example/azuread-auth/README.md new file mode 100644 index 0000000..1e6b33b --- /dev/null +++ b/example/azuread-auth/README.md @@ -0,0 +1,19 @@ +## Example: using Azure Active Directory authentication + +This example provisions a Storage Container using Azure Active Directory for authentication. + +To run this example you need the following Environment Variables set: + +* `ARM_CLIENT_ID` - The UUID of the Service Principal/Application +* `ARM_CLIENT_SECRET` - The Secret associated with the Service Principal +* `ARM_ENVIRONMENT` - The Azure Environment (`public`, `germany` etc) +* `ARM_SUBSCRIPTION_ID` - The UUID of the Azure Subscription +* `ARM_TENANT_ID` - The UUID of the Azure Tenant + +You also need to update `main.go` to set the variable `storageAccountName` to an existing Storage Account (since we don't provision one for you). + +Assuming you've got Go installed - you can then run this using: + +```bash +$ go run main.go +``` \ No newline at end of file diff --git a/example/azuread-auth/main.go b/example/azuread-auth/main.go new file mode 100644 index 0000000..e72537c --- /dev/null +++ b/example/azuread-auth/main.go @@ -0,0 +1,109 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/adal" + "github.com/hashicorp/go-azure-helpers/authentication" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/containers" +) + +func main() { + log.Printf("[DEBUG] Started..") + + // NOTE: fill this in + storageAccountName := "example" + + log.Printf("[DEBUG] Building Client..") + client, err := buildClient() + if err != nil { + panic(fmt.Errorf("Error building client: %s", err)) + } + + ctx := context.TODO() + containerName := "armauth" + input := containers.CreateInput{ + AccessLevel: containers.Private, + MetaData: map[string]string{ + "hello": "world", + }, + } + log.Printf("[DEBUG] Creating Container..") + if _, err := client.ContainersClient.Create(ctx, storageAccountName, containerName, input); err != nil { + panic(fmt.Errorf("Error creating container: %s", err)) + } + + log.Printf("[DEBUG] Retrieving Container..") + container, err := client.ContainersClient.GetProperties(ctx, storageAccountName, containerName) + if err != nil { + panic(fmt.Errorf("Error reading properties for container: %s", err)) + } + + log.Printf("[DEBUG] MetaData: %+v", container.MetaData) +} + +type Client struct { + ContainersClient containers.Client +} + +func buildClient() (*Client, error) { + // we're using github.com/hashicorp/go-azure-helpers since it makes this simpler + // but you can use an Authorizer from github.com/Azure/go-autorest directly too + builder := &authentication.Builder{ + SubscriptionID: os.Getenv("ARM_SUBSCRIPTION_ID"), + ClientID: os.Getenv("ARM_CLIENT_ID"), + ClientSecret: os.Getenv("ARM_CLIENT_SECRET"), + TenantID: os.Getenv("ARM_TENANT_ID"), + Environment: os.Getenv("ARM_ENVIRONMENT"), + + // Feature Toggles + SupportsClientSecretAuth: true, + SupportsAzureCliToken: true, + } + + config, err := builder.Build() + if err != nil { + return nil, fmt.Errorf("Error building AzureRM Client: %s", err) + } + + env, err := authentication.DetermineEnvironment(config.Environment) + if err != nil { + return nil, err + } + + oauthConfig, err := adal.NewOAuthConfig(env.ActiveDirectoryEndpoint, config.TenantID) + if err != nil { + return nil, err + } + + // OAuthConfigForTenant returns a pointer, which can be nil. + if oauthConfig == nil { + return nil, fmt.Errorf("Unable to configure OAuthConfig for tenant %s", config.TenantID) + } + + // support for HTTP Proxies + sender := autorest.DecorateSender(&http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + }, + }) + + storageAuth, err := config.GetAuthorizationToken(sender, oauthConfig, "https://storage.azure.com/") + if err != nil { + return nil, err + } + + containersClient := containers.New() + containersClient.Client.Authorizer = storageAuth + + result := &Client{ + ContainersClient: containersClient, + } + + return result, nil +} diff --git a/example/file-auth/README.md b/example/file-auth/README.md new file mode 100644 index 0000000..98f93bc --- /dev/null +++ b/example/file-auth/README.md @@ -0,0 +1,15 @@ +## Example: using a Shared Key + +This example provisions a Storage Container using a Shared Key for authentication. + +To run this example you need the following Environment Variables set: + +* `ARM_ENVIRONMENT` - The Azure Environment (`public`, `germany` etc) + +You also need to update `main.go` to set the variable `storageAccountName` and `storageAccountKey` to an existing Storage Account (since we don't provision one for you). + +Assuming you've got Go installed - you can then run this using: + +```bash +$ go run main.go +``` \ No newline at end of file diff --git a/example/file-auth/main.go b/example/file-auth/main.go new file mode 100644 index 0000000..7f55be1 --- /dev/null +++ b/example/file-auth/main.go @@ -0,0 +1,68 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/Azure/go-autorest/autorest" + "github.com/hashicorp/go-azure-helpers/authentication" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/containers" +) + +func main() { + log.Printf("[DEBUG] Started..") + + // NOTE: fill this in + storageAccountName := "example" + storageAccountKey := "example" + + log.Printf("[DEBUG] Building Client..") + client, err := buildClient(storageAccountName, storageAccountKey) + if err != nil { + panic(fmt.Errorf("Error building client: %s", err)) + } + + ctx := context.TODO() + containerName := "armauth" + input := containers.CreateInput{ + AccessLevel: containers.Private, + MetaData: map[string]string{ + "hello": "world", + }, + } + log.Printf("[DEBUG] Creating Container..") + if _, err := client.ContainersClient.Create(ctx, storageAccountName, containerName, input); err != nil { + panic(fmt.Errorf("Error creating container: %s", err)) + } + + log.Printf("[DEBUG] Retrieving Container..") + container, err := client.ContainersClient.GetProperties(ctx, storageAccountName, containerName) + if err != nil { + panic(fmt.Errorf("Error reading properties for container: %s", err)) + } + + log.Printf("[DEBUG] MetaData: %+v", container.MetaData) +} + +type Client struct { + ContainersClient containers.Client +} + +func buildClient(accountName, accountKey string) (*Client, error) { + env, err := authentication.DetermineEnvironment(os.Getenv("ARM_ENVIRONMENT")) + if err != nil { + return nil, err + } + + storageAuth := autorest.NewSharedKeyLiteAuthorizer(accountName, accountKey) + containersClient := containers.New() + containersClient.Client.Authorizer = storageAuth + + result := &Client{ + ContainersClient: containersClient, + } + + return result, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..60bd9c5 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module github.com/tombuildsstuff/giovanni + +go 1.12 + +require ( + contrib.go.opencensus.io/exporter/ocagent v0.5.0 // indirect + github.com/Azure/azure-sdk-for-go v30.0.0+incompatible + github.com/Azure/go-autorest v12.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.3.0 + github.com/Azure/go-autorest/autorest/adal v0.1.0 + github.com/Azure/go-autorest/autorest/azure/cli v0.1.0 // indirect + github.com/Azure/go-autorest/autorest/to v0.2.0 // indirect + github.com/Azure/go-autorest/autorest/validation v0.1.0 + github.com/hashicorp/go-azure-helpers v0.4.1 + go.opencensus.io v0.22.0 // indirect + golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5c2ebe2 --- /dev/null +++ b/go.sum @@ -0,0 +1,198 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA= +contrib.go.opencensus.io/exporter/ocagent v0.5.0 h1:TKXjQSRS0/cCDrP7KvkgU6SmILtF/yV2TOs/02K/WZQ= +contrib.go.opencensus.io/exporter/ocagent v0.5.0/go.mod h1:ImxhfLRpxoYiSq891pBrLVhN+qmP8BTVvdH2YLs7Gl0= +github.com/Azure/azure-sdk-for-go v21.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v29.0.0+incompatible h1:CYPU39ULbGjQBo3gXIqiWouK0C4F+Pt2Zx5CqGvqknE= +github.com/Azure/azure-sdk-for-go v29.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v30.0.0+incompatible h1:6o1Yzl7wTBYg+xw0pY4qnalaPmEQolubEEdepo1/kmI= +github.com/Azure/azure-sdk-for-go v30.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-autorest v10.15.4+incompatible h1:q+DRrRdbCnkY7f2WxQBx58TwCGkEdMAK/hkZ10g0Pzk= +github.com/Azure/go-autorest v10.15.4+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest v11.7.0+incompatible h1:gzma19dc9ejB75D90E5S+/wXouzpZyA+CV+/MJPSD/k= +github.com/Azure/go-autorest v11.7.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest v12.2.0+incompatible h1:2Fxszbg492oAJrcvJlgyVaTqnQYRkxmEK6VPCLLVpBI= +github.com/Azure/go-autorest v12.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.3.0 h1:yOmXNB2qa2Kx40wMZB19YyafzjCHacXPk8u0neqa+M0= +github.com/Azure/go-autorest/autorest v0.3.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg= +github.com/Azure/go-autorest/autorest/adal v0.1.0 h1:RSw/7EAullliqwkZvgIGDYZWQm1PGKXI8c4aY/87yuU= +github.com/Azure/go-autorest/autorest/adal v0.1.0/go.mod h1:MeS4XhScH55IST095THyTxElntu7WqB7pNbZo8Q5G3E= +github.com/Azure/go-autorest/autorest/azure/cli v0.1.0 h1:YTtBrcb6mhA+PoSW8WxFDoIIyjp13XqJeX80ssQtri4= +github.com/Azure/go-autorest/autorest/azure/cli v0.1.0/go.mod h1:Dk8CUAt/b/PzkfeRsWzVG9Yj3ps8mS8ECztu43rdU8U= +github.com/Azure/go-autorest/autorest/date v0.1.0 h1:YGrhWfrgtFs84+h0o46rJrlmsZtyZRg470CqAXTZaGM= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/to v0.2.0 h1:nQOZzFCudTh+TvquAtCRjM01VEYx85e9qbwt5ncW4L8= +github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc= +github.com/Azure/go-autorest/autorest/validation v0.1.0 h1:ISSNzGUh+ZSzizJWOWzs8bwpXIePbGLW4z/AmUFGH5A= +github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8= +github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.1.0 h1:TRBxC5Pj/fIuh4Qob0ZpkggbfT8RC0SubHbpV3p4/Vc= +github.com/Azure/go-autorest/tracing v0.1.0/go.mod h1:ROEEAFwXycQw7Sn3DXNtEedEvdeRAgDr0izn4z5Ij88= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/census-instrumentation/opencensus-proto v0.2.0 h1:LzQXZOgg4CQfE6bFvXGM30YZL1WW/M337pXml+GrcZ4= +github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dimchansky/utfbom v1.0.0 h1:fGC2kkf4qOoKqZ4q7iIh+Vef4ubC1c38UDsEyZynZPc= +github.com/dimchansky/utfbom v1.0.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4= +github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/grpc-ecosystem/grpc-gateway v1.8.5 h1:2+KSC78XiO6Qy0hIjfc1OD9H+hsaJdJlb8Kqsd41CTE= +github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-azure-helpers v0.4.1 h1:aEWYW4hxAVVmxmq7nPXGK8F44A6HBXQ4m0vB1M3/20g= +github.com/hashicorp/go-azure-helpers v0.4.1/go.mod h1:lu62V//auUow6k0IykxLK2DCNW8qTmpm8KqhYVWattA= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284 h1:rlLehGeYg6jfoyz/eDqDU1iRXLKfR42nnNh57ytKEWo= +golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09 h1:KaQtG+aDELoNmXYas3TVkGNYRuq8JQ1aa7LJt8EXVyo= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd h1:r7DufRZuZbWB7j439YfAzP8RPDa9unLkpwQKUYbIMPI= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.4.0 h1:KKgc1aqhV8wDPbDzlDtpvyjZFY3vjz85FP7p4wcQUyI= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb h1:i1Ppqkc3WQXikh8bXiwHqAN5Rv3/qDCcRk0/Otx73BY= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/storage/2017-07-29/README.md b/storage/2017-07-29/README.md new file mode 100644 index 0000000..6aa8c0b --- /dev/null +++ b/storage/2017-07-29/README.md @@ -0,0 +1,25 @@ +# Storage API Version 2017-07-29 + +The following API's are supported by this SDK - more information about each SDK can be found within the README in each package. + +## Blob Storage + +- [Blobs API](blob/blobs) +- [Containers API](blob/containers) + +## File Storage + +- [Directories API](file/directories) +- [Files API](file/files) +- [Shares API](file/shares) + +## Queue Storage + +- [Queues API](queue/queues) +- [Messages API](queue/messages) + +## Table Storage + +- [Entities API](table/entities) +- [Tables API](table/tables) + diff --git a/storage/2017-07-29/blob/blobs/README.md b/storage/2017-07-29/blob/blobs/README.md new file mode 100644 index 0000000..b15f42c --- /dev/null +++ b/storage/2017-07-29/blob/blobs/README.md @@ -0,0 +1,45 @@ +## Blob Storage Blobs SDK for API version 2017-07-29 + +This package allows you to interact with the Blobs Blob Storage API + +### Supported Authorizers + +* SharedKeyLite (Blob, File & Queue) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2017-07-29/blob/blobs" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + containerName := "mycontainer" + fileName := "example-large-file.iso" + + storageAuth := autorest.NewSharedKeyLiteAuthorizer(accountName, storageAccountKey) + blobClient := blobs.New() + blobClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + copyInput := blobs.CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + refreshInterval := 5 * time.Second + if err := blobClient.CopyAndWait(ctx, accountName, containerName, fileName, copyInput, refreshInterval); err != nil { + return fmt.Errorf("Error copying: %s", err) + } + + return nil +} + +``` \ No newline at end of file diff --git a/storage/2017-07-29/blob/blobs/append_block.go b/storage/2017-07-29/blob/blobs/append_block.go new file mode 100644 index 0000000..7fed86a --- /dev/null +++ b/storage/2017-07-29/blob/blobs/append_block.go @@ -0,0 +1,170 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type AppendBlockInput struct { + + // A number indicating the byte offset to compare. + // Append Block will succeed only if the append position is equal to this number. + // If it is not, the request will fail with an AppendPositionConditionNotMet + // error (HTTP status code 412 – Precondition Failed) + BlobConditionAppendPosition *int64 + + // The max length in bytes permitted for the append blob. + // If the Append Block operation would cause the blob to exceed that limit or if the blob size + // is already greater than the value specified in this header, the request will fail with + // an MaxBlobSizeConditionNotMet error (HTTP status code 412 – Precondition Failed). + BlobConditionMaxSize *int64 + + // The Bytes which should be appended to the end of this Append Blob. + Content []byte + + // An MD5 hash of the block content. + // This hash is used to verify the integrity of the block during transport. + // When this header is specified, the storage service compares the hash of the content + // that has arrived with this header value. + // + // Note that this MD5 hash is not stored with the blob. + // If the two hashes do not match, the operation will fail with error code 400 (Bad Request). + ContentMD5 *string + + // Required if the blob has an active lease. + // To perform this operation on a blob with an active lease, specify the valid lease ID for this header. + LeaseID *string +} + +type AppendBlockResult struct { + autorest.Response + + BlobAppendOffset string + BlobCommittedBlockCount int64 + ContentMD5 string + ETag string + LastModified string +} + +// AppendBlock commits a new block of data to the end of an existing append blob. +func (client Client) AppendBlock(ctx context.Context, accountName, containerName, blobName string, input AppendBlockInput) (result AppendBlockResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "AppendBlock", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "AppendBlock", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "AppendBlock", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "AppendBlock", "`blobName` cannot be an empty string.") + } + if len(input.Content) > (4 * 1024 * 1024) { + return result, validation.NewError("files.Client", "PutByteRange", "`input.Content` must be at most 4MB.") + } + + req, err := client.AppendBlockPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "AppendBlock", nil, "Failure preparing request") + return + } + + resp, err := client.AppendBlockSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "AppendBlock", resp, "Failure sending request") + return + } + + result, err = client.AppendBlockResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "AppendBlock", resp, "Failure responding to request") + return + } + + return +} + +// AppendBlockPreparer prepares the AppendBlock request. +func (client Client) AppendBlockPreparer(ctx context.Context, accountName, containerName, blobName string, input AppendBlockInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "appendblock"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.BlobConditionAppendPosition != nil { + headers["x-ms-blob-condition-appendpos"] = *input.BlobConditionAppendPosition + } + if input.BlobConditionMaxSize != nil { + headers["x-ms-blob-condition-maxsize"] = *input.BlobConditionMaxSize + } + if input.ContentMD5 != nil { + headers["x-ms-blob-content-md5"] = *input.ContentMD5 + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers), + autorest.WithBytes(&input.Content)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// AppendBlockSender sends the AppendBlock request. The method will close the +// http.Response Body if it receives an error. +func (client Client) AppendBlockSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// AppendBlockResponder handles the response to the AppendBlock request. The method always +// closes the http.Response Body. +func (client Client) AppendBlockResponder(resp *http.Response) (result AppendBlockResult, err error) { + if resp != nil && resp.Header != nil { + result.BlobAppendOffset = resp.Header.Get("x-ms-blob-append-offset") + result.ContentMD5 = resp.Header.Get("ETag") + result.ETag = resp.Header.Get("ETag") + result.LastModified = resp.Header.Get("Last-Modified") + + if v := resp.Header.Get("x-ms-blob-committed-block-count"); v != "" { + i, innerErr := strconv.Atoi(v) + if innerErr != nil { + err = fmt.Errorf("Error parsing %q as an integer: %s", v, innerErr) + return + } + + result.BlobCommittedBlockCount = int64(i) + } + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/blobs/blob_append_test.go b/storage/2017-07-29/blob/blobs/blob_append_test.go new file mode 100644 index 0000000..78b2bcd --- /dev/null +++ b/storage/2017-07-29/blob/blobs/blob_append_test.go @@ -0,0 +1,155 @@ +package blobs + +import ( + "context" + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2017-07-29/blob/containers" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestAppendBlobLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + fileName := "append-blob.txt" + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.BlobStorage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + containersClient := containers.NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithAuthorizer(containersClient.Client, storageAuth) + + _, err = containersClient.Create(ctx, accountName, containerName, containers.CreateInput{}) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + defer containersClient.Delete(ctx, accountName, containerName) + + blobClient := NewWithEnvironment(client.Environment) + blobClient.Client = client.PrepareWithAuthorizer(blobClient.Client, storageAuth) + + t.Logf("[DEBUG] Putting Append Blob..") + if _, err := blobClient.PutAppendBlob(ctx, accountName, containerName, fileName, PutAppendBlobInput{}); err != nil { + t.Fatalf("Error putting append blob: %s", err) + } + + t.Logf("[DEBUG] Retrieving Properties..") + props, err := blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error retrieving properties: %s", err) + } + if props.ContentLength != 0 { + t.Fatalf("Expected Content-Length to be 0 but it was %d", props.ContentLength) + } + + t.Logf("[DEBUG] Appending First Block..") + appendInput := AppendBlockInput{ + Content: []byte{ + 12, + 48, + 93, + 76, + 29, + 10, + }, + } + if _, err := blobClient.AppendBlock(ctx, accountName, containerName, fileName, appendInput); err != nil { + t.Fatalf("Error appending first block: %s", err) + } + + t.Logf("[DEBUG] Re-Retrieving Properties..") + props, err = blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error retrieving properties: %s", err) + } + if props.ContentLength != 6 { + t.Fatalf("Expected Content-Length to be 6 but it was %d", props.ContentLength) + } + + t.Logf("[DEBUG] Appending Second Block..") + appendInput = AppendBlockInput{ + Content: []byte{ + 92, + 62, + 64, + 47, + 83, + 77, + }, + } + if _, err := blobClient.AppendBlock(ctx, accountName, containerName, fileName, appendInput); err != nil { + t.Fatalf("Error appending Second block: %s", err) + } + + t.Logf("[DEBUG] Re-Retrieving Properties..") + props, err = blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error retrieving properties: %s", err) + } + if props.ContentLength != 12 { + t.Fatalf("Expected Content-Length to be 12 but it was %d", props.ContentLength) + } + + t.Logf("[DEBUG] Acquiring Lease..") + leaseDetails, err := blobClient.AcquireLease(ctx, accountName, containerName, fileName, AcquireLeaseInput{ + LeaseDuration: -1, + }) + if err != nil { + t.Fatalf("Error acquiring Lease: %s", err) + } + t.Logf("[DEBUG] Lease ID is %q", leaseDetails.LeaseID) + + t.Logf("[DEBUG] Appending Third Block..") + appendInput = AppendBlockInput{ + Content: []byte{ + 64, + 35, + 28, + 93, + 11, + 23, + }, + LeaseID: &leaseDetails.LeaseID, + } + if _, err := blobClient.AppendBlock(ctx, accountName, containerName, fileName, appendInput); err != nil { + t.Fatalf("Error appending Third block: %s", err) + } + + t.Logf("[DEBUG] Re-Retrieving Properties..") + props, err = blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{ + LeaseID: &leaseDetails.LeaseID, + }) + if err != nil { + t.Fatalf("Error retrieving properties: %s", err) + } + if props.ContentLength != 18 { + t.Fatalf("Expected Content-Length to be 18 but it was %d", props.ContentLength) + } + + t.Logf("[DEBUG] Breaking Lease..") + breakLeaseInput := BreakLeaseInput{ + LeaseID: leaseDetails.LeaseID, + } + if _, err := blobClient.BreakLease(ctx, accountName, containerName, fileName, breakLeaseInput); err != nil { + t.Fatalf("Error breaking lease: %s", err) + } + + t.Logf("[DEBUG] Deleting Lease..") + if _, err := blobClient.Delete(ctx, accountName, containerName, fileName, DeleteInput{}); err != nil { + t.Fatalf("Error deleting: %s", err) + } +} diff --git a/storage/2017-07-29/blob/blobs/blob_page_test.go b/storage/2017-07-29/blob/blobs/blob_page_test.go new file mode 100644 index 0000000..6f6b87e --- /dev/null +++ b/storage/2017-07-29/blob/blobs/blob_page_test.go @@ -0,0 +1,89 @@ +package blobs + +import ( + "context" + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2017-07-29/blob/containers" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestPageBlobLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + fileName := "append-blob.txt" + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.StorageV2) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + containersClient := containers.NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithAuthorizer(containersClient.Client, storageAuth) + + _, err = containersClient.Create(ctx, accountName, containerName, containers.CreateInput{}) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + defer containersClient.Delete(ctx, accountName, containerName) + + blobClient := NewWithEnvironment(client.Environment) + blobClient.Client = client.PrepareWithAuthorizer(blobClient.Client, storageAuth) + + t.Logf("[DEBUG] Putting Page Blob..") + fileSize := int64(10240000) + if _, err := blobClient.PutPageBlob(ctx, accountName, containerName, fileName, PutPageBlobInput{ + BlobContentLengthBytes: fileSize, + }); err != nil { + t.Fatalf("Error putting page blob: %s", err) + } + + t.Logf("[DEBUG] Retrieving Properties..") + props, err := blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error retrieving properties: %s", err) + } + if props.ContentLength != fileSize { + t.Fatalf("Expected Content-Length to be %d but it was %d", fileSize, props.ContentLength) + } + + for iteration := 1; iteration <= 3; iteration++ { + t.Logf("[DEBUG] Putting Page %d of 3..", iteration) + byteArray := func() []byte { + o := make([]byte, 0) + + for i := 0; i < 512; i++ { + o = append(o, byte(i)) + } + + return o + }() + startByte := int64(512 * iteration) + endByte := int64(startByte + 511) + putPageInput := PutPageUpdateInput{ + StartByte: startByte, + EndByte: endByte, + Content: byteArray, + } + if _, err := blobClient.PutPageUpdate(ctx, accountName, containerName, fileName, putPageInput); err != nil { + t.Fatalf("Error putting page: %s", err) + } + } + + t.Logf("[DEBUG] Deleting..") + if _, err := blobClient.Delete(ctx, accountName, containerName, fileName, DeleteInput{}); err != nil { + t.Fatalf("Error deleting: %s", err) + } +} diff --git a/storage/2017-07-29/blob/blobs/client.go b/storage/2017-07-29/blob/blobs/client.go new file mode 100644 index 0000000..db20391 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/client.go @@ -0,0 +1,25 @@ +package blobs + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for Blob Storage Blobs. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithBaseURI creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2017-07-29/blob/blobs/copy.go b/storage/2017-07-29/blob/blobs/copy.go new file mode 100644 index 0000000..febaab5 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/copy.go @@ -0,0 +1,235 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type CopyInput struct { + // Specifies the name of the source blob or file. + // Beginning with version 2012-02-12, this value may be a URL of up to 2 KB in length that specifies a blob. + // The value should be URL-encoded as it would appear in a request URI. + // A source blob in the same storage account can be authenticated via Shared Key. + // However, if the source is a blob in another account, + // the source blob must either be public or must be authenticated via a shared access signature. + // If the source blob is public, no authentication is required to perform the copy operation. + // + // Beginning with version 2015-02-21, the source object may be a file in the Azure File service. + // If the source object is a file that is to be copied to a blob, then the source file must be authenticated + // using a shared access signature, whether it resides in the same account or in a different account. + // + // Only storage accounts created on or after June 7th, 2012 allow the Copy Blob operation to + // copy from another storage account. + CopySource string + + // The ID of the Lease + // Required if the destination blob has an active lease. + // The lease ID specified for this header must match the lease ID of the destination blob. + // If the request does not include the lease ID or it is not valid, + // the operation fails with status code 412 (Precondition Failed). + // + // If this header is specified and the destination blob does not currently have an active lease, + // the operation will also fail with status code 412 (Precondition Failed). + LeaseID *string + + // The ID of the Lease on the Source Blob + // Specify to perform the Copy Blob operation only if the lease ID matches the active lease ID of the source blob. + SourceLeaseID *string + + // For page blobs on a premium account only. Specifies the tier to be set on the target blob + AccessTier *AccessTier + + // A user-defined name-value pair associated with the blob. + // If no name-value pairs are specified, the operation will copy the metadata from the source blob or + // file to the destination blob. + // If one or more name-value pairs are specified, the destination blob is created with the specified metadata, + // and metadata is not copied from the source blob or file. + MetaData map[string]string + + // An ETag value. + // Specify an ETag value for this conditional header to copy the blob only if the specified + // ETag value matches the ETag value for an existing destination blob. + // If the ETag for the destination blob does not match the ETag specified for If-Match, + // the Blob service returns status code 412 (Precondition Failed). + IfMatch *string + + // An ETag value, or the wildcard character (*). + // Specify an ETag value for this conditional header to copy the blob only if the specified + // ETag value does not match the ETag value for the destination blob. + // Specify the wildcard character (*) to perform the operation only if the destination blob does not exist. + // If the specified condition isn't met, the Blob service returns status code 412 (Precondition Failed). + IfNoneMatch *string + + // A DateTime value. + // Specify this conditional header to copy the blob only if the destination blob + // has been modified since the specified date/time. + // If the destination blob has not been modified, the Blob service returns status code 412 (Precondition Failed). + IfModifiedSince *string + + // A DateTime value. + // Specify this conditional header to copy the blob only if the destination blob + // has not been modified since the specified date/time. + // If the destination blob has been modified, the Blob service returns status code 412 (Precondition Failed). + IfUnmodifiedSince *string + + // An ETag value. + // Specify this conditional header to copy the source blob only if its ETag matches the value specified. + // If the ETag values do not match, the Blob service returns status code 412 (Precondition Failed). + // This cannot be specified if the source is an Azure File. + SourceIfMatch *string + + // An ETag value. + // Specify this conditional header to copy the blob only if its ETag does not match the value specified. + // If the values are identical, the Blob service returns status code 412 (Precondition Failed). + // This cannot be specified if the source is an Azure File. + SourceIfNoneMatch *string + + // A DateTime value. + // Specify this conditional header to copy the blob only if the source blob has been modified + // since the specified date/time. + // If the source blob has not been modified, the Blob service returns status code 412 (Precondition Failed). + // This cannot be specified if the source is an Azure File. + SourceIfModifiedSince *string + + // A DateTime value. + // Specify this conditional header to copy the blob only if the source blob has not been modified + // since the specified date/time. + // If the source blob has been modified, the Blob service returns status code 412 (Precondition Failed). + // This header cannot be specified if the source is an Azure File. + SourceIfUnmodifiedSince *string +} + +type CopyResult struct { + autorest.Response + + CopyID string + CopyStatus string +} + +// Copy copies a blob to a destination within the storage account asynchronously. +func (client Client) Copy(ctx context.Context, accountName, containerName, blobName string, input CopyInput) (result CopyResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "Copy", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "Copy", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "Copy", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "Copy", "`blobName` cannot be an empty string.") + } + if input.CopySource == "" { + return result, validation.NewError("blobs.Client", "Copy", "`input.CopySource` cannot be an empty string.") + } + + req, err := client.CopyPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Copy", nil, "Failure preparing request") + return + } + + resp, err := client.CopySender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "Copy", resp, "Failure sending request") + return + } + + result, err = client.CopyResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Copy", resp, "Failure responding to request") + return + } + + return +} + +// CopyPreparer prepares the Copy request. +func (client Client) CopyPreparer(ctx context.Context, accountName, containerName, blobName string, input CopyInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-copy-source": autorest.Encode("header", input.CopySource), + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + if input.SourceLeaseID != nil { + headers["x-ms-source-lease-id"] = *input.SourceLeaseID + } + if input.AccessTier != nil { + headers["x-ms-access-tier"] = string(*input.AccessTier) + } + + if input.IfMatch != nil { + headers["If-Match"] = *input.IfMatch + } + if input.IfNoneMatch != nil { + headers["If-None-Match"] = *input.IfNoneMatch + } + if input.IfUnmodifiedSince != nil { + headers["If-Unmodified-Since"] = *input.IfUnmodifiedSince + } + if input.IfModifiedSince != nil { + headers["If-Modified-Since"] = *input.IfModifiedSince + } + + if input.SourceIfMatch != nil { + headers["x-ms-source-if-match"] = *input.SourceIfMatch + } + if input.SourceIfNoneMatch != nil { + headers["x-ms-source-if-none-match"] = *input.SourceIfNoneMatch + } + if input.SourceIfModifiedSince != nil { + headers["x-ms-source-if-modified-since"] = *input.SourceIfModifiedSince + } + if input.SourceIfUnmodifiedSince != nil { + headers["x-ms-source-if-unmodified-since"] = *input.SourceIfUnmodifiedSince + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CopySender sends the Copy request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CopySender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CopyResponder handles the response to the Copy request. The method always +// closes the http.Response Body. +func (client Client) CopyResponder(resp *http.Response) (result CopyResult, err error) { + if resp != nil && resp.Header != nil { + result.CopyID = resp.Header.Get("x-ms-copy-id") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + return +} diff --git a/storage/2017-07-29/blob/blobs/copy_abort.go b/storage/2017-07-29/blob/blobs/copy_abort.go new file mode 100644 index 0000000..a992ff1 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/copy_abort.go @@ -0,0 +1,110 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type AbortCopyInput struct { + // The Copy ID which should be aborted + CopyID string + + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string +} + +// AbortCopy aborts a pending Copy Blob operation, and leaves a destination blob with zero length and full metadata. +func (client Client) AbortCopy(ctx context.Context, accountName, containerName, blobName string, input AbortCopyInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "AbortCopy", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "AbortCopy", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "AbortCopy", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "AbortCopy", "`blobName` cannot be an empty string.") + } + if input.CopyID == "" { + return result, validation.NewError("blobs.Client", "AbortCopy", "`input.CopyID` cannot be an empty string.") + } + + req, err := client.AbortCopyPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "AbortCopy", nil, "Failure preparing request") + return + } + + resp, err := client.AbortCopySender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "AbortCopy", resp, "Failure sending request") + return + } + + result, err = client.AbortCopyResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "AbortCopy", resp, "Failure responding to request") + return + } + + return +} + +// AbortCopyPreparer prepares the AbortCopy request. +func (client Client) AbortCopyPreparer(ctx context.Context, accountName, containerName, blobName string, input AbortCopyInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "copy"), + "copyid": autorest.Encode("query", input.CopyID), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-copy-action": "abort", + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// AbortCopySender sends the AbortCopy request. The method will close the +// http.Response Body if it receives an error. +func (client Client) AbortCopySender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// AbortCopyResponder handles the response to the AbortCopy request. The method always +// closes the http.Response Body. +func (client Client) AbortCopyResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2017-07-29/blob/blobs/copy_and_wait.go b/storage/2017-07-29/blob/blobs/copy_and_wait.go new file mode 100644 index 0000000..a1e7fa4 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/copy_and_wait.go @@ -0,0 +1,41 @@ +package blobs + +import ( + "context" + "fmt" + "time" +) + +// CopyAndWait copies a blob to a destination within the storage account and waits for it to finish copying. +func (client Client) CopyAndWait(ctx context.Context, accountName, containerName, blobName string, input CopyInput, pollingInterval time.Duration) error { + if _, err := client.Copy(ctx, accountName, containerName, blobName, input); err != nil { + return fmt.Errorf("Error copying: %s", err) + } + + for true { + getInput := GetPropertiesInput{ + LeaseID: input.LeaseID, + } + getResult, err := client.GetProperties(ctx, accountName, containerName, blobName, getInput) + if err != nil { + return fmt.Errorf("") + } + + switch getResult.CopyStatus { + case Aborted: + return fmt.Errorf("Copy was aborted: %s", getResult.CopyStatusDescription) + + case Failed: + return fmt.Errorf("Copy failed: %s", getResult.CopyStatusDescription) + + case Success: + return nil + + case Pending: + time.Sleep(pollingInterval) + continue + } + } + + return fmt.Errorf("Unexpected error waiting for the copy to complete") +} diff --git a/storage/2017-07-29/blob/blobs/copy_test.go b/storage/2017-07-29/blob/blobs/copy_test.go new file mode 100644 index 0000000..76bc62f --- /dev/null +++ b/storage/2017-07-29/blob/blobs/copy_test.go @@ -0,0 +1,148 @@ +package blobs + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2017-07-29/blob/containers" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestCopyFromExistingFile(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + fileName := "ubuntu.iso" + copiedFileName := "copied.iso" + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.BlobStorage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + containersClient := containers.NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithAuthorizer(containersClient.Client, storageAuth) + + _, err = containersClient.Create(ctx, accountName, containerName, containers.CreateInput{}) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + defer containersClient.Delete(ctx, accountName, containerName) + + blobClient := NewWithEnvironment(client.Environment) + blobClient.Client = client.PrepareWithAuthorizer(blobClient.Client, storageAuth) + + t.Logf("[DEBUG] Copying file to Blob Storage..") + copyInput := CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + + refreshInterval := 5 * time.Second + if err := blobClient.CopyAndWait(ctx, accountName, containerName, fileName, copyInput, refreshInterval); err != nil { + t.Fatalf("Error copying: %s", err) + } + + t.Logf("[DEBUG] Duplicating that file..") + copiedInput := CopyInput{ + CopySource: fmt.Sprintf("%s/%s/%s", endpoints.GetBlobEndpoint(blobClient.BaseURI, accountName), containerName, fileName), + } + if err := blobClient.CopyAndWait(ctx, accountName, containerName, copiedFileName, copiedInput, refreshInterval); err != nil { + t.Fatalf("Error duplicating file: %s", err) + } + + t.Logf("[DEBUG] Retrieving Properties for the Original File..") + props, err := blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error getting properties for the original file: %s", err) + } + + t.Logf("[DEBUG] Retrieving Properties for the Copied File..") + copiedProps, err := blobClient.GetProperties(ctx, accountName, containerName, copiedFileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error getting properties for the copied file: %s", err) + } + + if props.ContentLength != copiedProps.ContentLength { + t.Fatalf("Expected the content length to be %d but it was %d", props.ContentLength, copiedProps.ContentLength) + } + + t.Logf("[DEBUG] Deleting copied file..") + if _, err := blobClient.Delete(ctx, accountName, containerName, copiedFileName, DeleteInput{}); err != nil { + t.Fatalf("Error deleting file: %s", err) + } + + t.Logf("[DEBUG] Deleting original file..") + if _, err := blobClient.Delete(ctx, accountName, containerName, fileName, DeleteInput{}); err != nil { + t.Fatalf("Error deleting file: %s", err) + } +} + +func TestCopyFromURL(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + fileName := "ubuntu.iso" + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.BlobStorage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + containersClient := containers.NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithAuthorizer(containersClient.Client, storageAuth) + + _, err = containersClient.Create(ctx, accountName, containerName, containers.CreateInput{}) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + defer containersClient.Delete(ctx, accountName, containerName) + + blobClient := NewWithEnvironment(client.Environment) + blobClient.Client = client.PrepareWithAuthorizer(blobClient.Client, storageAuth) + + t.Logf("[DEBUG] Copying file to Blob Storage..") + copyInput := CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + + refreshInterval := 5 * time.Second + if err := blobClient.CopyAndWait(ctx, accountName, containerName, fileName, copyInput, refreshInterval); err != nil { + t.Fatalf("Error copying: %s", err) + } + + t.Logf("[DEBUG] Retrieving Properties..") + props, err := blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error getting properties: %s", err) + } + + if props.ContentLength == 0 { + t.Fatalf("Expected the file to be there but looks like it isn't: %d", props.ContentLength) + } + + t.Logf("[DEBUG] Deleting file..") + if _, err := blobClient.Delete(ctx, accountName, containerName, fileName, DeleteInput{}); err != nil { + t.Fatalf("Error deleting file: %s", err) + } +} diff --git a/storage/2017-07-29/blob/blobs/delete.go b/storage/2017-07-29/blob/blobs/delete.go new file mode 100644 index 0000000..c1c642d --- /dev/null +++ b/storage/2017-07-29/blob/blobs/delete.go @@ -0,0 +1,105 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type DeleteInput struct { + // Should any Snapshots for this Blob also be deleted? + // If the Blob has Snapshots and this is set to False a 409 Conflict will be returned + DeleteSnapshots bool + + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string +} + +// Delete marks the specified blob or snapshot for deletion. The blob is later deleted during garbage collection. +func (client Client) Delete(ctx context.Context, accountName, containerName, blobName string, input DeleteInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "Delete", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "Delete", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "Delete", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "Delete", "`blobName` cannot be an empty string.") + } + + req, err := client.DeletePreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName, containerName, blobName string, input DeleteInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + if input.DeleteSnapshots { + headers["x-ms-delete-snapshots"] = "include" + } + + preparer := autorest.CreatePreparer( + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2017-07-29/blob/blobs/delete_snapshot.go b/storage/2017-07-29/blob/blobs/delete_snapshot.go new file mode 100644 index 0000000..18c3d4c --- /dev/null +++ b/storage/2017-07-29/blob/blobs/delete_snapshot.go @@ -0,0 +1,108 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type DeleteSnapshotInput struct { + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string + + // The DateTime of the Snapshot which should be marked for Deletion + SnapshotDateTime string +} + +// DeleteSnapshot marks a single Snapshot of a Blob for Deletion based on it's DateTime, which will be deleted during the next Garbage Collection cycle. +func (client Client) DeleteSnapshot(ctx context.Context, accountName, containerName, blobName string, input DeleteSnapshotInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "DeleteSnapshot", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "DeleteSnapshot", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "DeleteSnapshot", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "DeleteSnapshot", "`blobName` cannot be an empty string.") + } + if input.SnapshotDateTime == "" { + return result, validation.NewError("blobs.Client", "DeleteSnapshot", "`input.SnapshotDateTime` cannot be an empty string.") + } + + req, err := client.DeleteSnapshotPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "DeleteSnapshot", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSnapshotSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "DeleteSnapshot", resp, "Failure sending request") + return + } + + result, err = client.DeleteSnapshotResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "DeleteSnapshot", resp, "Failure responding to request") + return + } + + return +} + +// DeleteSnapshotPreparer prepares the DeleteSnapshot request. +func (client Client) DeleteSnapshotPreparer(ctx context.Context, accountName, containerName, blobName string, input DeleteSnapshotInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "snapshot": autorest.Encode("query", input.SnapshotDateTime), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSnapshotSender sends the DeleteSnapshot request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSnapshotSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteSnapshotResponder handles the response to the DeleteSnapshot request. The method always +// closes the http.Response Body. +func (client Client) DeleteSnapshotResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2017-07-29/blob/blobs/delete_snapshots.go b/storage/2017-07-29/blob/blobs/delete_snapshots.go new file mode 100644 index 0000000..e7e2b66 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/delete_snapshots.go @@ -0,0 +1,99 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type DeleteSnapshotsInput struct { + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string +} + +// DeleteSnapshots marks all Snapshots of a Blob for Deletion, which will be deleted during the next Garbage Collection Cycle. +func (client Client) DeleteSnapshots(ctx context.Context, accountName, containerName, blobName string, input DeleteSnapshotsInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "DeleteSnapshots", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "DeleteSnapshots", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "DeleteSnapshots", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "DeleteSnapshots", "`blobName` cannot be an empty string.") + } + + req, err := client.DeleteSnapshotsPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "DeleteSnapshots", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSnapshotsSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "DeleteSnapshots", resp, "Failure sending request") + return + } + + result, err = client.DeleteSnapshotsResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "DeleteSnapshots", resp, "Failure responding to request") + return + } + + return +} + +// DeleteSnapshotsPreparer prepares the DeleteSnapshots request. +func (client Client) DeleteSnapshotsPreparer(ctx context.Context, accountName, containerName, blobName string, input DeleteSnapshotsInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + // only delete the snapshots but leave the blob as-is + "x-ms-delete-snapshots": "only", + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSnapshotsSender sends the DeleteSnapshots request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSnapshotsSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteSnapshotsResponder handles the response to the DeleteSnapshots request. The method always +// closes the http.Response Body. +func (client Client) DeleteSnapshotsResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2017-07-29/blob/blobs/get.go b/storage/2017-07-29/blob/blobs/get.go new file mode 100644 index 0000000..fa88081 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/get.go @@ -0,0 +1,116 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetInput struct { + LeaseID *string + StartByte *int64 + EndByte *int64 +} + +type GetResult struct { + autorest.Response + + Contents []byte +} + +// Get reads or downloads a blob from the system, including its metadata and properties. +func (client Client) Get(ctx context.Context, accountName, containerName, blobName string, input GetInput) (result GetResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "Get", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "Get", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "Get", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "Get", "`blobName` cannot be an empty string.") + } + if input.LeaseID != nil && *input.LeaseID == "" { + return result, validation.NewError("blobs.Client", "Get", "`input.LeaseID` should either be specified or nil, not an empty string.") + } + if (input.StartByte != nil && input.EndByte == nil) || input.StartByte == nil && input.EndByte != nil { + return result, validation.NewError("blobs.Client", "Get", "`input.StartByte` and `input.EndByte` must both be specified, or both be nil.") + } + + req, err := client.GetPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Get", nil, "Failure preparing request") + return + } + + resp, err := client.GetSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "Get", resp, "Failure sending request") + return + } + + result, err = client.GetResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Get", resp, "Failure responding to request") + return + } + + return +} + +// GetPreparer prepares the Get request. +func (client Client) GetPreparer(ctx context.Context, accountName, containerName, blobName string, input GetInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.StartByte != nil && input.EndByte != nil { + headers["x-ms-range"] = fmt.Sprintf("bytes=%d-%d", *input.StartByte, *input.EndByte) + } + + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetSender sends the Get request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetResponder handles the response to the Get request. The method always +// closes the http.Response Body. +func (client Client) GetResponder(resp *http.Response) (result GetResult, err error) { + if resp != nil { + result.Contents = make([]byte, resp.ContentLength) + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusPartialContent), + autorest.ByUnmarshallingBytes(&result.Contents), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/blobs/get_block_list.go b/storage/2017-07-29/blob/blobs/get_block_list.go new file mode 100644 index 0000000..9f8120c --- /dev/null +++ b/storage/2017-07-29/blob/blobs/get_block_list.go @@ -0,0 +1,140 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetBlockListInput struct { + BlockListType BlockListType + LeaseID *string +} + +type GetBlockListResult struct { + autorest.Response + + // The size of the blob in bytes + ContentLength *int64 + + // The Content Type of the blob + ContentType string + + // The ETag associated with this blob + ETag string + + // A list of blocks which have been committed + CommittedBlocks CommittedBlocks `xml:"CommittedBlocks,omitempty"` + + // A list of blocks which have not yet been committed + UncommittedBlocks UncommittedBlocks `xml:"UncommittedBlocks,omitempty"` +} + +// GetBlockList retrieves the list of blocks that have been uploaded as part of a block blob. +func (client Client) GetBlockList(ctx context.Context, accountName, containerName, blobName string, input GetBlockListInput) (result GetBlockListResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "GetBlockList", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "GetBlockList", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "GetBlockList", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "GetBlockList", "`blobName` cannot be an empty string.") + } + + req, err := client.GetBlockListPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetBlockList", nil, "Failure preparing request") + return + } + + resp, err := client.GetBlockListSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "GetBlockList", resp, "Failure sending request") + return + } + + result, err = client.GetBlockListResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetBlockList", resp, "Failure responding to request") + return + } + + return +} + +// GetBlockListPreparer prepares the GetBlockList request. +func (client Client) GetBlockListPreparer(ctx context.Context, accountName, containerName, blobName string, input GetBlockListInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "blocklisttype": autorest.Encode("query", string(input.BlockListType)), + "comp": autorest.Encode("query", "blocklist"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetBlockListSender sends the GetBlockList request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetBlockListSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetBlockListResponder handles the response to the GetBlockList request. The method always +// closes the http.Response Body. +func (client Client) GetBlockListResponder(resp *http.Response) (result GetBlockListResult, err error) { + if resp != nil && resp.Header != nil { + result.ContentType = resp.Header.Get("Content-Type") + result.ETag = resp.Header.Get("ETag") + + if v := resp.Header.Get("x-ms-blob-content-length"); v != "" { + i, innerErr := strconv.Atoi(v) + if innerErr != nil { + err = fmt.Errorf("Error parsing %q as an integer: %s", v, innerErr) + return + } + + i64 := int64(i) + result.ContentLength = &i64 + } + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + return +} diff --git a/storage/2017-07-29/blob/blobs/get_page_ranges.go b/storage/2017-07-29/blob/blobs/get_page_ranges.go new file mode 100644 index 0000000..37abf63 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/get_page_ranges.go @@ -0,0 +1,152 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetPageRangesInput struct { + LeaseID *string + + StartByte *int64 + EndByte *int64 +} + +type GetPageRangesResult struct { + autorest.Response + + // The size of the blob in bytes + ContentLength *int64 + + // The Content Type of the blob + ContentType string + + // The ETag associated with this blob + ETag string + + PageRanges []PageRange `xml:"PageRange"` +} + +type PageRange struct { + // The start byte offset for this range, inclusive + Start int64 `xml:"Start"` + + // The end byte offset for this range, inclusive + End int64 `xml:"End"` +} + +// GetPageRanges returns the list of valid page ranges for a page blob or snapshot of a page blob. +func (client Client) GetPageRanges(ctx context.Context, accountName, containerName, blobName string, input GetPageRangesInput) (result GetPageRangesResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "GetPageRanges", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "GetPageRanges", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "GetPageRanges", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "GetPageRanges", "`blobName` cannot be an empty string.") + } + if (input.StartByte != nil && input.EndByte == nil) || input.StartByte == nil && input.EndByte != nil { + return result, validation.NewError("blobs.Client", "GetPageRanges", "`input.StartByte` and `input.EndByte` must both be specified, or both be nil.") + } + + req, err := client.GetPageRangesPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetPageRanges", nil, "Failure preparing request") + return + } + + resp, err := client.GetPageRangesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "GetPageRanges", resp, "Failure sending request") + return + } + + result, err = client.GetPageRangesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetPageRanges", resp, "Failure responding to request") + return + } + + return +} + +// GetPageRangesPreparer prepares the GetPageRanges request. +func (client Client) GetPageRangesPreparer(ctx context.Context, accountName, containerName, blobName string, input GetPageRangesInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "pagelist"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + if input.StartByte != nil && input.EndByte != nil { + headers["x-ms-range"] = fmt.Sprintf("bytes=%d-%d", *input.StartByte, *input.EndByte) + } + + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetPageRangesSender sends the GetPageRanges request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetPageRangesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetPageRangesResponder handles the response to the GetPageRanges request. The method always +// closes the http.Response Body. +func (client Client) GetPageRangesResponder(resp *http.Response) (result GetPageRangesResult, err error) { + if resp != nil && resp.Header != nil { + result.ContentType = resp.Header.Get("Content-Type") + result.ETag = resp.Header.Get("ETag") + + if v := resp.Header.Get("x-ms-blob-content-length"); v != "" { + i, innerErr := strconv.Atoi(v) + if innerErr != nil { + err = fmt.Errorf("Error parsing %q as an integer: %s", v, innerErr) + return + } + + i64 := int64(i) + result.ContentLength = &i64 + } + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + return +} diff --git a/storage/2017-07-29/blob/blobs/incremental_copy_blob.go b/storage/2017-07-29/blob/blobs/incremental_copy_blob.go new file mode 100644 index 0000000..7fb7e6b --- /dev/null +++ b/storage/2017-07-29/blob/blobs/incremental_copy_blob.go @@ -0,0 +1,120 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type IncrementalCopyBlobInput struct { + CopySource string + IfModifiedSince *string + IfUnmodifiedSince *string + IfMatch *string + IfNoneMatch *string +} + +// IncrementalCopyBlob copies a snapshot of the source page blob to a destination page blob. +// The snapshot is copied such that only the differential changes between the previously copied +// snapshot are transferred to the destination. +// The copied snapshots are complete copies of the original snapshot and can be read or copied from as usual. +func (client Client) IncrementalCopyBlob(ctx context.Context, accountName, containerName, blobName string, input IncrementalCopyBlobInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "IncrementalCopyBlob", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "IncrementalCopyBlob", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "IncrementalCopyBlob", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "IncrementalCopyBlob", "`blobName` cannot be an empty string.") + } + if input.CopySource == "" { + return result, validation.NewError("blobs.Client", "IncrementalCopyBlob", "`input.CopySource` cannot be an empty string.") + } + + req, err := client.IncrementalCopyBlobPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "IncrementalCopyBlob", nil, "Failure preparing request") + return + } + + resp, err := client.IncrementalCopyBlobSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "IncrementalCopyBlob", resp, "Failure sending request") + return + } + + result, err = client.IncrementalCopyBlobResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "IncrementalCopyBlob", resp, "Failure responding to request") + return + } + + return +} + +// IncrementalCopyBlobPreparer prepares the IncrementalCopyBlob request. +func (client Client) IncrementalCopyBlobPreparer(ctx context.Context, accountName, containerName, blobName string, input IncrementalCopyBlobInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "incrementalcopy"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-copy-source": input.CopySource, + } + + if input.IfModifiedSince != nil { + headers["If-Modified-Since"] = *input.IfModifiedSince + } + if input.IfUnmodifiedSince != nil { + headers["If-Unmodified-Since"] = *input.IfUnmodifiedSince + } + if input.IfMatch != nil { + headers["If-Match"] = *input.IfMatch + } + if input.IfNoneMatch != nil { + headers["If-None-Match"] = *input.IfNoneMatch + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// IncrementalCopyBlobSender sends the IncrementalCopyBlob request. The method will close the +// http.Response Body if it receives an error. +func (client Client) IncrementalCopyBlobSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// IncrementalCopyBlobResponder handles the response to the IncrementalCopyBlob request. The method always +// closes the http.Response Body. +func (client Client) IncrementalCopyBlobResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2017-07-29/blob/blobs/lease_acquire.go b/storage/2017-07-29/blob/blobs/lease_acquire.go new file mode 100644 index 0000000..432c1f5 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/lease_acquire.go @@ -0,0 +1,135 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type AcquireLeaseInput struct { + // The ID of the existing Lease, if leased + LeaseID *string + + // Specifies the duration of the lease, in seconds, or negative one (-1) for a lease that never expires. + // A non-infinite lease can be between 15 and 60 seconds + LeaseDuration int + + // The Proposed new ID for the Lease + ProposedLeaseID *string +} + +type AcquireLeaseResult struct { + autorest.Response + + LeaseID string +} + +// AcquireLease establishes and manages a lock on a blob for write and delete operations. +func (client Client) AcquireLease(ctx context.Context, accountName, containerName, blobName string, input AcquireLeaseInput) (result AcquireLeaseResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "AcquireLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "AcquireLease", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "AcquireLease", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "AcquireLease", "`blobName` cannot be an empty string.") + } + if input.LeaseID != nil && *input.LeaseID == "" { + return result, validation.NewError("blobs.Client", "AcquireLease", "`input.LeaseID` cannot be an empty string, if specified.") + } + if input.ProposedLeaseID != nil && *input.ProposedLeaseID == "" { + return result, validation.NewError("blobs.Client", "AcquireLease", "`input.ProposedLeaseID` cannot be an empty string, if specified.") + } + // An infinite lease duration is -1 seconds. A non-infinite lease can be between 15 and 60 seconds + if input.LeaseDuration != -1 && (input.LeaseDuration <= 15 || input.LeaseDuration >= 60) { + return result, validation.NewError("blobs.Client", "AcquireLease", "`input.LeaseDuration` must be -1 (infinite), or between 15 and 60 seconds.") + } + + req, err := client.AcquireLeasePreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "AcquireLease", nil, "Failure preparing request") + return + } + + resp, err := client.AcquireLeaseSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "AcquireLease", resp, "Failure sending request") + return + } + + result, err = client.AcquireLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "AcquireLease", resp, "Failure responding to request") + return + } + + return +} + +// AcquireLeasePreparer prepares the AcquireLease request. +func (client Client) AcquireLeasePreparer(ctx context.Context, accountName, containerName, blobName string, input AcquireLeaseInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "acquire", + "x-ms-lease-duration": input.LeaseDuration, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + if input.ProposedLeaseID != nil { + headers["x-ms-proposed-lease-id"] = input.ProposedLeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// AcquireLeaseSender sends the AcquireLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) AcquireLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// AcquireLeaseResponder handles the response to the AcquireLease request. The method always +// closes the http.Response Body. +func (client Client) AcquireLeaseResponder(resp *http.Response) (result AcquireLeaseResult, err error) { + if resp != nil && resp.Header != nil { + result.LeaseID = resp.Header.Get("x-ms-lease-id") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/blobs/lease_break.go b/storage/2017-07-29/blob/blobs/lease_break.go new file mode 100644 index 0000000..d564204 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/lease_break.go @@ -0,0 +1,124 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type BreakLeaseInput struct { + // For a break operation, proposed duration the lease should continue + // before it is broken, in seconds, between 0 and 60. + // This break period is only used if it is shorter than the time remaining on the lease. + // If longer, the time remaining on the lease is used. + // A new lease will not be available before the break period has expired, + // but the lease may be held for longer than the break period. + // If this header does not appear with a break operation, a fixed-duration lease breaks + // after the remaining lease period elapses, and an infinite lease breaks immediately. + BreakPeriod *int + + LeaseID string +} + +type BreakLeaseResponse struct { + autorest.Response + + // Approximate time remaining in the lease period, in seconds. + // If the break is immediate, 0 is returned. + LeaseTime int +} + +// BreakLease breaks an existing lock on a blob using the LeaseID. +func (client Client) BreakLease(ctx context.Context, accountName, containerName, blobName string, input BreakLeaseInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "BreakLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "BreakLease", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "BreakLease", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "BreakLease", "`blobName` cannot be an empty string.") + } + if input.LeaseID == "" { + return result, validation.NewError("blobs.Client", "BreakLease", "`input.LeaseID` cannot be an empty string.") + } + + req, err := client.BreakLeasePreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "BreakLease", nil, "Failure preparing request") + return + } + + resp, err := client.BreakLeaseSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "BreakLease", resp, "Failure sending request") + return + } + + result, err = client.BreakLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "BreakLease", resp, "Failure responding to request") + return + } + + return +} + +// BreakLeasePreparer prepares the BreakLease request. +func (client Client) BreakLeasePreparer(ctx context.Context, accountName, containerName, blobName string, input BreakLeaseInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "break", + "x-ms-lease-id": input.LeaseID, + } + + if input.BreakPeriod != nil { + headers["x-ms-lease-break-period"] = *input.BreakPeriod + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// BreakLeaseSender sends the BreakLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) BreakLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// BreakLeaseResponder handles the response to the BreakLease request. The method always +// closes the http.Response Body. +func (client Client) BreakLeaseResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/blobs/lease_change.go b/storage/2017-07-29/blob/blobs/lease_change.go new file mode 100644 index 0000000..c57f9db --- /dev/null +++ b/storage/2017-07-29/blob/blobs/lease_change.go @@ -0,0 +1,117 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type ChangeLeaseInput struct { + ExistingLeaseID string + ProposedLeaseID string +} + +type ChangeLeaseResponse struct { + autorest.Response + + LeaseID string +} + +// ChangeLease changes an existing lock on a blob for another lock. +func (client Client) ChangeLease(ctx context.Context, accountName, containerName, blobName string, input ChangeLeaseInput) (result ChangeLeaseResponse, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "ChangeLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "ChangeLease", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "ChangeLease", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "ChangeLease", "`blobName` cannot be an empty string.") + } + if input.ExistingLeaseID == "" { + return result, validation.NewError("blobs.Client", "ChangeLease", "`input.ExistingLeaseID` cannot be an empty string.") + } + if input.ProposedLeaseID == "" { + return result, validation.NewError("blobs.Client", "ChangeLease", "`input.ProposedLeaseID` cannot be an empty string.") + } + + req, err := client.ChangeLeasePreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "ChangeLease", nil, "Failure preparing request") + return + } + + resp, err := client.ChangeLeaseSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "ChangeLease", resp, "Failure sending request") + return + } + + result, err = client.ChangeLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "ChangeLease", resp, "Failure responding to request") + return + } + + return +} + +// ChangeLeasePreparer prepares the ChangeLease request. +func (client Client) ChangeLeasePreparer(ctx context.Context, accountName, containerName, blobName string, input ChangeLeaseInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "change", + "x-ms-lease-id": input.ExistingLeaseID, + "x-ms-proposed-lease-id": input.ProposedLeaseID, + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ChangeLeaseSender sends the ChangeLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ChangeLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ChangeLeaseResponder handles the response to the ChangeLease request. The method always +// closes the http.Response Body. +func (client Client) ChangeLeaseResponder(resp *http.Response) (result ChangeLeaseResponse, err error) { + if resp != nil && resp.Header != nil { + result.LeaseID = resp.Header.Get("x-ms-lease-id") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/blobs/lease_release.go b/storage/2017-07-29/blob/blobs/lease_release.go new file mode 100644 index 0000000..0226cdf --- /dev/null +++ b/storage/2017-07-29/blob/blobs/lease_release.go @@ -0,0 +1,98 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// ReleaseLease releases a lock based on the Lease ID. +func (client Client) ReleaseLease(ctx context.Context, accountName, containerName, blobName, leaseID string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "ReleaseLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "ReleaseLease", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "ReleaseLease", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "ReleaseLease", "`blobName` cannot be an empty string.") + } + if leaseID == "" { + return result, validation.NewError("blobs.Client", "ReleaseLease", "`leaseID` cannot be an empty string.") + } + + req, err := client.ReleaseLeasePreparer(ctx, accountName, containerName, blobName, leaseID) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "ReleaseLease", nil, "Failure preparing request") + return + } + + resp, err := client.ReleaseLeaseSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "ReleaseLease", resp, "Failure sending request") + return + } + + result, err = client.ReleaseLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "ReleaseLease", resp, "Failure responding to request") + return + } + + return +} + +// ReleaseLeasePreparer prepares the ReleaseLease request. +func (client Client) ReleaseLeasePreparer(ctx context.Context, accountName, containerName, blobName, leaseID string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "release", + "x-ms-lease-id": leaseID, + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ReleaseLeaseSender sends the ReleaseLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ReleaseLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ReleaseLeaseResponder handles the response to the ReleaseLease request. The method always +// closes the http.Response Body. +func (client Client) ReleaseLeaseResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/blobs/lease_renew.go b/storage/2017-07-29/blob/blobs/lease_renew.go new file mode 100644 index 0000000..69c495b --- /dev/null +++ b/storage/2017-07-29/blob/blobs/lease_renew.go @@ -0,0 +1,97 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +func (client Client) RenewLease(ctx context.Context, accountName, containerName, blobName, leaseID string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "RenewLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "RenewLease", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "RenewLease", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "RenewLease", "`blobName` cannot be an empty string.") + } + if leaseID == "" { + return result, validation.NewError("blobs.Client", "RenewLease", "`leaseID` cannot be an empty string.") + } + + req, err := client.RenewLeasePreparer(ctx, accountName, containerName, blobName, leaseID) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "RenewLease", nil, "Failure preparing request") + return + } + + resp, err := client.RenewLeaseSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "RenewLease", resp, "Failure sending request") + return + } + + result, err = client.RenewLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "RenewLease", resp, "Failure responding to request") + return + } + + return +} + +// RenewLeasePreparer prepares the RenewLease request. +func (client Client) RenewLeasePreparer(ctx context.Context, accountName, containerName, blobName, leaseID string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "renew", + "x-ms-lease-id": leaseID, + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// RenewLeaseSender sends the RenewLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) RenewLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// RenewLeaseResponder handles the response to the RenewLease request. The method always +// closes the http.Response Body. +func (client Client) RenewLeaseResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/blobs/lease_test.go b/storage/2017-07-29/blob/blobs/lease_test.go new file mode 100644 index 0000000..e450d15 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/lease_test.go @@ -0,0 +1,106 @@ +package blobs + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2017-07-29/blob/containers" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestLeaseLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + fileName := "ubuntu.iso" + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.BlobStorage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + containersClient := containers.NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithAuthorizer(containersClient.Client, storageAuth) + + _, err = containersClient.Create(ctx, accountName, containerName, containers.CreateInput{}) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + defer containersClient.Delete(ctx, accountName, containerName) + + blobClient := NewWithEnvironment(client.Environment) + blobClient.Client = client.PrepareWithAuthorizer(blobClient.Client, storageAuth) + + t.Logf("[DEBUG] Copying file to Blob Storage..") + copyInput := CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + + refreshInterval := 5 * time.Second + if err := blobClient.CopyAndWait(ctx, accountName, containerName, fileName, copyInput, refreshInterval); err != nil { + t.Fatalf("Error copying: %s", err) + } + defer blobClient.Delete(ctx, accountName, containerName, fileName, DeleteInput{}) + + // Test begins here + t.Logf("[DEBUG] Acquiring Lease..") + leaseInput := AcquireLeaseInput{ + LeaseDuration: -1, + } + leaseInfo, err := blobClient.AcquireLease(ctx, accountName, containerName, fileName, leaseInput) + if err != nil { + t.Fatalf("Error acquiring lease: %s", err) + } + t.Logf("[DEBUG] Lease ID: %q", leaseInfo.LeaseID) + + t.Logf("[DEBUG] Changing Lease..") + changeLeaseInput := ChangeLeaseInput{ + ExistingLeaseID: leaseInfo.LeaseID, + ProposedLeaseID: "31f5bb01-cdd9-4166-bcdc-95186076bde0", + } + changeLeaseResult, err := blobClient.ChangeLease(ctx, accountName, containerName, fileName, changeLeaseInput) + if err != nil { + t.Fatalf("Error changing lease: %s", err) + } + t.Logf("[DEBUG] New Lease ID: %q", changeLeaseResult.LeaseID) + + t.Logf("[DEBUG] Releasing Lease..") + if _, err := blobClient.ReleaseLease(ctx, accountName, containerName, fileName, changeLeaseResult.LeaseID); err != nil { + t.Fatalf("Error releasing lease: %s", err) + } + + t.Logf("[DEBUG] Acquiring a new lease..") + leaseInput = AcquireLeaseInput{ + LeaseDuration: 30, + } + leaseInfo, err = blobClient.AcquireLease(ctx, accountName, containerName, fileName, leaseInput) + if err != nil { + t.Fatalf("Error acquiring lease: %s", err) + } + t.Logf("[DEBUG] Lease ID: %q", leaseInfo.LeaseID) + + t.Logf("[DEBUG] Renewing lease..") + if _, err := blobClient.RenewLease(ctx, accountName, containerName, fileName, leaseInfo.LeaseID); err != nil { + t.Fatalf("Error renewing lease: %s", err) + } + + t.Logf("[DEBUG] Breaking lease..") + breakLeaseInput := BreakLeaseInput{ + LeaseID: leaseInfo.LeaseID, + } + if _, err := blobClient.BreakLease(ctx, accountName, containerName, fileName, breakLeaseInput); err != nil { + t.Fatalf("Error breaking lease: %s", err) + } +} diff --git a/storage/2017-07-29/blob/blobs/lifecycle_test.go b/storage/2017-07-29/blob/blobs/lifecycle_test.go new file mode 100644 index 0000000..6b8cb59 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/lifecycle_test.go @@ -0,0 +1,158 @@ +package blobs + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2017-07-29/blob/containers" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + fileName := "example.txt" + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.BlobStorage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + containersClient := containers.NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithAuthorizer(containersClient.Client, storageAuth) + + _, err = containersClient.Create(ctx, accountName, containerName, containers.CreateInput{}) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + defer containersClient.Delete(ctx, accountName, containerName) + + blobClient := NewWithEnvironment(client.Environment) + blobClient.Client = client.PrepareWithAuthorizer(blobClient.Client, storageAuth) + + t.Logf("[DEBUG] Copying file to Blob Storage..") + copyInput := CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + + refreshInterval := 5 * time.Second + if err := blobClient.CopyAndWait(ctx, accountName, containerName, fileName, copyInput, refreshInterval); err != nil { + t.Fatalf("Error copying: %s", err) + } + + t.Logf("[DEBUG] Retrieving Blob Properties..") + details, err := blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error retrieving properties: %s", err) + } + + // default value + if details.AccessTier != Hot { + t.Fatalf("Expected the AccessTier to be %q but got %q", Hot, details.AccessTier) + } + if details.BlobType != BlockBlob { + t.Fatalf("Expected BlobType to be %q but got %q", BlockBlob, details.BlobType) + } + if len(details.MetaData) != 0 { + t.Fatalf("Expected there to be no items of metadata but got %d", len(details.MetaData)) + } + + t.Logf("[DEBUG] Checking it's returned in the List API..") + listInput := containers.ListBlobsInput{} + listResult, err := containersClient.ListBlobs(ctx, accountName, containerName, listInput) + if err != nil { + t.Fatalf("Error listing blobs: %s", err) + } + + if len(listResult.Blobs.Blobs) != 1 { + t.Fatalf("Expected there to be 1 blob in the container but got %d", len(listResult.Blobs.Blobs)) + } + + t.Logf("[DEBUG] Setting MetaData..") + metaDataInput := SetMetaDataInput{ + MetaData: map[string]string{ + "hello": "there", + }, + } + if _, err := blobClient.SetMetaData(ctx, accountName, containerName, fileName, metaDataInput); err != nil { + t.Fatalf("Error setting MetaData: %s", err) + } + + t.Logf("[DEBUG] Re-retrieving Blob Properties..") + details, err = blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error re-retrieving properties: %s", err) + } + + // default value + if details.AccessTier != Hot { + t.Fatalf("Expected the AccessTier to be %q but got %q", Hot, details.AccessTier) + } + if details.BlobType != BlockBlob { + t.Fatalf("Expected BlobType to be %q but got %q", BlockBlob, details.BlobType) + } + if len(details.MetaData) != 1 { + t.Fatalf("Expected there to be 1 item of metadata but got %d", len(details.MetaData)) + } + if details.MetaData["hello"] != "there" { + t.Fatalf("Expected `hello` to be `there` but got %q", details.MetaData["there"]) + } + + t.Logf("[DEBUG] Retrieving the Block List..") + getBlockListInput := GetBlockListInput{ + BlockListType: All, + } + blockList, err := blobClient.GetBlockList(ctx, accountName, containerName, fileName, getBlockListInput) + if err != nil { + t.Fatalf("Error retrieving Block List: %s", err) + } + + // since this is a copy from an existing file, all blocks should be present + if len(blockList.CommittedBlocks.Blocks) == 0 { + t.Fatalf("Expected there to be committed blocks but there weren't!") + } + if len(blockList.UncommittedBlocks.Blocks) != 0 { + t.Fatalf("Expected all blocks to be committed but got %d uncommitted blocks", len(blockList.UncommittedBlocks.Blocks)) + } + + t.Logf("[DEBUG] Changing the Access Tiers..") + tiers := []AccessTier{ + Hot, + Cool, + Archive, + } + for _, tier := range tiers { + t.Logf("[DEBUG] Updating the Access Tier to %q..", string(tier)) + if _, err := blobClient.SetTier(ctx, accountName, containerName, fileName, tier); err != nil { + t.Fatalf("Error setting the Access Tier: %s", err) + } + + t.Logf("[DEBUG] Re-retrieving Blob Properties..") + details, err = blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error re-retrieving properties: %s", err) + } + + if details.AccessTier != tier { + t.Fatalf("Expected the AccessTier to be %q but got %q", tier, details.AccessTier) + } + } + + t.Logf("[DEBUG] Deleting Blob") + if _, err := blobClient.Delete(ctx, accountName, containerName, fileName, DeleteInput{}); err != nil { + t.Fatalf("Error deleting Blob: %s", err) + } +} diff --git a/storage/2017-07-29/blob/blobs/metadata_set.go b/storage/2017-07-29/blob/blobs/metadata_set.go new file mode 100644 index 0000000..ec69152 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/metadata_set.go @@ -0,0 +1,113 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type SetMetaDataInput struct { + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string + + // Any metadata which should be added to this blob + MetaData map[string]string +} + +// SetMetaData marks the specified blob or snapshot for deletion. The blob is later deleted during garbage collection. +func (client Client) SetMetaData(ctx context.Context, accountName, containerName, blobName string, input SetMetaDataInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "GetProperties", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "GetProperties", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "GetProperties", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "GetProperties", "`blobName` cannot be an empty string.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("blobs.Client", "GetProperties", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.SetMetaDataPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "SetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.SetMetaDataSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "SetMetaData", resp, "Failure sending request") + return + } + + result, err = client.SetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "SetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// SetMetaDataPreparer prepares the SetMetaData request. +func (client Client) SetMetaDataPreparer(ctx context.Context, accountName, containerName, blobName string, input SetMetaDataInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetMetaDataSender sends the SetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetMetaDataResponder handles the response to the SetMetaData request. The method always +// closes the http.Response Body. +func (client Client) SetMetaDataResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/blobs/models.go b/storage/2017-07-29/blob/blobs/models.go new file mode 100644 index 0000000..d7d83aa --- /dev/null +++ b/storage/2017-07-29/blob/blobs/models.go @@ -0,0 +1,82 @@ +package blobs + +type AccessTier string + +var ( + Archive AccessTier = "Archive" + Cool AccessTier = "Cool" + Hot AccessTier = "Hot" +) + +type ArchiveStatus string + +var ( + None ArchiveStatus = "" + RehydratePendingToCool ArchiveStatus = "rehydrate-pending-to-cool" + RehydratePendingToHot ArchiveStatus = "rehydrate-pending-to-hot" +) + +type BlockListType string + +var ( + All BlockListType = "all" + Committed BlockListType = "committed" + Uncommitted BlockListType = "uncommitted" +) + +type Block struct { + // The base64-encoded Block ID + Name string `xml:"Name"` + + // The size of the Block in Bytes + Size int64 `xml:"Size"` +} + +type BlobType string + +var ( + AppendBlob BlobType = "AppendBlob" + BlockBlob BlobType = "BlockBlob" + PageBlob BlobType = "PageBlob" +) + +type CommittedBlocks struct { + Blocks []Block `xml:"Block"` +} + +type CopyStatus string + +var ( + Aborted CopyStatus = "aborted" + Failed CopyStatus = "failed" + Pending CopyStatus = "pending" + Success CopyStatus = "success" +) + +type LeaseDuration string + +var ( + Fixed LeaseDuration = "fixed" + Infinite LeaseDuration = "infinite" +) + +type LeaseState string + +var ( + Available LeaseState = "available" + Breaking LeaseState = "breaking" + Broken LeaseState = "broken" + Expired LeaseState = "expired" + Leased LeaseState = "leased" +) + +type LeaseStatus string + +var ( + Locked LeaseStatus = "locked" + Unlocked LeaseStatus = "unlocked" +) + +type UncommittedBlocks struct { + Blocks []Block `xml:"Block"` +} diff --git a/storage/2017-07-29/blob/blobs/properties_get.go b/storage/2017-07-29/blob/blobs/properties_get.go new file mode 100644 index 0000000..de7c5fc --- /dev/null +++ b/storage/2017-07-29/blob/blobs/properties_get.go @@ -0,0 +1,310 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetPropertiesInput struct { + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string +} + +type GetPropertiesResult struct { + autorest.Response + + // The tier of page blob on a premium storage account or tier of block blob on blob storage or general purpose v2 account. + AccessTier AccessTier + + // This gives the last time tier was changed on the object. + // This header is returned only if tier on block blob was ever set. + // The date format follows RFC 1123 + AccessTierChangeTime string + + // For page blobs on a premium storage account only. + // If the access tier is not explicitly set on the blob, the tier is inferred based on its content length + // and this header will be returned with true value. + // For block blobs on Blob Storage or general purpose v2 account, if the blob does not have the access tier + // set then we infer the tier from the storage account properties. This header is set only if the block blob + // tier is inferred + AccessTierInferred bool + + // For blob storage or general purpose v2 account. + // If the blob is being rehydrated and is not complete then this header is returned indicating + // that rehydrate is pending and also tells the destination tier + ArchiveStatus ArchiveStatus + + // The number of committed blocks present in the blob. + // This header is returned only for append blobs. + BlobCommittedBlockCount string + + // The current sequence number for a page blob. + // This header is not returned for block blobs or append blobs. + // This header is not returned for block blobs. + BlobSequenceNumber string + + // The blob type. + BlobType BlobType + + // If the Cache-Control request header has previously been set for the blob, that value is returned in this header. + CacheControl string + + // The Content-Disposition response header field conveys additional information about how to process + // the response payload, and also can be used to attach additional metadata. + // For example, if set to attachment, it indicates that the user-agent should not display the response, + // but instead show a Save As dialog. + ContentDisposition string + + // If the Content-Encoding request header has previously been set for the blob, + // that value is returned in this header. + ContentEncoding string + + // If the Content-Language request header has previously been set for the blob, + // that value is returned in this header. + ContentLanguage string + + // The size of the blob in bytes. + // For a page blob, this header returns the value of the x-ms-blob-content-length header stored with the blob. + ContentLength int64 + + // The content type specified for the blob. + // If no content type was specified, the default content type is `application/octet-stream`. + ContentType string + + // If the Content-MD5 header has been set for the blob, this response header is returned so that + // the client can check for message content integrity. + ContentMD5 string + + // Conclusion time of the last attempted Copy Blob operation where this blob was the destination blob. + // This value can specify the time of a completed, aborted, or failed copy attempt. + // This header does not appear if a copy is pending, if this blob has never been the + // destination in a Copy Blob operation, or if this blob has been modified after a concluded Copy Blob + // operation using Set Blob Properties, Put Blob, or Put Block List. + CopyCompletionTime string + + // Included if the blob is incremental copy blob or incremental copy snapshot, if x-ms-copy-status is success. + // Snapshot time of the last successful incremental copy snapshot for this blob + CopyDestinationSnapshot string + + // String identifier for the last attempted Copy Blob operation where this blob was the destination blob. + // This header does not appear if this blob has never been the destination in a Copy Blob operation, + // or if this blob has been modified after a concluded Copy Blob operation using Set Blob Properties, + // Put Blob, or Put Block List. + CopyID string + + // Contains the number of bytes copied and the total bytes in the source in the last attempted + // Copy Blob operation where this blob was the destination blob. + // Can show between 0 and Content-Length bytes copied. + // This header does not appear if this blob has never been the destination in a Copy Blob operation, + // or if this blob has been modified after a concluded Copy Blob operation using Set Blob Properties, + // Put Blob, or Put Block List. + CopyProgress string + + // URL up to 2 KB in length that specifies the source blob used in the last attempted Copy Blob operation + // where this blob was the destination blob. + // This header does not appear if this blob has never been the destination in a Copy Blob operation, + // or if this blob has been modified after a concluded Copy Blob operation using Set Blob Properties, + // Put Blob, or Put Block List + CopySource string + + // State of the copy operation identified by x-ms-copy-id, with these values: + // - success: Copy completed successfully. + // - pending: Copy is in progress. + // Check x-ms-copy-status-description if intermittent, non-fatal errors + // impede copy progress but don’t cause failure. + // - aborted: Copy was ended by Abort Copy Blob. + // - failed: Copy failed. See x-ms- copy-status-description for failure details. + // This header does not appear if this blob has never been the destination in a Copy Blob operation, + // or if this blob has been modified after a completed Copy Blob operation using Set Blob Properties, + // Put Blob, or Put Block List. + CopyStatus CopyStatus + + // Describes cause of fatal or non-fatal copy operation failure. + // This header does not appear if this blob has never been the destination in a Copy Blob operation, + // or if this blob has been modified after a concluded Copy Blob operation using Set Blob Properties, + // Put Blob, or Put Block List. + CopyStatusDescription string + + // The date/time at which the blob was created. The date format follows RFC 1123 + CreationTime string + + // The ETag contains a value that you can use to perform operations conditionally + ETag string + + // Included if the blob is incremental copy blob. + IncrementalCopy bool + + // The date/time that the blob was last modified. The date format follows RFC 1123. + LastModified string + + // When a blob is leased, specifies whether the lease is of infinite or fixed duration + LeaseDuration LeaseDuration + + // The lease state of the blob + LeaseState LeaseState + + LeaseStatus LeaseStatus + + // A set of name-value pairs that correspond to the user-defined metadata associated with this blob + MetaData map[string]string + + // Is the Storage Account encrypted using server-side encryption? This should always return true + ServerEncrypted bool +} + +// GetProperties returns all user-defined metadata, standard HTTP properties, and system properties for the blob +func (client Client) GetProperties(ctx context.Context, accountName, containerName, blobName string, input GetPropertiesInput) (result GetPropertiesResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "GetProperties", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "GetProperties", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "GetProperties", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "GetProperties", "`blobName` cannot be an empty string.") + } + + req, err := client.GetPropertiesPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetProperties", nil, "Failure preparing request") + return + } + + resp, err := client.GetPropertiesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "GetProperties", resp, "Failure sending request") + return + } + + result, err = client.GetPropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetProperties", resp, "Failure responding to request") + return + } + + return +} + +// GetPropertiesPreparer prepares the GetProperties request. +func (client Client) GetPropertiesPreparer(ctx context.Context, accountName, containerName, blobName string, input GetPropertiesInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsHead(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetPropertiesSender sends the GetProperties request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetPropertiesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetPropertiesResponder handles the response to the GetProperties request. The method always +// closes the http.Response Body. +func (client Client) GetPropertiesResponder(resp *http.Response) (result GetPropertiesResult, err error) { + if resp != nil && resp.Header != nil { + result.AccessTier = AccessTier(resp.Header.Get("x-ms-access-tier")) + result.AccessTierChangeTime = resp.Header.Get(" x-ms-access-tier-change-time") + result.ArchiveStatus = ArchiveStatus(resp.Header.Get(" x-ms-archive-status")) + result.BlobCommittedBlockCount = resp.Header.Get("x-ms-blob-committed-block-count") + result.BlobSequenceNumber = resp.Header.Get("x-ms-blob-sequence-number") + result.BlobType = BlobType(resp.Header.Get("x-ms-blob-type")) + result.CacheControl = resp.Header.Get("Cache-Control") + result.ContentDisposition = resp.Header.Get("Content-Disposition") + result.ContentEncoding = resp.Header.Get("Content-Encoding") + result.ContentLanguage = resp.Header.Get("Content-Language") + result.ContentMD5 = resp.Header.Get("Content-MD5") + result.ContentType = resp.Header.Get("Content-Type") + result.CopyCompletionTime = resp.Header.Get("x-ms-copy-completion-time") + result.CopyDestinationSnapshot = resp.Header.Get("x-ms-copy-destination-snapshot") + result.CopyID = resp.Header.Get("x-ms-copy-id") + result.CopyProgress = resp.Header.Get(" x-ms-copy-progress") + result.CopySource = resp.Header.Get("x-ms-copy-source") + result.CopyStatus = CopyStatus(resp.Header.Get("x-ms-copy-status")) + result.CopyStatusDescription = resp.Header.Get("x-ms-copy-status-description") + result.CreationTime = resp.Header.Get("x-ms-creation-time") + result.ETag = resp.Header.Get("Etag") + result.LastModified = resp.Header.Get("Last-Modified") + result.LeaseDuration = LeaseDuration(resp.Header.Get("x-ms-lease-duration")) + result.LeaseState = LeaseState(resp.Header.Get("x-ms-lease-state")) + result.LeaseStatus = LeaseStatus(resp.Header.Get("x-ms-lease-status")) + result.MetaData = metadata.ParseFromHeaders(resp.Header) + + if v := resp.Header.Get("x-ms-access-tier-inferred"); v != "" { + b, innerErr := strconv.ParseBool(v) + if innerErr != nil { + err = fmt.Errorf("Error parsing %q as a bool: %s", v, innerErr) + return + } + + result.AccessTierInferred = b + } + + if v := resp.Header.Get("Content-Length"); v != "" { + i, innerErr := strconv.Atoi(v) + if innerErr != nil { + err = fmt.Errorf("Error parsing %q as an integer: %s", v, innerErr) + } + + result.ContentLength = int64(i) + } + + if v := resp.Header.Get("x-ms-incremental-copy"); v != "" { + b, innerErr := strconv.ParseBool(v) + if innerErr != nil { + err = fmt.Errorf("Error parsing %q as a bool: %s", v, innerErr) + return + } + + result.IncrementalCopy = b + } + + if v := resp.Header.Get("x-ms-server-encrypted"); v != "" { + b, innerErr := strconv.ParseBool(v) + if innerErr != nil { + err = fmt.Errorf("Error parsing %q as a bool: %s", v, innerErr) + return + } + + result.IncrementalCopy = b + } + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + return +} diff --git a/storage/2017-07-29/blob/blobs/properties_set.go b/storage/2017-07-29/blob/blobs/properties_set.go new file mode 100644 index 0000000..a8c0ed8 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/properties_set.go @@ -0,0 +1,156 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type SetPropertiesInput struct { + CacheControl *string + ContentType *string + ContentMD5 *string + ContentEncoding *string + ContentLanguage *string + LeaseID *string + ContentDisposition *string + ContentLength *int64 + SequenceNumberAction *SequenceNumberAction + BlobSequenceNumber *string +} + +type SetPropertiesResult struct { + autorest.Response + + BlobSequenceNumber string + Etag string +} + +// SetProperties sets system properties on the blob. +func (client Client) SetProperties(ctx context.Context, accountName, containerName, blobName string, input SetPropertiesInput) (result SetPropertiesResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "SetProperties", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "SetProperties", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "SetProperties", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "SetProperties", "`blobName` cannot be an empty string.") + } + + req, err := client.SetPropertiesPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "SetProperties", nil, "Failure preparing request") + return + } + + resp, err := client.SetPropertiesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "SetProperties", resp, "Failure sending request") + return + } + + result, err = client.SetPropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "SetProperties", resp, "Failure responding to request") + return + } + + return +} + +type SequenceNumberAction string + +var ( + Increment SequenceNumberAction = "increment" + Max SequenceNumberAction = "max" + Update SequenceNumberAction = "update" +) + +// SetPropertiesPreparer prepares the SetProperties request. +func (client Client) SetPropertiesPreparer(ctx context.Context, accountName, containerName, blobName string, input SetPropertiesInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "properties"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.CacheControl != nil { + headers["x-ms-blob-cache-control"] = *input.CacheControl + } + if input.ContentDisposition != nil { + headers["x-ms-blob-content-disposition"] = *input.ContentDisposition + } + if input.ContentEncoding != nil { + headers["x-ms-blob-content-encoding"] = *input.ContentEncoding + } + if input.ContentLanguage != nil { + headers["x-ms-blob-content-language"] = *input.ContentLanguage + } + if input.ContentMD5 != nil { + headers["x-ms-blob-content-md5"] = *input.ContentMD5 + } + if input.ContentType != nil { + headers["x-ms-blob-content-type"] = *input.ContentType + } + if input.ContentLength != nil { + headers["x-ms-blob-content-length"] = *input.ContentLength + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + if input.SequenceNumberAction != nil { + headers["x-ms-sequence-number-action"] = string(*input.SequenceNumberAction) + } + if input.BlobSequenceNumber != nil { + headers["x-ms-blob-sequence-number"] = *input.BlobSequenceNumber + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetPropertiesSender sends the SetProperties request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetPropertiesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetPropertiesResponder handles the response to the SetProperties request. The method always +// closes the http.Response Body. +func (client Client) SetPropertiesResponder(resp *http.Response) (result SetPropertiesResult, err error) { + if resp != nil && resp.Header != nil { + result.BlobSequenceNumber = resp.Header.Get("x-ms-blob-sequence-number") + result.Etag = resp.Header.Get("Etag") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + return +} diff --git a/storage/2017-07-29/blob/blobs/put_append_blob.go b/storage/2017-07-29/blob/blobs/put_append_blob.go new file mode 100644 index 0000000..ef2c502 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/put_append_blob.go @@ -0,0 +1,134 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type PutAppendBlobInput struct { + CacheControl *string + ContentDisposition *string + ContentEncoding *string + ContentLanguage *string + ContentMD5 *string + ContentType *string + LeaseID *string + MetaData map[string]string +} + +// PutAppendBlob is a wrapper around the Put API call (with a stricter input object) +// which creates a new append blob, or updates the content of an existing blob. +func (client Client) PutAppendBlob(ctx context.Context, accountName, containerName, blobName string, input PutAppendBlobInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutAppendBlob", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutAppendBlob", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutAppendBlob", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutAppendBlob", "`blobName` cannot be an empty string.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("blobs.Client", "PutAppendBlob", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.PutAppendBlobPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutAppendBlob", nil, "Failure preparing request") + return + } + + resp, err := client.PutAppendBlobSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutAppendBlob", resp, "Failure sending request") + return + } + + result, err = client.PutAppendBlobResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutAppendBlob", resp, "Failure responding to request") + return + } + + return +} + +// PutAppendBlobPreparer prepares the PutAppendBlob request. +func (client Client) PutAppendBlobPreparer(ctx context.Context, accountName, containerName, blobName string, input PutAppendBlobInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-blob-type": string(AppendBlob), + "x-ms-version": APIVersion, + + // For a page blob or an append blob, the value of this header must be set to zero, + // as Put Blob is used only to initialize the blob + "Content-Length": 0, + } + + if input.CacheControl != nil { + headers["x-ms-blob-cache-control"] = *input.CacheControl + } + if input.ContentDisposition != nil { + headers["x-ms-blob-content-disposition"] = *input.ContentDisposition + } + if input.ContentEncoding != nil { + headers["x-ms-blob-content-encoding"] = *input.ContentEncoding + } + if input.ContentLanguage != nil { + headers["x-ms-blob-content-language"] = *input.ContentLanguage + } + if input.ContentMD5 != nil { + headers["x-ms-blob-content-md5"] = *input.ContentMD5 + } + if input.ContentType != nil { + headers["x-ms-blob-content-type"] = *input.ContentType + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutAppendBlobSender sends the PutAppendBlob request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutAppendBlobSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutAppendBlobResponder handles the response to the PutAppendBlob request. The method always +// closes the http.Response Body. +func (client Client) PutAppendBlobResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/blobs/put_block.go b/storage/2017-07-29/blob/blobs/put_block.go new file mode 100644 index 0000000..5256013 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/put_block.go @@ -0,0 +1,125 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type PutBlockInput struct { + BlockID string + Content []byte + ContentMD5 *string + LeaseID *string +} + +type PutBlockResult struct { + autorest.Response + + ContentMD5 string +} + +// PutBlock creates a new block to be committed as part of a blob. +func (client Client) PutBlock(ctx context.Context, accountName, containerName, blobName string, input PutBlockInput) (result PutBlockResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutBlock", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutBlock", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutBlock", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutBlock", "`blobName` cannot be an empty string.") + } + if input.BlockID == "" { + return result, validation.NewError("blobs.Client", "PutBlock", "`input.BlockID` cannot be an empty string.") + } + if len(input.Content) == 0 { + return result, validation.NewError("blobs.Client", "PutBlock", "`input.Content` cannot be empty.") + } + + req, err := client.PutBlockPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlock", nil, "Failure preparing request") + return + } + + resp, err := client.PutBlockSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlock", resp, "Failure sending request") + return + } + + result, err = client.PutBlockResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlock", resp, "Failure responding to request") + return + } + + return +} + +// PutBlockPreparer prepares the PutBlock request. +func (client Client) PutBlockPreparer(ctx context.Context, accountName, containerName, blobName string, input PutBlockInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "block"), + "blockid": autorest.Encode("query", input.BlockID), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.ContentMD5 != nil { + headers["x-ms-blob-content-md5"] = *input.ContentMD5 + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers), + autorest.WithBytes(&input.Content)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutBlockSender sends the PutBlock request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutBlockSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutBlockResponder handles the response to the PutBlock request. The method always +// closes the http.Response Body. +func (client Client) PutBlockResponder(resp *http.Response) (result PutBlockResult, err error) { + if resp != nil && resp.Header != nil { + result.ContentMD5 = resp.Header.Get("Content-MD5") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/blobs/put_block_blob.go b/storage/2017-07-29/blob/blobs/put_block_blob.go new file mode 100644 index 0000000..fa29dd3 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/put_block_blob.go @@ -0,0 +1,135 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type PutBlockBlobInput struct { + CacheControl *string + Content []byte + ContentDisposition *string + ContentEncoding *string + ContentLanguage *string + ContentMD5 *string + ContentType *string + LeaseID *string + MetaData map[string]string +} + +// PutBlockBlob is a wrapper around the Put API call (with a stricter input object) +// which creates a new block append blob, or updates the content of an existing block blob. +func (client Client) PutBlockBlob(ctx context.Context, accountName, containerName, blobName string, input PutBlockBlobInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutBlockBlob", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutBlockBlob", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutBlockBlob", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutBlockBlob", "`blobName` cannot be an empty string.") + } + if len(input.Content) == 0 { + return result, validation.NewError("blobs.Client", "PutBlockBlob", "`input.Content` cannot be empty.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("blobs.Client", "PutBlockBlob", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.PutBlockBlobPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockBlob", nil, "Failure preparing request") + return + } + + resp, err := client.PutBlockBlobSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockBlob", resp, "Failure sending request") + return + } + + result, err = client.PutBlockBlobResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockBlob", resp, "Failure responding to request") + return + } + + return +} + +// PutBlockBlobPreparer prepares the PutBlockBlob request. +func (client Client) PutBlockBlobPreparer(ctx context.Context, accountName, containerName, blobName string, input PutBlockBlobInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-blob-type": string(BlockBlob), + "x-ms-version": APIVersion, + } + + if input.CacheControl != nil { + headers["x-ms-blob-cache-control"] = *input.CacheControl + } + if input.ContentDisposition != nil { + headers["x-ms-blob-content-disposition"] = *input.ContentDisposition + } + if input.ContentEncoding != nil { + headers["x-ms-blob-content-encoding"] = *input.ContentEncoding + } + if input.ContentLanguage != nil { + headers["x-ms-blob-content-language"] = *input.ContentLanguage + } + if input.ContentMD5 != nil { + headers["x-ms-blob-content-md5"] = *input.ContentMD5 + } + if input.ContentType != nil { + headers["x-ms-blob-content-type"] = *input.ContentType + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithBytes(&input.Content)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutBlockBlobSender sends the PutBlockBlob request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutBlockBlobSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutBlockBlobResponder handles the response to the PutBlockBlob request. The method always +// closes the http.Response Body. +func (client Client) PutBlockBlobResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/blobs/put_block_blob_file.go b/storage/2017-07-29/blob/blobs/put_block_blob_file.go new file mode 100644 index 0000000..7232e5e --- /dev/null +++ b/storage/2017-07-29/blob/blobs/put_block_blob_file.go @@ -0,0 +1,34 @@ +package blobs + +import ( + "context" + "fmt" + "io" + "os" +) + +// PutBlockBlobFromFile is a helper method which takes a file, and automatically chunks it up, rather than having to do this yourself +func (client Client) PutBlockBlobFromFile(ctx context.Context, accountName, containerName, blobName string, file *os.File, input PutBlockBlobInput) error { + fileInfo, err := file.Stat() + if err != nil { + return fmt.Errorf("Error loading file info: %s", err) + } + + fileSize := fileInfo.Size() + bytes := make([]byte, fileSize) + + _, err = file.ReadAt(bytes, 0) + if err != nil { + if err != io.EOF { + return fmt.Errorf("Error reading bytes: %s", err) + } + } + + input.Content = bytes + + if _, err = client.PutBlockBlob(ctx, accountName, containerName, blobName, input); err != nil { + return fmt.Errorf("Error putting bytes: %s", err) + } + + return nil +} diff --git a/storage/2017-07-29/blob/blobs/put_block_list.go b/storage/2017-07-29/blob/blobs/put_block_list.go new file mode 100644 index 0000000..f805247 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/put_block_list.go @@ -0,0 +1,157 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type BlockList struct { + CommittedBlockIDs []BlockID `xml:"Committed,omitempty"` + UncommittedBlockIDs []BlockID `xml:"Uncommitted,omitempty"` + LatestBlockIDs []BlockID `xml:"Latest,omitempty"` +} + +type BlockID struct { + Value string `xml:",chardata"` +} + +type PutBlockListInput struct { + BlockList BlockList + CacheControl *string + ContentDisposition *string + ContentEncoding *string + ContentLanguage *string + ContentMD5 *string + ContentType *string + MetaData map[string]string + LeaseID *string +} + +type PutBlockListResult struct { + autorest.Response + + ContentMD5 string + ETag string + LastModified string +} + +// PutBlockList writes a blob by specifying the list of block IDs that make up the blob. +// In order to be written as part of a blob, a block must have been successfully written +// to the server in a prior Put Block operation. +func (client Client) PutBlockList(ctx context.Context, accountName, containerName, blobName string, input PutBlockListInput) (result PutBlockListResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutBlockList", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutBlockList", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutBlockList", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutBlockList", "`blobName` cannot be an empty string.") + } + + req, err := client.PutBlockListPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockList", nil, "Failure preparing request") + return + } + + resp, err := client.PutBlockListSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockList", resp, "Failure sending request") + return + } + + result, err = client.PutBlockListResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockList", resp, "Failure responding to request") + return + } + + return +} + +// PutBlockListPreparer prepares the PutBlockList request. +func (client Client) PutBlockListPreparer(ctx context.Context, accountName, containerName, blobName string, input PutBlockListInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "blocklist"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.CacheControl != nil { + headers["x-ms-blob-cache-control"] = *input.CacheControl + } + if input.ContentDisposition != nil { + headers["x-ms-blob-content-disposition"] = *input.ContentDisposition + } + if input.ContentEncoding != nil { + headers["x-ms-blob-content-encoding"] = *input.ContentEncoding + } + if input.ContentLanguage != nil { + headers["x-ms-blob-content-language"] = *input.ContentLanguage + } + if input.ContentMD5 != nil { + headers["x-ms-blob-content-md5"] = *input.ContentMD5 + } + if input.ContentType != nil { + headers["x-ms-blob-content-type"] = *input.ContentType + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers), + autorest.WithXML(input.BlockList)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutBlockListSender sends the PutBlockList request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutBlockListSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutBlockListResponder handles the response to the PutBlockList request. The method always +// closes the http.Response Body. +func (client Client) PutBlockListResponder(resp *http.Response) (result PutBlockListResult, err error) { + if resp != nil && resp.Header != nil { + result.ContentMD5 = resp.Header.Get("Content-MD5") + result.ETag = resp.Header.Get("ETag") + result.LastModified = resp.Header.Get("Last-Modified") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/blobs/put_page_blob.go b/storage/2017-07-29/blob/blobs/put_page_blob.go new file mode 100644 index 0000000..ad3c878 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/put_page_blob.go @@ -0,0 +1,148 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type PutPageBlobInput struct { + CacheControl *string + ContentDisposition *string + ContentEncoding *string + ContentLanguage *string + ContentMD5 *string + ContentType *string + LeaseID *string + MetaData map[string]string + + BlobContentLengthBytes int64 + BlobSequenceNumber *int64 + AccessTier *AccessTier +} + +// PutPageBlob is a wrapper around the Put API call (with a stricter input object) +// which creates a new block blob, or updates the content of an existing page blob. +func (client Client) PutPageBlob(ctx context.Context, accountName, containerName, blobName string, input PutPageBlobInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutPageBlob", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutPageBlob", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutPageBlob", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutPageBlob", "`blobName` cannot be an empty string.") + } + if input.BlobContentLengthBytes == 0 || input.BlobContentLengthBytes%512 != 0 { + return result, validation.NewError("blobs.Client", "PutPageBlob", "`blobName` must be aligned to a 512-byte boundary.") + } + + req, err := client.PutPageBlobPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageBlob", nil, "Failure preparing request") + return + } + + resp, err := client.PutPageBlobSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageBlob", resp, "Failure sending request") + return + } + + result, err = client.PutPageBlobResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageBlob", resp, "Failure responding to request") + return + } + + return +} + +// PutPageBlobPreparer prepares the PutPageBlob request. +func (client Client) PutPageBlobPreparer(ctx context.Context, accountName, containerName, blobName string, input PutPageBlobInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-blob-type": string(PageBlob), + "x-ms-version": APIVersion, + + // For a page blob or an page blob, the value of this header must be set to zero, + // as Put Blob is used only to initialize the blob + "Content-Length": 0, + + // This header specifies the maximum size for the page blob, up to 8 TB. + // The page blob size must be aligned to a 512-byte boundary. + "x-ms-blob-content-length": input.BlobContentLengthBytes, + } + + if input.AccessTier != nil { + headers["x-ms-access-tier"] = string(*input.AccessTier) + } + if input.BlobSequenceNumber != nil { + headers["x-ms-blob-sequence-number"] = *input.BlobSequenceNumber + } + + if input.CacheControl != nil { + headers["x-ms-blob-cache-control"] = *input.CacheControl + } + if input.ContentDisposition != nil { + headers["x-ms-blob-content-disposition"] = *input.ContentDisposition + } + if input.ContentEncoding != nil { + headers["x-ms-blob-content-encoding"] = *input.ContentEncoding + } + if input.ContentLanguage != nil { + headers["x-ms-blob-content-language"] = *input.ContentLanguage + } + if input.ContentMD5 != nil { + headers["x-ms-blob-content-md5"] = *input.ContentMD5 + } + if input.ContentType != nil { + headers["x-ms-blob-content-type"] = *input.ContentType + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutPageBlobSender sends the PutPageBlob request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutPageBlobSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutPageBlobResponder handles the response to the PutPageBlob request. The method always +// closes the http.Response Body. +func (client Client) PutPageBlobResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/blobs/put_page_clear.go b/storage/2017-07-29/blob/blobs/put_page_clear.go new file mode 100644 index 0000000..59feaa5 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/put_page_clear.go @@ -0,0 +1,113 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type PutPageClearInput struct { + StartByte int64 + EndByte int64 + + LeaseID *string +} + +// PutPageClear clears a range of pages within a page blob. +func (client Client) PutPageClear(ctx context.Context, accountName, containerName, blobName string, input PutPageClearInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutPageClear", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutPageClear", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutPageClear", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutPageClear", "`blobName` cannot be an empty string.") + } + if input.StartByte < 0 { + return result, validation.NewError("blobs.Client", "PutPageClear", "`input.StartByte` must be greater than or equal to 0.") + } + if input.EndByte <= 0 { + return result, validation.NewError("blobs.Client", "PutPageClear", "`input.EndByte` must be greater than 0.") + } + + req, err := client.PutPageClearPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageClear", nil, "Failure preparing request") + return + } + + resp, err := client.PutPageClearSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageClear", resp, "Failure sending request") + return + } + + result, err = client.PutPageClearResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageClear", resp, "Failure responding to request") + return + } + + return +} + +// PutPageClearPreparer prepares the PutPageClear request. +func (client Client) PutPageClearPreparer(ctx context.Context, accountName, containerName, blobName string, input PutPageClearInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "page"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-page-write": "clear", + "x-ms-range": fmt.Sprintf("bytes=%d-%d", input.StartByte, input.EndByte), + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutPageClearSender sends the PutPageClear request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutPageClearSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutPageClearResponder handles the response to the PutPageClear request. The method always +// closes the http.Response Body. +func (client Client) PutPageClearResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/blobs/put_page_update.go b/storage/2017-07-29/blob/blobs/put_page_update.go new file mode 100644 index 0000000..a47e8ca --- /dev/null +++ b/storage/2017-07-29/blob/blobs/put_page_update.go @@ -0,0 +1,163 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type PutPageUpdateInput struct { + StartByte int64 + EndByte int64 + Content []byte + + IfSequenceNumberEQ *string + IfSequenceNumberLE *string + IfSequenceNumberLT *string + IfModifiedSince *string + IfUnmodifiedSince *string + IfMatch *string + IfNoneMatch *string + LeaseID *string +} + +type PutPageUpdateResult struct { + autorest.Response + + BlobSequenceNumber string + ContentMD5 string + LastModified string +} + +// PutPageUpdate writes a range of pages to a page blob. +func (client Client) PutPageUpdate(ctx context.Context, accountName, containerName, blobName string, input PutPageUpdateInput) (result PutPageUpdateResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutPageUpdate", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutPageUpdate", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutPageUpdate", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutPageUpdate", "`blobName` cannot be an empty string.") + } + if input.StartByte < 0 { + return result, validation.NewError("blobs.Client", "PutPageUpdate", "`input.StartByte` must be greater than or equal to 0.") + } + if input.EndByte <= 0 { + return result, validation.NewError("blobs.Client", "PutPageUpdate", "`input.EndByte` must be greater than 0.") + } + + expectedSize := (input.EndByte - input.StartByte) + 1 + actualSize := int64(len(input.Content)) + if expectedSize != actualSize { + return result, validation.NewError("blobs.Client", "PutPageUpdate", fmt.Sprintf("Content Size was defined as %d but got %d.", expectedSize, actualSize)) + } + + req, err := client.PutPageUpdatePreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageUpdate", nil, "Failure preparing request") + return + } + + resp, err := client.PutPageUpdateSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageUpdate", resp, "Failure sending request") + return + } + + result, err = client.PutPageUpdateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageUpdate", resp, "Failure responding to request") + return + } + + return +} + +// PutPageUpdatePreparer prepares the PutPageUpdate request. +func (client Client) PutPageUpdatePreparer(ctx context.Context, accountName, containerName, blobName string, input PutPageUpdateInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "page"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-page-write": "update", + "x-ms-range": fmt.Sprintf("bytes=%d-%d", input.StartByte, input.EndByte), + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + if input.IfSequenceNumberEQ != nil { + headers["x-ms-if-sequence-number-eq"] = *input.IfSequenceNumberEQ + } + if input.IfSequenceNumberLE != nil { + headers["x-ms-if-sequence-number-le"] = *input.IfSequenceNumberLE + } + if input.IfSequenceNumberLT != nil { + headers["x-ms-if-sequence-number-lt"] = *input.IfSequenceNumberLT + } + if input.IfModifiedSince != nil { + headers["If-Modified-Since"] = *input.IfModifiedSince + } + if input.IfUnmodifiedSince != nil { + headers["If-Unmodified-Since"] = *input.IfUnmodifiedSince + } + if input.IfMatch != nil { + headers["If-Match"] = *input.IfMatch + } + if input.IfNoneMatch != nil { + headers["If-None-Match"] = *input.IfNoneMatch + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers), + autorest.WithBytes(&input.Content)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutPageUpdateSender sends the PutPageUpdate request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutPageUpdateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutPageUpdateResponder handles the response to the PutPageUpdate request. The method always +// closes the http.Response Body. +func (client Client) PutPageUpdateResponder(resp *http.Response) (result PutPageUpdateResult, err error) { + if resp != nil && resp.Header != nil { + result.BlobSequenceNumber = resp.Header.Get("x-ms-blob-sequence-number") + result.ContentMD5 = resp.Header.Get("Content-MD5") + result.LastModified = resp.Header.Get("Last-Modified") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/blobs/resource_id.go b/storage/2017-07-29/blob/blobs/resource_id.go new file mode 100644 index 0000000..0f6dddf --- /dev/null +++ b/storage/2017-07-29/blob/blobs/resource_id.go @@ -0,0 +1,56 @@ +package blobs + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given Blob +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, containerName, blobName string) string { + domain := endpoints.GetBlobEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s/%s", domain, containerName, blobName) +} + +type ResourceID struct { + AccountName string + ContainerName string + BlobName string +} + +// ParseResourceID parses the Resource ID and returns an object which can be used +// to interact with the Blob Resource +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://foo.blob.core.windows.net/Bar/example.vhd + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + path := strings.TrimPrefix(uri.Path, "/") + segments := strings.Split(path, "/") + if len(segments) == 0 { + return nil, fmt.Errorf("Expected the path to contain segments but got none") + } + + containerName := segments[0] + blobName := strings.TrimPrefix(path, containerName) + blobName = strings.TrimPrefix(blobName, "/") + return &ResourceID{ + AccountName: *accountName, + ContainerName: containerName, + BlobName: blobName, + }, nil +} diff --git a/storage/2017-07-29/blob/blobs/resource_id_test.go b/storage/2017-07-29/blob/blobs/resource_id_test.go new file mode 100644 index 0000000..bb6cad1 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/resource_id_test.go @@ -0,0 +1,123 @@ +package blobs + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.blob.core.chinacloudapi.cn/container1/blob1.vhd", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.blob.core.cloudapi.de/container1/blob1.vhd", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.blob.core.windows.net/container1/blob1.vhd", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.blob.core.usgovcloudapi.net/container1/blob1.vhd", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "container1", "blob1.vhd") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.blob.core.chinacloudapi.cn/container1/blob1.vhd", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.blob.core.cloudapi.de/container1/blob1.vhd", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.blob.core.windows.net/container1/blob1.vhd", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.blob.core.usgovcloudapi.net/container1/blob1.vhd", + }, + } + t.Logf("[DEBUG] Top Level Files") + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.ContainerName != "container1" { + t.Fatalf("Expected Container Name to be `container1` but got %q", actual.ContainerName) + } + if actual.BlobName != "blob1.vhd" { + t.Fatalf("Expected Blob Name to be `blob1.vhd` but got %q", actual.BlobName) + } + } + + testData = []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.blob.core.chinacloudapi.cn/container1/example/blob1.vhd", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.blob.core.cloudapi.de/container1/example/blob1.vhd", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.blob.core.windows.net/container1/example/blob1.vhd", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.blob.core.usgovcloudapi.net/container1/example/blob1.vhd", + }, + } + t.Logf("[DEBUG] Nested Files") + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.ContainerName != "container1" { + t.Fatalf("Expected Container Name to be `container1` but got %q", actual.ContainerName) + } + if actual.BlobName != "example/blob1.vhd" { + t.Fatalf("Expected Blob Name to be `example/blob1.vhd` but got %q", actual.BlobName) + } + } +} diff --git a/storage/2017-07-29/blob/blobs/set_tier.go b/storage/2017-07-29/blob/blobs/set_tier.go new file mode 100644 index 0000000..dd0f0b8 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/set_tier.go @@ -0,0 +1,93 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// SetTier sets the tier on a blob. +func (client Client) SetTier(ctx context.Context, accountName, containerName, blobName string, tier AccessTier) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "SetTier", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "SetTier", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "SetTier", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "SetTier", "`blobName` cannot be an empty string.") + } + + req, err := client.SetTierPreparer(ctx, accountName, containerName, blobName, tier) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "SetTier", nil, "Failure preparing request") + return + } + + resp, err := client.SetTierSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "SetTier", resp, "Failure sending request") + return + } + + result, err = client.SetTierResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "SetTier", resp, "Failure responding to request") + return + } + + return +} + +// SetTierPreparer prepares the SetTier request. +func (client Client) SetTierPreparer(ctx context.Context, accountName, containerName, blobName string, tier AccessTier) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("path", "tier"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-access-tier": string(tier), + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetTierSender sends the SetTier request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetTierSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetTierResponder handles the response to the SetTier request. The method always +// closes the http.Response Body. +func (client Client) SetTierResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2017-07-29/blob/blobs/snapshot.go b/storage/2017-07-29/blob/blobs/snapshot.go new file mode 100644 index 0000000..180070b --- /dev/null +++ b/storage/2017-07-29/blob/blobs/snapshot.go @@ -0,0 +1,163 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type SnapshotInput struct { + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string + + // MetaData is a user-defined name-value pair associated with the blob. + // If no name-value pairs are specified, the operation will copy the base blob metadata to the snapshot. + // If one or more name-value pairs are specified, the snapshot is created with the specified metadata, + // and metadata is not copied from the base blob. + MetaData map[string]string + + // A DateTime value which will only snapshot the blob if it has been modified since the specified date/time + // If the base blob has not been modified, the Blob service returns status code 412 (Precondition Failed). + IfModifiedSince *string + + // A DateTime value which will only snapshot the blob if it has not been modified since the specified date/time + // If the base blob has been modified, the Blob service returns status code 412 (Precondition Failed). + IfUnmodifiedSince *string + + // An ETag value to snapshot the blob only if its ETag value matches the value specified. + // If the values do not match, the Blob service returns status code 412 (Precondition Failed). + IfMatch *string + + // An ETag value for this conditional header to snapshot the blob only if its ETag value + // does not match the value specified. + // If the values are identical, the Blob service returns status code 412 (Precondition Failed). + IfNoneMatch *string +} + +type SnapshotResult struct { + autorest.Response + + // The ETag of the snapshot + ETag string + + // A DateTime value that uniquely identifies the snapshot. + // The value of this header indicates the snapshot version, + // and may be used in subsequent requests to access the snapshot. + SnapshotDateTime string +} + +// Snapshot captures a Snapshot of a given Blob +func (client Client) Snapshot(ctx context.Context, accountName, containerName, blobName string, input SnapshotInput) (result SnapshotResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "Snapshot", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "Snapshot", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "Snapshot", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "Snapshot", "`blobName` cannot be an empty string.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("blobs.Client", "Snapshot", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.SnapshotPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Snapshot", nil, "Failure preparing request") + return + } + + resp, err := client.SnapshotSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "Snapshot", resp, "Failure sending request") + return + } + + result, err = client.SnapshotResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Snapshot", resp, "Failure responding to request") + return + } + + return +} + +// SnapshotPreparer prepares the Snapshot request. +func (client Client) SnapshotPreparer(ctx context.Context, accountName, containerName, blobName string, input SnapshotInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "snapshot"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + if input.IfModifiedSince != nil { + headers["If-Modified-Since"] = *input.IfModifiedSince + } + if input.IfUnmodifiedSince != nil { + headers["If-Unmodified-Since"] = *input.IfUnmodifiedSince + } + if input.IfMatch != nil { + headers["If-Match"] = *input.IfMatch + } + if input.IfNoneMatch != nil { + headers["If-None-Match"] = *input.IfNoneMatch + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SnapshotSender sends the Snapshot request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SnapshotSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SnapshotResponder handles the response to the Snapshot request. The method always +// closes the http.Response Body. +func (client Client) SnapshotResponder(resp *http.Response) (result SnapshotResult, err error) { + if resp != nil && resp.Header != nil { + result.ETag = resp.Header.Get("ETag") + result.SnapshotDateTime = resp.Header.Get("x-ms-snapshot") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/blobs/snapshot_get_properties.go b/storage/2017-07-29/blob/blobs/snapshot_get_properties.go new file mode 100644 index 0000000..fe1be63 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/snapshot_get_properties.go @@ -0,0 +1,90 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetSnapshotPropertiesInput struct { + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string + + // The ID of the Snapshot which should be retrieved + SnapshotID string +} + +// GetSnapshotProperties returns all user-defined metadata, standard HTTP properties, and system properties for +// the specified snapshot of a blob +func (client Client) GetSnapshotProperties(ctx context.Context, accountName, containerName, blobName string, input GetSnapshotPropertiesInput) (result GetPropertiesResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "GetSnapshotProperties", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "GetSnapshotProperties", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "GetSnapshotProperties", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "GetSnapshotProperties", "`blobName` cannot be an empty string.") + } + if input.SnapshotID == "" { + return result, validation.NewError("blobs.Client", "GetSnapshotProperties", "`input.SnapshotID` cannot be an empty string.") + } + + req, err := client.GetSnapshotPropertiesPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetSnapshotProperties", nil, "Failure preparing request") + return + } + + // we re-use the GetProperties methods since this is otherwise the same + resp, err := client.GetPropertiesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "GetSnapshotProperties", resp, "Failure sending request") + return + } + + result, err = client.GetPropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetSnapshotProperties", resp, "Failure responding to request") + return + } + + return +} + +// GetSnapshotPreparer prepares the GetSnapshot request. +func (client Client) GetSnapshotPropertiesPreparer(ctx context.Context, accountName, containerName, blobName string, input GetSnapshotPropertiesInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "snapshot": autorest.Encode("query", input.SnapshotID), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsHead(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} diff --git a/storage/2017-07-29/blob/blobs/snapshot_test.go b/storage/2017-07-29/blob/blobs/snapshot_test.go new file mode 100644 index 0000000..efc26e8 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/snapshot_test.go @@ -0,0 +1,159 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2017-07-29/blob/containers" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestSnapshotLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + fileName := "example.txt" + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.BlobStorage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + containersClient := containers.NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithAuthorizer(containersClient.Client, storageAuth) + + _, err = containersClient.Create(ctx, accountName, containerName, containers.CreateInput{}) + if err != nil { + t.Fatalf("Error creating: %s", err) + } + defer containersClient.Delete(ctx, accountName, containerName) + + blobClient := NewWithEnvironment(client.Environment) + blobClient.Client = client.PrepareWithAuthorizer(blobClient.Client, storageAuth) + + t.Logf("[DEBUG] Copying file to Blob Storage..") + copyInput := CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + + refreshInterval := 5 * time.Second + if err := blobClient.CopyAndWait(ctx, accountName, containerName, fileName, copyInput, refreshInterval); err != nil { + t.Fatalf("Error copying: %s", err) + } + + t.Logf("[DEBUG] First Snapshot..") + firstSnapshot, err := blobClient.Snapshot(ctx, accountName, containerName, fileName, SnapshotInput{}) + if err != nil { + t.Fatalf("Error taking first snapshot: %s", err) + } + t.Logf("[DEBUG] First Snapshot ID: %q", firstSnapshot.SnapshotDateTime) + + t.Log("[DEBUG] Waiting 2 seconds..") + time.Sleep(2 * time.Second) + + t.Logf("[DEBUG] Second Snapshot..") + secondSnapshot, err := blobClient.Snapshot(ctx, accountName, containerName, fileName, SnapshotInput{ + MetaData: map[string]string{ + "hello": "world", + }, + }) + if err != nil { + t.Fatalf("Error taking Second snapshot: %s", err) + } + t.Logf("[DEBUG] Second Snapshot ID: %q", secondSnapshot.SnapshotDateTime) + + t.Logf("[DEBUG] Leasing the Blob..") + leaseDetails, err := blobClient.AcquireLease(ctx, accountName, containerName, fileName, AcquireLeaseInput{ + // infinite + LeaseDuration: -1, + }) + if err != nil { + t.Fatalf("Error leasing Blob: %s", err) + } + t.Logf("[DEBUG] Lease ID: %q", leaseDetails.LeaseID) + + t.Logf("[DEBUG] Third Snapshot..") + thirdSnapshot, err := blobClient.Snapshot(ctx, accountName, containerName, fileName, SnapshotInput{ + LeaseID: &leaseDetails.LeaseID, + }) + if err != nil { + t.Fatalf("Error taking Third snapshot: %s", err) + } + t.Logf("[DEBUG] Third Snapshot ID: %q", thirdSnapshot.SnapshotDateTime) + + t.Logf("[DEBUG] Releasing Lease..") + if _, err := blobClient.ReleaseLease(ctx, accountName, containerName, fileName, leaseDetails.LeaseID); err != nil { + t.Fatalf("Error releasing Lease: %s", err) + } + + // get the properties from the blob, which should include the LastModifiedDate + t.Logf("[DEBUG] Retrieving Properties for Blob") + props, err := blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error getting properties: %s", err) + } + + // confirm that the If-Modified-None returns an error + t.Logf("[DEBUG] Third Snapshot..") + fourthSnapshot, err := blobClient.Snapshot(ctx, accountName, containerName, fileName, SnapshotInput{ + LeaseID: &leaseDetails.LeaseID, + IfModifiedSince: &props.LastModified, + }) + if err == nil { + t.Fatalf("Expected an error but didn't get one") + } + if fourthSnapshot.Response.StatusCode != http.StatusPreconditionFailed { + t.Fatalf("Expected the status code to be Precondition Failed but got: %d", fourthSnapshot.Response.StatusCode) + } + + t.Logf("[DEBUG] Retrieving the Second Snapshot Properties..") + getSecondSnapshotInput := GetSnapshotPropertiesInput{ + SnapshotID: secondSnapshot.SnapshotDateTime, + } + if _, err := blobClient.GetSnapshotProperties(ctx, accountName, containerName, fileName, getSecondSnapshotInput); err != nil { + t.Fatalf("Error retrieving properties for the second snapshot: %s", err) + } + + t.Logf("[DEBUG] Deleting the Second Snapshot..") + deleteSnapshotInput := DeleteSnapshotInput{ + SnapshotDateTime: secondSnapshot.SnapshotDateTime, + } + if _, err := blobClient.DeleteSnapshot(ctx, accountName, containerName, fileName, deleteSnapshotInput); err != nil { + t.Fatalf("Error deleting snapshot: %s", err) + } + + t.Logf("[DEBUG] Re-Retrieving the Second Snapshot Properties..") + secondSnapshotProps, err := blobClient.GetSnapshotProperties(ctx, accountName, containerName, fileName, getSecondSnapshotInput) + if err == nil { + t.Fatalf("Expected an error retrieving the snapshot but got none") + } + if secondSnapshotProps.Response.StatusCode != http.StatusNotFound { + t.Fatalf("Expected the status code to be %d but got %q", http.StatusNoContent, secondSnapshotProps.Response.StatusCode) + } + + t.Logf("[DEBUG] Deleting all the snapshots..") + if _, err := blobClient.DeleteSnapshots(ctx, accountName, containerName, fileName, DeleteSnapshotsInput{}); err != nil { + t.Fatalf("Error deleting snapshots: %s", err) + } + + t.Logf("[DEBUG] Deleting the Blob..") + deleteInput := DeleteInput{ + DeleteSnapshots: false, + } + if _, err := blobClient.Delete(ctx, accountName, containerName, fileName, deleteInput); err != nil { + t.Fatalf("Error deleting Blob: %s", err) + } +} diff --git a/storage/2017-07-29/blob/blobs/undelete.go b/storage/2017-07-29/blob/blobs/undelete.go new file mode 100644 index 0000000..9be2f81 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/undelete.go @@ -0,0 +1,92 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Undelete restores the contents and metadata of soft deleted blob and any associated soft deleted snapshots. +func (client Client) Undelete(ctx context.Context, accountName, containerName, blobName string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "Undelete", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "Undelete", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "Undelete", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "Undelete", "`blobName` cannot be an empty string.") + } + + req, err := client.UndeletePreparer(ctx, accountName, containerName, blobName) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Undelete", nil, "Failure preparing request") + return + } + + resp, err := client.UndeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "Undelete", resp, "Failure sending request") + return + } + + result, err = client.UndeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Undelete", resp, "Failure responding to request") + return + } + + return +} + +// UndeletePreparer prepares the Undelete request. +func (client Client) UndeletePreparer(ctx context.Context, accountName, containerName, blobName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("path", "undelete"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// UndeleteSender sends the Undelete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) UndeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// UndeleteResponder handles the response to the Undelete request. The method always +// closes the http.Response Body. +func (client Client) UndeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2017-07-29/blob/blobs/version.go b/storage/2017-07-29/blob/blobs/version.go new file mode 100644 index 0000000..69bbed2 --- /dev/null +++ b/storage/2017-07-29/blob/blobs/version.go @@ -0,0 +1,14 @@ +package blobs + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2017-07-29" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2017-07-29/blob/containers/README.md b/storage/2017-07-29/blob/containers/README.md new file mode 100644 index 0000000..cd6b1bf --- /dev/null +++ b/storage/2017-07-29/blob/containers/README.md @@ -0,0 +1,44 @@ +## Blob Storage Container SDK for API version 2017-07-29 + +This package allows you to interact with the Containers Blob Storage API + +### Supported Authorizers + +* SharedKeyLite (Blob, File & Queue) + +Note: when using the `ListBlobs` operation, only `SharedKeyLite` authentication is supported. + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2017-07-29/blob/containers" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + containerName := "mycontainer" + + storageAuth := autorest.NewSharedKeyLiteAuthorizer(accountName, storageAccountKey) + containersClient := containers.New() + containersClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + createInput := containers.CreateInput{ + AccessLevel: containers.Private, + } + if _, err := containersClient.Create(ctx, accountName, containerName, createInput); err != nil { + return fmt.Errorf("Error creating Container: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2017-07-29/blob/containers/client.go b/storage/2017-07-29/blob/containers/client.go new file mode 100644 index 0000000..7bf4947 --- /dev/null +++ b/storage/2017-07-29/blob/containers/client.go @@ -0,0 +1,34 @@ +package containers + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for Blob Storage Containers. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithBaseURI creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} + +func (client Client) setAccessLevelIntoHeaders(headers map[string]interface{}, level AccessLevel) map[string]interface{} { + // If this header is not included in the request, container data is private to the account owner. + if level != Private { + headers["x-ms-blob-public-access"] = string(level) + } + + return headers +} diff --git a/storage/2017-07-29/blob/containers/create.go b/storage/2017-07-29/blob/containers/create.go new file mode 100644 index 0000000..84c2887 --- /dev/null +++ b/storage/2017-07-29/blob/containers/create.go @@ -0,0 +1,123 @@ +package containers + +import ( + "context" + "fmt" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type CreateInput struct { + // Specifies whether data in the container may be accessed publicly and the level of access + AccessLevel AccessLevel + + // A name-value pair to associate with the container as metadata. + MetaData map[string]string +} + +type CreateResponse struct { + autorest.Response + Error *ErrorResponse `xml:"Error"` +} + +// Create creates a new container under the specified account. +// If the container with the same name already exists, the operation fails. +func (client Client) Create(ctx context.Context, accountName, containerName string, input CreateInput) (result CreateResponse, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "Create", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "Create", "`containerName` cannot be an empty string.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("containers.Client", "Create", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.CreatePreparer(ctx, accountName, containerName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "Create", nil, "Failure preparing request") + return + } + + resp, err := client.CreateSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "Create", resp, "Failure sending request") + return + } + + result, err = client.CreateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "Create", resp, "Failure responding to request") + return + } + + return +} + +// CreatePreparer prepares the Create request. +func (client Client) CreatePreparer(ctx context.Context, accountName string, containerName string, input CreateInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = client.setAccessLevelIntoHeaders(headers, input.AccessLevel) + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CreateSender sends the Create request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CreateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CreateResponder handles the response to the Create request. The method always +// closes the http.Response Body. +func (client Client) CreateResponder(resp *http.Response) (result CreateResponse, err error) { + successfulStatusCodes := []int{ + http.StatusCreated, + } + if autorest.ResponseHasStatusCode(resp, successfulStatusCodes...) { + // when successful there's no response + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(successfulStatusCodes...), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + } else { + // however when there's an error the error's in the response + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(successfulStatusCodes...), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + } + + return +} diff --git a/storage/2017-07-29/blob/containers/delete.go b/storage/2017-07-29/blob/containers/delete.go new file mode 100644 index 0000000..3095829 --- /dev/null +++ b/storage/2017-07-29/blob/containers/delete.go @@ -0,0 +1,85 @@ +package containers + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Delete marks the specified container for deletion. +// The container and any blobs contained within it are later deleted during garbage collection. +func (client Client) Delete(ctx context.Context, accountName, containerName string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "Delete", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "Delete", "`containerName` cannot be an empty string.") + } + + req, err := client.DeletePreparer(ctx, accountName, containerName) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName string, containerName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2017-07-29/blob/containers/get_properties.go b/storage/2017-07-29/blob/containers/get_properties.go new file mode 100644 index 0000000..1e308da --- /dev/null +++ b/storage/2017-07-29/blob/containers/get_properties.go @@ -0,0 +1,124 @@ +package containers + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// GetProperties returns the properties for this Container without a Lease +func (client Client) GetProperties(ctx context.Context, accountName, containerName string) (ContainerProperties, error) { + // If specified, Get Container Properties only succeeds if the container’s lease is active and matches this ID. + // If there is no active lease or the ID does not match, 412 (Precondition Failed) is returned. + return client.GetPropertiesWithLeaseID(ctx, accountName, containerName, "") +} + +// GetPropertiesWithLeaseID returns the properties for this Container using the specified LeaseID +func (client Client) GetPropertiesWithLeaseID(ctx context.Context, accountName, containerName, leaseID string) (result ContainerProperties, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "GetPropertiesWithLeaseID", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "GetPropertiesWithLeaseID", "`containerName` cannot be an empty string.") + } + + req, err := client.GetPropertiesWithLeaseIDPreparer(ctx, accountName, containerName, leaseID) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "GetProperties", nil, "Failure preparing request") + return + } + + resp, err := client.GetPropertiesWithLeaseIDSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "GetProperties", resp, "Failure sending request") + return + } + + result, err = client.GetPropertiesWithLeaseIDResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "GetProperties", resp, "Failure responding to request") + return + } + + return +} + +// GetPropertiesWithLeaseIDPreparer prepares the GetPropertiesWithLeaseID request. +func (client Client) GetPropertiesWithLeaseIDPreparer(ctx context.Context, accountName, containerName, leaseID string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + // If specified, Get Container Properties only succeeds if the container’s lease is active and matches this ID. + // If there is no active lease or the ID does not match, 412 (Precondition Failed) is returned. + if leaseID != "" { + headers["x-ms-lease-id"] = leaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetPropertiesWithLeaseIDSender sends the GetPropertiesWithLeaseID request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetPropertiesWithLeaseIDSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetPropertiesWithLeaseIDResponder handles the response to the GetPropertiesWithLeaseID request. The method always +// closes the http.Response Body. +func (client Client) GetPropertiesWithLeaseIDResponder(resp *http.Response) (result ContainerProperties, err error) { + if resp != nil { + result.LeaseStatus = LeaseStatus(resp.Header.Get("x-ms-lease-status")) + result.LeaseState = LeaseState(resp.Header.Get("x-ms-lease-state")) + if result.LeaseStatus == Locked { + duration := LeaseDuration(resp.Header.Get("x-ms-lease-duration")) + result.LeaseDuration = &duration + } + + // If this header is not returned in the response, the container is private to the account owner. + accessLevel := resp.Header.Get("x-ms-blob-public-access") + if accessLevel != "" { + result.AccessLevel = AccessLevel(accessLevel) + } else { + result.AccessLevel = Private + } + + // we can't necessarily use strconv.ParseBool here since this could be nil (only in some API versions) + result.HasImmutabilityPolicy = strings.EqualFold(resp.Header.Get("x-ms-has-immutability-policy"), "true") + result.HasLegalHold = strings.EqualFold(resp.Header.Get("x-ms-has-legal-hold"), "true") + + result.MetaData = metadata.ParseFromHeaders(resp.Header) + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/containers/lease_acquire.go b/storage/2017-07-29/blob/containers/lease_acquire.go new file mode 100644 index 0000000..061c863 --- /dev/null +++ b/storage/2017-07-29/blob/containers/lease_acquire.go @@ -0,0 +1,115 @@ +package containers + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type AcquireLeaseInput struct { + // Specifies the duration of the lease, in seconds, or negative one (-1) for a lease that never expires. + // A non-infinite lease can be between 15 and 60 seconds + LeaseDuration int + + ProposedLeaseID string +} + +type AcquireLeaseResponse struct { + autorest.Response + + LeaseID string +} + +// AcquireLease establishes and manages a lock on a container for delete operations. +func (client Client) AcquireLease(ctx context.Context, accountName, containerName string, input AcquireLeaseInput) (result AcquireLeaseResponse, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "AcquireLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "AcquireLease", "`containerName` cannot be an empty string.") + } + // An infinite lease duration is -1 seconds. A non-infinite lease can be between 15 and 60 seconds + if input.LeaseDuration != -1 && (input.LeaseDuration <= 15 || input.LeaseDuration >= 60) { + return result, validation.NewError("containers.Client", "AcquireLease", "`input.LeaseDuration` must be -1 (infinite), or between 15 and 60 seconds.") + } + + req, err := client.AcquireLeasePreparer(ctx, accountName, containerName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "AcquireLease", nil, "Failure preparing request") + return + } + + resp, err := client.AcquireLeaseSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "AcquireLease", resp, "Failure sending request") + return + } + + result, err = client.AcquireLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "AcquireLease", resp, "Failure responding to request") + return + } + + return +} + +// AcquireLeasePreparer prepares the AcquireLease request. +func (client Client) AcquireLeasePreparer(ctx context.Context, accountName string, containerName string, input AcquireLeaseInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + "comp": autorest.Encode("path", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "acquire", + "x-ms-lease-duration": input.LeaseDuration, + } + + if input.ProposedLeaseID != "" { + headers["x-ms-proposed-lease-id"] = input.ProposedLeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// AcquireLeaseSender sends the AcquireLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) AcquireLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// AcquireLeaseResponder handles the response to the AcquireLease request. The method always +// closes the http.Response Body. +func (client Client) AcquireLeaseResponder(resp *http.Response) (result AcquireLeaseResponse, err error) { + if resp != nil { + result.LeaseID = resp.Header.Get("x-ms-lease-id") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/containers/lease_break.go b/storage/2017-07-29/blob/containers/lease_break.go new file mode 100644 index 0000000..08acfb7 --- /dev/null +++ b/storage/2017-07-29/blob/containers/lease_break.go @@ -0,0 +1,129 @@ +package containers + +import ( + "context" + "net/http" + "strconv" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type BreakLeaseInput struct { + // For a break operation, proposed duration the lease should continue + // before it is broken, in seconds, between 0 and 60. + // This break period is only used if it is shorter than the time remaining on the lease. + // If longer, the time remaining on the lease is used. + // A new lease will not be available before the break period has expired, + // but the lease may be held for longer than the break period. + // If this header does not appear with a break operation, a fixed-duration lease breaks + // after the remaining lease period elapses, and an infinite lease breaks immediately. + BreakPeriod *int + + LeaseID string +} + +type BreakLeaseResponse struct { + autorest.Response + + // Approximate time remaining in the lease period, in seconds. + // If the break is immediate, 0 is returned. + LeaseTime int +} + +// BreakLease breaks a lock based on it's Lease ID +func (client Client) BreakLease(ctx context.Context, accountName, containerName string, input BreakLeaseInput) (result BreakLeaseResponse, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "BreakLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "BreakLease", "`containerName` cannot be an empty string.") + } + if input.LeaseID == "" { + return result, validation.NewError("containers.Client", "BreakLease", "`input.LeaseID` cannot be an empty string.") + } + + req, err := client.BreakLeasePreparer(ctx, accountName, containerName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "BreakLease", nil, "Failure preparing request") + return + } + + resp, err := client.BreakLeaseSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "BreakLease", resp, "Failure sending request") + return + } + + result, err = client.BreakLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "BreakLease", resp, "Failure responding to request") + return + } + + return +} + +// BreakLeasePreparer prepares the BreakLease request. +func (client Client) BreakLeasePreparer(ctx context.Context, accountName string, containerName string, input BreakLeaseInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + "comp": autorest.Encode("path", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "break", + "x-ms-lease-id": input.LeaseID, + } + + if input.BreakPeriod != nil { + headers["x-ms-lease-break-period"] = *input.BreakPeriod + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// BreakLeaseSender sends the BreakLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) BreakLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// BreakLeaseResponder handles the response to the BreakLease request. The method always +// closes the http.Response Body. +func (client Client) BreakLeaseResponder(resp *http.Response) (result BreakLeaseResponse, err error) { + if resp != nil { + leaseRaw := resp.Header.Get("x-ms-lease-time") + if leaseRaw != "" { + i, err := strconv.Atoi(leaseRaw) + if err == nil { + result.LeaseTime = i + } + } + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/containers/lease_change.go b/storage/2017-07-29/blob/containers/lease_change.go new file mode 100644 index 0000000..dfbcb13 --- /dev/null +++ b/storage/2017-07-29/blob/containers/lease_change.go @@ -0,0 +1,111 @@ +package containers + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type ChangeLeaseInput struct { + ExistingLeaseID string + ProposedLeaseID string +} + +type ChangeLeaseResponse struct { + autorest.Response + + LeaseID string +} + +// ChangeLease changes the lock from one Lease ID to another Lease ID +func (client Client) ChangeLease(ctx context.Context, accountName, containerName string, input ChangeLeaseInput) (result ChangeLeaseResponse, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "ChangeLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "ChangeLease", "`containerName` cannot be an empty string.") + } + if input.ExistingLeaseID == "" { + return result, validation.NewError("containers.Client", "ChangeLease", "`input.ExistingLeaseID` cannot be an empty string.") + } + if input.ProposedLeaseID == "" { + return result, validation.NewError("containers.Client", "ChangeLease", "`input.ProposedLeaseID` cannot be an empty string.") + } + + req, err := client.ChangeLeasePreparer(ctx, accountName, containerName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "ChangeLease", nil, "Failure preparing request") + return + } + + resp, err := client.ChangeLeaseSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "ChangeLease", resp, "Failure sending request") + return + } + + result, err = client.ChangeLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "ChangeLease", resp, "Failure responding to request") + return + } + + return +} + +// ChangeLeasePreparer prepares the ChangeLease request. +func (client Client) ChangeLeasePreparer(ctx context.Context, accountName string, containerName string, input ChangeLeaseInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + "comp": autorest.Encode("path", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "change", + "x-ms-lease-id": input.ExistingLeaseID, + "x-ms-proposed-lease-id": input.ProposedLeaseID, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ChangeLeaseSender sends the ChangeLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ChangeLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ChangeLeaseResponder handles the response to the ChangeLease request. The method always +// closes the http.Response Body. +func (client Client) ChangeLeaseResponder(resp *http.Response) (result ChangeLeaseResponse, err error) { + if resp != nil { + result.LeaseID = resp.Header.Get("x-ms-lease-id") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/containers/lease_release.go b/storage/2017-07-29/blob/containers/lease_release.go new file mode 100644 index 0000000..fafcf98 --- /dev/null +++ b/storage/2017-07-29/blob/containers/lease_release.go @@ -0,0 +1,92 @@ +package containers + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// ReleaseLease releases the lock based on the Lease ID +func (client Client) ReleaseLease(ctx context.Context, accountName, containerName, leaseID string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "ReleaseLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "ReleaseLease", "`containerName` cannot be an empty string.") + } + if leaseID == "" { + return result, validation.NewError("containers.Client", "ReleaseLease", "`leaseID` cannot be an empty string.") + } + + req, err := client.ReleaseLeasePreparer(ctx, accountName, containerName, leaseID) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "ReleaseLease", nil, "Failure preparing request") + return + } + + resp, err := client.ReleaseLeaseSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "ReleaseLease", resp, "Failure sending request") + return + } + + result, err = client.ReleaseLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "ReleaseLease", resp, "Failure responding to request") + return + } + + return +} + +// ReleaseLeasePreparer prepares the ReleaseLease request. +func (client Client) ReleaseLeasePreparer(ctx context.Context, accountName string, containerName string, leaseID string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + "comp": autorest.Encode("path", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "release", + "x-ms-lease-id": leaseID, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ReleaseLeaseSender sends the ReleaseLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ReleaseLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ReleaseLeaseResponder handles the response to the ReleaseLease request. The method always +// closes the http.Response Body. +func (client Client) ReleaseLeaseResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/containers/lease_renew.go b/storage/2017-07-29/blob/containers/lease_renew.go new file mode 100644 index 0000000..3fe1765 --- /dev/null +++ b/storage/2017-07-29/blob/containers/lease_renew.go @@ -0,0 +1,92 @@ +package containers + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// RenewLease renewes the lock based on the Lease ID +func (client Client) RenewLease(ctx context.Context, accountName, containerName, leaseID string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "RenewLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "RenewLease", "`containerName` cannot be an empty string.") + } + if leaseID == "" { + return result, validation.NewError("containers.Client", "RenewLease", "`leaseID` cannot be an empty string.") + } + + req, err := client.RenewLeasePreparer(ctx, accountName, containerName, leaseID) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "RenewLease", nil, "Failure preparing request") + return + } + + resp, err := client.RenewLeaseSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "RenewLease", resp, "Failure sending request") + return + } + + result, err = client.RenewLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "RenewLease", resp, "Failure responding to request") + return + } + + return +} + +// RenewLeasePreparer prepares the RenewLease request. +func (client Client) RenewLeasePreparer(ctx context.Context, accountName string, containerName string, leaseID string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + "comp": autorest.Encode("path", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "renew", + "x-ms-lease-id": leaseID, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// RenewLeaseSender sends the RenewLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) RenewLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// RenewLeaseResponder handles the response to the RenewLease request. The method always +// closes the http.Response Body. +func (client Client) RenewLeaseResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/containers/lifecycle_test.go b/storage/2017-07-29/blob/containers/lifecycle_test.go new file mode 100644 index 0000000..389c773 --- /dev/null +++ b/storage/2017-07-29/blob/containers/lifecycle_test.go @@ -0,0 +1,174 @@ +package containers + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestContainerLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.BlobStorage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + containersClient := NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithAuthorizer(containersClient.Client, storageAuth) + + // first let's test an empty container + input := CreateInput{} + _, err = containersClient.Create(ctx, accountName, containerName, input) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + + container, err := containersClient.GetProperties(ctx, accountName, containerName) + if err != nil { + t.Fatal(fmt.Errorf("Error retrieving: %s", err)) + } + + if container.AccessLevel != Private { + t.Fatalf("Expected Access Level to be Private but got %q", container.AccessLevel) + } + if len(container.MetaData) != 0 { + t.Fatalf("Expected MetaData to be empty but got: %s", container.MetaData) + } + if container.LeaseStatus != Unlocked { + t.Fatalf("Expected Container Lease to be Unlocked but was: %s", container.LeaseStatus) + } + + // then update the metadata + metaData := map[string]string{ + "dont": "kill-my-vibe", + } + _, err = containersClient.SetMetaData(ctx, accountName, containerName, metaData) + if err != nil { + t.Fatal(fmt.Errorf("Error updating metadata: %s", err)) + } + + // give azure time to replicate + time.Sleep(2 * time.Second) + + // then assert that + container, err = containersClient.GetProperties(ctx, accountName, containerName) + if err != nil { + t.Fatal(fmt.Errorf("Error re-retrieving: %s", err)) + } + if len(container.MetaData) != 1 { + t.Fatalf("Expected 1 item in the metadata but got: %s", container.MetaData) + } + if container.MetaData["dont"] != "kill-my-vibe" { + t.Fatalf("Expected `kill-my-vibe` but got %q", container.MetaData["dont"]) + } + if container.AccessLevel != Private { + t.Fatalf("Expected Access Level to be Private but got %q", container.AccessLevel) + } + if container.LeaseStatus != Unlocked { + t.Fatalf("Expected Container Lease to be Unlocked but was: %s", container.LeaseStatus) + } + + // then update the ACL + _, err = containersClient.SetAccessControl(ctx, accountName, containerName, Blob) + if err != nil { + t.Fatal(fmt.Errorf("Error updating ACL's: %s", err)) + } + + // give azure some time to replicate + time.Sleep(2 * time.Second) + + // then assert that + container, err = containersClient.GetProperties(ctx, accountName, containerName) + if err != nil { + t.Fatal(fmt.Errorf("Error re-retrieving: %s", err)) + } + if container.AccessLevel != Blob { + t.Fatalf("Expected Access Level to be Blob but got %q", container.AccessLevel) + } + if len(container.MetaData) != 1 { + t.Fatalf("Expected 1 item in the metadata but got: %s", container.MetaData) + } + if container.LeaseStatus != Unlocked { + t.Fatalf("Expected Container Lease to be Unlocked but was: %s", container.LeaseStatus) + } + + // acquire a lease for 30s + acquireLeaseInput := AcquireLeaseInput{ + LeaseDuration: 30, + } + acquireLeaseResp, err := containersClient.AcquireLease(ctx, accountName, containerName, acquireLeaseInput) + if err != nil { + t.Fatalf("Error acquiring lease: %s", err) + } + t.Logf("[DEBUG] Lease ID: %s", acquireLeaseResp.LeaseID) + + // we should then be able to update the ID + t.Logf("[DEBUG] Changing lease..") + updateLeaseInput := ChangeLeaseInput{ + ExistingLeaseID: acquireLeaseResp.LeaseID, + ProposedLeaseID: "aaaabbbb-aaaa-bbbb-cccc-aaaabbbbcccc", + } + updateLeaseResp, err := containersClient.ChangeLease(ctx, accountName, containerName, updateLeaseInput) + if err != nil { + t.Fatalf("Error changing lease: %s", err) + } + + // then renew it + _, err = containersClient.RenewLease(ctx, accountName, containerName, updateLeaseResp.LeaseID) + if err != nil { + t.Fatalf("Error renewing lease: %s", err) + } + + // and then give it a timeout + breakPeriod := 20 + breakLeaseInput := BreakLeaseInput{ + LeaseID: updateLeaseResp.LeaseID, + BreakPeriod: &breakPeriod, + } + breakLeaseResp, err := containersClient.BreakLease(ctx, accountName, containerName, breakLeaseInput) + if err != nil { + t.Fatalf("Error breaking lease: %s", err) + } + if breakLeaseResp.LeaseTime == 0 { + t.Fatalf("Lease broke immediately when should have waited: %d", breakLeaseResp.LeaseTime) + } + + // and finally ditch it + _, err = containersClient.ReleaseLease(ctx, accountName, containerName, updateLeaseResp.LeaseID) + if err != nil { + t.Fatalf("Error releasing lease: %s", err) + } + + t.Logf("[DEBUG] Listing blobs in the container..") + listInput := ListBlobsInput{} + listResult, err := containersClient.ListBlobs(ctx, accountName, containerName, listInput) + if err != nil { + t.Fatalf("Error listing blobs: %s", err) + } + + if len(listResult.Blobs.Blobs) != 0 { + t.Fatalf("Expected there to be no blobs in the container but got %d", len(listResult.Blobs.Blobs)) + } + + t.Logf("[DEBUG] Deleting..") + _, err = containersClient.Delete(ctx, accountName, containerName) + if err != nil { + t.Fatal(fmt.Errorf("Error deleting: %s", err)) + } +} diff --git a/storage/2017-07-29/blob/containers/list_blobs.go b/storage/2017-07-29/blob/containers/list_blobs.go new file mode 100644 index 0000000..82797d0 --- /dev/null +++ b/storage/2017-07-29/blob/containers/list_blobs.go @@ -0,0 +1,179 @@ +package containers + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type ListBlobsInput struct { + Delimiter *string + Include *[]Dataset + Marker *string + MaxResults *int + Prefix *string +} + +type ListBlobsResult struct { + autorest.Response + + Delimiter string `xml:"Delimiter"` + Marker string `xml:"Marker"` + MaxResults int `xml:"MaxResults"` + NextMarker *string `xml:"NextMarker,omitempty"` + Prefix string `xml:"Prefix"` + Blobs Blobs `xml:"Blobs"` +} + +type Blobs struct { + Blobs []BlobDetails `xml:"Blob"` + BlobPrefix *BlobPrefix `xml:"BlobPrefix"` +} + +type BlobDetails struct { + Name string `xml:"Name"` + Deleted bool `xml:"Deleted,omitempty"` + MetaData map[string]interface{} `map:"Metadata,omitempty"` + Properties *BlobProperties `xml:"Properties,omitempty"` + Snapshot *string `xml:"Snapshot,omitempty"` +} + +type BlobProperties struct { + AccessTier *string `xml:"AccessTier,omitempty"` + AccessTierInferred *bool `xml:"AccessTierInferred,omitempty"` + AccessTierChangeTime *string `xml:"AccessTierChangeTime,omitempty"` + BlobType *string `xml:"BlobType,omitempty"` + BlobSequenceNumber *string `xml:"x-ms-blob-sequence-number,omitempty"` + CacheControl *string `xml:"Cache-Control,omitempty"` + ContentEncoding *string `xml:"ContentEncoding,omitempty"` + ContentLanguage *string `xml:"Content-Language,omitempty"` + ContentLength *int64 `xml:"Content-Length,omitempty"` + ContentMD5 *string `xml:"Content-MD5,omitempty"` + ContentType *string `xml:"Content-Type,omitempty"` + CopyCompletionTime *string `xml:"CopyCompletionTime,omitempty"` + CopyId *string `xml:"CopyId,omitempty"` + CopyStatus *string `xml:"CopyStatus,omitempty"` + CopySource *string `xml:"CopySource,omitempty"` + CopyProgress *string `xml:"CopyProgress,omitempty"` + CopyStatusDescription *string `xml:"CopyStatusDescription,omitempty"` + CreationTime *string `xml:"CreationTime,omitempty"` + ETag *string `xml:"Etag,omitempty"` + DeletedTime *string `xml:"DeletedTime,omitempty"` + IncrementalCopy *bool `xml:"IncrementalCopy,omitempty"` + LastModified *string `xml:"Last-Modified,omitempty"` + LeaseDuration *string `xml:"LeaseDuration,omitempty"` + LeaseState *string `xml:"LeaseState,omitempty"` + LeaseStatus *string `xml:"LeaseStatus,omitempty"` + RemainingRetentionDays *string `xml:"RemainingRetentionDays,omitempty"` + ServerEncrypted *bool `xml:"ServerEncrypted,omitempty"` +} + +type BlobPrefix struct { + Name string `xml:"Name"` +} + +// ListBlobs lists the blobs matching the specified query within the specified Container +func (client Client) ListBlobs(ctx context.Context, accountName, containerName string, input ListBlobsInput) (result ListBlobsResult, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "ListBlobs", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "ListBlobs", "`containerName` cannot be an empty string.") + } + if input.MaxResults != nil && (*input.MaxResults <= 0 || *input.MaxResults > 5000) { + return result, validation.NewError("containers.Client", "ListBlobs", "`input.MaxResults` can either be nil or between 0 and 5000.") + } + + req, err := client.ListBlobsPreparer(ctx, accountName, containerName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "ListBlobs", nil, "Failure preparing request") + return + } + + resp, err := client.ListBlobsSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "ListBlobs", resp, "Failure sending request") + return + } + + result, err = client.ListBlobsResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "ListBlobs", resp, "Failure responding to request") + return + } + + return +} + +// ListBlobsPreparer prepares the ListBlobs request. +func (client Client) ListBlobsPreparer(ctx context.Context, accountName, containerName string, input ListBlobsInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "list"), + "restype": autorest.Encode("query", "container"), + } + + if input.Delimiter != nil { + queryParameters["delimiter"] = autorest.Encode("query", *input.Delimiter) + } + if input.Include != nil { + vals := make([]string, 0) + for _, v := range *input.Include { + vals = append(vals, string(v)) + } + include := strings.Join(vals, ",") + queryParameters["include"] = autorest.Encode("query", include) + } + if input.Marker != nil { + queryParameters["marker"] = autorest.Encode("query", *input.Marker) + } + if input.MaxResults != nil { + queryParameters["maxresults"] = autorest.Encode("query", *input.MaxResults) + } + if input.Prefix != nil { + queryParameters["prefix"] = autorest.Encode("query", *input.Prefix) + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ListBlobsSender sends the ListBlobs request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ListBlobsSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ListBlobsResponder handles the response to the ListBlobs request. The method always +// closes the http.Response Body. +func (client Client) ListBlobsResponder(resp *http.Response) (result ListBlobsResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/containers/models.go b/storage/2017-07-29/blob/containers/models.go new file mode 100644 index 0000000..adba368 --- /dev/null +++ b/storage/2017-07-29/blob/containers/models.go @@ -0,0 +1,75 @@ +package containers + +import "github.com/Azure/go-autorest/autorest" + +type AccessLevel string + +var ( + // Blob specifies public read access for blobs. + // Blob data within this container can be read via anonymous request, + // but container data is not available. + // Clients cannot enumerate blobs within the container via anonymous request. + Blob AccessLevel = "blob" + + // Container specifies full public read access for container and blob data. + // Clients can enumerate blobs within the container via anonymous request, + // but cannot enumerate containers within the storage account. + Container AccessLevel = "container" + + // Private specifies that container data is private to the account owner + Private AccessLevel = "" +) + +type ContainerProperties struct { + autorest.Response + + AccessLevel AccessLevel + LeaseStatus LeaseStatus + LeaseState LeaseState + LeaseDuration *LeaseDuration + MetaData map[string]string + HasImmutabilityPolicy bool + HasLegalHold bool +} + +type Dataset string + +var ( + Copy Dataset = "copy" + Deleted Dataset = "deleted" + MetaData Dataset = "metadata" + Snapshots Dataset = "snapshots" + UncommittedBlobs Dataset = "uncommittedblobs" +) + +type ErrorResponse struct { + Code *string `xml:"Code"` + Message *string `xml:"Message"` +} + +type LeaseDuration string + +var ( + // If this lease is for a Fixed Duration + Fixed LeaseDuration = "fixed" + + // If this lease is for an Indefinite Duration + Infinite LeaseDuration = "infinite" +) + +type LeaseState string + +var ( + Available LeaseState = "available" + Breaking LeaseState = "breaking" + Broken LeaseState = "broken" + Expired LeaseState = "expired" + Leased LeaseState = "leased" +) + +type LeaseStatus string + +var ( + Locked LeaseStatus = "locked" + Unlocked LeaseStatus = "unlocked" +) diff --git a/storage/2017-07-29/blob/containers/resource_id.go b/storage/2017-07-29/blob/containers/resource_id.go new file mode 100644 index 0000000..a5bfd6e --- /dev/null +++ b/storage/2017-07-29/blob/containers/resource_id.go @@ -0,0 +1,46 @@ +package containers + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given Container +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, containerName string) string { + domain := endpoints.GetBlobEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s", domain, containerName) +} + +type ResourceID struct { + AccountName string + ContainerName string +} + +// ParseResourceID parses the Resource ID and returns an object which can be used +// to interact with the Container Resource +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://foo.blob.core.windows.net/Bar + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + containerName := strings.TrimPrefix(uri.Path, "/") + return &ResourceID{ + AccountName: *accountName, + ContainerName: containerName, + }, nil +} diff --git a/storage/2017-07-29/blob/containers/resource_id_test.go b/storage/2017-07-29/blob/containers/resource_id_test.go new file mode 100644 index 0000000..e27bc9d --- /dev/null +++ b/storage/2017-07-29/blob/containers/resource_id_test.go @@ -0,0 +1,79 @@ +package containers + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.blob.core.chinacloudapi.cn/container1", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.blob.core.cloudapi.de/container1", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.blob.core.windows.net/container1", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.blob.core.usgovcloudapi.net/container1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "container1") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.blob.core.chinacloudapi.cn/container1", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.blob.core.cloudapi.de/container1", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.blob.core.windows.net/container1", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.blob.core.usgovcloudapi.net/container1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected the account name to be `account1` but got %q", actual.AccountName) + } + + if actual.ContainerName != "container1" { + t.Fatalf("Expected the container name to be `container1` but got %q", actual.ContainerName) + } + } +} diff --git a/storage/2017-07-29/blob/containers/set_acl.go b/storage/2017-07-29/blob/containers/set_acl.go new file mode 100644 index 0000000..fcf4e10 --- /dev/null +++ b/storage/2017-07-29/blob/containers/set_acl.go @@ -0,0 +1,100 @@ +package containers + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// SetAccessControl sets the Access Control for a Container without a Lease ID +func (client Client) SetAccessControl(ctx context.Context, accountName, containerName string, level AccessLevel) (autorest.Response, error) { + return client.SetAccessControlWithLeaseID(ctx, accountName, containerName, "", level) +} + +// SetAccessControlWithLeaseID sets the Access Control for a Container using the specified Lease ID +func (client Client) SetAccessControlWithLeaseID(ctx context.Context, accountName, containerName, leaseID string, level AccessLevel) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "SetAccessControl", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "SetAccessControl", "`containerName` cannot be an empty string.") + } + + req, err := client.SetAccessControlWithLeaseIDPreparer(ctx, accountName, containerName, leaseID, level) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "SetAccessControl", nil, "Failure preparing request") + return + } + + resp, err := client.SetAccessControlWithLeaseIDSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "SetAccessControl", resp, "Failure sending request") + return + } + + result, err = client.SetAccessControlWithLeaseIDResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "SetAccessControl", resp, "Failure responding to request") + return + } + + return +} + +// SetAccessControlWithLeaseIDPreparer prepares the SetAccessControlWithLeaseID request. +func (client Client) SetAccessControlWithLeaseIDPreparer(ctx context.Context, accountName, containerName, leaseID string, level AccessLevel) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("path", "acl"), + "restype": autorest.Encode("path", "container"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = client.setAccessLevelIntoHeaders(headers, level) + + // If specified, Get Container Properties only succeeds if the container’s lease is active and matches this ID. + // If there is no active lease or the ID does not match, 412 (Precondition Failed) is returned. + if leaseID != "" { + headers["x-ms-lease-id"] = leaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetAccessControlWithLeaseIDSender sends the SetAccessControlWithLeaseID request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetAccessControlWithLeaseIDSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetAccessControlWithLeaseIDResponder handles the response to the SetAccessControlWithLeaseID request. The method always +// closes the http.Response Body. +func (client Client) SetAccessControlWithLeaseIDResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/containers/set_metadata.go b/storage/2017-07-29/blob/containers/set_metadata.go new file mode 100644 index 0000000..fb9e07f --- /dev/null +++ b/storage/2017-07-29/blob/containers/set_metadata.go @@ -0,0 +1,105 @@ +package containers + +import ( + "context" + "fmt" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// SetMetaData sets the specified MetaData on the Container without a Lease ID +func (client Client) SetMetaData(ctx context.Context, accountName, containerName string, metaData map[string]string) (autorest.Response, error) { + return client.SetMetaDataWithLeaseID(ctx, accountName, containerName, "", metaData) +} + +// SetMetaDataWithLeaseID sets the specified MetaData on the Container using the specified Lease ID +func (client Client) SetMetaDataWithLeaseID(ctx context.Context, accountName, containerName, leaseID string, metaData map[string]string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "SetMetaData", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "SetMetaData", "`containerName` cannot be an empty string.") + } + if err := metadata.Validate(metaData); err != nil { + return result, validation.NewError("containers.Client", "SetMetaData", fmt.Sprintf("`metaData` is not valid: %s.", err)) + } + + req, err := client.SetMetaDataWithLeaseIDPreparer(ctx, accountName, containerName, leaseID, metaData) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "SetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.SetMetaDataWithLeaseIDSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "SetMetaData", resp, "Failure sending request") + return + } + + result, err = client.SetMetaDataWithLeaseIDResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "SetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// SetMetaDataWithLeaseIDPreparer prepares the SetMetaDataWithLeaseID request. +func (client Client) SetMetaDataWithLeaseIDPreparer(ctx context.Context, accountName, containerName, leaseID string, metaData map[string]string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("path", "metadata"), + "restype": autorest.Encode("path", "container"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = metadata.SetIntoHeaders(headers, metaData) + + // If specified, Get Container Properties only succeeds if the container’s lease is active and matches this ID. + // If there is no active lease or the ID does not match, 412 (Precondition Failed) is returned. + if leaseID != "" { + headers["x-ms-lease-id"] = leaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetMetaDataWithLeaseIDSender sends the SetMetaDataWithLeaseID request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetMetaDataWithLeaseIDSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetMetaDataWithLeaseIDResponder handles the response to the SetMetaDataWithLeaseID request. The method always +// closes the http.Response Body. +func (client Client) SetMetaDataWithLeaseIDResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/blob/containers/version.go b/storage/2017-07-29/blob/containers/version.go new file mode 100644 index 0000000..5fd1a4a --- /dev/null +++ b/storage/2017-07-29/blob/containers/version.go @@ -0,0 +1,14 @@ +package containers + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2017-07-29" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2017-07-29/file/directories/README.md b/storage/2017-07-29/file/directories/README.md new file mode 100644 index 0000000..07ccafd --- /dev/null +++ b/storage/2017-07-29/file/directories/README.md @@ -0,0 +1,43 @@ +## File Storage Directories SDK for API version 2017-07-29 + +This package allows you to interact with the Directories File Storage API + +### Supported Authorizers + +* SharedKeyLite (Blob, File & Queue) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2017-07-29/file/directories" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + shareName := "myshare" + directoryName := "myfiles" + + storageAuth := autorest.NewSharedKeyLiteAuthorizer(accountName, storageAccountKey) + directoriesClient := directories.New() + directoriesClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + metadata := map[string]string{ + "hello": "world", + } + if _, err := directoriesClient.Create(ctx, accountName, shareName, directoryName, metadata); err != nil { + return fmt.Errorf("Error creating Directory: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2017-07-29/file/directories/client.go b/storage/2017-07-29/file/directories/client.go new file mode 100644 index 0000000..bf2d315 --- /dev/null +++ b/storage/2017-07-29/file/directories/client.go @@ -0,0 +1,25 @@ +package directories + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for File Storage Shares. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithEnvironment creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2017-07-29/file/directories/create.go b/storage/2017-07-29/file/directories/create.go new file mode 100644 index 0000000..93f5c82 --- /dev/null +++ b/storage/2017-07-29/file/directories/create.go @@ -0,0 +1,101 @@ +package directories + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// Create creates a new directory under the specified share or parent directory. +func (client Client) Create(ctx context.Context, accountName, shareName, path string, metaData map[string]string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("directories.Client", "Create", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("directories.Client", "Create", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("directories.Client", "Create", "`shareName` must be a lower-cased string.") + } + if path == "" { + return result, validation.NewError("directories.Client", "Create", "`path` cannot be an empty string.") + } + if err := metadata.Validate(metaData); err != nil { + return result, validation.NewError("directories.Client", "Create", fmt.Sprintf("`metadata` is not valid: %s.", err)) + } + + req, err := client.CreatePreparer(ctx, accountName, shareName, path, metaData) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "Create", nil, "Failure preparing request") + return + } + + resp, err := client.CreateSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "directories.Client", "Create", resp, "Failure sending request") + return + } + + result, err = client.CreateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "Create", resp, "Failure responding to request") + return + } + + return +} + +// CreatePreparer prepares the Create request. +func (client Client) CreatePreparer(ctx context.Context, accountName, shareName, path string, metaData map[string]string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "directory"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = metadata.SetIntoHeaders(headers, metaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CreateSender sends the Create request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CreateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CreateResponder handles the response to the Create request. The method always +// closes the http.Response Body. +func (client Client) CreateResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/directories/delete.go b/storage/2017-07-29/file/directories/delete.go new file mode 100644 index 0000000..9443c25 --- /dev/null +++ b/storage/2017-07-29/file/directories/delete.go @@ -0,0 +1,95 @@ +package directories + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Delete removes the specified empty directory +// Note that the directory must be empty before it can be deleted. +func (client Client) Delete(ctx context.Context, accountName, shareName, path string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("directories.Client", "Delete", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("directories.Client", "Delete", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("directories.Client", "Delete", "`shareName` must be a lower-cased string.") + } + if path == "" { + return result, validation.NewError("directories.Client", "Delete", "`path` cannot be an empty string.") + } + + req, err := client.DeletePreparer(ctx, accountName, shareName, path) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "directories.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName, shareName, path string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "directory"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/directories/get.go b/storage/2017-07-29/file/directories/get.go new file mode 100644 index 0000000..817d680 --- /dev/null +++ b/storage/2017-07-29/file/directories/get.go @@ -0,0 +1,112 @@ +package directories + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetResult struct { + autorest.Response + + // A set of name-value pairs that contain metadata for the directory. + MetaData map[string]string + + // The value of this header is set to true if the directory metadata is completely + // encrypted using the specified algorithm. Otherwise, the value is set to false. + DirectoryMetaDataEncrypted bool +} + +// Get returns all system properties for the specified directory, +// and can also be used to check the existence of a directory. +func (client Client) Get(ctx context.Context, accountName, shareName, path string) (result GetResult, err error) { + if accountName == "" { + return result, validation.NewError("directories.Client", "Get", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("directories.Client", "Get", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("directories.Client", "Get", "`shareName` must be a lower-cased string.") + } + if path == "" { + return result, validation.NewError("directories.Client", "Get", "`path` cannot be an empty string.") + } + + req, err := client.GetPreparer(ctx, accountName, shareName, path) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "Get", nil, "Failure preparing request") + return + } + + resp, err := client.GetSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "directories.Client", "Get", resp, "Failure sending request") + return + } + + result, err = client.GetResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "Get", resp, "Failure responding to request") + return + } + + return +} + +// GetPreparer prepares the Get request. +func (client Client) GetPreparer(ctx context.Context, accountName, shareName, path string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "directory"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetSender sends the Get request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetResponder handles the response to the Get request. The method always +// closes the http.Response Body. +func (client Client) GetResponder(resp *http.Response) (result GetResult, err error) { + if resp != nil && resp.Header != nil { + result.MetaData = metadata.ParseFromHeaders(resp.Header) + result.DirectoryMetaDataEncrypted = strings.EqualFold(resp.Header.Get("x-ms-server-encrypted"), "true") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/directories/lifecycle_test.go b/storage/2017-07-29/file/directories/lifecycle_test.go new file mode 100644 index 0000000..855ddf7 --- /dev/null +++ b/storage/2017-07-29/file/directories/lifecycle_test.go @@ -0,0 +1,107 @@ +package directories + +import ( + "context" + "fmt" + "log" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2017-07-29/file/shares" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestDirectoriesLifeCycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + shareName := fmt.Sprintf("share-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + sharesClient := shares.NewWithEnvironment(client.Environment) + sharesClient.Client = client.PrepareWithAuthorizer(sharesClient.Client, storageAuth) + + directoriesClient := NewWithEnvironment(client.Environment) + directoriesClient.Client = client.PrepareWithAuthorizer(directoriesClient.Client, storageAuth) + + input := shares.CreateInput{ + QuotaInGB: 1, + } + _, err = sharesClient.Create(ctx, accountName, shareName, input) + if err != nil { + t.Fatalf("Error creating fileshare: %s", err) + } + defer sharesClient.Delete(ctx, accountName, shareName, true) + + metaData := map[string]string{ + "hello": "world", + } + + log.Printf("[DEBUG] Creating Top Level..") + if _, err := directoriesClient.Create(ctx, accountName, shareName, "hello", metaData); err != nil { + t.Fatalf("Error creating Top Level Directory: %s", err) + } + + log.Printf("[DEBUG] Creating Inner..") + if _, err := directoriesClient.Create(ctx, accountName, shareName, "hello/there", metaData); err != nil { + t.Fatalf("Error creating Inner Directory: %s", err) + } + + log.Printf("[DEBUG] Retrieving share") + innerDir, err := directoriesClient.Get(ctx, accountName, shareName, "hello/there") + if err != nil { + t.Fatalf("Error retrieving Inner Directory: %s", err) + } + + if innerDir.DirectoryMetaDataEncrypted != true { + t.Fatalf("Expected MetaData to be encrypted but got: %t", innerDir.DirectoryMetaDataEncrypted) + } + + if len(innerDir.MetaData) != 1 { + t.Fatalf("Expected MetaData to contain 1 item but got %d", len(innerDir.MetaData)) + } + if innerDir.MetaData["hello"] != "world" { + t.Fatalf("Expected MetaData `hello` to be `world`: %s", innerDir.MetaData["hello"]) + } + + log.Printf("[DEBUG] Setting MetaData") + updatedMetaData := map[string]string{ + "panda": "pops", + } + if _, err := directoriesClient.SetMetaData(ctx, accountName, shareName, "hello/there", updatedMetaData); err != nil { + t.Fatalf("Error updating MetaData: %s", err) + } + + log.Printf("[DEBUG] Retrieving MetaData") + retrievedMetaData, err := directoriesClient.GetMetaData(ctx, accountName, shareName, "hello/there") + if err != nil { + t.Fatalf("Error retrieving the updated metadata: %s", err) + } + if len(retrievedMetaData.MetaData) != 1 { + t.Fatalf("Expected the updated metadata to have 1 item but got %d", len(retrievedMetaData.MetaData)) + } + if retrievedMetaData.MetaData["panda"] != "pops" { + t.Fatalf("Expected the metadata `panda` to be `pops` but got %q", retrievedMetaData.MetaData["panda"]) + } + + t.Logf("[DEBUG] Deleting Inner..") + if _, err := directoriesClient.Delete(ctx, accountName, shareName, "hello/there"); err != nil { + t.Fatalf("Error deleting Inner Directory: %s", err) + } + + t.Logf("[DEBUG] Deleting Top Level..") + if _, err := directoriesClient.Delete(ctx, accountName, shareName, "hello"); err != nil { + t.Fatalf("Error deleting Top Level Directory: %s", err) + } +} diff --git a/storage/2017-07-29/file/directories/metadata_get.go b/storage/2017-07-29/file/directories/metadata_get.go new file mode 100644 index 0000000..173716d --- /dev/null +++ b/storage/2017-07-29/file/directories/metadata_get.go @@ -0,0 +1,106 @@ +package directories + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetMetaDataResult struct { + autorest.Response + + MetaData map[string]string +} + +// GetMetaData returns all user-defined metadata for the specified directory +func (client Client) GetMetaData(ctx context.Context, accountName, shareName, path string) (result GetMetaDataResult, err error) { + if accountName == "" { + return result, validation.NewError("directories.Client", "GetMetaData", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("directories.Client", "GetMetaData", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("directories.Client", "GetMetaData", "`shareName` must be a lower-cased string.") + } + if path == "" { + return result, validation.NewError("directories.Client", "GetMetaData", "`path` cannot be an empty string.") + } + + req, err := client.GetMetaDataPreparer(ctx, accountName, shareName, path) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "GetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.GetMetaDataSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "directories.Client", "GetMetaData", resp, "Failure sending request") + return + } + + result, err = client.GetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "GetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// GetMetaDataPreparer prepares the GetMetaData request. +func (client Client) GetMetaDataPreparer(ctx context.Context, accountName, shareName, path string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "directory"), + "comp": autorest.Encode("query", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetMetaDataSender sends the GetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetMetaDataResponder handles the response to the GetMetaData request. The method always +// closes the http.Response Body. +func (client Client) GetMetaDataResponder(resp *http.Response) (result GetMetaDataResult, err error) { + if resp != nil && resp.Header != nil { + result.MetaData = metadata.ParseFromHeaders(resp.Header) + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/directories/metadata_set.go b/storage/2017-07-29/file/directories/metadata_set.go new file mode 100644 index 0000000..cb13312 --- /dev/null +++ b/storage/2017-07-29/file/directories/metadata_set.go @@ -0,0 +1,102 @@ +package directories + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// SetMetaData updates user defined metadata for the specified directory +func (client Client) SetMetaData(ctx context.Context, accountName, shareName, path string, metaData map[string]string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("directories.Client", "SetMetaData", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("directories.Client", "SetMetaData", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("directories.Client", "SetMetaData", "`shareName` must be a lower-cased string.") + } + if path == "" { + return result, validation.NewError("directories.Client", "SetMetaData", "`path` cannot be an empty string.") + } + if err := metadata.Validate(metaData); err != nil { + return result, validation.NewError("directories.Client", "SetMetaData", fmt.Sprintf("`metaData` is not valid: %s.", err)) + } + + req, err := client.SetMetaDataPreparer(ctx, accountName, shareName, path, metaData) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "SetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.SetMetaDataSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "directories.Client", "SetMetaData", resp, "Failure sending request") + return + } + + result, err = client.SetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "SetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// SetMetaDataPreparer prepares the SetMetaData request. +func (client Client) SetMetaDataPreparer(ctx context.Context, accountName, shareName, path string, metaData map[string]string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "directory"), + "comp": autorest.Encode("query", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = metadata.SetIntoHeaders(headers, metaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetMetaDataSender sends the SetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetMetaDataResponder handles the response to the SetMetaData request. The method always +// closes the http.Response Body. +func (client Client) SetMetaDataResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/directories/resource_id.go b/storage/2017-07-29/file/directories/resource_id.go new file mode 100644 index 0000000..44607c4 --- /dev/null +++ b/storage/2017-07-29/file/directories/resource_id.go @@ -0,0 +1,56 @@ +package directories + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given Directory +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, shareName, directoryName string) string { + domain := endpoints.GetFileEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s/%s", domain, shareName, directoryName) +} + +type ResourceID struct { + AccountName string + DirectoryName string + ShareName string +} + +// ParseResourceID parses the Resource ID into an Object +// which can be used to interact with the Directory within the File Share +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://foo.file.core.windows.net/Bar/Folder + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + path := strings.TrimPrefix(uri.Path, "/") + segments := strings.Split(path, "/") + if len(segments) == 0 { + return nil, fmt.Errorf("Expected the path to contain segments but got none") + } + + shareName := segments[0] + directoryName := strings.TrimPrefix(path, shareName) + directoryName = strings.TrimPrefix(directoryName, "/") + return &ResourceID{ + AccountName: *accountName, + ShareName: shareName, + DirectoryName: directoryName, + }, nil +} diff --git a/storage/2017-07-29/file/directories/resource_id_test.go b/storage/2017-07-29/file/directories/resource_id_test.go new file mode 100644 index 0000000..0be800d --- /dev/null +++ b/storage/2017-07-29/file/directories/resource_id_test.go @@ -0,0 +1,81 @@ +package directories + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.file.core.chinacloudapi.cn/share1/directory1", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.file.core.cloudapi.de/share1/directory1", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.file.core.windows.net/share1/directory1", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.file.core.usgovcloudapi.net/share1/directory1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "share1", "directory1") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.file.core.chinacloudapi.cn/share1/directory1", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.file.core.cloudapi.de/share1/directory1", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.file.core.windows.net/share1/directory1", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.file.core.usgovcloudapi.net/share1/directory1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.ShareName != "share1" { + t.Fatalf("Expected Share Name to be `share1` but got %q", actual.ShareName) + } + if actual.DirectoryName != "directory1" { + t.Fatalf("Expected Directory Name to be `directory1` but got %q", actual.DirectoryName) + } + } +} diff --git a/storage/2017-07-29/file/directories/version.go b/storage/2017-07-29/file/directories/version.go new file mode 100644 index 0000000..ae46ef5 --- /dev/null +++ b/storage/2017-07-29/file/directories/version.go @@ -0,0 +1,14 @@ +package directories + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2017-07-29" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2017-07-29/file/files/README.md b/storage/2017-07-29/file/files/README.md new file mode 100644 index 0000000..fe13669 --- /dev/null +++ b/storage/2017-07-29/file/files/README.md @@ -0,0 +1,42 @@ +## File Storage Files SDK for API version 2017-07-29 + +This package allows you to interact with the Files File Storage API + +### Supported Authorizers + +* SharedKeyLite (Blob, File & Queue) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2017-07-29/file/files" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + shareName := "myshare" + directoryName := "myfiles" + fileName := "example.txt" + + storageAuth := autorest.NewSharedKeyLiteAuthorizer(accountName, storageAccountKey) + filesClient := files.New() + filesClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + input := files.CreateInput{} + if _, err := filesClient.Create(ctx, accountName, shareName, directoryName, fileName, input); err != nil { + return fmt.Errorf("Error creating File: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2017-07-29/file/files/client.go b/storage/2017-07-29/file/files/client.go new file mode 100644 index 0000000..ecca815 --- /dev/null +++ b/storage/2017-07-29/file/files/client.go @@ -0,0 +1,25 @@ +package files + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for File Storage Shares. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithEnvironment creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2017-07-29/file/files/copy.go b/storage/2017-07-29/file/files/copy.go new file mode 100644 index 0000000..31768b3 --- /dev/null +++ b/storage/2017-07-29/file/files/copy.go @@ -0,0 +1,132 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type CopyInput struct { + // Specifies the URL of the source file or blob, up to 2 KB in length. + // + // To copy a file to another file within the same storage account, you may use Shared Key to authenticate + // the source file. If you are copying a file from another storage account, or if you are copying a blob from + // the same storage account or another storage account, then you must authenticate the source file or blob using a + // shared access signature. If the source is a public blob, no authentication is required to perform the copy + // operation. A file in a share snapshot can also be specified as a copy source. + CopySource string + + MetaData map[string]string +} + +type CopyResult struct { + autorest.Response + + // The CopyID, which can be passed to AbortCopy to abort the copy. + CopyID string + + // Either `success` or `pending` + CopySuccess string +} + +// Copy copies a blob or file to a destination file within the storage account asynchronously. +func (client Client) Copy(ctx context.Context, accountName, shareName, path, fileName string, input CopyInput) (result CopyResult, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "Copy", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "Copy", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "Copy", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "Copy", "`fileName` cannot be an empty string.") + } + if input.CopySource == "" { + return result, validation.NewError("files.Client", "Copy", "`input.CopySource` cannot be an empty string.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("files.Client", "Copy", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.CopyPreparer(ctx, accountName, shareName, path, fileName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "Copy", nil, "Failure preparing request") + return + } + + resp, err := client.CopySender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "Copy", resp, "Failure sending request") + return + } + + result, err = client.CopyResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "Copy", resp, "Failure responding to request") + return + } + + return +} + +// CopyPreparer prepares the Copy request. +func (client Client) CopyPreparer(ctx context.Context, accountName, shareName, path, fileName string, input CopyInput) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-copy-source": input.CopySource, + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CopySender sends the Copy request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CopySender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CopyResponder handles the response to the Copy request. The method always +// closes the http.Response Body. +func (client Client) CopyResponder(resp *http.Response) (result CopyResult, err error) { + if resp != nil && resp.Header != nil { + result.CopyID = resp.Header.Get("x-ms-copy-id") + result.CopySuccess = resp.Header.Get("x-ms-copy-status") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/files/copy_abort.go b/storage/2017-07-29/file/files/copy_abort.go new file mode 100644 index 0000000..2f09131 --- /dev/null +++ b/storage/2017-07-29/file/files/copy_abort.go @@ -0,0 +1,104 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// AbortCopy aborts a pending Copy File operation, and leaves a destination file with zero length and full metadata +func (client Client) AbortCopy(ctx context.Context, accountName, shareName, path, fileName, copyID string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "AbortCopy", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "AbortCopy", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "AbortCopy", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "AbortCopy", "`fileName` cannot be an empty string.") + } + if copyID == "" { + return result, validation.NewError("files.Client", "AbortCopy", "`copyID` cannot be an empty string.") + } + + req, err := client.AbortCopyPreparer(ctx, accountName, shareName, path, fileName, copyID) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "AbortCopy", nil, "Failure preparing request") + return + } + + resp, err := client.AbortCopySender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "AbortCopy", resp, "Failure sending request") + return + } + + result, err = client.AbortCopyResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "AbortCopy", resp, "Failure responding to request") + return + } + + return +} + +// AbortCopyPreparer prepares the AbortCopy request. +func (client Client) AbortCopyPreparer(ctx context.Context, accountName, shareName, path, fileName, copyID string) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "copy"), + "copyid": autorest.Encode("query", copyID), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-copy-action": "abort", + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// AbortCopySender sends the AbortCopy request. The method will close the +// http.Response Body if it receives an error. +func (client Client) AbortCopySender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// AbortCopyResponder handles the response to the AbortCopy request. The method always +// closes the http.Response Body. +func (client Client) AbortCopyResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/files/copy_wait.go b/storage/2017-07-29/file/files/copy_wait.go new file mode 100644 index 0000000..e6a646b --- /dev/null +++ b/storage/2017-07-29/file/files/copy_wait.go @@ -0,0 +1,55 @@ +package files + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/Azure/go-autorest/autorest" +) + +type CopyAndWaitResult struct { + autorest.Response + + CopyID string +} + +const DefaultCopyPollDuration = 15 * time.Second + +// CopyAndWait is a convenience method which doesn't exist in the API, which copies the file and then waits for the copy to complete +func (client Client) CopyAndWait(ctx context.Context, accountName, shareName, path, fileName string, input CopyInput, pollDuration time.Duration) (result CopyResult, err error) { + copy, e := client.Copy(ctx, accountName, shareName, path, fileName, input) + if err != nil { + result.Response = copy.Response + err = fmt.Errorf("Error copying: %s", e) + return + } + + result.CopyID = copy.CopyID + + // since the API doesn't return a LRO, this is a hack which also polls every 10s, but should be sufficient + for true { + props, e := client.GetProperties(ctx, accountName, shareName, path, fileName) + if e != nil { + result.Response = copy.Response + err = fmt.Errorf("Error waiting for copy: %s", e) + return + } + + switch strings.ToLower(props.CopyStatus) { + case "pending": + time.Sleep(pollDuration) + continue + + case "success": + return + + default: + err = fmt.Errorf("Unexpected CopyState %q", e) + return + } + } + + return +} diff --git a/storage/2017-07-29/file/files/copy_wait_test.go b/storage/2017-07-29/file/files/copy_wait_test.go new file mode 100644 index 0000000..bd67929 --- /dev/null +++ b/storage/2017-07-29/file/files/copy_wait_test.go @@ -0,0 +1,129 @@ +package files + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2017-07-29/file/shares" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestFilesCopyAndWaitFromURL(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + shareName := fmt.Sprintf("share-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + sharesClient := shares.NewWithEnvironment(client.Environment) + sharesClient.Client = client.PrepareWithAuthorizer(sharesClient.Client, storageAuth) + + input := shares.CreateInput{ + QuotaInGB: 10, + } + _, err = sharesClient.Create(ctx, accountName, shareName, input) + if err != nil { + t.Fatalf("Error creating fileshare: %s", err) + } + defer sharesClient.Delete(ctx, accountName, shareName, false) + + filesClient := NewWithEnvironment(client.Environment) + filesClient.Client = client.PrepareWithAuthorizer(filesClient.Client, storageAuth) + + copiedFileName := "ubuntu.iso" + copyInput := CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + + t.Logf("[DEBUG] Copy And Waiting..") + if _, err := filesClient.CopyAndWait(ctx, accountName, shareName, "", copiedFileName, copyInput, DefaultCopyPollDuration); err != nil { + t.Fatalf("Error copy & waiting: %s", err) + } + + t.Logf("[DEBUG] Asserting that the file's ready..") + + props, err := filesClient.GetProperties(ctx, accountName, shareName, "", copiedFileName) + if err != nil { + t.Fatalf("Error retrieving file: %s", err) + } + + if !strings.EqualFold(props.CopyStatus, "success") { + t.Fatalf("Expected the Copy Status to be `Success` but got %q", props.CopyStatus) + } +} + +func TestFilesCopyAndWaitFromBlob(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + shareName := fmt.Sprintf("share-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + sharesClient := shares.NewWithEnvironment(client.Environment) + sharesClient.Client = client.PrepareWithAuthorizer(sharesClient.Client, storageAuth) + + input := shares.CreateInput{ + QuotaInGB: 10, + } + _, err = sharesClient.Create(ctx, accountName, shareName, input) + if err != nil { + t.Fatalf("Error creating fileshare: %s", err) + } + defer sharesClient.Delete(ctx, accountName, shareName, false) + + filesClient := NewWithEnvironment(client.Environment) + filesClient.Client = client.PrepareWithAuthorizer(filesClient.Client, storageAuth) + + originalFileName := "ubuntu.iso" + copiedFileName := "ubuntu-copied.iso" + copyInput := CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + t.Logf("[DEBUG] Copy And Waiting the original file..") + if _, err := filesClient.CopyAndWait(ctx, accountName, shareName, "", originalFileName, copyInput, DefaultCopyPollDuration); err != nil { + t.Fatalf("Error copy & waiting: %s", err) + } + + t.Logf("[DEBUG] Now copying that blob..") + duplicateInput := CopyInput{ + CopySource: fmt.Sprintf("%s/%s/%s", endpoints.GetFileEndpoint(filesClient.BaseURI, accountName), shareName, originalFileName), + } + if _, err := filesClient.CopyAndWait(ctx, accountName, shareName, "", copiedFileName, duplicateInput, DefaultCopyPollDuration); err != nil { + t.Fatalf("Error copying duplicate: %s", err) + } + + t.Logf("[DEBUG] Asserting that the file's ready..") + props, err := filesClient.GetProperties(ctx, accountName, shareName, "", copiedFileName) + if err != nil { + t.Fatalf("Error retrieving file: %s", err) + } + + if !strings.EqualFold(props.CopyStatus, "success") { + t.Fatalf("Expected the Copy Status to be `Success` but got %q", props.CopyStatus) + } +} diff --git a/storage/2017-07-29/file/files/create.go b/storage/2017-07-29/file/files/create.go new file mode 100644 index 0000000..85d4b0b --- /dev/null +++ b/storage/2017-07-29/file/files/create.go @@ -0,0 +1,146 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type CreateInput struct { + // This header specifies the maximum size for the file, up to 1 TiB. + ContentLength int64 + + // The MIME content type of the file + // If not specified, the default type is application/octet-stream. + ContentType *string + + // Specifies which content encodings have been applied to the file. + // This value is returned to the client when the Get File operation is performed + // on the file resource and can be used to decode file content. + ContentEncoding *string + + // Specifies the natural languages used by this resource. + ContentLanguage *string + + // The File service stores this value but does not use or modify it. + CacheControl *string + + // Sets the file's MD5 hash. + ContentMD5 *string + + // Sets the file’s Content-Disposition header. + ContentDisposition *string + + MetaData map[string]string +} + +// Create creates a new file or replaces a file. +func (client Client) Create(ctx context.Context, accountName, shareName, path, fileName string, input CreateInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "Create", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "Create", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "Create", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "Create", "`fileName` cannot be an empty string.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("files.Client", "Create", "`input.MetaData` cannot be an empty string.") + } + + req, err := client.CreatePreparer(ctx, accountName, shareName, path, fileName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "Create", nil, "Failure preparing request") + return + } + + resp, err := client.CreateSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "Create", resp, "Failure sending request") + return + } + + result, err = client.CreateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "Create", resp, "Failure responding to request") + return + } + + return +} + +// CreatePreparer prepares the Create request. +func (client Client) CreatePreparer(ctx context.Context, accountName, shareName, path, fileName string, input CreateInput) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-content-length": input.ContentLength, + "x-ms-type": "file", + } + + if input.ContentDisposition != nil { + headers["x-ms-content-disposition"] = *input.ContentDisposition + } + + if input.ContentEncoding != nil { + headers["x-ms-content-encoding"] = *input.ContentEncoding + } + + if input.ContentMD5 != nil { + headers["x-ms-content-md5"] = *input.ContentMD5 + } + + if input.ContentType != nil { + headers["x-ms-content-type"] = *input.ContentType + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CreateSender sends the Create request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CreateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CreateResponder handles the response to the Create request. The method always +// closes the http.Response Body. +func (client Client) CreateResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/files/delete.go b/storage/2017-07-29/file/files/delete.go new file mode 100644 index 0000000..5debd76 --- /dev/null +++ b/storage/2017-07-29/file/files/delete.go @@ -0,0 +1,94 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Delete immediately deletes the file from the File Share. +func (client Client) Delete(ctx context.Context, accountName, shareName, path, fileName string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "Delete", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "Delete", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "Delete", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "Delete", "`fileName` cannot be an empty string.") + } + + req, err := client.DeletePreparer(ctx, accountName, shareName, path, fileName) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName, shareName, path, fileName string) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/files/lifecycle_test.go b/storage/2017-07-29/file/files/lifecycle_test.go new file mode 100644 index 0000000..714c802 --- /dev/null +++ b/storage/2017-07-29/file/files/lifecycle_test.go @@ -0,0 +1,144 @@ +package files + +import ( + "context" + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2017-07-29/file/shares" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestFilesLifeCycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + shareName := fmt.Sprintf("share-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + sharesClient := shares.NewWithEnvironment(client.Environment) + sharesClient.Client = client.PrepareWithAuthorizer(sharesClient.Client, storageAuth) + + input := shares.CreateInput{ + QuotaInGB: 1, + } + _, err = sharesClient.Create(ctx, accountName, shareName, input) + if err != nil { + t.Fatalf("Error creating fileshare: %s", err) + } + defer sharesClient.Delete(ctx, accountName, shareName, false) + + filesClient := NewWithEnvironment(client.Environment) + filesClient.Client = client.PrepareWithAuthorizer(filesClient.Client, storageAuth) + + fileName := "bled5.png" + contentEncoding := "application/vnd+panda" + + t.Logf("[DEBUG] Creating Top Level File..") + createInput := CreateInput{ + ContentLength: 1024, + ContentEncoding: &contentEncoding, + } + if _, err := filesClient.Create(ctx, accountName, shareName, "", fileName, createInput); err != nil { + t.Fatalf("Error creating Top-Level File: %s", err) + } + + t.Logf("[DEBUG] Retrieving Properties for the Top-Level File..") + file, err := filesClient.GetProperties(ctx, accountName, shareName, "", fileName) + if err != nil { + t.Fatalf("Error retrieving Top-Level File: %s", err) + } + + if *file.ContentLength != 1024 { + t.Fatalf("Expected the Content-Length to be 1024 but got %d", *file.ContentLength) + } + + if file.ContentEncoding != contentEncoding { + t.Fatalf("Expected the Content-Encoding to be %q but got %q", contentEncoding, file.ContentEncoding) + } + + updatedSize := int64(2048) + updatedEncoding := "application/vnd+pandas2" + updatedInput := SetPropertiesInput{ + ContentEncoding: &updatedEncoding, + ContentLength: &updatedSize, + } + if _, err := filesClient.SetProperties(ctx, accountName, shareName, "", fileName, updatedInput); err != nil { + t.Fatalf("Error setting properties: %s", err) + } + + t.Logf("[DEBUG] Re-retrieving Properties for the Top-Level File..") + file, err = filesClient.GetProperties(ctx, accountName, shareName, "", fileName) + if err != nil { + t.Fatalf("Error retrieving Top-Level File: %s", err) + } + + if *file.ContentLength != 2048 { + t.Fatalf("Expected the Content-Length to be 1024 but got %d", *file.ContentLength) + } + + if file.ContentEncoding != updatedEncoding { + t.Fatalf("Expected the Content-Encoding to be %q but got %q", updatedEncoding, file.ContentEncoding) + } + + t.Logf("[DEBUG] Setting MetaData..") + metaData := map[string]string{ + "hello": "there", + } + if _, err := filesClient.SetMetaData(ctx, accountName, shareName, "", fileName, metaData); err != nil { + t.Fatalf("Error setting MetaData: %s", err) + } + + t.Logf("[DEBUG] Retrieving MetaData..") + retrievedMetaData, err := filesClient.GetMetaData(ctx, accountName, shareName, "", fileName) + if err != nil { + t.Fatalf("Error retrieving MetaData: %s", err) + } + if len(retrievedMetaData.MetaData) != 1 { + t.Fatalf("Expected 1 item but got %d", len(retrievedMetaData.MetaData)) + } + if retrievedMetaData.MetaData["hello"] != "there" { + t.Fatalf("Expected `hello` to be `there` but got %q", retrievedMetaData.MetaData["hello"]) + } + + t.Logf("[DEBUG] Re-Setting MetaData..") + metaData = map[string]string{ + "hello": "there", + "second": "thing", + } + if _, err := filesClient.SetMetaData(ctx, accountName, shareName, "", fileName, metaData); err != nil { + t.Fatalf("Error setting MetaData: %s", err) + } + + t.Logf("[DEBUG] Re-Retrieving MetaData..") + retrievedMetaData, err = filesClient.GetMetaData(ctx, accountName, shareName, "", fileName) + if err != nil { + t.Fatalf("Error retrieving MetaData: %s", err) + } + if len(retrievedMetaData.MetaData) != 2 { + t.Fatalf("Expected 2 items but got %d", len(retrievedMetaData.MetaData)) + } + if retrievedMetaData.MetaData["hello"] != "there" { + t.Fatalf("Expected `hello` to be `there` but got %q", retrievedMetaData.MetaData["hello"]) + } + if retrievedMetaData.MetaData["second"] != "thing" { + t.Fatalf("Expected `second` to be `thing` but got %q", retrievedMetaData.MetaData["second"]) + } + + t.Logf("[DEBUG] Deleting Top Level File..") + if _, err := filesClient.Delete(ctx, accountName, shareName, "", fileName); err != nil { + t.Fatalf("Error deleting Top-Level File: %s", err) + } +} diff --git a/storage/2017-07-29/file/files/metadata_get.go b/storage/2017-07-29/file/files/metadata_get.go new file mode 100644 index 0000000..fd62f90 --- /dev/null +++ b/storage/2017-07-29/file/files/metadata_get.go @@ -0,0 +1,111 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetMetaDataResult struct { + autorest.Response + + MetaData map[string]string +} + +// GetMetaData returns the MetaData for the specified File. +func (client Client) GetMetaData(ctx context.Context, accountName, shareName, path, fileName string) (result GetMetaDataResult, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "GetMetaData", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "GetMetaData", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "GetMetaData", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "GetMetaData", "`fileName` cannot be an empty string.") + } + + req, err := client.GetMetaDataPreparer(ctx, accountName, shareName, path, fileName) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "GetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.GetMetaDataSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "GetMetaData", resp, "Failure sending request") + return + } + + result, err = client.GetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "GetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// GetMetaDataPreparer prepares the GetMetaData request. +func (client Client) GetMetaDataPreparer(ctx context.Context, accountName, shareName, path, fileName string) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetMetaDataSender sends the GetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetMetaDataResponder handles the response to the GetMetaData request. The method always +// closes the http.Response Body. +func (client Client) GetMetaDataResponder(resp *http.Response) (result GetMetaDataResult, err error) { + if resp != nil && resp.Header != nil { + result.MetaData = metadata.ParseFromHeaders(resp.Header) + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + //metadata.ByParsingFromHeaders(&result.MetaData), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/files/metadata_set.go b/storage/2017-07-29/file/files/metadata_set.go new file mode 100644 index 0000000..41e3ffc --- /dev/null +++ b/storage/2017-07-29/file/files/metadata_set.go @@ -0,0 +1,105 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// SetMetaData updates the specified File to have the specified MetaData. +func (client Client) SetMetaData(ctx context.Context, accountName, shareName, path, fileName string, metaData map[string]string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "SetMetaData", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "SetMetaData", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "SetMetaData", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "SetMetaData", "`fileName` cannot be an empty string.") + } + if err := metadata.Validate(metaData); err != nil { + return result, validation.NewError("files.Client", "SetMetaData", fmt.Sprintf("`metaData` is not valid: %s.", err)) + } + + req, err := client.SetMetaDataPreparer(ctx, accountName, shareName, path, fileName, metaData) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "SetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.SetMetaDataSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "SetMetaData", resp, "Failure sending request") + return + } + + result, err = client.SetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "SetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// SetMetaDataPreparer prepares the SetMetaData request. +func (client Client) SetMetaDataPreparer(ctx context.Context, accountName, shareName, path, fileName string, metaData map[string]string) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = metadata.SetIntoHeaders(headers, metaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetMetaDataSender sends the SetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetMetaDataResponder handles the response to the SetMetaData request. The method always +// closes the http.Response Body. +func (client Client) SetMetaDataResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/files/properties_get.go b/storage/2017-07-29/file/files/properties_get.go new file mode 100644 index 0000000..c6a0c39 --- /dev/null +++ b/storage/2017-07-29/file/files/properties_get.go @@ -0,0 +1,144 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetResult struct { + autorest.Response + + CacheControl string + ContentDisposition string + ContentEncoding string + ContentLanguage string + ContentLength *int64 + ContentMD5 string + ContentType string + CopyID string + CopyStatus string + CopySource string + CopyProgress string + CopyStatusDescription string + CopyCompletionTime string + Encrypted bool + + MetaData map[string]string +} + +// GetProperties returns the Properties for the specified file +func (client Client) GetProperties(ctx context.Context, accountName, shareName, path, fileName string) (result GetResult, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "GetProperties", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "GetProperties", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "GetProperties", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "GetProperties", "`fileName` cannot be an empty string.") + } + + req, err := client.GetPropertiesPreparer(ctx, accountName, shareName, path, fileName) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "GetProperties", nil, "Failure preparing request") + return + } + + resp, err := client.GetPropertiesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "GetProperties", resp, "Failure sending request") + return + } + + result, err = client.GetPropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "GetProperties", resp, "Failure responding to request") + return + } + + return +} + +// GetPropertiesPreparer prepares the GetProperties request. +func (client Client) GetPropertiesPreparer(ctx context.Context, accountName, shareName, path, fileName string) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsHead(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetPropertiesSender sends the GetProperties request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetPropertiesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetPropertiesResponder handles the response to the GetProperties request. The method always +// closes the http.Response Body. +func (client Client) GetPropertiesResponder(resp *http.Response) (result GetResult, err error) { + if resp != nil && resp.Header != nil { + result.CacheControl = resp.Header.Get("Cache-Control") + result.ContentDisposition = resp.Header.Get("Content-Disposition") + result.ContentEncoding = resp.Header.Get("Content-Encoding") + result.ContentLanguage = resp.Header.Get("Content-Language") + result.ContentMD5 = resp.Header.Get("x-ms-content-md5") + result.ContentType = resp.Header.Get("Content-Type") + result.CopyID = resp.Header.Get("x-ms-copy-id") + result.CopyProgress = resp.Header.Get("x-ms-copy-progress") + result.CopySource = resp.Header.Get("x-ms-copy-source") + result.CopyStatus = resp.Header.Get("x-ms-copy-status") + result.CopyStatusDescription = resp.Header.Get("x-ms-copy-status-description") + result.CopyCompletionTime = resp.Header.Get("x-ms-copy-completion-time") + result.Encrypted = strings.EqualFold(resp.Header.Get("x-ms-server-encrypted"), "true") + result.MetaData = metadata.ParseFromHeaders(resp.Header) + + contentLengthRaw := resp.Header.Get("Content-Length") + if contentLengthRaw != "" { + contentLength, err := strconv.Atoi(contentLengthRaw) + if err != nil { + return result, fmt.Errorf("Error parsing %q for Content-Length as an integer: %s", contentLengthRaw, err) + } + contentLengthI64 := int64(contentLength) + result.ContentLength = &contentLengthI64 + } + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/files/properties_set.go b/storage/2017-07-29/file/files/properties_set.go new file mode 100644 index 0000000..79fffc2 --- /dev/null +++ b/storage/2017-07-29/file/files/properties_set.go @@ -0,0 +1,160 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type SetPropertiesInput struct { + // Resizes a file to the specified size. + // If the specified byte value is less than the current size of the file, + // then all ranges above the specified byte value are cleared. + ContentLength *int64 + + // Modifies the cache control string for the file. + // If this property is not specified on the request, then the property will be cleared for the file. + // Subsequent calls to Get File Properties will not return this property, + // unless it is explicitly set on the file again. + ContentControl *string + + // Sets the file’s Content-Disposition header. + // If this property is not specified on the request, then the property will be cleared for the file. + // Subsequent calls to Get File Properties will not return this property, + // unless it is explicitly set on the file again. + ContentDisposition *string + + // Sets the file's content encoding. + // If this property is not specified on the request, then the property will be cleared for the file. + // Subsequent calls to Get File Properties will not return this property, + // unless it is explicitly set on the file again. + ContentEncoding *string + + // Sets the file's content language. + // If this property is not specified on the request, then the property will be cleared for the file. + // Subsequent calls to Get File Properties will not return this property, + // unless it is explicitly set on the file again. + ContentLanguage *string + + // Sets the file's MD5 hash. + // If this property is not specified on the request, then the property will be cleared for the file. + // Subsequent calls to Get File Properties will not return this property, + // unless it is explicitly set on the file again. + ContentMD5 *string + + // Sets the file's content type. + // If this property is not specified on the request, then the property will be cleared for the file. + // Subsequent calls to Get File Properties will not return this property, + // unless it is explicitly set on the file again. + ContentType *string +} + +// SetProperties sets the specified properties on the specified File +func (client Client) SetProperties(ctx context.Context, accountName, shareName, path, fileName string, input SetPropertiesInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "SetProperties", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "SetProperties", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "SetProperties", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "SetProperties", "`fileName` cannot be an empty string.") + } + + req, err := client.SetPropertiesPreparer(ctx, accountName, shareName, path, fileName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "SetProperties", nil, "Failure preparing request") + return + } + + resp, err := client.SetPropertiesSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "SetProperties", resp, "Failure sending request") + return + } + + result, err = client.SetPropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "SetProperties", resp, "Failure responding to request") + return + } + + return +} + +// SetPropertiesPreparer prepares the SetProperties request. +func (client Client) SetPropertiesPreparer(ctx context.Context, accountName, shareName, path, fileName string, input SetPropertiesInput) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-type": "file", + } + + if input.ContentControl != nil { + headers["x-ms-cache-control"] = *input.ContentControl + } + if input.ContentDisposition != nil { + headers["x-ms-content-disposition"] = *input.ContentDisposition + } + if input.ContentEncoding != nil { + headers["x-ms-content-encoding"] = *input.ContentEncoding + } + if input.ContentLanguage != nil { + headers["x-ms-content-language"] = *input.ContentLanguage + } + if input.ContentLength != nil { + headers["x-ms-content-length"] = *input.ContentLength + } + if input.ContentMD5 != nil { + headers["x-ms-content-md5"] = *input.ContentMD5 + } + if input.ContentType != nil { + headers["x-ms-content-type"] = *input.ContentType + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetPropertiesSender sends the SetProperties request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetPropertiesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetPropertiesResponder handles the response to the SetProperties request. The method always +// closes the http.Response Body. +func (client Client) SetPropertiesResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/files/range_clear.go b/storage/2017-07-29/file/files/range_clear.go new file mode 100644 index 0000000..5d8145f --- /dev/null +++ b/storage/2017-07-29/file/files/range_clear.go @@ -0,0 +1,112 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type ClearByteRangeInput struct { + StartBytes int64 + EndBytes int64 +} + +// ClearByteRange clears the specified Byte Range from within the specified File +func (client Client) ClearByteRange(ctx context.Context, accountName, shareName, path, fileName string, input ClearByteRangeInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "ClearByteRange", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "ClearByteRange", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "ClearByteRange", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "ClearByteRange", "`fileName` cannot be an empty string.") + } + if input.StartBytes < 0 { + return result, validation.NewError("files.Client", "ClearByteRange", "`input.StartBytes` must be greater or equal to 0.") + } + if input.EndBytes <= 0 { + return result, validation.NewError("files.Client", "ClearByteRange", "`input.EndBytes` must be greater than 0.") + } + + req, err := client.ClearByteRangePreparer(ctx, accountName, shareName, path, fileName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "ClearByteRange", nil, "Failure preparing request") + return + } + + resp, err := client.ClearByteRangeSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "ClearByteRange", resp, "Failure sending request") + return + } + + result, err = client.ClearByteRangeResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "ClearByteRange", resp, "Failure responding to request") + return + } + + return +} + +// ClearByteRangePreparer prepares the ClearByteRange request. +func (client Client) ClearByteRangePreparer(ctx context.Context, accountName, shareName, path, fileName string, input ClearByteRangeInput) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "range"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-write": "clear", + "x-ms-range": fmt.Sprintf("bytes=%d-%d", input.StartBytes, input.EndBytes), + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ClearByteRangeSender sends the ClearByteRange request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ClearByteRangeSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ClearByteRangeResponder handles the response to the ClearByteRange request. The method always +// closes the http.Response Body. +func (client Client) ClearByteRangeResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/files/range_get.go b/storage/2017-07-29/file/files/range_get.go new file mode 100644 index 0000000..733d3f5 --- /dev/null +++ b/storage/2017-07-29/file/files/range_get.go @@ -0,0 +1,121 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetByteRangeInput struct { + StartBytes int64 + EndBytes int64 +} + +type GetByteRangeResult struct { + autorest.Response + + Contents []byte +} + +// GetByteRange returns the specified Byte Range from the specified File. +func (client Client) GetByteRange(ctx context.Context, accountName, shareName, path, fileName string, input GetByteRangeInput) (result GetByteRangeResult, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "GetByteRange", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "GetByteRange", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "GetByteRange", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "GetByteRange", "`fileName` cannot be an empty string.") + } + if input.StartBytes < 0 { + return result, validation.NewError("files.Client", "GetByteRange", "`input.StartBytes` must be greater or equal to 0.") + } + if input.EndBytes <= 0 { + return result, validation.NewError("files.Client", "GetByteRange", "`input.EndBytes` must be greater than 0.") + } + expectedBytes := input.EndBytes - input.StartBytes + if expectedBytes < (4 * 1024) { + return result, validation.NewError("files.Client", "GetByteRange", "Requested Byte Range must be at least 4KB.") + } + if expectedBytes > (4 * 1024 * 1024) { + return result, validation.NewError("files.Client", "GetByteRange", "Requested Byte Range must be at most 4MB.") + } + + req, err := client.GetByteRangePreparer(ctx, accountName, shareName, path, fileName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "GetByteRange", nil, "Failure preparing request") + return + } + + resp, err := client.GetByteRangeSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "GetByteRange", resp, "Failure sending request") + return + } + + result, err = client.GetByteRangeResponder(resp, expectedBytes) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "GetByteRange", resp, "Failure responding to request") + return + } + + return +} + +// GetByteRangePreparer prepares the GetByteRange request. +func (client Client) GetByteRangePreparer(ctx context.Context, accountName, shareName, path, fileName string, input GetByteRangeInput) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-range": fmt.Sprintf("bytes=%d-%d", input.StartBytes, input.EndBytes-1), + } + + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetByteRangeSender sends the GetByteRange request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetByteRangeSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetByteRangeResponder handles the response to the GetByteRange request. The method always +// closes the http.Response Body. +func (client Client) GetByteRangeResponder(resp *http.Response, length int64) (result GetByteRangeResult, err error) { + result.Contents = make([]byte, length) + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusPartialContent), + autorest.ByUnmarshallingBytes(&result.Contents), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/files/range_get_file.go b/storage/2017-07-29/file/files/range_get_file.go new file mode 100644 index 0000000..9e5be17 --- /dev/null +++ b/storage/2017-07-29/file/files/range_get_file.go @@ -0,0 +1,128 @@ +package files + +import ( + "context" + "fmt" + "log" + "math" + "runtime" + "sync" + + "github.com/Azure/go-autorest/autorest" +) + +// GetFile is a helper method to download a file by chunking it automatically +func (client Client) GetFile(ctx context.Context, accountName, shareName, path, fileName string, parallelism int) (result autorest.Response, outputBytes []byte, err error) { + + // first look up the file and check out how many bytes it is + file, e := client.GetProperties(ctx, accountName, shareName, path, fileName) + if err != nil { + result = file.Response + err = e + return + } + + if file.ContentLength == nil { + err = fmt.Errorf("Content-Length was nil!") + return + } + + length := int64(*file.ContentLength) + chunkSize := int64(4 * 1024 * 1024) // 4MB + + if chunkSize > length { + chunkSize = length + } + + // then split that up into chunks and retrieve it retrieve it into the 'results' set + chunks := int(math.Ceil(float64(length) / float64(chunkSize))) + workerCount := parallelism * runtime.NumCPU() + if workerCount > chunks { + workerCount = chunks + } + + var waitGroup sync.WaitGroup + waitGroup.Add(workerCount) + + results := make([]*downloadFileChunkResult, chunks) + errors := make(chan error, chunkSize) + + for i := 0; i < chunks; i++ { + go func(i int) { + log.Printf("[DEBUG] Downloading Chunk %d of %d", i+1, chunks) + + dfci := downloadFileChunkInput{ + thisChunk: i, + chunkSize: chunkSize, + fileSize: length, + } + + result, err := client.downloadFileChunk(ctx, accountName, shareName, path, fileName, dfci) + if err != nil { + errors <- err + waitGroup.Done() + return + } + + // if there's no error, we should have bytes, so this is safe + results[i] = result + + waitGroup.Done() + }(i) + } + waitGroup.Wait() + + // TODO: we should switch to hashicorp/multi-error here + if len(errors) > 0 { + err = fmt.Errorf("Error downloading file: %s", <-errors) + return + } + + // then finally put it all together, in order and return it + output := make([]byte, length) + for _, v := range results { + copy(output[v.startBytes:v.endBytes], v.bytes) + } + + outputBytes = output + return +} + +type downloadFileChunkInput struct { + thisChunk int + chunkSize int64 + fileSize int64 +} + +type downloadFileChunkResult struct { + startBytes int64 + endBytes int64 + bytes []byte +} + +func (client Client) downloadFileChunk(ctx context.Context, accountName, shareName, path, fileName string, input downloadFileChunkInput) (*downloadFileChunkResult, error) { + startBytes := input.chunkSize * int64(input.thisChunk) + endBytes := startBytes + input.chunkSize + + // the last chunk may exceed the size of the file + remaining := input.fileSize - startBytes + if input.chunkSize > remaining { + endBytes = startBytes + remaining + } + + getInput := GetByteRangeInput{ + StartBytes: startBytes, + EndBytes: endBytes, + } + result, err := client.GetByteRange(ctx, accountName, shareName, path, fileName, getInput) + if err != nil { + return nil, fmt.Errorf("Error putting bytes: %s", err) + } + + output := downloadFileChunkResult{ + startBytes: startBytes, + endBytes: endBytes, + bytes: result.Contents, + } + return &output, nil +} diff --git a/storage/2017-07-29/file/files/range_get_file_test.go b/storage/2017-07-29/file/files/range_get_file_test.go new file mode 100644 index 0000000..3c0162a --- /dev/null +++ b/storage/2017-07-29/file/files/range_get_file_test.go @@ -0,0 +1,108 @@ +package files + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2017-07-29/file/shares" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestGetSmallFile(t *testing.T) { + // the purpose of this test is to verify that the small, single-chunked file gets downloaded correctly + testGetFile(t, "small-file.png", "image/png") +} + +func TestGetLargeFile(t *testing.T) { + // the purpose of this test is to verify that the large, multi-chunked file gets downloaded correctly + testGetFile(t, "blank-large-file.dmg", "application/x-apple-diskimage") +} + +func testGetFile(t *testing.T, fileName string, contentType string) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + shareName := fmt.Sprintf("share-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + sharesClient := shares.NewWithEnvironment(client.Environment) + sharesClient.Client = client.PrepareWithAuthorizer(sharesClient.Client, storageAuth) + + input := shares.CreateInput{ + QuotaInGB: 10, + } + _, err = sharesClient.Create(ctx, accountName, shareName, input) + if err != nil { + t.Fatalf("Error creating fileshare: %s", err) + } + defer sharesClient.Delete(ctx, accountName, shareName, false) + + filesClient := NewWithEnvironment(client.Environment) + filesClient.Client = client.PrepareWithAuthorizer(filesClient.Client, storageAuth) + + // store files outside of this directory, since they're reused + file, err := os.Open("../../../testdata/" + fileName) + if err != nil { + t.Fatalf("Error opening: %s", err) + } + + info, err := file.Stat() + if err != nil { + t.Fatalf("Error 'stat'-ing: %s", err) + } + + t.Logf("[DEBUG] Creating Top Level File..") + createFileInput := CreateInput{ + ContentLength: info.Size(), + ContentType: &contentType, + } + if _, err := filesClient.Create(ctx, accountName, shareName, "", fileName, createFileInput); err != nil { + t.Fatalf("Error creating Top-Level File: %s", err) + } + + t.Logf("[DEBUG] Uploading File..") + if err := filesClient.PutFile(ctx, accountName, shareName, "", fileName, file, 4); err != nil { + t.Fatalf("Error uploading File: %s", err) + } + + t.Logf("[DEBUG] Downloading file..") + _, downloadedBytes, err := filesClient.GetFile(ctx, accountName, shareName, "", fileName, 4) + if err != nil { + t.Fatalf("Error downloading file: %s", err) + } + + t.Logf("[DEBUG] Asserting the files are the same size..") + expectedBytes := make([]byte, info.Size()) + file.Read(expectedBytes) + if len(expectedBytes) != len(downloadedBytes) { + t.Fatalf("Expected %d bytes but got %d", len(expectedBytes), len(downloadedBytes)) + } + + t.Logf("[DEBUG] Asserting the files are the same content-wise..") + // overkill, but it's this or shasum-ing + for i := int64(0); i < info.Size(); i++ { + if expectedBytes[i] != downloadedBytes[i] { + t.Fatalf("Expected byte %d to be %q but got %q", i, expectedBytes[i], downloadedBytes[i]) + } + } + + t.Logf("[DEBUG] Deleting Top Level File..") + if _, err := filesClient.Delete(ctx, accountName, shareName, "", fileName); err != nil { + t.Fatalf("Error deleting Top-Level File: %s", err) + } + +} diff --git a/storage/2017-07-29/file/files/range_put.go b/storage/2017-07-29/file/files/range_put.go new file mode 100644 index 0000000..77fe101 --- /dev/null +++ b/storage/2017-07-29/file/files/range_put.go @@ -0,0 +1,129 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type PutByteRangeInput struct { + StartBytes int64 + EndBytes int64 + + // Content is the File Contents for the specified range + // which can be at most 4MB + Content []byte +} + +// PutByteRange puts the specified Byte Range in the specified File. +func (client Client) PutByteRange(ctx context.Context, accountName, shareName, path, fileName string, input PutByteRangeInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "PutByteRange", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "PutByteRange", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "PutByteRange", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "PutByteRange", "`fileName` cannot be an empty string.") + } + if input.StartBytes < 0 { + return result, validation.NewError("files.Client", "PutByteRange", "`input.StartBytes` must be greater or equal to 0.") + } + if input.EndBytes <= 0 { + return result, validation.NewError("files.Client", "PutByteRange", "`input.EndBytes` must be greater than 0.") + } + + expectedBytes := input.EndBytes - input.StartBytes + actualBytes := len(input.Content) + if expectedBytes != int64(actualBytes) { + return result, validation.NewError("files.Client", "PutByteRange", fmt.Sprintf("The specified byte-range (%d) didn't match the content size (%d).", expectedBytes, actualBytes)) + } + if expectedBytes < (4 * 1024) { + return result, validation.NewError("files.Client", "PutByteRange", "Specified Byte Range must be at least 4KB.") + } + + if expectedBytes > (4 * 1024 * 1024) { + return result, validation.NewError("files.Client", "PutByteRange", "Specified Byte Range must be at most 4MB.") + } + + req, err := client.PutByteRangePreparer(ctx, accountName, shareName, path, fileName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "PutByteRange", nil, "Failure preparing request") + return + } + + resp, err := client.PutByteRangeSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "PutByteRange", resp, "Failure sending request") + return + } + + result, err = client.PutByteRangeResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "PutByteRange", resp, "Failure responding to request") + return + } + + return +} + +// PutByteRangePreparer prepares the PutByteRange request. +func (client Client) PutByteRangePreparer(ctx context.Context, accountName, shareName, path, fileName string, input PutByteRangeInput) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "range"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-write": "update", + "x-ms-range": fmt.Sprintf("bytes=%d-%d", input.StartBytes, input.EndBytes-1), + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters), + autorest.WithBytes(&input.Content)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutByteRangeSender sends the PutByteRange request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutByteRangeSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutByteRangeResponder handles the response to the PutByteRange request. The method always +// closes the http.Response Body. +func (client Client) PutByteRangeResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/files/range_put_file.go b/storage/2017-07-29/file/files/range_put_file.go new file mode 100644 index 0000000..a39cd37 --- /dev/null +++ b/storage/2017-07-29/file/files/range_put_file.go @@ -0,0 +1,107 @@ +package files + +import ( + "context" + "fmt" + "io" + "log" + "math" + "os" + "runtime" + "sync" + + "github.com/Azure/go-autorest/autorest" +) + +// PutFile is a helper method which takes a file, and automatically chunks it up, rather than having to do this yourself +func (client Client) PutFile(ctx context.Context, accountName, shareName, path, fileName string, file *os.File, parallelism int) error { + fileInfo, err := file.Stat() + if err != nil { + return fmt.Errorf("Error loading file info: %s", err) + } + + fileSize := fileInfo.Size() + chunkSize := 4 * 1024 * 1024 // 4MB + if chunkSize > int(fileSize) { + chunkSize = int(fileSize) + } + chunks := int(math.Ceil(float64(fileSize) / float64(chunkSize*1.0))) + + workerCount := parallelism * runtime.NumCPU() + if workerCount > chunks { + workerCount = chunks + } + + var waitGroup sync.WaitGroup + waitGroup.Add(workerCount) + errors := make(chan error, chunkSize) + + for i := 0; i < chunks; i++ { + go func(i int) { + log.Printf("[DEBUG] Chunk %d of %d", i+1, chunks) + + uci := uploadChunkInput{ + thisChunk: i, + chunkSize: chunkSize, + fileSize: fileSize, + } + + _, err := client.uploadChunk(ctx, accountName, shareName, path, fileName, uci, file) + if err != nil { + errors <- err + waitGroup.Done() + return + } + + waitGroup.Done() + return + }(i) + } + waitGroup.Wait() + + // TODO: we should switch to hashicorp/multi-error here + if len(errors) > 0 { + return fmt.Errorf("Error uploading file: %s", <-errors) + } + + return nil +} + +type uploadChunkInput struct { + thisChunk int + chunkSize int + fileSize int64 +} + +func (client Client) uploadChunk(ctx context.Context, accountName, shareName, path, fileName string, input uploadChunkInput, file *os.File) (result autorest.Response, err error) { + startBytes := int64(input.chunkSize * input.thisChunk) + endBytes := startBytes + int64(input.chunkSize) + + // the last size may exceed the size of the file + remaining := input.fileSize - startBytes + if int64(input.chunkSize) > remaining { + endBytes = startBytes + remaining + } + + bytesToRead := int(endBytes) - int(startBytes) + bytes := make([]byte, bytesToRead) + + _, err = file.ReadAt(bytes, startBytes) + if err != nil { + if err != io.EOF { + return result, fmt.Errorf("Error reading bytes: %s", err) + } + } + + putBytesInput := PutByteRangeInput{ + StartBytes: startBytes, + EndBytes: endBytes, + Content: bytes, + } + result, err = client.PutByteRange(ctx, accountName, shareName, path, fileName, putBytesInput) + if err != nil { + return result, fmt.Errorf("Error putting bytes: %s", err) + } + + return +} diff --git a/storage/2017-07-29/file/files/range_put_file_test.go b/storage/2017-07-29/file/files/range_put_file_test.go new file mode 100644 index 0000000..6708614 --- /dev/null +++ b/storage/2017-07-29/file/files/range_put_file_test.go @@ -0,0 +1,86 @@ +package files + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2017-07-29/file/shares" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestPutSmallFile(t *testing.T) { + // the purpose of this test is to ensure that a small file (< 4MB) is a single chunk + testPutFile(t, "small-file.png", "image/png") +} + +func TestPutLargeFile(t *testing.T) { + // the purpose of this test is to ensure that large files (> 4MB) are chunked + testPutFile(t, "blank-large-file.dmg", "application/x-apple-diskimage") +} + +func testPutFile(t *testing.T, fileName string, contentType string) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + shareName := fmt.Sprintf("share-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + sharesClient := shares.NewWithEnvironment(client.Environment) + sharesClient.Client = client.PrepareWithAuthorizer(sharesClient.Client, storageAuth) + + input := shares.CreateInput{ + QuotaInGB: 10, + } + _, err = sharesClient.Create(ctx, accountName, shareName, input) + if err != nil { + t.Fatalf("Error creating fileshare: %s", err) + } + defer sharesClient.Delete(ctx, accountName, shareName, false) + + filesClient := NewWithEnvironment(client.Environment) + filesClient.Client = client.PrepareWithAuthorizer(filesClient.Client, storageAuth) + + // store files outside of this directory, since they're reused + file, err := os.Open("../../../testdata/" + fileName) + if err != nil { + t.Fatalf("Error opening: %s", err) + } + + info, err := file.Stat() + if err != nil { + t.Fatalf("Error 'stat'-ing: %s", err) + } + + t.Logf("[DEBUG] Creating Top Level File..") + createFileInput := CreateInput{ + ContentLength: info.Size(), + ContentType: &contentType, + } + if _, err := filesClient.Create(ctx, accountName, shareName, "", fileName, createFileInput); err != nil { + t.Fatalf("Error creating Top-Level File: %s", err) + } + + t.Logf("[DEBUG] Uploading File..") + if err := filesClient.PutFile(ctx, accountName, shareName, "", fileName, file, 4); err != nil { + t.Fatalf("Error uploading File: %s", err) + } + + t.Logf("[DEBUG] Deleting Top Level File..") + if _, err := filesClient.Delete(ctx, accountName, shareName, "", fileName); err != nil { + t.Fatalf("Error deleting Top-Level File: %s", err) + } +} diff --git a/storage/2017-07-29/file/files/ranges_list.go b/storage/2017-07-29/file/files/ranges_list.go new file mode 100644 index 0000000..ea309f9 --- /dev/null +++ b/storage/2017-07-29/file/files/ranges_list.go @@ -0,0 +1,114 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type ListRangesResult struct { + autorest.Response + + Ranges []Range `xml:"Range"` +} + +type Range struct { + Start string `xml:"Start"` + End string `xml:"End"` +} + +// ListRanges returns the list of valid ranges for the specified File. +func (client Client) ListRanges(ctx context.Context, accountName, shareName, path, fileName string) (result ListRangesResult, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "ListRanges", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "ListRanges", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "ListRanges", "`shareName` must be a lower-cased string.") + } + if path == "" { + return result, validation.NewError("files.Client", "ListRanges", "`path` cannot be an empty string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "ListRanges", "`fileName` cannot be an empty string.") + } + + req, err := client.ListRangesPreparer(ctx, accountName, shareName, path, fileName) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "ListRanges", nil, "Failure preparing request") + return + } + + resp, err := client.ListRangesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "ListRanges", resp, "Failure sending request") + return + } + + result, err = client.ListRangesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "ListRanges", resp, "Failure responding to request") + return + } + + return +} + +// ListRangesPreparer prepares the ListRanges request. +func (client Client) ListRangesPreparer(ctx context.Context, accountName, shareName, path, fileName string) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "rangelist"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ListRangesSender sends the ListRanges request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ListRangesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ListRangesResponder handles the response to the ListRanges request. The method always +// closes the http.Response Body. +func (client Client) ListRangesResponder(resp *http.Response) (result ListRangesResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/files/resource_id.go b/storage/2017-07-29/file/files/resource_id.go new file mode 100644 index 0000000..ed1208d --- /dev/null +++ b/storage/2017-07-29/file/files/resource_id.go @@ -0,0 +1,64 @@ +package files + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given File +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, shareName, directoryName, filePath string) string { + domain := endpoints.GetFileEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s/%s/%s", domain, shareName, directoryName, filePath) +} + +type ResourceID struct { + AccountName string + DirectoryName string + FileName string + ShareName string +} + +// ParseResourceID parses the specified Resource ID and returns an object +// which can be used to interact with Files within a Storage Share. +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://account1.file.core.chinacloudapi.cn/share1/directory1/file1.txt + // example: https://account1.file.core.chinacloudapi.cn/share1/directory1/directory2/file1.txt + + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + path := strings.TrimPrefix(uri.Path, "/") + segments := strings.Split(path, "/") + if len(segments) == 0 { + return nil, fmt.Errorf("Expected the path to contain segments but got none") + } + + shareName := segments[0] + fileName := segments[len(segments)-1] + + directoryName := strings.TrimPrefix(path, shareName) + directoryName = strings.TrimPrefix(directoryName, "/") + directoryName = strings.TrimSuffix(directoryName, fileName) + directoryName = strings.TrimSuffix(directoryName, "/") + return &ResourceID{ + AccountName: *accountName, + ShareName: shareName, + DirectoryName: directoryName, + FileName: fileName, + }, nil +} diff --git a/storage/2017-07-29/file/files/resource_id_test.go b/storage/2017-07-29/file/files/resource_id_test.go new file mode 100644 index 0000000..1b521ac --- /dev/null +++ b/storage/2017-07-29/file/files/resource_id_test.go @@ -0,0 +1,131 @@ +package files + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.file.core.chinacloudapi.cn/share1/directory1/file1.txt", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.file.core.cloudapi.de/share1/directory1/file1.txt", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.file.core.windows.net/share1/directory1/file1.txt", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.file.core.usgovcloudapi.net/share1/directory1/file1.txt", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "share1", "directory1", "file1.txt") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.file.core.chinacloudapi.cn/share1/directory1/file1.txt", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.file.core.cloudapi.de/share1/directory1/file1.txt", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.file.core.windows.net/share1/directory1/file1.txt", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.file.core.usgovcloudapi.net/share1/directory1/file1.txt", + }, + } + + t.Logf("[DEBUG] Top Level Files") + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.ShareName != "share1" { + t.Fatalf("Expected Share Name to be `share1` but got %q", actual.ShareName) + } + if actual.DirectoryName != "directory1" { + t.Fatalf("Expected Directory Name to be `directory1` but got %q", actual.DirectoryName) + } + if actual.FileName != "file1.txt" { + t.Fatalf("Expected File Name to be `file1.txt` but got %q", actual.FileName) + } + } + + testData = []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.file.core.chinacloudapi.cn/share1/directory1/directory2/file1.txt", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.file.core.cloudapi.de/share1/directory1/directory2/file1.txt", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.file.core.windows.net/share1/directory1/directory2/file1.txt", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.file.core.usgovcloudapi.net/share1/directory1/directory2/file1.txt", + }, + } + + t.Logf("[DEBUG] Nested Files") + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.ShareName != "share1" { + t.Fatalf("Expected Share Name to be `share1` but got %q", actual.ShareName) + } + if actual.DirectoryName != "directory1/directory2" { + t.Fatalf("Expected Directory Name to be `directory1/directory2` but got %q", actual.DirectoryName) + } + if actual.FileName != "file1.txt" { + t.Fatalf("Expected File Name to be `file1.txt` but got %q", actual.FileName) + } + } +} diff --git a/storage/2017-07-29/file/files/version.go b/storage/2017-07-29/file/files/version.go new file mode 100644 index 0000000..c3604d3 --- /dev/null +++ b/storage/2017-07-29/file/files/version.go @@ -0,0 +1,14 @@ +package files + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2017-07-29" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2017-07-29/file/shares/README.md b/storage/2017-07-29/file/shares/README.md new file mode 100644 index 0000000..53c72a0 --- /dev/null +++ b/storage/2017-07-29/file/shares/README.md @@ -0,0 +1,42 @@ +## File Storage Shares SDK for API version 2017-07-29 + +This package allows you to interact with the Shares File Storage API + +### Supported Authorizers + +* SharedKeyLite (Blob, File & Queue) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2017-07-29/file/shares" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + shareName := "myshare" + + storageAuth := autorest.NewSharedKeyLiteAuthorizer(accountName, storageAccountKey) + sharesClient := shares.New() + sharesClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + input := shares.CreateInput{ + QuotaInGB: 2, + } + if _, err := sharesClient.Create(ctx, accountName, shareName, input); err != nil { + return fmt.Errorf("Error creating Share: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2017-07-29/file/shares/acl_get.go b/storage/2017-07-29/file/shares/acl_get.go new file mode 100644 index 0000000..ea6ff4c --- /dev/null +++ b/storage/2017-07-29/file/shares/acl_get.go @@ -0,0 +1,98 @@ +package shares + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetACLResult struct { + autorest.Response + + SignedIdentifiers []SignedIdentifier `xml:"SignedIdentifier"` +} + +// GetACL get the Access Control List for the specified Storage Share +func (client Client) GetACL(ctx context.Context, accountName, shareName string) (result GetACLResult, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "GetACL", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "GetACL", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "GetACL", "`shareName` must be a lower-cased string.") + } + + req, err := client.GetACLPreparer(ctx, accountName, shareName) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetACL", nil, "Failure preparing request") + return + } + + resp, err := client.GetACLSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "GetACL", resp, "Failure sending request") + return + } + + result, err = client.GetACLResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetACL", resp, "Failure responding to request") + return + } + + return +} + +// GetACLPreparer prepares the GetACL request. +func (client Client) GetACLPreparer(ctx context.Context, accountName, shareName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + "comp": autorest.Encode("query", "acl"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetACLSender sends the GetACL request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetACLSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetACLResponder handles the response to the GetACL request. The method always +// closes the http.Response Body. +func (client Client) GetACLResponder(resp *http.Response) (result GetACLResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/shares/acl_set.go b/storage/2017-07-29/file/shares/acl_set.go new file mode 100644 index 0000000..18d1788 --- /dev/null +++ b/storage/2017-07-29/file/shares/acl_set.go @@ -0,0 +1,103 @@ +package shares + +import ( + "context" + "encoding/xml" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type setAcl struct { + SignedIdentifiers []SignedIdentifier `xml:"SignedIdentifier"` + + XMLName xml.Name `xml:"SignedIdentifiers"` +} + +// SetACL sets the specified Access Control List on the specified Storage Share +func (client Client) SetACL(ctx context.Context, accountName, shareName string, acls []SignedIdentifier) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "SetACL", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "SetACL", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "SetACL", "`shareName` must be a lower-cased string.") + } + + req, err := client.SetACLPreparer(ctx, accountName, shareName, acls) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "SetACL", nil, "Failure preparing request") + return + } + + resp, err := client.SetACLSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "SetACL", resp, "Failure sending request") + return + } + + result, err = client.SetACLResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "SetACL", resp, "Failure responding to request") + return + } + + return +} + +// SetACLPreparer prepares the SetACL request. +func (client Client) SetACLPreparer(ctx context.Context, accountName, shareName string, acls []SignedIdentifier) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + "comp": autorest.Encode("query", "acl"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + input := setAcl{ + SignedIdentifiers: acls, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers), + autorest.WithXML(&input)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetACLSender sends the SetACL request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetACLSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetACLResponder handles the response to the SetACL request. The method always +// closes the http.Response Body. +func (client Client) SetACLResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/shares/client.go b/storage/2017-07-29/file/shares/client.go new file mode 100644 index 0000000..4f3a6f9 --- /dev/null +++ b/storage/2017-07-29/file/shares/client.go @@ -0,0 +1,25 @@ +package shares + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for File Storage Shares. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithEnvironment creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2017-07-29/file/shares/create.go b/storage/2017-07-29/file/shares/create.go new file mode 100644 index 0000000..84fd40d --- /dev/null +++ b/storage/2017-07-29/file/shares/create.go @@ -0,0 +1,109 @@ +package shares + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type CreateInput struct { + // Specifies the maximum size of the share, in gigabytes. + // Must be greater than 0, and less than or equal to 5TB (5120). + QuotaInGB int + + MetaData map[string]string +} + +// Create creates the specified Storage Share within the specified Storage Account +func (client Client) Create(ctx context.Context, accountName, shareName string, input CreateInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "Create", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "Create", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "Create", "`shareName` must be a lower-cased string.") + } + if input.QuotaInGB <= 0 || input.QuotaInGB > 5120 { + return result, validation.NewError("shares.Client", "Create", "`input.QuotaInGB` must be greater than 0, and less than/equal to 5TB (5120 GB)") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("shares.Client", "Create", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.CreatePreparer(ctx, accountName, shareName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "Create", nil, "Failure preparing request") + return + } + + resp, err := client.CreateSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "Create", resp, "Failure sending request") + return + } + + result, err = client.CreateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "Create", resp, "Failure responding to request") + return + } + + return +} + +// CreatePreparer prepares the Create request. +func (client Client) CreatePreparer(ctx context.Context, accountName, shareName string, input CreateInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "share"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-share-quota": input.QuotaInGB, + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CreateSender sends the Create request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CreateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CreateResponder handles the response to the Create request. The method always +// closes the http.Response Body. +func (client Client) CreateResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/shares/delete.go b/storage/2017-07-29/file/shares/delete.go new file mode 100644 index 0000000..70ef985 --- /dev/null +++ b/storage/2017-07-29/file/shares/delete.go @@ -0,0 +1,94 @@ +package shares + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Delete deletes the specified Storage Share from within a Storage Account +func (client Client) Delete(ctx context.Context, accountName, shareName string, deleteSnapshots bool) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "Delete", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "Delete", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "Delete", "`shareName` must be a lower-cased string.") + } + + req, err := client.DeletePreparer(ctx, accountName, shareName, deleteSnapshots) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName, shareName string, deleteSnapshots bool) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "share"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if deleteSnapshots { + headers["x-ms-delete-snapshots"] = "include" + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/shares/lifecycle_test.go b/storage/2017-07-29/file/shares/lifecycle_test.go new file mode 100644 index 0000000..fbab96d --- /dev/null +++ b/storage/2017-07-29/file/shares/lifecycle_test.go @@ -0,0 +1,152 @@ +package shares + +import ( + "context" + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestSharesLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + shareName := fmt.Sprintf("share-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + sharesClient := NewWithEnvironment(client.Environment) + sharesClient.Client = client.PrepareWithAuthorizer(sharesClient.Client, storageAuth) + + input := CreateInput{ + QuotaInGB: 1, + } + _, err = sharesClient.Create(ctx, accountName, shareName, input) + if err != nil { + t.Fatalf("Error creating fileshare: %s", err) + } + + snapshot, err := sharesClient.CreateSnapshot(ctx, accountName, shareName, CreateSnapshotInput{}) + if err != nil { + t.Fatalf("Error taking snapshot: %s", err) + } + t.Logf("Snapshot Date Time: %s", snapshot.SnapshotDateTime) + + snapshotDetails, err := sharesClient.GetSnapshot(ctx, accountName, shareName, snapshot.SnapshotDateTime) + if err != nil { + t.Fatalf("Error retrieving snapshot: %s", err) + } + + t.Logf("MetaData: %s", snapshotDetails.MetaData) + + _, err = sharesClient.DeleteSnapshot(ctx, accountName, shareName, snapshot.SnapshotDateTime) + if err != nil { + t.Fatalf("Error deleting snapshot: %s", err) + } + + stats, err := sharesClient.GetStats(ctx, accountName, shareName) + if err != nil { + t.Fatalf("Error retrieving stats: %s", err) + } + + if stats.ShareUsageBytes != 0 { + t.Fatalf("Expected `stats.ShareUsageBytes` to be 0 but got: %d", stats.ShareUsageBytes) + } + + share, err := sharesClient.GetProperties(ctx, accountName, shareName) + if err != nil { + t.Fatalf("Error retrieving share: %s", err) + } + if share.ShareQuota != 1 { + t.Fatalf("Expected Quota to be 1 but got: %d", share.ShareQuota) + } + + _, err = sharesClient.SetProperties(ctx, accountName, shareName, 5) + if err != nil { + t.Fatalf("Error updating quota: %s", err) + } + + share, err = sharesClient.GetProperties(ctx, accountName, shareName) + if err != nil { + t.Fatalf("Error retrieving share: %s", err) + } + if share.ShareQuota != 5 { + t.Fatalf("Expected Quota to be 5 but got: %d", share.ShareQuota) + } + + updatedMetaData := map[string]string{ + "hello": "world", + } + _, err = sharesClient.SetMetaData(ctx, accountName, shareName, updatedMetaData) + if err != nil { + t.Fatalf("Erorr setting metadata: %s", err) + } + + result, err := sharesClient.GetMetaData(ctx, accountName, shareName) + if err != nil { + t.Fatalf("Error retrieving metadata: %s", err) + } + + if result.MetaData["hello"] != "world" { + t.Fatalf("Expected metadata `hello` to be `world` but got: %q", result.MetaData["hello"]) + } + if len(result.MetaData) != 1 { + t.Fatalf("Expected metadata to be 1 item but got: %s", result.MetaData) + } + + acls, err := sharesClient.GetACL(ctx, accountName, shareName) + if err != nil { + t.Fatalf("Error retrieving ACL's: %s", err) + } + if len(acls.SignedIdentifiers) != 0 { + t.Fatalf("Expected 0 identifiers but got %d", len(acls.SignedIdentifiers)) + } + + updatedAcls := []SignedIdentifier{ + { + Id: "abc123", + AccessPolicy: AccessPolicy{ + Start: "2020-07-01T08:49:37.0000000Z", + Expiry: "2020-07-01T09:49:37.0000000Z", + Permission: "rwd", + }, + }, + { + Id: "bcd234", + AccessPolicy: AccessPolicy{ + Start: "2020-07-01T08:49:37.0000000Z", + Expiry: "2020-07-01T09:49:37.0000000Z", + Permission: "rwd", + }, + }, + } + _, err = sharesClient.SetACL(ctx, accountName, shareName, updatedAcls) + if err != nil { + t.Fatalf("Error setting ACL's: %s", err) + } + + acls, err = sharesClient.GetACL(ctx, accountName, shareName) + if err != nil { + t.Fatalf("Error retrieving ACL's: %s", err) + } + if len(acls.SignedIdentifiers) != 2 { + t.Fatalf("Expected 2 identifiers but got %d", len(acls.SignedIdentifiers)) + } + + _, err = sharesClient.Delete(ctx, accountName, shareName, false) + if err != nil { + t.Fatalf("Error deleting Share: %s", err) + } +} diff --git a/storage/2017-07-29/file/shares/metadata_get.go b/storage/2017-07-29/file/shares/metadata_get.go new file mode 100644 index 0000000..9fa4d9f --- /dev/null +++ b/storage/2017-07-29/file/shares/metadata_get.go @@ -0,0 +1,102 @@ +package shares + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetMetaDataResult struct { + autorest.Response + + MetaData map[string]string +} + +// GetMetaData returns the MetaData associated with the specified Storage Share +func (client Client) GetMetaData(ctx context.Context, accountName, shareName string) (result GetMetaDataResult, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "GetMetaData", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "GetMetaData", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "GetMetaData", "`shareName` must be a lower-cased string.") + } + + req, err := client.GetMetaDataPreparer(ctx, accountName, shareName) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.GetMetaDataSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "GetMetaData", resp, "Failure sending request") + return + } + + result, err = client.GetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// GetMetaDataPreparer prepares the GetMetaData request. +func (client Client) GetMetaDataPreparer(ctx context.Context, accountName, shareName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + "comp": autorest.Encode("query", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetMetaDataSender sends the GetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetMetaDataResponder handles the response to the GetMetaData request. The method always +// closes the http.Response Body. +func (client Client) GetMetaDataResponder(resp *http.Response) (result GetMetaDataResult, err error) { + if resp.Header != nil { + result.MetaData = metadata.ParseFromHeaders(resp.Header) + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/shares/metadata_set.go b/storage/2017-07-29/file/shares/metadata_set.go new file mode 100644 index 0000000..7e64e60 --- /dev/null +++ b/storage/2017-07-29/file/shares/metadata_set.go @@ -0,0 +1,97 @@ +package shares + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// SetMetaData sets the MetaData on the specified Storage Share +func (client Client) SetMetaData(ctx context.Context, accountName, shareName string, metaData map[string]string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "SetMetaData", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "SetMetaData", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "SetMetaData", "`shareName` must be a lower-cased string.") + } + if err := metadata.Validate(metaData); err != nil { + return result, validation.NewError("shares.Client", "SetMetaData", fmt.Sprintf("`metadata` is not valid: %s.", err)) + } + + req, err := client.SetMetaDataPreparer(ctx, accountName, shareName, metaData) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "SetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.SetMetaDataSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "SetMetaData", resp, "Failure sending request") + return + } + + result, err = client.SetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "SetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// SetMetaDataPreparer prepares the SetMetaData request. +func (client Client) SetMetaDataPreparer(ctx context.Context, accountName, shareName string, metaData map[string]string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + "comp": autorest.Encode("query", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + headers = metadata.SetIntoHeaders(headers, metaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetPropertiesSetMetaDataSender sends the SetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetMetaDataResponder handles the response to the SetMetaData request. The method always +// closes the http.Response Body. +func (client Client) SetMetaDataResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/shares/models.go b/storage/2017-07-29/file/shares/models.go new file mode 100644 index 0000000..31ef7c2 --- /dev/null +++ b/storage/2017-07-29/file/shares/models.go @@ -0,0 +1,12 @@ +package shares + +type SignedIdentifier struct { + Id string `xml:"Id"` + AccessPolicy AccessPolicy `xml:"AccessPolicy"` +} + +type AccessPolicy struct { + Start string `xml:"Start"` + Expiry string `xml:"Expiry"` + Permission string `xml:"Permission"` +} diff --git a/storage/2017-07-29/file/shares/properties_get.go b/storage/2017-07-29/file/shares/properties_get.go new file mode 100644 index 0000000..80e26a4 --- /dev/null +++ b/storage/2017-07-29/file/shares/properties_get.go @@ -0,0 +1,111 @@ +package shares + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetPropertiesResult struct { + autorest.Response + + MetaData map[string]string + ShareQuota int +} + +// GetProperties returns the properties about the specified Storage Share +func (client Client) GetProperties(ctx context.Context, accountName, shareName string) (result GetPropertiesResult, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "GetProperties", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "GetProperties", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "GetProperties", "`shareName` must be a lower-cased string.") + } + + req, err := client.GetPropertiesPreparer(ctx, accountName, shareName) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetProperties", nil, "Failure preparing request") + return + } + + resp, err := client.GetPropertiesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "GetProperties", resp, "Failure sending request") + return + } + + result, err = client.GetPropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetProperties", resp, "Failure responding to request") + return + } + + return +} + +// GetPropertiesPreparer prepares the GetProperties request. +func (client Client) GetPropertiesPreparer(ctx context.Context, accountName, shareName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetPropertiesSender sends the GetProperties request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetPropertiesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetPropertiesResponder handles the response to the GetProperties request. The method always +// closes the http.Response Body. +func (client Client) GetPropertiesResponder(resp *http.Response) (result GetPropertiesResult, err error) { + if resp.Header != nil { + result.MetaData = metadata.ParseFromHeaders(resp.Header) + + quotaRaw := resp.Header.Get("x-ms-share-quota") + quota, e := strconv.Atoi(quotaRaw) + if e != nil { + return result, fmt.Errorf("Error converting %q to an integer: %s", quotaRaw, err) + } + result.ShareQuota = quota + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/shares/properties_set.go b/storage/2017-07-29/file/shares/properties_set.go new file mode 100644 index 0000000..4553e5e --- /dev/null +++ b/storage/2017-07-29/file/shares/properties_set.go @@ -0,0 +1,95 @@ +package shares + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// SetProperties lets you update the Quota for the specified Storage Share +func (client Client) SetProperties(ctx context.Context, accountName, shareName string, newQuotaGB int) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "SetProperties", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "SetProperties", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "SetProperties", "`shareName` must be a lower-cased string.") + } + if newQuotaGB <= 0 || newQuotaGB > 5120 { + return result, validation.NewError("shares.Client", "SetProperties", "`newQuotaGB` must be greater than 0, and less than/equal to 5TB (5120 GB)") + } + + req, err := client.SetPropertiesPreparer(ctx, accountName, shareName, newQuotaGB) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "SetProperties", nil, "Failure preparing request") + return + } + + resp, err := client.SetPropertiesSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "SetProperties", resp, "Failure sending request") + return + } + + result, err = client.SetPropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "SetProperties", resp, "Failure responding to request") + return + } + + return +} + +// SetPropertiesPreparer prepares the SetProperties request. +func (client Client) SetPropertiesPreparer(ctx context.Context, accountName, shareName string, quotaGB int) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + "comp": autorest.Encode("query", "properties"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-share-quota": quotaGB, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetPropertiesSender sends the SetProperties request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetPropertiesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetPropertiesResponder handles the response to the SetProperties request. The method always +// closes the http.Response Body. +func (client Client) SetPropertiesResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/shares/resource_id.go b/storage/2017-07-29/file/shares/resource_id.go new file mode 100644 index 0000000..bfdcbfd --- /dev/null +++ b/storage/2017-07-29/file/shares/resource_id.go @@ -0,0 +1,46 @@ +package shares + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given File Share +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, shareName string) string { + domain := endpoints.GetFileEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s", domain, shareName) +} + +type ResourceID struct { + AccountName string + ShareName string +} + +// ParseResourceID parses the specified Resource ID and returns an object +// which can be used to interact with the Storage Shares SDK +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://foo.file.core.windows.net/Bar + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + shareName := strings.TrimPrefix(uri.Path, "/") + return &ResourceID{ + AccountName: *accountName, + ShareName: shareName, + }, nil +} diff --git a/storage/2017-07-29/file/shares/resource_id_test.go b/storage/2017-07-29/file/shares/resource_id_test.go new file mode 100644 index 0000000..1b7eea3 --- /dev/null +++ b/storage/2017-07-29/file/shares/resource_id_test.go @@ -0,0 +1,79 @@ +package shares + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.file.core.chinacloudapi.cn/share1", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.file.core.cloudapi.de/share1", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.file.core.windows.net/share1", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.file.core.usgovcloudapi.net/share1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "share1") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.file.core.chinacloudapi.cn/share1", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.file.core.cloudapi.de/share1", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.file.core.windows.net/share1", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.file.core.usgovcloudapi.net/share1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected the account name to be `account1` but got %q", actual.AccountName) + } + + if actual.ShareName != "share1" { + t.Fatalf("Expected the share name to be `share1` but got %q", actual.ShareName) + } + } +} diff --git a/storage/2017-07-29/file/shares/snapshot_create.go b/storage/2017-07-29/file/shares/snapshot_create.go new file mode 100644 index 0000000..0ded38b --- /dev/null +++ b/storage/2017-07-29/file/shares/snapshot_create.go @@ -0,0 +1,115 @@ +package shares + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type CreateSnapshotInput struct { + MetaData map[string]string +} + +type CreateSnapshotResult struct { + autorest.Response + + // This header is a DateTime value that uniquely identifies the share snapshot. + // The value of this header may be used in subsequent requests to access the share snapshot. + // This value is opaque. + SnapshotDateTime string +} + +// CreateSnapshot creates a read-only snapshot of the share +// A share can support creation of 200 share snapshots. Attempting to create more than 200 share snapshots fails with 409 (Conflict). +// Attempting to create a share snapshot while a previous Snapshot Share operation is in progress fails with 409 (Conflict). +func (client Client) CreateSnapshot(ctx context.Context, accountName, shareName string, input CreateSnapshotInput) (result CreateSnapshotResult, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "CreateSnapshot", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "CreateSnapshot", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "CreateSnapshot", "`shareName` must be a lower-cased string.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("shares.Client", "CreateSnapshot", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.CreateSnapshotPreparer(ctx, accountName, shareName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "CreateSnapshot", nil, "Failure preparing request") + return + } + + resp, err := client.CreateSnapshotSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "CreateSnapshot", resp, "Failure sending request") + return + } + + result, err = client.CreateSnapshotResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "CreateSnapshot", resp, "Failure responding to request") + return + } + + return +} + +// CreateSnapshotPreparer prepares the CreateSnapshot request. +func (client Client) CreateSnapshotPreparer(ctx context.Context, accountName, shareName string, input CreateSnapshotInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "snapshot"), + "restype": autorest.Encode("query", "share"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CreateSnapshotSender sends the CreateSnapshot request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CreateSnapshotSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CreateSnapshotResponder handles the response to the CreateSnapshot request. The method always +// closes the http.Response Body. +func (client Client) CreateSnapshotResponder(resp *http.Response) (result CreateSnapshotResult, err error) { + result.SnapshotDateTime = resp.Header.Get("x-ms-snapshot") + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/shares/snapshot_delete.go b/storage/2017-07-29/file/shares/snapshot_delete.go new file mode 100644 index 0000000..1f5d665 --- /dev/null +++ b/storage/2017-07-29/file/shares/snapshot_delete.go @@ -0,0 +1,94 @@ +package shares + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// DeleteSnapshot deletes the specified Snapshot of a Storage Share +func (client Client) DeleteSnapshot(ctx context.Context, accountName, shareName string, shareSnapshot string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "DeleteSnapshot", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "DeleteSnapshot", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "DeleteSnapshot", "`shareName` must be a lower-cased string.") + } + if shareSnapshot == "" { + return result, validation.NewError("shares.Client", "DeleteSnapshot", "`shareSnapshot` cannot be an empty string.") + } + + req, err := client.DeleteSnapshotPreparer(ctx, accountName, shareName, shareSnapshot) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "DeleteSnapshot", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSnapshotSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "DeleteSnapshot", resp, "Failure sending request") + return + } + + result, err = client.DeleteSnapshotResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "DeleteSnapshot", resp, "Failure responding to request") + return + } + + return +} + +// DeleteSnapshotPreparer prepares the DeleteSnapshot request. +func (client Client) DeleteSnapshotPreparer(ctx context.Context, accountName, shareName string, shareSnapshot string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "share"), + "sharesnapshot": autorest.Encode("query", shareSnapshot), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSnapshotSender sends the DeleteSnapshot request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSnapshotSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteSnapshotResponder handles the response to the DeleteSnapshot request. The method always +// closes the http.Response Body. +func (client Client) DeleteSnapshotResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/shares/snapshot_get.go b/storage/2017-07-29/file/shares/snapshot_get.go new file mode 100644 index 0000000..2cf5f16 --- /dev/null +++ b/storage/2017-07-29/file/shares/snapshot_get.go @@ -0,0 +1,105 @@ +package shares + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetSnapshotPropertiesResult struct { + autorest.Response + + MetaData map[string]string +} + +// GetSnapshot gets information about the specified Snapshot of the specified Storage Share +func (client Client) GetSnapshot(ctx context.Context, accountName, shareName, snapshotShare string) (result GetSnapshotPropertiesResult, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "GetSnapshot", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "GetSnapshot", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "GetSnapshot", "`shareName` must be a lower-cased string.") + } + if snapshotShare == "" { + return result, validation.NewError("shares.Client", "GetSnapshot", "`snapshotShare` cannot be an empty string.") + } + + req, err := client.GetSnapshotPreparer(ctx, accountName, shareName, snapshotShare) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetSnapshot", nil, "Failure preparing request") + return + } + + resp, err := client.GetSnapshotSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "GetSnapshot", resp, "Failure sending request") + return + } + + result, err = client.GetSnapshotResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetSnapshot", resp, "Failure responding to request") + return + } + + return +} + +// GetSnapshotPreparer prepares the GetSnapshot request. +func (client Client) GetSnapshotPreparer(ctx context.Context, accountName, shareName, snapshotShare string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + "snapshot": autorest.Encode("query", snapshotShare), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetSnapshotSender sends the GetSnapshot request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetSnapshotSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetSnapshotResponder handles the response to the GetSnapshot request. The method always +// closes the http.Response Body. +func (client Client) GetSnapshotResponder(resp *http.Response) (result GetSnapshotPropertiesResult, err error) { + if resp.Header != nil { + result.MetaData = metadata.ParseFromHeaders(resp.Header) + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/shares/stats.go b/storage/2017-07-29/file/shares/stats.go new file mode 100644 index 0000000..3539ecc --- /dev/null +++ b/storage/2017-07-29/file/shares/stats.go @@ -0,0 +1,100 @@ +package shares + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetStatsResult struct { + autorest.Response + + // The approximate size of the data stored on the share. + // Note that this value may not include all recently created or recently resized files. + ShareUsageBytes int64 `xml:"ShareUsageBytes"` +} + +// GetStats returns information about the specified Storage Share +func (client Client) GetStats(ctx context.Context, accountName, shareName string) (result GetStatsResult, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "GetStats", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "GetStats", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "GetStats", "`shareName` must be a lower-cased string.") + } + + req, err := client.GetStatsPreparer(ctx, accountName, shareName) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetStats", nil, "Failure preparing request") + return + } + + resp, err := client.GetStatsSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "GetStats", resp, "Failure sending request") + return + } + + result, err = client.GetStatsResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetStats", resp, "Failure responding to request") + return + } + + return +} + +// GetStatsPreparer prepares the GetStats request. +func (client Client) GetStatsPreparer(ctx context.Context, accountName, shareName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + "comp": autorest.Encode("query", "stats"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetStatsSender sends the GetStats request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetStatsSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetStatsResponder handles the response to the GetStats request. The method always +// closes the http.Response Body. +func (client Client) GetStatsResponder(resp *http.Response) (result GetStatsResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/file/shares/version.go b/storage/2017-07-29/file/shares/version.go new file mode 100644 index 0000000..f9d82d9 --- /dev/null +++ b/storage/2017-07-29/file/shares/version.go @@ -0,0 +1,14 @@ +package shares + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2017-07-29" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2017-07-29/queue/messages/README.md b/storage/2017-07-29/queue/messages/README.md new file mode 100644 index 0000000..2a75b54 --- /dev/null +++ b/storage/2017-07-29/queue/messages/README.md @@ -0,0 +1,42 @@ +## Queue Storage Messages SDK for API version 2017-07-29 + +This package allows you to interact with the Messages Queue Storage API + +### Supported Authorizers + +* SharedKeyLite (Blob, File & Queue) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2017-07-29/queue/messages" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + queueName := "myqueue" + + storageAuth := autorest.NewSharedKeyLiteAuthorizer(accountName, storageAccountKey) + messagesClient := messages.New() + messagesClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + input := messages.PutInput{ + Message: "hello", + } + if _, err := messagesClient.Put(ctx, accountName, queueName, input); err != nil { + return fmt.Errorf("Error creating Message: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2017-07-29/queue/messages/client.go b/storage/2017-07-29/queue/messages/client.go new file mode 100644 index 0000000..08b1801 --- /dev/null +++ b/storage/2017-07-29/queue/messages/client.go @@ -0,0 +1,25 @@ +package messages + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for Messages. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithEnvironment creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2017-07-29/queue/messages/delete.go b/storage/2017-07-29/queue/messages/delete.go new file mode 100644 index 0000000..1ec0e1a --- /dev/null +++ b/storage/2017-07-29/queue/messages/delete.go @@ -0,0 +1,97 @@ +package messages + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Delete deletes a specific message +func (client Client) Delete(ctx context.Context, accountName, queueName, messageID, popReceipt string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("messages.Client", "Delete", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("messages.Client", "Delete", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("messages.Client", "Delete", "`queueName` must be a lower-cased string.") + } + if messageID == "" { + return result, validation.NewError("messages.Client", "Delete", "`messageID` cannot be an empty string.") + } + if popReceipt == "" { + return result, validation.NewError("messages.Client", "Delete", "`popReceipt` cannot be an empty string.") + } + + req, err := client.DeletePreparer(ctx, accountName, queueName, messageID, popReceipt) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "messages.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName, queueName, messageID, popReceipt string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + "messageID": autorest.Encode("path", messageID), + } + + queryParameters := map[string]interface{}{ + "popreceipt": autorest.Encode("query", popReceipt), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}/messages/{messageID}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/queue/messages/get.go b/storage/2017-07-29/queue/messages/get.go new file mode 100644 index 0000000..4edeb6d --- /dev/null +++ b/storage/2017-07-29/queue/messages/get.go @@ -0,0 +1,112 @@ +package messages + +import ( + "context" + "net/http" + "strings" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetInput struct { + // VisibilityTimeout specifies the new visibility timeout value, in seconds, relative to server time. + // The new value must be larger than or equal to 0, and cannot be larger than 7 days. + VisibilityTimeout *int +} + +// Get retrieves one or more messages from the front of the queue +func (client Client) Get(ctx context.Context, accountName, queueName string, numberOfMessages int, input GetInput) (result QueueMessagesListResult, err error) { + if accountName == "" { + return result, validation.NewError("messages.Client", "Get", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("messages.Client", "Get", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("messages.Client", "Get", "`queueName` must be a lower-cased string.") + } + if numberOfMessages < 1 || numberOfMessages > 32 { + return result, validation.NewError("messages.Client", "Get", "`numberOfMessages` must be between 1 and 32.") + } + if input.VisibilityTimeout != nil { + t := *input.VisibilityTimeout + maxTime := (time.Hour * 24 * 7).Seconds() + if t < 1 || t < int(maxTime) { + return result, validation.NewError("messages.Client", "Get", "`input.VisibilityTimeout` must be larger than or equal to 1 second, and cannot be larger than 7 days.") + } + } + + req, err := client.GetPreparer(ctx, accountName, queueName, numberOfMessages, input) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Get", nil, "Failure preparing request") + return + } + + resp, err := client.GetSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "messages.Client", "Get", resp, "Failure sending request") + return + } + + result, err = client.GetResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Get", resp, "Failure responding to request") + return + } + + return +} + +// GetPreparer prepares the Get request. +func (client Client) GetPreparer(ctx context.Context, accountName, queueName string, numberOfMessages int, input GetInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + } + + queryParameters := map[string]interface{}{ + "numofmessages": autorest.Encode("query", numberOfMessages), + } + + if input.VisibilityTimeout != nil { + queryParameters["visibilitytimeout"] = autorest.Encode("query", *input.VisibilityTimeout) + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}/messages", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetSender sends the Get request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetResponder handles the response to the Get request. The method always +// closes the http.Response Body. +func (client Client) GetResponder(resp *http.Response) (result QueueMessagesListResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + autorest.ByUnmarshallingXML(&result), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/queue/messages/lifecycle_test.go b/storage/2017-07-29/queue/messages/lifecycle_test.go new file mode 100644 index 0000000..4093d9f --- /dev/null +++ b/storage/2017-07-29/queue/messages/lifecycle_test.go @@ -0,0 +1,95 @@ +package messages + +import ( + "context" + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2017-07-29/queue/queues" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestLifeCycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + queueName := fmt.Sprintf("queue-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + queuesClient := queues.NewWithEnvironment(client.Environment) + queuesClient.Client = client.PrepareWithAuthorizer(queuesClient.Client, storageAuth) + + messagesClient := NewWithEnvironment(client.Environment) + messagesClient.Client = client.PrepareWithAuthorizer(messagesClient.Client, storageAuth) + + _, err = queuesClient.Create(ctx, accountName, queueName, map[string]string{}) + if err != nil { + t.Fatalf("Error creating queue: %s", err) + } + defer queuesClient.Delete(ctx, accountName, queueName) + + input := PutInput{ + Message: "ohhai", + } + putResp, err := messagesClient.Put(ctx, accountName, queueName, input) + if err != nil { + t.Fatalf("Error putting message in queue: %s", err) + } + + messageId := (*putResp.QueueMessages)[0].MessageId + popReceipt := (*putResp.QueueMessages)[0].PopReceipt + + _, err = messagesClient.Update(ctx, accountName, queueName, messageId, UpdateInput{ + PopReceipt: popReceipt, + Message: "Updated message", + VisibilityTimeout: 65, + }) + if err != nil { + t.Fatalf("Error updating: %s", err) + } + + for i := 0; i < 5; i++ { + input := PutInput{ + Message: fmt.Sprintf("Message %d", i), + } + _, err := messagesClient.Put(ctx, accountName, queueName, input) + if err != nil { + t.Fatalf("Error putting message %d in queue: %s", i, err) + } + } + + peakedMessages, err := messagesClient.Peek(ctx, accountName, queueName, 3) + if err != nil { + t.Fatalf("Error peaking messages: %s", err) + } + + for _, v := range *peakedMessages.QueueMessages { + t.Logf("Message: %q", v.MessageId) + } + + retrievedMessages, err := messagesClient.Get(ctx, accountName, queueName, 6, GetInput{}) + if err != nil { + t.Fatalf("Error retrieving messages: %s", err) + } + + for _, v := range *retrievedMessages.QueueMessages { + t.Logf("Message: %q", v.MessageId) + + _, err = messagesClient.Delete(ctx, accountName, queueName, v.MessageId, v.PopReceipt) + if err != nil { + t.Fatalf("Error deleting message from queue: %s", err) + } + } +} diff --git a/storage/2017-07-29/queue/messages/models.go b/storage/2017-07-29/queue/messages/models.go new file mode 100644 index 0000000..67815a8 --- /dev/null +++ b/storage/2017-07-29/queue/messages/models.go @@ -0,0 +1,21 @@ +package messages + +import "github.com/Azure/go-autorest/autorest" + +type QueueMessage struct { + MessageText string `xml:"MessageText"` +} + +type QueueMessagesListResult struct { + autorest.Response + + QueueMessages *[]QueueMessageResponse `xml:"QueueMessage"` +} + +type QueueMessageResponse struct { + MessageId string `xml:"MessageId"` + InsertionTime string `xml:"InsertionTime"` + ExpirationTime string `xml:"ExpirationTime"` + PopReceipt string `xml:"PopReceipt"` + TimeNextVisible string `xml:"TimeNextVisible"` +} diff --git a/storage/2017-07-29/queue/messages/peek.go b/storage/2017-07-29/queue/messages/peek.go new file mode 100644 index 0000000..7288bd5 --- /dev/null +++ b/storage/2017-07-29/queue/messages/peek.go @@ -0,0 +1,95 @@ +package messages + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Peek retrieves one or more messages from the front of the queue, but doesn't alter the visibility of the messages +func (client Client) Peek(ctx context.Context, accountName, queueName string, numberOfMessages int) (result QueueMessagesListResult, err error) { + if accountName == "" { + return result, validation.NewError("messages.Client", "Peek", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("messages.Client", "Peek", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("messages.Client", "Peek", "`queueName` must be a lower-cased string.") + } + if numberOfMessages < 1 || numberOfMessages > 32 { + return result, validation.NewError("messages.Client", "Peek", "`numberOfMessages` must be between 1 and 32.") + } + + req, err := client.PeekPreparer(ctx, accountName, queueName, numberOfMessages) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Peek", nil, "Failure preparing request") + return + } + + resp, err := client.PeekSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "messages.Client", "Peek", resp, "Failure sending request") + return + } + + result, err = client.PeekResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Peek", resp, "Failure responding to request") + return + } + + return +} + +// PeekPreparer prepares the Peek request. +func (client Client) PeekPreparer(ctx context.Context, accountName, queueName string, numberOfMessages int) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + } + + queryParameters := map[string]interface{}{ + "numofmessages": autorest.Encode("query", numberOfMessages), + "peekonly": autorest.Encode("query", true), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}/messages", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PeekSender sends the Peek request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PeekSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PeekResponder handles the response to the Peek request. The method always +// closes the http.Response Body. +func (client Client) PeekResponder(resp *http.Response) (result QueueMessagesListResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + autorest.ByUnmarshallingXML(&result), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/queue/messages/put.go b/storage/2017-07-29/queue/messages/put.go new file mode 100644 index 0000000..612b4a1 --- /dev/null +++ b/storage/2017-07-29/queue/messages/put.go @@ -0,0 +1,120 @@ +package messages + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type PutInput struct { + // A message must be in a format that can be included in an XML request with UTF-8 encoding. + // The encoded message can be up to 64 KB in size. + Message string + + // The maximum time-to-live can be any positive number, + // as well as -1 indicating that the message does not expire. + // If this parameter is omitted, the default time-to-live is 7 days. + MessageTtl *int + + // Specifies the new visibility timeout value, in seconds, relative to server time. + // The new value must be larger than or equal to 0, and cannot be larger than 7 days. + // The visibility timeout of a message cannot be set to a value later than the expiry time. + // visibilitytimeout should be set to a value smaller than the time-to-live value. + // If not specified, the default value is 0. + VisibilityTimeout *int +} + +// Put adds a new message to the back of the message queue +func (client Client) Put(ctx context.Context, accountName, queueName string, input PutInput) (result QueueMessagesListResult, err error) { + if accountName == "" { + return result, validation.NewError("messages.Client", "Put", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("messages.Client", "Put", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("messages.Client", "Put", "`queueName` must be a lower-cased string.") + } + + req, err := client.PutPreparer(ctx, accountName, queueName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Put", nil, "Failure preparing request") + return + } + + resp, err := client.PutSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "messages.Client", "Put", resp, "Failure sending request") + return + } + + result, err = client.PutResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Put", resp, "Failure responding to request") + return + } + + return +} + +// PutPreparer prepares the Put request. +func (client Client) PutPreparer(ctx context.Context, accountName, queueName string, input PutInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + } + + queryParameters := map[string]interface{}{} + + if input.MessageTtl != nil { + queryParameters["messagettl"] = autorest.Encode("path", *input.MessageTtl) + } + + if input.VisibilityTimeout != nil { + queryParameters["visibilitytimeout"] = autorest.Encode("path", *input.VisibilityTimeout) + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + body := QueueMessage{ + MessageText: input.Message, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPost(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}/messages", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithXML(body), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutSender sends the Put request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutResponder handles the response to the Put request. The method always +// closes the http.Response Body. +func (client Client) PutResponder(resp *http.Response) (result QueueMessagesListResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + autorest.ByUnmarshallingXML(&result), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/queue/messages/resource_id.go b/storage/2017-07-29/queue/messages/resource_id.go new file mode 100644 index 0000000..7ece98a --- /dev/null +++ b/storage/2017-07-29/queue/messages/resource_id.go @@ -0,0 +1,56 @@ +package messages + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given Message within a Queue +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, queueName, messageID string) string { + domain := endpoints.GetQueueEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s/messages/%s", domain, queueName, messageID) +} + +type ResourceID struct { + AccountName string + QueueName string + MessageID string +} + +// ParseResourceID parses the specified Resource ID and returns an object +// which can be used to interact with the Message within a Queue +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://account1.queue.core.chinacloudapi.cn/queue1/messages/message1 + + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + path := strings.TrimPrefix(uri.Path, "/") + segments := strings.Split(path, "/") + if len(segments) != 3 { + return nil, fmt.Errorf("Expected the path to contain 3 segments but got %d", len(segments)) + } + + queueName := segments[0] + messageID := segments[2] + return &ResourceID{ + AccountName: *accountName, + MessageID: messageID, + QueueName: queueName, + }, nil +} diff --git a/storage/2017-07-29/queue/messages/resource_id_test.go b/storage/2017-07-29/queue/messages/resource_id_test.go new file mode 100644 index 0000000..5053279 --- /dev/null +++ b/storage/2017-07-29/queue/messages/resource_id_test.go @@ -0,0 +1,81 @@ +package messages + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.queue.core.chinacloudapi.cn/queue1/messages/message1", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.queue.core.cloudapi.de/queue1/messages/message1", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.queue.core.windows.net/queue1/messages/message1", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.queue.core.usgovcloudapi.net/queue1/messages/message1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "queue1", "message1") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.queue.core.chinacloudapi.cn/queue1/messages/message1", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.queue.core.cloudapi.de/queue1/messages/message1", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.queue.core.windows.net/queue1/messages/message1", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.queue.core.usgovcloudapi.net/queue1/messages/message1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.QueueName != "queue1" { + t.Fatalf("Expected Queue Name to be `queue1` but got %q", actual.QueueName) + } + if actual.MessageID != "message1" { + t.Fatalf("Expected Message ID to be `message1` but got %q", actual.MessageID) + } + } +} diff --git a/storage/2017-07-29/queue/messages/update.go b/storage/2017-07-29/queue/messages/update.go new file mode 100644 index 0000000..fb10fad --- /dev/null +++ b/storage/2017-07-29/queue/messages/update.go @@ -0,0 +1,115 @@ +package messages + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type UpdateInput struct { + // A message must be in a format that can be included in an XML request with UTF-8 encoding. + // The encoded message can be up to 64 KB in size. + Message string + + // Specifies the valid pop receipt value required to modify this message. + PopReceipt string + + // Specifies the new visibility timeout value, in seconds, relative to server time. + // The new value must be larger than or equal to 0, and cannot be larger than 7 days. + // The visibility timeout of a message cannot be set to a value later than the expiry time. + // A message can be updated until it has been deleted or has expired. + VisibilityTimeout int +} + +// Update updates an existing message based on it's Pop Receipt +func (client Client) Update(ctx context.Context, accountName, queueName string, messageID string, input UpdateInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("messages.Client", "Update", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("messages.Client", "Update", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("messages.Client", "Update", "`queueName` must be a lower-cased string.") + } + if input.PopReceipt == "" { + return result, validation.NewError("messages.Client", "Update", "`input.PopReceipt` cannot be an empty string.") + } + + req, err := client.UpdatePreparer(ctx, accountName, queueName, messageID, input) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Update", nil, "Failure preparing request") + return + } + + resp, err := client.UpdateSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "messages.Client", "Update", resp, "Failure sending request") + return + } + + result, err = client.UpdateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Update", resp, "Failure responding to request") + return + } + + return +} + +// UpdatePreparer prepares the Update request. +func (client Client) UpdatePreparer(ctx context.Context, accountName, queueName string, messageID string, input UpdateInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + "messageID": autorest.Encode("path", messageID), + } + + queryParameters := map[string]interface{}{ + "popreceipt": autorest.Encode("query", input.PopReceipt), + "visibilitytimeout": autorest.Encode("query", input.VisibilityTimeout), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + body := QueueMessage{ + MessageText: input.Message, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}/messages/{messageID}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithXML(body), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// UpdateSender sends the Update request. The method will close the +// http.Response Body if it receives an error. +func (client Client) UpdateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// UpdateResponder handles the response to the Update request. The method always +// closes the http.Response Body. +func (client Client) UpdateResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/queue/messages/version.go b/storage/2017-07-29/queue/messages/version.go new file mode 100644 index 0000000..53be1c0 --- /dev/null +++ b/storage/2017-07-29/queue/messages/version.go @@ -0,0 +1,14 @@ +package messages + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2017-07-29" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2017-07-29/queue/queues/README.md b/storage/2017-07-29/queue/queues/README.md new file mode 100644 index 0000000..fe85b8a --- /dev/null +++ b/storage/2017-07-29/queue/queues/README.md @@ -0,0 +1,42 @@ +## Queue Storage Queues SDK for API version 2017-07-29 + +This package allows you to interact with the Queues Queue Storage API + +### Supported Authorizers + +* SharedKeyLite (Blob, File & Queue) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2017-07-29/queue/queues" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + queueName := "myqueue" + + storageAuth := autorest.NewSharedKeyLiteAuthorizer(accountName, storageAccountKey) + queuesClient := queues.New() + queuesClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + metadata := map[string]string{ + "hello": "world", + } + if _, err := queuesClient.Create(ctx, accountName, queueName, metadata); err != nil { + return fmt.Errorf("Error creating Queue: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2017-07-29/queue/queues/client.go b/storage/2017-07-29/queue/queues/client.go new file mode 100644 index 0000000..2f80085 --- /dev/null +++ b/storage/2017-07-29/queue/queues/client.go @@ -0,0 +1,25 @@ +package queues + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for Queue Storage Shares. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithEnvironment creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2017-07-29/queue/queues/create.go b/storage/2017-07-29/queue/queues/create.go new file mode 100644 index 0000000..f18910a --- /dev/null +++ b/storage/2017-07-29/queue/queues/create.go @@ -0,0 +1,92 @@ +package queues + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// Create creates the specified Queue within the specified Storage Account +func (client Client) Create(ctx context.Context, accountName, queueName string, metaData map[string]string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("queues.Client", "Create", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("queues.Client", "Create", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("queues.Client", "Create", "`queueName` must be a lower-cased string.") + } + if err := metadata.Validate(metaData); err != nil { + return result, validation.NewError("queues.Client", "Create", fmt.Sprintf("`metadata` is not valid: %s.", err)) + } + + req, err := client.CreatePreparer(ctx, accountName, queueName, metaData) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "Create", nil, "Failure preparing request") + return + } + + resp, err := client.CreateSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "queues.Client", "Create", resp, "Failure sending request") + return + } + + result, err = client.CreateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "Create", resp, "Failure responding to request") + return + } + + return +} + +// CreatePreparer prepares the Create request. +func (client Client) CreatePreparer(ctx context.Context, accountName string, queueName string, metaData map[string]string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = metadata.SetIntoHeaders(headers, metaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CreateSender sends the Create request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CreateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CreateResponder handles the response to the Create request. The method always +// closes the http.Response Body. +func (client Client) CreateResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/queue/queues/delete.go b/storage/2017-07-29/queue/queues/delete.go new file mode 100644 index 0000000..5f70595 --- /dev/null +++ b/storage/2017-07-29/queue/queues/delete.go @@ -0,0 +1,85 @@ +package queues + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Delete deletes the specified Queue within the specified Storage Account +func (client Client) Delete(ctx context.Context, accountName, queueName string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("queues.Client", "Delete", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("queues.Client", "Delete", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("queues.Client", "Delete", "`queueName` must be a lower-cased string.") + } + + req, err := client.DeletePreparer(ctx, accountName, queueName) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "queues.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName string, queueName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/queue/queues/lifecycle_test.go b/storage/2017-07-29/queue/queues/lifecycle_test.go new file mode 100644 index 0000000..d24245c --- /dev/null +++ b/storage/2017-07-29/queue/queues/lifecycle_test.go @@ -0,0 +1,93 @@ +package queues + +import ( + "context" + "fmt" + "log" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestQueuesLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + queueName := fmt.Sprintf("queue-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + queuesClient := NewWithEnvironment(client.Environment) + queuesClient.Client = client.PrepareWithAuthorizer(queuesClient.Client, storageAuth) + + // first let's test an empty container + _, err = queuesClient.Create(ctx, accountName, queueName, map[string]string{}) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + + // then let's retrieve it to ensure there's no metadata.. + resp, err := queuesClient.GetMetaData(ctx, accountName, queueName) + if err != nil { + t.Fatalf("Error retrieving MetaData: %s", err) + } + if len(resp.MetaData) != 0 { + t.Fatalf("Expected no MetaData but got: %s", err) + } + + // then let's add some.. + updatedMetaData := map[string]string{ + "band": "panic", + "boots": "the-overpass", + } + _, err = queuesClient.SetMetaData(ctx, accountName, queueName, updatedMetaData) + if err != nil { + t.Fatalf("Error setting MetaData: %s", err) + } + + resp, err = queuesClient.GetMetaData(ctx, accountName, queueName) + if err != nil { + t.Fatalf("Error re-retrieving MetaData: %s", err) + } + + if len(resp.MetaData) != 2 { + t.Fatalf("Expected metadata to have 2 items but got: %s", resp.MetaData) + } + if resp.MetaData["band"] != "panic" { + t.Fatalf("Expected `band` to be `panic` but got: %s", resp.MetaData["band"]) + } + if resp.MetaData["boots"] != "the-overpass" { + t.Fatalf("Expected `boots` to be `the-overpass` but got: %s", resp.MetaData["boots"]) + } + + // and woo let's remove it again + _, err = queuesClient.SetMetaData(ctx, accountName, queueName, map[string]string{}) + if err != nil { + t.Fatalf("Error setting MetaData: %s", err) + } + + resp, err = queuesClient.GetMetaData(ctx, accountName, queueName) + if err != nil { + t.Fatalf("Error retrieving MetaData: %s", err) + } + if len(resp.MetaData) != 0 { + t.Fatalf("Expected no MetaData but got: %s", err) + } + + log.Printf("[DEBUG] Deleting..") + _, err = queuesClient.Delete(ctx, accountName, queueName) + if err != nil { + t.Fatal(fmt.Errorf("Error deleting: %s", err)) + } +} diff --git a/storage/2017-07-29/queue/queues/metadata_get.go b/storage/2017-07-29/queue/queues/metadata_get.go new file mode 100644 index 0000000..9c230b6 --- /dev/null +++ b/storage/2017-07-29/queue/queues/metadata_get.go @@ -0,0 +1,101 @@ +package queues + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetMetaDataResult struct { + autorest.Response + + MetaData map[string]string +} + +// GetMetaData returns the metadata for this Queue +func (client Client) GetMetaData(ctx context.Context, accountName, queueName string) (result GetMetaDataResult, err error) { + if accountName == "" { + return result, validation.NewError("queues.Client", "GetMetaData", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("queues.Client", "GetMetaData", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("queues.Client", "GetMetaData", "`queueName` must be a lower-cased string.") + } + + req, err := client.GetMetaDataPreparer(ctx, accountName, queueName) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "GetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.GetMetaDataSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "queues.Client", "GetMetaData", resp, "Failure sending request") + return + } + + result, err = client.GetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "GetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// GetMetaDataPreparer prepares the GetMetaData request. +func (client Client) GetMetaDataPreparer(ctx context.Context, accountName, queueName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("path", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetMetaDataSender sends the GetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetMetaDataResponder handles the response to the GetMetaData request. The method always +// closes the http.Response Body. +func (client Client) GetMetaDataResponder(resp *http.Response) (result GetMetaDataResult, err error) { + if resp != nil { + result.MetaData = metadata.ParseFromHeaders(resp.Header) + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/queue/queues/metadata_set.go b/storage/2017-07-29/queue/queues/metadata_set.go new file mode 100644 index 0000000..51154a5 --- /dev/null +++ b/storage/2017-07-29/queue/queues/metadata_set.go @@ -0,0 +1,97 @@ +package queues + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// SetMetaData returns the metadata for this Queue +func (client Client) SetMetaData(ctx context.Context, accountName, queueName string, metaData map[string]string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("queues.Client", "SetMetaData", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("queues.Client", "SetMetaData", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("queues.Client", "SetMetaData", "`queueName` must be a lower-cased string.") + } + if err := metadata.Validate(metaData); err != nil { + return result, validation.NewError("queues.Client", "SetMetaData", fmt.Sprintf("`metadata` is not valid: %s.", err)) + } + + req, err := client.SetMetaDataPreparer(ctx, accountName, queueName, metaData) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "SetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.SetMetaDataSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "queues.Client", "SetMetaData", resp, "Failure sending request") + return + } + + result, err = client.SetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "SetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// SetMetaDataPreparer prepares the SetMetaData request. +func (client Client) SetMetaDataPreparer(ctx context.Context, accountName, queueName string, metaData map[string]string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("path", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = metadata.SetIntoHeaders(headers, metaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetMetaDataSender sends the SetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetMetaDataResponder handles the response to the SetMetaData request. The method always +// closes the http.Response Body. +func (client Client) SetMetaDataResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/queue/queues/models.go b/storage/2017-07-29/queue/queues/models.go new file mode 100644 index 0000000..89c2380 --- /dev/null +++ b/storage/2017-07-29/queue/queues/models.go @@ -0,0 +1,42 @@ +package queues + +type StorageServiceProperties struct { + Logging *LoggingConfig `xml:"Logging,omitempty"` + HourMetrics *MetricsConfig `xml:"HourMetrics,omitempty"` + MinuteMetrics *MetricsConfig `xml:"MinuteMetrics,omitempty"` + Cors *Cors `xml:"Cors,omitempty"` +} + +type LoggingConfig struct { + Version string `xml:"Version"` + Delete bool `xml:"Delete"` + Read bool `xml:"Read"` + Write bool `xml:"Write"` + RetentionPolicy RetentionPolicy `xml:"RetentionPolicy"` +} + +type MetricsConfig struct { + Version string `xml:"Version"` + Enabled bool `xml:"Enabled"` + RetentionPolicy RetentionPolicy `xml:"RetentionPolicy"` + + // Element IncludeAPIs is only expected when Metrics is enabled + IncludeAPIs *bool `xml:"IncludeAPIs,omitempty"` +} + +type RetentionPolicy struct { + Enabled bool `xml:"Enabled"` + Days int `xml:"Days"` +} + +type Cors struct { + CorsRule CorsRule `xml:"CorsRule"` +} + +type CorsRule struct { + AllowedOrigins string `xml:"AllowedOrigins"` + AllowedMethods string `xml:"AllowedMethods"` + AllowedHeaders string `xml:"AllowedHeaders` + ExposedHeaders string `xml:"ExposedHeaders"` + MaxAgeInSeconds int `xml:"MaxAgeInSeconds"` +} diff --git a/storage/2017-07-29/queue/queues/resource_id.go b/storage/2017-07-29/queue/queues/resource_id.go new file mode 100644 index 0000000..ee28b8b --- /dev/null +++ b/storage/2017-07-29/queue/queues/resource_id.go @@ -0,0 +1,46 @@ +package queues + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given Queue +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, queueName string) string { + domain := endpoints.GetQueueEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s", domain, queueName) +} + +type ResourceID struct { + AccountName string + QueueName string +} + +// ParseResourceID parses the Resource ID and returns an Object which +// can be used to interact with a Queue within a Storage Account +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://foo.queue.core.windows.net/Bar + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + queueName := strings.TrimPrefix(uri.Path, "/") + return &ResourceID{ + AccountName: *accountName, + QueueName: queueName, + }, nil +} diff --git a/storage/2017-07-29/queue/queues/resource_id_test.go b/storage/2017-07-29/queue/queues/resource_id_test.go new file mode 100644 index 0000000..89323d7 --- /dev/null +++ b/storage/2017-07-29/queue/queues/resource_id_test.go @@ -0,0 +1,79 @@ +package queues + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.queue.core.chinacloudapi.cn/queue1", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.queue.core.cloudapi.de/queue1", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.queue.core.windows.net/queue1", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.queue.core.usgovcloudapi.net/queue1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "queue1") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.queue.core.chinacloudapi.cn/queue1", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.queue.core.cloudapi.de/queue1", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.queue.core.windows.net/queue1", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.queue.core.usgovcloudapi.net/queue1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected the account name to be `account1` but got %q", actual.AccountName) + } + + if actual.QueueName != "queue1" { + t.Fatalf("Expected the queue name to be `queue1` but got %q", actual.QueueName) + } + } +} diff --git a/storage/2017-07-29/queue/queues/version.go b/storage/2017-07-29/queue/queues/version.go new file mode 100644 index 0000000..0142ce3 --- /dev/null +++ b/storage/2017-07-29/queue/queues/version.go @@ -0,0 +1,14 @@ +package queues + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2017-07-29" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2017-07-29/table/entities/README.md b/storage/2017-07-29/table/entities/README.md new file mode 100644 index 0000000..e29382d --- /dev/null +++ b/storage/2017-07-29/table/entities/README.md @@ -0,0 +1,48 @@ +## Table Storage Entities SDK for API version 2017-07-29 + +This package allows you to interact with the Entities Table Storage API + +### Supported Authorizers + +* SharedKeyLite (Table) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2017-07-29/table/entities" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + tableName := "mytable" + + storageAuth := autorest.NewSharedKeyLiteTableAuthorizer(accountName, storageAccountKey) + entitiesClient := entities.New() + entitiesClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + input := entities.InsertEntityInput{ + PartitionKey: "abc", + RowKey: "123", + MetaDataLevel: entities.NoMetaData, + Entity: map[string]interface{}{ + "title": "Don't Kill My Vibe", + "artist": "Sigrid", + }, + } + if _, err := entitiesClient.Insert(ctx, accountName, tableName, input); err != nil { + return fmt.Errorf("Error creating Entity: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2017-07-29/table/entities/client.go b/storage/2017-07-29/table/entities/client.go new file mode 100644 index 0000000..17e9d75 --- /dev/null +++ b/storage/2017-07-29/table/entities/client.go @@ -0,0 +1,25 @@ +package entities + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for Table Storage Shares. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithEnvironment creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2017-07-29/table/entities/delete.go b/storage/2017-07-29/table/entities/delete.go new file mode 100644 index 0000000..83e9188 --- /dev/null +++ b/storage/2017-07-29/table/entities/delete.go @@ -0,0 +1,99 @@ +package entities + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type DeleteEntityInput struct { + // When inserting an entity into a table, you must specify values for the PartitionKey and RowKey system properties. + // Together, these properties form the primary key and must be unique within the table. + // Both the PartitionKey and RowKey values must be string values; each key value may be up to 64 KB in size. + // If you are using an integer value for the key value, you should convert the integer to a fixed-width string, + // because they are canonically sorted. For example, you should convert the value 1 to 0000001 to ensure proper sorting. + RowKey string + PartitionKey string +} + +// Delete deletes an existing entity in a table. +func (client Client) Delete(ctx context.Context, accountName, tableName string, input DeleteEntityInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("entities.Client", "Delete", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("entities.Client", "Delete", "`tableName` cannot be an empty string.") + } + if input.PartitionKey == "" { + return result, validation.NewError("entities.Client", "Delete", "`input.PartitionKey` cannot be an empty string.") + } + if input.RowKey == "" { + return result, validation.NewError("entities.Client", "Delete", "`input.RowKey` cannot be an empty string.") + } + + req, err := client.DeletePreparer(ctx, accountName, tableName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "entities.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName, tableName string, input DeleteEntityInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + "partitionKey": autorest.Encode("path", input.PartitionKey), + "rowKey": autorest.Encode("path", input.RowKey), + } + + headers := map[string]interface{}{ + // TODO: support for eTags + "If-Match": "*", + } + + preparer := autorest.CreatePreparer( + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}(PartitionKey='{partitionKey}', RowKey='{rowKey}')", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/table/entities/get.go b/storage/2017-07-29/table/entities/get.go new file mode 100644 index 0000000..bdb4018 --- /dev/null +++ b/storage/2017-07-29/table/entities/get.go @@ -0,0 +1,108 @@ +package entities + +import ( + "context" + "fmt" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetEntityInput struct { + PartitionKey string + RowKey string + + // The Level of MetaData which should be returned + MetaDataLevel MetaDataLevel +} + +type GetEntityResult struct { + autorest.Response + + Entity map[string]interface{} +} + +// Get queries entities in a table and includes the $filter and $select options. +func (client Client) Get(ctx context.Context, accountName, tableName string, input GetEntityInput) (result GetEntityResult, err error) { + if accountName == "" { + return result, validation.NewError("entities.Client", "Get", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("entities.Client", "Get", "`tableName` cannot be an empty string.") + } + if input.PartitionKey == "" { + return result, validation.NewError("entities.Client", "Get", "`input.PartitionKey` cannot be an empty string.") + } + if input.RowKey == "" { + return result, validation.NewError("entities.Client", "Get", "`input.RowKey` cannot be an empty string.") + } + + req, err := client.GetPreparer(ctx, accountName, tableName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Get", nil, "Failure preparing request") + return + } + + resp, err := client.GetSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "entities.Client", "Get", resp, "Failure sending request") + return + } + + result, err = client.GetResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Get", resp, "Failure responding to request") + return + } + + return +} + +// GetPreparer prepares the Get request. +func (client Client) GetPreparer(ctx context.Context, accountName, tableName string, input GetEntityInput) (*http.Request, error) { + + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + "partitionKey": autorest.Encode("path", input.PartitionKey), + "rowKey": autorest.Encode("path", input.RowKey), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "Accept": fmt.Sprintf("application/json;odata=%s", input.MetaDataLevel), + "DataServiceVersion": "3.0;NetFx", + "MaxDataServiceVersion": "3.0;NetFx", + } + + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}(PartitionKey='{partitionKey}',RowKey='{rowKey}')", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetSender sends the Get request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetResponder handles the response to the Get request. The method always +// closes the http.Response Body. +func (client Client) GetResponder(resp *http.Response) (result GetEntityResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingJSON(&result.Entity), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/table/entities/insert.go b/storage/2017-07-29/table/entities/insert.go new file mode 100644 index 0000000..92b05ce --- /dev/null +++ b/storage/2017-07-29/table/entities/insert.go @@ -0,0 +1,112 @@ +package entities + +import ( + "context" + "fmt" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type InsertEntityInput struct { + // The level of MetaData provided for this Entity + MetaDataLevel MetaDataLevel + + // The Entity which should be inserted, by default all values are strings + // To explicitly type a property, specify the appropriate OData data type by setting + // the m:type attribute within the property definition + Entity map[string]interface{} + + // When inserting an entity into a table, you must specify values for the PartitionKey and RowKey system properties. + // Together, these properties form the primary key and must be unique within the table. + // Both the PartitionKey and RowKey values must be string values; each key value may be up to 64 KB in size. + // If you are using an integer value for the key value, you should convert the integer to a fixed-width string, + // because they are canonically sorted. For example, you should convert the value 1 to 0000001 to ensure proper sorting. + RowKey string + PartitionKey string +} + +// Insert inserts a new entity into a table. +func (client Client) Insert(ctx context.Context, accountName, tableName string, input InsertEntityInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("entities.Client", "Insert", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("entities.Client", "Insert", "`tableName` cannot be an empty string.") + } + if input.PartitionKey == "" { + return result, validation.NewError("entities.Client", "Insert", "`input.PartitionKey` cannot be an empty string.") + } + if input.RowKey == "" { + return result, validation.NewError("entities.Client", "Insert", "`input.RowKey` cannot be an empty string.") + } + + req, err := client.InsertPreparer(ctx, accountName, tableName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Insert", nil, "Failure preparing request") + return + } + + resp, err := client.InsertSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "entities.Client", "Insert", resp, "Failure sending request") + return + } + + result, err = client.InsertResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Insert", resp, "Failure responding to request") + return + } + + return +} + +// InsertPreparer prepares the Insert request. +func (client Client) InsertPreparer(ctx context.Context, accountName, tableName string, input InsertEntityInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "Accept": fmt.Sprintf("application/json;odata=%s", input.MetaDataLevel), + "Prefer": "return-no-content", + } + + input.Entity["PartitionKey"] = input.PartitionKey + input.Entity["RowKey"] = input.RowKey + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/json"), + autorest.AsPost(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}", pathParameters), + autorest.WithJSON(input.Entity), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// InsertSender sends the Insert request. The method will close the +// http.Response Body if it receives an error. +func (client Client) InsertSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// InsertResponder handles the response to the Insert request. The method always +// closes the http.Response Body. +func (client Client) InsertResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/table/entities/insert_or_merge.go b/storage/2017-07-29/table/entities/insert_or_merge.go new file mode 100644 index 0000000..1fb4ed3 --- /dev/null +++ b/storage/2017-07-29/table/entities/insert_or_merge.go @@ -0,0 +1,108 @@ +package entities + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type InsertOrMergeEntityInput struct { + // The Entity which should be inserted, by default all values are strings + // To explicitly type a property, specify the appropriate OData data type by setting + // the m:type attribute within the property definition + Entity map[string]interface{} + + // When inserting an entity into a table, you must specify values for the PartitionKey and RowKey system properties. + // Together, these properties form the primary key and must be unique within the table. + // Both the PartitionKey and RowKey values must be string values; each key value may be up to 64 KB in size. + // If you are using an integer value for the key value, you should convert the integer to a fixed-width string, + // because they are canonically sorted. For example, you should convert the value 1 to 0000001 to ensure proper sorting. + RowKey string + PartitionKey string +} + +// InsertOrMerge updates an existing entity or inserts a new entity if it does not exist in the table. +// Because this operation can insert or update an entity, it is also known as an upsert operation. +func (client Client) InsertOrMerge(ctx context.Context, accountName, tableName string, input InsertOrMergeEntityInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("entities.Client", "InsertOrMerge", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("entities.Client", "InsertOrMerge", "`tableName` cannot be an empty string.") + } + if input.PartitionKey == "" { + return result, validation.NewError("entities.Client", "InsertOrMerge", "`input.PartitionKey` cannot be an empty string.") + } + if input.RowKey == "" { + return result, validation.NewError("entities.Client", "InsertOrMerge", "`input.RowKey` cannot be an empty string.") + } + + req, err := client.InsertOrMergePreparer(ctx, accountName, tableName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "InsertOrMerge", nil, "Failure preparing request") + return + } + + resp, err := client.InsertOrMergeSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "entities.Client", "InsertOrMerge", resp, "Failure sending request") + return + } + + result, err = client.InsertOrMergeResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "InsertOrMerge", resp, "Failure responding to request") + return + } + + return +} + +// InsertOrMergePreparer prepares the InsertOrMerge request. +func (client Client) InsertOrMergePreparer(ctx context.Context, accountName, tableName string, input InsertOrMergeEntityInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + "partitionKey": autorest.Encode("path", input.PartitionKey), + "rowKey": autorest.Encode("path", input.RowKey), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "Accept": "application/json", + "Prefer": "return-no-content", + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/json"), + autorest.AsMerge(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}(PartitionKey='{partitionKey}', RowKey='{rowKey}')", pathParameters), + autorest.WithJSON(input.Entity), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// InsertOrMergeSender sends the InsertOrMerge request. The method will close the +// http.Response Body if it receives an error. +func (client Client) InsertOrMergeSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// InsertOrMergeResponder handles the response to the InsertOrMerge request. The method always +// closes the http.Response Body. +func (client Client) InsertOrMergeResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/table/entities/insert_or_replace.go b/storage/2017-07-29/table/entities/insert_or_replace.go new file mode 100644 index 0000000..036ba5d --- /dev/null +++ b/storage/2017-07-29/table/entities/insert_or_replace.go @@ -0,0 +1,108 @@ +package entities + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type InsertOrReplaceEntityInput struct { + // The Entity which should be inserted, by default all values are strings + // To explicitly type a property, specify the appropriate OData data type by setting + // the m:type attribute within the property definition + Entity map[string]interface{} + + // When inserting an entity into a table, you must specify values for the PartitionKey and RowKey system properties. + // Together, these properties form the primary key and must be unique within the table. + // Both the PartitionKey and RowKey values must be string values; each key value may be up to 64 KB in size. + // If you are using an integer value for the key value, you should convert the integer to a fixed-width string, + // because they are canonically sorted. For example, you should convert the value 1 to 0000001 to ensure proper sorting. + RowKey string + PartitionKey string +} + +// InsertOrReplace replaces an existing entity or inserts a new entity if it does not exist in the table. +// Because this operation can insert or update an entity, it is also known as an upsert operation. +func (client Client) InsertOrReplace(ctx context.Context, accountName, tableName string, input InsertOrReplaceEntityInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("entities.Client", "InsertOrReplace", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("entities.Client", "InsertOrReplace", "`tableName` cannot be an empty string.") + } + if input.PartitionKey == "" { + return result, validation.NewError("entities.Client", "InsertOrReplace", "`input.PartitionKey` cannot be an empty string.") + } + if input.RowKey == "" { + return result, validation.NewError("entities.Client", "InsertOrReplace", "`input.RowKey` cannot be an empty string.") + } + + req, err := client.InsertOrReplacePreparer(ctx, accountName, tableName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "InsertOrReplace", nil, "Failure preparing request") + return + } + + resp, err := client.InsertOrReplaceSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "entities.Client", "InsertOrReplace", resp, "Failure sending request") + return + } + + result, err = client.InsertOrReplaceResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "InsertOrReplace", resp, "Failure responding to request") + return + } + + return +} + +// InsertOrReplacePreparer prepares the InsertOrReplace request. +func (client Client) InsertOrReplacePreparer(ctx context.Context, accountName, tableName string, input InsertOrReplaceEntityInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + "partitionKey": autorest.Encode("path", input.PartitionKey), + "rowKey": autorest.Encode("path", input.RowKey), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "Accept": "application/json", + "Prefer": "return-no-content", + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/json"), + autorest.AsMerge(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}(PartitionKey='{partitionKey}', RowKey='{rowKey}')", pathParameters), + autorest.WithJSON(input.Entity), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// InsertOrReplaceSender sends the InsertOrReplace request. The method will close the +// http.Response Body if it receives an error. +func (client Client) InsertOrReplaceSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// InsertOrReplaceResponder handles the response to the InsertOrReplace request. The method always +// closes the http.Response Body. +func (client Client) InsertOrReplaceResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/table/entities/lifecycle_test.go b/storage/2017-07-29/table/entities/lifecycle_test.go new file mode 100644 index 0000000..8e05049 --- /dev/null +++ b/storage/2017-07-29/table/entities/lifecycle_test.go @@ -0,0 +1,135 @@ +package entities + +import ( + "context" + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2017-07-29/table/tables" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestEntitiesLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + tableName := fmt.Sprintf("table%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteTableAuthorizer(accountName, testData.StorageAccountKey) + tablesClient := tables.NewWithEnvironment(client.Environment) + tablesClient.Client = client.PrepareWithAuthorizer(tablesClient.Client, storageAuth) + + t.Logf("[DEBUG] Creating Table..") + if _, err := tablesClient.Create(ctx, accountName, tableName); err != nil { + t.Fatalf("Error creating Table %q: %s", tableName, err) + } + defer tablesClient.Delete(ctx, accountName, tableName) + + entitiesClient := NewWithEnvironment(client.Environment) + entitiesClient.Client = client.PrepareWithAuthorizer(entitiesClient.Client, storageAuth) + + partitionKey := "hello" + rowKey := "there" + + t.Logf("[DEBUG] Inserting..") + insertInput := InsertEntityInput{ + MetaDataLevel: NoMetaData, + PartitionKey: partitionKey, + RowKey: rowKey, + Entity: map[string]interface{}{ + "hello": "world", + }, + } + if _, err := entitiesClient.Insert(ctx, accountName, tableName, insertInput); err != nil { + t.Logf("Error retrieving: %s", err) + } + + t.Logf("[DEBUG] Insert or Merging..") + insertOrMergeInput := InsertOrMergeEntityInput{ + PartitionKey: partitionKey, + RowKey: rowKey, + Entity: map[string]interface{}{ + "hello": "ther88e", + }, + } + if _, err := entitiesClient.InsertOrMerge(ctx, accountName, tableName, insertOrMergeInput); err != nil { + t.Logf("Error insert/merging: %s", err) + } + + t.Logf("[DEBUG] Insert or Replacing..") + insertOrReplaceInput := InsertOrReplaceEntityInput{ + PartitionKey: partitionKey, + RowKey: rowKey, + Entity: map[string]interface{}{ + "hello": "pandas", + }, + } + if _, err := entitiesClient.InsertOrReplace(ctx, accountName, tableName, insertOrReplaceInput); err != nil { + t.Logf("Error inserting/replacing: %s", err) + } + + t.Logf("[DEBUG] Querying..") + queryInput := QueryEntitiesInput{ + MetaDataLevel: NoMetaData, + } + results, err := entitiesClient.Query(ctx, accountName, tableName, queryInput) + if err != nil { + t.Logf("Error querying: %s", err) + } + + if len(results.Entities) != 1 { + t.Fatalf("Expected 1 item but got %d", len(results.Entities)) + } + + for _, v := range results.Entities { + thisPartitionKey := v["PartitionKey"].(string) + thisRowKey := v["RowKey"].(string) + if partitionKey != thisPartitionKey { + t.Fatalf("Expected Partition Key to be %q but got %q", partitionKey, thisPartitionKey) + } + if rowKey != thisRowKey { + t.Fatalf("Expected Partition Key to be %q but got %q", rowKey, thisRowKey) + } + } + + t.Logf("[DEBUG] Retrieving..") + getInput := GetEntityInput{ + MetaDataLevel: MinimalMetaData, + PartitionKey: partitionKey, + RowKey: rowKey, + } + getResults, err := entitiesClient.Get(ctx, accountName, tableName, getInput) + if err != nil { + t.Logf("Error querying: %s", err) + } + + partitionKey2 := getResults.Entity["PartitionKey"].(string) + rowKey2 := getResults.Entity["RowKey"].(string) + if partitionKey2 != partitionKey { + t.Fatalf("Expected Partition Key to be %q but got %q", partitionKey, partitionKey2) + } + if rowKey2 != rowKey { + t.Fatalf("Expected Row Key to be %q but got %q", rowKey, rowKey2) + } + + t.Logf("[DEBUG] Deleting..") + deleteInput := DeleteEntityInput{ + PartitionKey: partitionKey, + RowKey: rowKey, + } + if _, err := entitiesClient.Delete(ctx, accountName, tableName, deleteInput); err != nil { + t.Logf("Error deleting: %s", err) + } +} diff --git a/storage/2017-07-29/table/entities/models.go b/storage/2017-07-29/table/entities/models.go new file mode 100644 index 0000000..e3c6ccc --- /dev/null +++ b/storage/2017-07-29/table/entities/models.go @@ -0,0 +1,9 @@ +package entities + +type MetaDataLevel string + +var ( + NoMetaData MetaDataLevel = "nometadata" + MinimalMetaData MetaDataLevel = "minimalmetadata" + FullMetaData MetaDataLevel = "fullmetadata" +) diff --git a/storage/2017-07-29/table/entities/query.go b/storage/2017-07-29/table/entities/query.go new file mode 100644 index 0000000..a768b83 --- /dev/null +++ b/storage/2017-07-29/table/entities/query.go @@ -0,0 +1,155 @@ +package entities + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type QueryEntitiesInput struct { + // An optional OData filter + Filter *string + + // An optional comma-separated + PropertyNamesToSelect *[]string + + PartitionKey string + RowKey string + + // The Level of MetaData which should be returned + MetaDataLevel MetaDataLevel + + // The Next Partition Key used to load data from a previous point + NextPartitionKey *string + + // The Next Row Key used to load data from a previous point + NextRowKey *string +} + +type QueryEntitiesResult struct { + autorest.Response + + NextPartitionKey string + NextRowKey string + + MetaData string `json:"odata.metadata,omitempty"` + Entities []map[string]interface{} `json:"value"` +} + +// Query queries entities in a table and includes the $filter and $select options. +func (client Client) Query(ctx context.Context, accountName, tableName string, input QueryEntitiesInput) (result QueryEntitiesResult, err error) { + if accountName == "" { + return result, validation.NewError("entities.Client", "Query", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("entities.Client", "Query", "`tableName` cannot be an empty string.") + } + + req, err := client.QueryPreparer(ctx, accountName, tableName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Query", nil, "Failure preparing request") + return + } + + resp, err := client.QuerySender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "entities.Client", "Query", resp, "Failure sending request") + return + } + + result, err = client.QueryResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Query", resp, "Failure responding to request") + return + } + + return +} + +// QueryPreparer prepares the Query request. +func (client Client) QueryPreparer(ctx context.Context, accountName, tableName string, input QueryEntitiesInput) (*http.Request, error) { + + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + "additionalParameters": "", + } + + //PartitionKey='',RowKey='' + additionalParams := make([]string, 0) + if input.PartitionKey != "" { + additionalParams = append(additionalParams, fmt.Sprintf("PartitionKey='%s'", input.PartitionKey)) + } + if input.RowKey != "" { + additionalParams = append(additionalParams, fmt.Sprintf("RowKey='%s'", input.RowKey)) + } + if len(additionalParams) > 0 { + pathParameters["additionalParameters"] = autorest.Encode("path", strings.Join(additionalParams, ",")) + } + + queryParameters := map[string]interface{}{} + + if input.Filter != nil { + queryParameters["filter"] = autorest.Encode("query", input.Filter) + } + + if input.PropertyNamesToSelect != nil { + queryParameters["$select"] = autorest.Encode("query", strings.Join(*input.PropertyNamesToSelect, ",")) + } + + if input.NextPartitionKey != nil { + queryParameters["NextPartitionKey"] = *input.NextPartitionKey + } + + if input.NextRowKey != nil { + queryParameters["NextRowKey"] = *input.NextRowKey + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "Accept": fmt.Sprintf("application/json;odata=%s", input.MetaDataLevel), + "DataServiceVersion": "3.0;NetFx", + "MaxDataServiceVersion": "3.0;NetFx", + } + + // GET /myaccount/Customers()?$filter=(Rating%20ge%203)%20and%20(Rating%20le%206)&$select=PartitionKey,RowKey,Address,CustomerSince + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}({additionalParameters})", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// QuerySender sends the Query request. The method will close the +// http.Response Body if it receives an error. +func (client Client) QuerySender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// QueryResponder handles the response to the Query request. The method always +// closes the http.Response Body. +func (client Client) QueryResponder(resp *http.Response) (result QueryEntitiesResult, err error) { + if resp != nil && resp.Header != nil { + result.NextPartitionKey = resp.Header.Get("x-ms-continuation-NextPartitionKey") + result.NextRowKey = resp.Header.Get("x-ms-continuation-NextRowKey") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingJSON(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/table/entities/resource_id.go b/storage/2017-07-29/table/entities/resource_id.go new file mode 100644 index 0000000..59366a2 --- /dev/null +++ b/storage/2017-07-29/table/entities/resource_id.go @@ -0,0 +1,91 @@ +package entities + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given Entity +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, tableName, partitionKey, rowKey string) string { + domain := endpoints.GetTableEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s(PartitionKey='%s',RowKey='%s')", domain, tableName, partitionKey, rowKey) +} + +type ResourceID struct { + AccountName string + TableName string + PartitionKey string + RowKey string +} + +// ParseResourceID parses the specified Resource ID and returns an object which +// can be used to look up the specified Entity within the specified Table +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://account1.table.core.chinacloudapi.cn/table1(PartitionKey='partition1',RowKey='row1') + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + // assume there a `Table('')` + path := strings.TrimPrefix(uri.Path, "/") + if !strings.Contains(uri.Path, "(") || !strings.HasSuffix(uri.Path, ")") { + return nil, fmt.Errorf("Expected the Table Name to be in the format `tables(PartitionKey='',RowKey='')` but got %q", path) + } + + // NOTE: honestly this could probably be a RegEx, but this seemed like the simplest way to + // allow these two fields to be specified in either order + indexOfBracket := strings.IndexByte(path, '(') + tableName := path[0:indexOfBracket] + + // trim off the brackets + temp := strings.TrimPrefix(path, fmt.Sprintf("%s(", tableName)) + temp = strings.TrimSuffix(temp, ")") + + dictionary := strings.Split(temp, ",") + partitionKey := "" + rowKey := "" + for _, v := range dictionary { + split := strings.Split(v, "=") + if len(split) != 2 { + return nil, fmt.Errorf("Expected 2 segments but got %d for %q", len(split), v) + } + + key := split[0] + value := strings.TrimSuffix(strings.TrimPrefix(split[1], "'"), "'") + if strings.EqualFold(key, "PartitionKey") { + partitionKey = value + } else if strings.EqualFold(key, "RowKey") { + rowKey = value + } else { + return nil, fmt.Errorf("Unexpected Key %q", key) + } + } + + if partitionKey == "" { + return nil, fmt.Errorf("Expected a PartitionKey but didn't get one") + } + if rowKey == "" { + return nil, fmt.Errorf("Expected a RowKey but didn't get one") + } + + return &ResourceID{ + AccountName: *accountName, + TableName: tableName, + PartitionKey: partitionKey, + RowKey: rowKey, + }, nil +} diff --git a/storage/2017-07-29/table/entities/resource_id_test.go b/storage/2017-07-29/table/entities/resource_id_test.go new file mode 100644 index 0000000..e85af79 --- /dev/null +++ b/storage/2017-07-29/table/entities/resource_id_test.go @@ -0,0 +1,84 @@ +package entities + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.table.core.chinacloudapi.cn/table1(PartitionKey='partition1',RowKey='row1')", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.table.core.cloudapi.de/table1(PartitionKey='partition1',RowKey='row1')", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.table.core.windows.net/table1(PartitionKey='partition1',RowKey='row1')", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.table.core.usgovcloudapi.net/table1(PartitionKey='partition1',RowKey='row1')", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "table1", "partition1", "row1") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.table.core.chinacloudapi.cn/table1(PartitionKey='partition1',RowKey='row1')", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.table.core.cloudapi.de/table1(PartitionKey='partition1',RowKey='row1')", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.table.core.windows.net/table1(PartitionKey='partition1',RowKey='row1')", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.table.core.usgovcloudapi.net/table1(PartitionKey='partition1',RowKey='row1')", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.TableName != "table1" { + t.Fatalf("Expected Table Name to be `table1` but got %q", actual.TableName) + } + if actual.PartitionKey != "partition1" { + t.Fatalf("Expected Partition Key to be `partition1` but got %q", actual.PartitionKey) + } + if actual.RowKey != "row1" { + t.Fatalf("Expected Row Key to be `row1` but got %q", actual.RowKey) + } + } +} diff --git a/storage/2017-07-29/table/entities/version.go b/storage/2017-07-29/table/entities/version.go new file mode 100644 index 0000000..3b8c657 --- /dev/null +++ b/storage/2017-07-29/table/entities/version.go @@ -0,0 +1,14 @@ +package entities + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2017-07-29" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2017-07-29/table/tables/README.md b/storage/2017-07-29/table/tables/README.md new file mode 100644 index 0000000..38f14ed --- /dev/null +++ b/storage/2017-07-29/table/tables/README.md @@ -0,0 +1,39 @@ +## Table Storage Tables SDK for API version 2017-07-29 + +This package allows you to interact with the Tables Table Storage API + +### Supported Authorizers + +* SharedKeyLite (Table) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2017-07-29/table/tables" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + tableName := "mytable" + + storageAuth := autorest.NewSharedKeyLiteTableAuthorizer(accountName, storageAccountKey) + tablesClient := tables.New() + tablesClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + if _, err := tablesClient.Insert(ctx, accountName, tableName); err != nil { + return fmt.Errorf("Error creating Table: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2017-07-29/table/tables/acl_get.go b/storage/2017-07-29/table/tables/acl_get.go new file mode 100644 index 0000000..0ef0000 --- /dev/null +++ b/storage/2017-07-29/table/tables/acl_get.go @@ -0,0 +1,93 @@ +package tables + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetACLResult struct { + autorest.Response + + SignedIdentifiers []SignedIdentifier `xml:"SignedIdentifier"` +} + +// GetACL returns the Access Control List for the specified Table +func (client Client) GetACL(ctx context.Context, accountName, tableName string) (result GetACLResult, err error) { + if accountName == "" { + return result, validation.NewError("tables.Client", "GetACL", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("tables.Client", "GetACL", "`tableName` cannot be an empty string.") + } + + req, err := client.GetACLPreparer(ctx, accountName, tableName) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "GetACL", nil, "Failure preparing request") + return + } + + resp, err := client.GetACLSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "tables.Client", "GetACL", resp, "Failure sending request") + return + } + + result, err = client.GetACLResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "GetACL", resp, "Failure responding to request") + return + } + + return +} + +// GetACLPreparer prepares the GetACL request. +func (client Client) GetACLPreparer(ctx context.Context, accountName, tableName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "acl"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetACLSender sends the GetACL request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetACLSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetACLResponder handles the response to the GetACL request. The method always +// closes the http.Response Body. +func (client Client) GetACLResponder(resp *http.Response) (result GetACLResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/table/tables/acl_set.go b/storage/2017-07-29/table/tables/acl_set.go new file mode 100644 index 0000000..c26bffc --- /dev/null +++ b/storage/2017-07-29/table/tables/acl_set.go @@ -0,0 +1,98 @@ +package tables + +import ( + "context" + "encoding/xml" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type setAcl struct { + SignedIdentifiers []SignedIdentifier `xml:"SignedIdentifier"` + + XMLName xml.Name `xml:"SignedIdentifiers"` +} + +// SetACL sets the specified Access Control List for the specified Table +func (client Client) SetACL(ctx context.Context, accountName, tableName string, acls []SignedIdentifier) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("tables.Client", "SetACL", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("tables.Client", "SetACL", "`tableName` cannot be an empty string.") + } + + req, err := client.SetACLPreparer(ctx, accountName, tableName, acls) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "SetACL", nil, "Failure preparing request") + return + } + + resp, err := client.SetACLSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "tables.Client", "SetACL", resp, "Failure sending request") + return + } + + result, err = client.SetACLResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "SetACL", resp, "Failure responding to request") + return + } + + return +} + +// SetACLPreparer prepares the SetACL request. +func (client Client) SetACLPreparer(ctx context.Context, accountName, tableName string, acls []SignedIdentifier) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "acl"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + input := setAcl{ + SignedIdentifiers: acls, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers), + autorest.WithXML(&input)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetACLSender sends the SetACL request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetACLSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetACLResponder handles the response to the SetACL request. The method always +// closes the http.Response Body. +func (client Client) SetACLResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/table/tables/client.go b/storage/2017-07-29/table/tables/client.go new file mode 100644 index 0000000..56724b9 --- /dev/null +++ b/storage/2017-07-29/table/tables/client.go @@ -0,0 +1,25 @@ +package tables + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for Table Storage Shares. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithEnvironment creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2017-07-29/table/tables/create.go b/storage/2017-07-29/table/tables/create.go new file mode 100644 index 0000000..561f574 --- /dev/null +++ b/storage/2017-07-29/table/tables/create.go @@ -0,0 +1,90 @@ +package tables + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type createTableRequest struct { + TableName string `json:"TableName"` +} + +// Create creates a new table in the storage account. +func (client Client) Create(ctx context.Context, accountName, tableName string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("tables.Client", "Create", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("tables.Client", "Create", "`tableName` cannot be an empty string.") + } + + req, err := client.CreatePreparer(ctx, accountName, tableName) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Create", nil, "Failure preparing request") + return + } + + resp, err := client.CreateSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "tables.Client", "Create", resp, "Failure sending request") + return + } + + result, err = client.CreateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Create", resp, "Failure responding to request") + return + } + + return +} + +// CreatePreparer prepares the Create request. +func (client Client) CreatePreparer(ctx context.Context, accountName, tableName string) (*http.Request, error) { + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + // NOTE: we could support returning metadata here, but it doesn't appear to be directly useful + // vs making a request using the Get methods as-necessary? + "Accept": "application/json;odata=nometadata", + "Prefer": "return-no-content", + } + + body := createTableRequest{ + TableName: tableName, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/json"), + autorest.AsPost(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPath("/Tables"), + autorest.WithJSON(body), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CreateSender sends the Create request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CreateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CreateResponder handles the response to the Create request. The method always +// closes the http.Response Body. +func (client Client) CreateResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/table/tables/delete.go b/storage/2017-07-29/table/tables/delete.go new file mode 100644 index 0000000..5b5ec86 --- /dev/null +++ b/storage/2017-07-29/table/tables/delete.go @@ -0,0 +1,79 @@ +package tables + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Delete deletes the specified table and any data it contains. +func (client Client) Delete(ctx context.Context, accountName, tableName string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("tables.Client", "Delete", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("tables.Client", "Delete", "`tableName` cannot be an empty string.") + } + + req, err := client.DeletePreparer(ctx, accountName, tableName) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "tables.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName, tableName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + } + + // NOTE: whilst the API documentation says that API Version is Optional + // apparently specifying it causes an "invalid content type" to always be returned + // as such we omit it here :shrug: + + preparer := autorest.CreatePreparer( + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/Tables('{tableName}')", pathParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/table/tables/exists.go b/storage/2017-07-29/table/tables/exists.go new file mode 100644 index 0000000..b3a2718 --- /dev/null +++ b/storage/2017-07-29/table/tables/exists.go @@ -0,0 +1,80 @@ +package tables + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Exists checks that the specified table exists +func (client Client) Exists(ctx context.Context, accountName, tableName string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("tables.Client", "Exists", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("tables.Client", "Exists", "`tableName` cannot be an empty string.") + } + + req, err := client.ExistsPreparer(ctx, accountName, tableName) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Exists", nil, "Failure preparing request") + return + } + + resp, err := client.ExistsSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "tables.Client", "Exists", resp, "Failure sending request") + return + } + + result, err = client.ExistsResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Exists", resp, "Failure responding to request") + return + } + + return +} + +// ExistsPreparer prepares the Exists request. +func (client Client) ExistsPreparer(ctx context.Context, accountName, tableName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + } + + // NOTE: whilst the API documentation says that API Version is Optional + // apparently specifying it causes an "invalid content type" to always be returned + // as such we omit it here :shrug: + + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.AsContentType("application/xml"), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/Tables('{tableName}')", pathParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ExistsSender sends the Exists request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ExistsSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ExistsResponder handles the response to the Exists request. The method always +// closes the http.Response Body. +func (client Client) ExistsResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/table/tables/lifecycle_test.go b/storage/2017-07-29/table/tables/lifecycle_test.go new file mode 100644 index 0000000..74ab0fe --- /dev/null +++ b/storage/2017-07-29/table/tables/lifecycle_test.go @@ -0,0 +1,112 @@ +package tables + +import ( + "context" + "fmt" + "log" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestTablesLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + tableName := fmt.Sprintf("table%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteTableAuthorizer(accountName, testData.StorageAccountKey) + tablesClient := NewWithEnvironment(client.Environment) + tablesClient.Client = client.PrepareWithAuthorizer(tablesClient.Client, storageAuth) + + t.Logf("[DEBUG] Creating Table..") + if _, err := tablesClient.Create(ctx, accountName, tableName); err != nil { + t.Fatalf("Error creating Table %q: %s", tableName, err) + } + + // first look it up directly and confirm it's there + t.Logf("[DEBUG] Checking if Table exists..") + if _, err := tablesClient.Exists(ctx, accountName, tableName); err != nil { + t.Fatalf("Error checking if Table %q exists: %s", tableName, err) + } + + // then confirm it exists in the Query too + t.Logf("[DEBUG] Querying for Tables..") + result, err := tablesClient.Query(ctx, accountName, NoMetaData) + if err != nil { + t.Fatalf("Error retrieving Tables: %s", err) + } + found := false + for _, v := range result.Tables { + log.Printf("[DEBUG] Table: %q", v.TableName) + + if v.TableName == tableName { + found = true + } + } + if !found { + t.Fatalf("%q was not found in the Query response!", tableName) + } + + t.Logf("[DEBUG] Setting ACL's for Table %q..", tableName) + acls := []SignedIdentifier{ + { + Id: "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=", + AccessPolicy: AccessPolicy{ + Permission: "raud", + Start: "2020-11-26T08:49:37.0000000Z", + Expiry: "2020-11-27T08:49:37.0000000Z", + }, + }, + } + if _, err := tablesClient.SetACL(ctx, accountName, tableName, acls); err != nil { + t.Fatalf("Error setting ACLs: %s", err) + } + + t.Logf("[DEBUG] Retrieving ACL's for Table %q..", tableName) + retrievedACLs, err := tablesClient.GetACL(ctx, accountName, tableName) + if err != nil { + t.Fatalf("Error retrieving ACLs: %s", err) + } + + if len(retrievedACLs.SignedIdentifiers) != len(acls) { + t.Fatalf("Expected %d but got %q ACLs", len(retrievedACLs.SignedIdentifiers), len(acls)) + } + + for i, retrievedAcl := range retrievedACLs.SignedIdentifiers { + expectedAcl := acls[i] + + if retrievedAcl.Id != expectedAcl.Id { + t.Fatalf("Expected ID to be %q but got %q", retrievedAcl.Id, expectedAcl.Id) + } + + if retrievedAcl.AccessPolicy.Start != expectedAcl.AccessPolicy.Start { + t.Fatalf("Expected Start to be %q but got %q", retrievedAcl.AccessPolicy.Start, expectedAcl.AccessPolicy.Start) + } + + if retrievedAcl.AccessPolicy.Expiry != expectedAcl.AccessPolicy.Expiry { + t.Fatalf("Expected Expiry to be %q but got %q", retrievedAcl.AccessPolicy.Expiry, expectedAcl.AccessPolicy.Expiry) + } + + if retrievedAcl.AccessPolicy.Permission != expectedAcl.AccessPolicy.Permission { + t.Fatalf("Expected Permission to be %q but got %q", retrievedAcl.AccessPolicy.Permission, expectedAcl.AccessPolicy.Permission) + } + } + + t.Logf("[DEBUG] Deleting Table %q..", tableName) + if _, err := tablesClient.Delete(ctx, accountName, tableName); err != nil { + t.Fatalf("Error deleting %q: %s", tableName, err) + } +} diff --git a/storage/2017-07-29/table/tables/models.go b/storage/2017-07-29/table/tables/models.go new file mode 100644 index 0000000..d7c382a --- /dev/null +++ b/storage/2017-07-29/table/tables/models.go @@ -0,0 +1,29 @@ +package tables + +type MetaDataLevel string + +var ( + NoMetaData MetaDataLevel = "nometadata" + MinimalMetaData MetaDataLevel = "minimalmetadata" + FullMetaData MetaDataLevel = "fullmetadata" +) + +type GetResultItem struct { + TableName string `json:"TableName"` + + // Optional, depending on the MetaData Level + ODataType string `json:"odata.type,omitempty"` + ODataID string `json:"odata.id,omitEmpty"` + ODataEditLink string `json:"odata.editLink,omitEmpty"` +} + +type SignedIdentifier struct { + Id string `xml:"Id"` + AccessPolicy AccessPolicy `xml:"AccessPolicy"` +} + +type AccessPolicy struct { + Start string `xml:"Start"` + Expiry string `xml:"Expiry"` + Permission string `xml:"Permission"` +} diff --git a/storage/2017-07-29/table/tables/query.go b/storage/2017-07-29/table/tables/query.go new file mode 100644 index 0000000..475370f --- /dev/null +++ b/storage/2017-07-29/table/tables/query.go @@ -0,0 +1,87 @@ +package tables + +import ( + "context" + "fmt" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetResult struct { + autorest.Response + + MetaData string `json:"odata.metadata,omitempty"` + Tables []GetResultItem `json:"value"` +} + +// Query returns a list of tables under the specified account. +func (client Client) Query(ctx context.Context, accountName string, metaDataLevel MetaDataLevel) (result GetResult, err error) { + if accountName == "" { + return result, validation.NewError("tables.Client", "Query", "`accountName` cannot be an empty string.") + } + + req, err := client.QueryPreparer(ctx, accountName, metaDataLevel) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Query", nil, "Failure preparing request") + return + } + + resp, err := client.QuerySender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "tables.Client", "Query", resp, "Failure sending request") + return + } + + result, err = client.QueryResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Query", resp, "Failure responding to request") + return + } + + return +} + +// QueryPreparer prepares the Query request. +func (client Client) QueryPreparer(ctx context.Context, accountName string, metaDataLevel MetaDataLevel) (*http.Request, error) { + // NOTE: whilst this supports ContinuationTokens and 'Top' + // it appears that 'Skip' returns a '501 Not Implemented' + // as such, we intentionally don't support those right now + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "Accept": fmt.Sprintf("application/json;odata=%s", metaDataLevel), + } + + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPath("/Tables"), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// QuerySender sends the Query request. The method will close the +// http.Response Body if it receives an error. +func (client Client) QuerySender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// QueryResponder handles the response to the Query request. The method always +// closes the http.Response Body. +func (client Client) QueryResponder(resp *http.Response) (result GetResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingJSON(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2017-07-29/table/tables/resource_id.go b/storage/2017-07-29/table/tables/resource_id.go new file mode 100644 index 0000000..1052317 --- /dev/null +++ b/storage/2017-07-29/table/tables/resource_id.go @@ -0,0 +1,54 @@ +package tables + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given Table +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, tableName string) string { + domain := endpoints.GetTableEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/Tables('%s')", domain, tableName) +} + +type ResourceID struct { + AccountName string + TableName string +} + +// ParseResourceID parses the Resource ID and returns an object which +// can be used to interact with the Table within the specified Storage Account +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://foo.table.core.windows.net/Table('foo') + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + // assume there a `Table('')` + path := strings.TrimPrefix(uri.Path, "/") + if !strings.HasPrefix(path, "Tables('") || !strings.HasSuffix(path, "')") { + return nil, fmt.Errorf("Expected the Table Name to be in the format `Tables('name')` but got %q", path) + } + + // strip off the `Table('')` + tableName := strings.TrimPrefix(uri.Path, "/Tables('") + tableName = strings.TrimSuffix(tableName, "')") + return &ResourceID{ + AccountName: *accountName, + TableName: tableName, + }, nil +} diff --git a/storage/2017-07-29/table/tables/resource_id_test.go b/storage/2017-07-29/table/tables/resource_id_test.go new file mode 100644 index 0000000..5557f81 --- /dev/null +++ b/storage/2017-07-29/table/tables/resource_id_test.go @@ -0,0 +1,78 @@ +package tables + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.table.core.chinacloudapi.cn/Tables('table1')", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.table.core.cloudapi.de/Tables('table1')", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.table.core.windows.net/Tables('table1')", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.table.core.usgovcloudapi.net/Tables('table1')", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "table1") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.table.core.chinacloudapi.cn/Tables('table1')", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.table.core.cloudapi.de/Tables('table1')", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.table.core.windows.net/Tables('table1')", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.table.core.usgovcloudapi.net/Tables('table1')", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.TableName != "table1" { + t.Fatalf("Expected Table Name to be `table1` but got %q", actual.TableName) + } + } +} diff --git a/storage/2017-07-29/table/tables/version.go b/storage/2017-07-29/table/tables/version.go new file mode 100644 index 0000000..a174db6 --- /dev/null +++ b/storage/2017-07-29/table/tables/version.go @@ -0,0 +1,14 @@ +package tables + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2017-07-29" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2018-03-28/README.md b/storage/2018-03-28/README.md new file mode 100644 index 0000000..375d9d9 --- /dev/null +++ b/storage/2018-03-28/README.md @@ -0,0 +1,25 @@ +# Storage API Version 2018-03-28 + +The following API's are supported by this SDK - more information about each SDK can be found within the README in each package. + +## Blob Storage + +- [Blobs API](blob/blobs) +- [Containers API](blob/containers) + +## File Storage + +- [Directories API](file/directories) +- [Files API](file/files) +- [Shares API](file/shares) + +## Queue Storage + +- [Queues API](queue/queues) +- [Messages API](queue/messages) + +## Table Storage + +- [Entities API](table/entities) +- [Tables API](table/tables) + diff --git a/storage/2018-03-28/blob/blobs/README.md b/storage/2018-03-28/blob/blobs/README.md new file mode 100644 index 0000000..930e2d7 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/README.md @@ -0,0 +1,46 @@ +## Blob Storage Blobs SDK for API version 2018-03-28 + +This package allows you to interact with the Blobs Blob Storage API + +### Supported Authorizers + +* Azure Active Directory (for the Resource Endpoint `https://storage.azure.com`) +* SharedKeyLite (Blob, File & Queue) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2018-03-28/blob/blobs" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + containerName := "mycontainer" + fileName := "example-large-file.iso" + + storageAuth := autorest.NewSharedKeyLiteAuthorizer(accountName, storageAccountKey) + blobClient := blobs.New() + blobClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + copyInput := blobs.CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + refreshInterval := 5 * time.Second + if err := blobClient.CopyAndWait(ctx, accountName, containerName, fileName, copyInput, refreshInterval); err != nil { + return fmt.Errorf("Error copying: %s", err) + } + + return nil +} + +``` \ No newline at end of file diff --git a/storage/2018-03-28/blob/blobs/append_block.go b/storage/2018-03-28/blob/blobs/append_block.go new file mode 100644 index 0000000..7fed86a --- /dev/null +++ b/storage/2018-03-28/blob/blobs/append_block.go @@ -0,0 +1,170 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type AppendBlockInput struct { + + // A number indicating the byte offset to compare. + // Append Block will succeed only if the append position is equal to this number. + // If it is not, the request will fail with an AppendPositionConditionNotMet + // error (HTTP status code 412 – Precondition Failed) + BlobConditionAppendPosition *int64 + + // The max length in bytes permitted for the append blob. + // If the Append Block operation would cause the blob to exceed that limit or if the blob size + // is already greater than the value specified in this header, the request will fail with + // an MaxBlobSizeConditionNotMet error (HTTP status code 412 – Precondition Failed). + BlobConditionMaxSize *int64 + + // The Bytes which should be appended to the end of this Append Blob. + Content []byte + + // An MD5 hash of the block content. + // This hash is used to verify the integrity of the block during transport. + // When this header is specified, the storage service compares the hash of the content + // that has arrived with this header value. + // + // Note that this MD5 hash is not stored with the blob. + // If the two hashes do not match, the operation will fail with error code 400 (Bad Request). + ContentMD5 *string + + // Required if the blob has an active lease. + // To perform this operation on a blob with an active lease, specify the valid lease ID for this header. + LeaseID *string +} + +type AppendBlockResult struct { + autorest.Response + + BlobAppendOffset string + BlobCommittedBlockCount int64 + ContentMD5 string + ETag string + LastModified string +} + +// AppendBlock commits a new block of data to the end of an existing append blob. +func (client Client) AppendBlock(ctx context.Context, accountName, containerName, blobName string, input AppendBlockInput) (result AppendBlockResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "AppendBlock", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "AppendBlock", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "AppendBlock", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "AppendBlock", "`blobName` cannot be an empty string.") + } + if len(input.Content) > (4 * 1024 * 1024) { + return result, validation.NewError("files.Client", "PutByteRange", "`input.Content` must be at most 4MB.") + } + + req, err := client.AppendBlockPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "AppendBlock", nil, "Failure preparing request") + return + } + + resp, err := client.AppendBlockSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "AppendBlock", resp, "Failure sending request") + return + } + + result, err = client.AppendBlockResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "AppendBlock", resp, "Failure responding to request") + return + } + + return +} + +// AppendBlockPreparer prepares the AppendBlock request. +func (client Client) AppendBlockPreparer(ctx context.Context, accountName, containerName, blobName string, input AppendBlockInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "appendblock"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.BlobConditionAppendPosition != nil { + headers["x-ms-blob-condition-appendpos"] = *input.BlobConditionAppendPosition + } + if input.BlobConditionMaxSize != nil { + headers["x-ms-blob-condition-maxsize"] = *input.BlobConditionMaxSize + } + if input.ContentMD5 != nil { + headers["x-ms-blob-content-md5"] = *input.ContentMD5 + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers), + autorest.WithBytes(&input.Content)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// AppendBlockSender sends the AppendBlock request. The method will close the +// http.Response Body if it receives an error. +func (client Client) AppendBlockSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// AppendBlockResponder handles the response to the AppendBlock request. The method always +// closes the http.Response Body. +func (client Client) AppendBlockResponder(resp *http.Response) (result AppendBlockResult, err error) { + if resp != nil && resp.Header != nil { + result.BlobAppendOffset = resp.Header.Get("x-ms-blob-append-offset") + result.ContentMD5 = resp.Header.Get("ETag") + result.ETag = resp.Header.Get("ETag") + result.LastModified = resp.Header.Get("Last-Modified") + + if v := resp.Header.Get("x-ms-blob-committed-block-count"); v != "" { + i, innerErr := strconv.Atoi(v) + if innerErr != nil { + err = fmt.Errorf("Error parsing %q as an integer: %s", v, innerErr) + return + } + + result.BlobCommittedBlockCount = int64(i) + } + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/blobs/blob_append_test.go b/storage/2018-03-28/blob/blobs/blob_append_test.go new file mode 100644 index 0000000..ad1ca56 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/blob_append_test.go @@ -0,0 +1,155 @@ +package blobs + +import ( + "context" + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-03-28/blob/containers" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestAppendBlobLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + fileName := "append-blob.txt" + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.BlobStorage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + containersClient := containers.NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithStorageResourceManagerAuth(containersClient.Client) + + _, err = containersClient.Create(ctx, accountName, containerName, containers.CreateInput{}) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + defer containersClient.Delete(ctx, accountName, containerName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + blobClient := NewWithEnvironment(client.Environment) + blobClient.Client = client.PrepareWithAuthorizer(blobClient.Client, storageAuth) + + t.Logf("[DEBUG] Putting Append Blob..") + if _, err := blobClient.PutAppendBlob(ctx, accountName, containerName, fileName, PutAppendBlobInput{}); err != nil { + t.Fatalf("Error putting append blob: %s", err) + } + + t.Logf("[DEBUG] Retrieving Properties..") + props, err := blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error retrieving properties: %s", err) + } + if props.ContentLength != 0 { + t.Fatalf("Expected Content-Length to be 0 but it was %d", props.ContentLength) + } + + t.Logf("[DEBUG] Appending First Block..") + appendInput := AppendBlockInput{ + Content: []byte{ + 12, + 48, + 93, + 76, + 29, + 10, + }, + } + if _, err := blobClient.AppendBlock(ctx, accountName, containerName, fileName, appendInput); err != nil { + t.Fatalf("Error appending first block: %s", err) + } + + t.Logf("[DEBUG] Re-Retrieving Properties..") + props, err = blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error retrieving properties: %s", err) + } + if props.ContentLength != 6 { + t.Fatalf("Expected Content-Length to be 6 but it was %d", props.ContentLength) + } + + t.Logf("[DEBUG] Appending Second Block..") + appendInput = AppendBlockInput{ + Content: []byte{ + 92, + 62, + 64, + 47, + 83, + 77, + }, + } + if _, err := blobClient.AppendBlock(ctx, accountName, containerName, fileName, appendInput); err != nil { + t.Fatalf("Error appending Second block: %s", err) + } + + t.Logf("[DEBUG] Re-Retrieving Properties..") + props, err = blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error retrieving properties: %s", err) + } + if props.ContentLength != 12 { + t.Fatalf("Expected Content-Length to be 12 but it was %d", props.ContentLength) + } + + t.Logf("[DEBUG] Acquiring Lease..") + leaseDetails, err := blobClient.AcquireLease(ctx, accountName, containerName, fileName, AcquireLeaseInput{ + LeaseDuration: -1, + }) + if err != nil { + t.Fatalf("Error acquiring Lease: %s", err) + } + t.Logf("[DEBUG] Lease ID is %q", leaseDetails.LeaseID) + + t.Logf("[DEBUG] Appending Third Block..") + appendInput = AppendBlockInput{ + Content: []byte{ + 64, + 35, + 28, + 93, + 11, + 23, + }, + LeaseID: &leaseDetails.LeaseID, + } + if _, err := blobClient.AppendBlock(ctx, accountName, containerName, fileName, appendInput); err != nil { + t.Fatalf("Error appending Third block: %s", err) + } + + t.Logf("[DEBUG] Re-Retrieving Properties..") + props, err = blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{ + LeaseID: &leaseDetails.LeaseID, + }) + if err != nil { + t.Fatalf("Error retrieving properties: %s", err) + } + if props.ContentLength != 18 { + t.Fatalf("Expected Content-Length to be 18 but it was %d", props.ContentLength) + } + + t.Logf("[DEBUG] Breaking Lease..") + breakLeaseInput := BreakLeaseInput{ + LeaseID: leaseDetails.LeaseID, + } + if _, err := blobClient.BreakLease(ctx, accountName, containerName, fileName, breakLeaseInput); err != nil { + t.Fatalf("Error breaking lease: %s", err) + } + + t.Logf("[DEBUG] Deleting Lease..") + if _, err := blobClient.Delete(ctx, accountName, containerName, fileName, DeleteInput{}); err != nil { + t.Fatalf("Error deleting: %s", err) + } +} diff --git a/storage/2018-03-28/blob/blobs/blob_page_test.go b/storage/2018-03-28/blob/blobs/blob_page_test.go new file mode 100644 index 0000000..ceb58d3 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/blob_page_test.go @@ -0,0 +1,89 @@ +package blobs + +import ( + "context" + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-03-28/blob/containers" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestPageBlobLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + fileName := "append-blob.txt" + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.StorageV2) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + containersClient := containers.NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithStorageResourceManagerAuth(containersClient.Client) + + _, err = containersClient.Create(ctx, accountName, containerName, containers.CreateInput{}) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + defer containersClient.Delete(ctx, accountName, containerName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + blobClient := NewWithEnvironment(client.Environment) + blobClient.Client = client.PrepareWithAuthorizer(blobClient.Client, storageAuth) + + t.Logf("[DEBUG] Putting Page Blob..") + fileSize := int64(10240000) + if _, err := blobClient.PutPageBlob(ctx, accountName, containerName, fileName, PutPageBlobInput{ + BlobContentLengthBytes: fileSize, + }); err != nil { + t.Fatalf("Error putting page blob: %s", err) + } + + t.Logf("[DEBUG] Retrieving Properties..") + props, err := blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error retrieving properties: %s", err) + } + if props.ContentLength != fileSize { + t.Fatalf("Expected Content-Length to be %d but it was %d", fileSize, props.ContentLength) + } + + for iteration := 1; iteration <= 3; iteration++ { + t.Logf("[DEBUG] Putting Page %d of 3..", iteration) + byteArray := func() []byte { + o := make([]byte, 0) + + for i := 0; i < 512; i++ { + o = append(o, byte(i)) + } + + return o + }() + startByte := int64(512 * iteration) + endByte := int64(startByte + 511) + putPageInput := PutPageUpdateInput{ + StartByte: startByte, + EndByte: endByte, + Content: byteArray, + } + if _, err := blobClient.PutPageUpdate(ctx, accountName, containerName, fileName, putPageInput); err != nil { + t.Fatalf("Error putting page: %s", err) + } + } + + t.Logf("[DEBUG] Deleting..") + if _, err := blobClient.Delete(ctx, accountName, containerName, fileName, DeleteInput{}); err != nil { + t.Fatalf("Error deleting: %s", err) + } +} diff --git a/storage/2018-03-28/blob/blobs/client.go b/storage/2018-03-28/blob/blobs/client.go new file mode 100644 index 0000000..db20391 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/client.go @@ -0,0 +1,25 @@ +package blobs + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for Blob Storage Blobs. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithBaseURI creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2018-03-28/blob/blobs/copy.go b/storage/2018-03-28/blob/blobs/copy.go new file mode 100644 index 0000000..febaab5 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/copy.go @@ -0,0 +1,235 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type CopyInput struct { + // Specifies the name of the source blob or file. + // Beginning with version 2012-02-12, this value may be a URL of up to 2 KB in length that specifies a blob. + // The value should be URL-encoded as it would appear in a request URI. + // A source blob in the same storage account can be authenticated via Shared Key. + // However, if the source is a blob in another account, + // the source blob must either be public or must be authenticated via a shared access signature. + // If the source blob is public, no authentication is required to perform the copy operation. + // + // Beginning with version 2015-02-21, the source object may be a file in the Azure File service. + // If the source object is a file that is to be copied to a blob, then the source file must be authenticated + // using a shared access signature, whether it resides in the same account or in a different account. + // + // Only storage accounts created on or after June 7th, 2012 allow the Copy Blob operation to + // copy from another storage account. + CopySource string + + // The ID of the Lease + // Required if the destination blob has an active lease. + // The lease ID specified for this header must match the lease ID of the destination blob. + // If the request does not include the lease ID or it is not valid, + // the operation fails with status code 412 (Precondition Failed). + // + // If this header is specified and the destination blob does not currently have an active lease, + // the operation will also fail with status code 412 (Precondition Failed). + LeaseID *string + + // The ID of the Lease on the Source Blob + // Specify to perform the Copy Blob operation only if the lease ID matches the active lease ID of the source blob. + SourceLeaseID *string + + // For page blobs on a premium account only. Specifies the tier to be set on the target blob + AccessTier *AccessTier + + // A user-defined name-value pair associated with the blob. + // If no name-value pairs are specified, the operation will copy the metadata from the source blob or + // file to the destination blob. + // If one or more name-value pairs are specified, the destination blob is created with the specified metadata, + // and metadata is not copied from the source blob or file. + MetaData map[string]string + + // An ETag value. + // Specify an ETag value for this conditional header to copy the blob only if the specified + // ETag value matches the ETag value for an existing destination blob. + // If the ETag for the destination blob does not match the ETag specified for If-Match, + // the Blob service returns status code 412 (Precondition Failed). + IfMatch *string + + // An ETag value, or the wildcard character (*). + // Specify an ETag value for this conditional header to copy the blob only if the specified + // ETag value does not match the ETag value for the destination blob. + // Specify the wildcard character (*) to perform the operation only if the destination blob does not exist. + // If the specified condition isn't met, the Blob service returns status code 412 (Precondition Failed). + IfNoneMatch *string + + // A DateTime value. + // Specify this conditional header to copy the blob only if the destination blob + // has been modified since the specified date/time. + // If the destination blob has not been modified, the Blob service returns status code 412 (Precondition Failed). + IfModifiedSince *string + + // A DateTime value. + // Specify this conditional header to copy the blob only if the destination blob + // has not been modified since the specified date/time. + // If the destination blob has been modified, the Blob service returns status code 412 (Precondition Failed). + IfUnmodifiedSince *string + + // An ETag value. + // Specify this conditional header to copy the source blob only if its ETag matches the value specified. + // If the ETag values do not match, the Blob service returns status code 412 (Precondition Failed). + // This cannot be specified if the source is an Azure File. + SourceIfMatch *string + + // An ETag value. + // Specify this conditional header to copy the blob only if its ETag does not match the value specified. + // If the values are identical, the Blob service returns status code 412 (Precondition Failed). + // This cannot be specified if the source is an Azure File. + SourceIfNoneMatch *string + + // A DateTime value. + // Specify this conditional header to copy the blob only if the source blob has been modified + // since the specified date/time. + // If the source blob has not been modified, the Blob service returns status code 412 (Precondition Failed). + // This cannot be specified if the source is an Azure File. + SourceIfModifiedSince *string + + // A DateTime value. + // Specify this conditional header to copy the blob only if the source blob has not been modified + // since the specified date/time. + // If the source blob has been modified, the Blob service returns status code 412 (Precondition Failed). + // This header cannot be specified if the source is an Azure File. + SourceIfUnmodifiedSince *string +} + +type CopyResult struct { + autorest.Response + + CopyID string + CopyStatus string +} + +// Copy copies a blob to a destination within the storage account asynchronously. +func (client Client) Copy(ctx context.Context, accountName, containerName, blobName string, input CopyInput) (result CopyResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "Copy", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "Copy", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "Copy", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "Copy", "`blobName` cannot be an empty string.") + } + if input.CopySource == "" { + return result, validation.NewError("blobs.Client", "Copy", "`input.CopySource` cannot be an empty string.") + } + + req, err := client.CopyPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Copy", nil, "Failure preparing request") + return + } + + resp, err := client.CopySender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "Copy", resp, "Failure sending request") + return + } + + result, err = client.CopyResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Copy", resp, "Failure responding to request") + return + } + + return +} + +// CopyPreparer prepares the Copy request. +func (client Client) CopyPreparer(ctx context.Context, accountName, containerName, blobName string, input CopyInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-copy-source": autorest.Encode("header", input.CopySource), + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + if input.SourceLeaseID != nil { + headers["x-ms-source-lease-id"] = *input.SourceLeaseID + } + if input.AccessTier != nil { + headers["x-ms-access-tier"] = string(*input.AccessTier) + } + + if input.IfMatch != nil { + headers["If-Match"] = *input.IfMatch + } + if input.IfNoneMatch != nil { + headers["If-None-Match"] = *input.IfNoneMatch + } + if input.IfUnmodifiedSince != nil { + headers["If-Unmodified-Since"] = *input.IfUnmodifiedSince + } + if input.IfModifiedSince != nil { + headers["If-Modified-Since"] = *input.IfModifiedSince + } + + if input.SourceIfMatch != nil { + headers["x-ms-source-if-match"] = *input.SourceIfMatch + } + if input.SourceIfNoneMatch != nil { + headers["x-ms-source-if-none-match"] = *input.SourceIfNoneMatch + } + if input.SourceIfModifiedSince != nil { + headers["x-ms-source-if-modified-since"] = *input.SourceIfModifiedSince + } + if input.SourceIfUnmodifiedSince != nil { + headers["x-ms-source-if-unmodified-since"] = *input.SourceIfUnmodifiedSince + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CopySender sends the Copy request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CopySender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CopyResponder handles the response to the Copy request. The method always +// closes the http.Response Body. +func (client Client) CopyResponder(resp *http.Response) (result CopyResult, err error) { + if resp != nil && resp.Header != nil { + result.CopyID = resp.Header.Get("x-ms-copy-id") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-03-28/blob/blobs/copy_abort.go b/storage/2018-03-28/blob/blobs/copy_abort.go new file mode 100644 index 0000000..a992ff1 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/copy_abort.go @@ -0,0 +1,110 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type AbortCopyInput struct { + // The Copy ID which should be aborted + CopyID string + + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string +} + +// AbortCopy aborts a pending Copy Blob operation, and leaves a destination blob with zero length and full metadata. +func (client Client) AbortCopy(ctx context.Context, accountName, containerName, blobName string, input AbortCopyInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "AbortCopy", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "AbortCopy", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "AbortCopy", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "AbortCopy", "`blobName` cannot be an empty string.") + } + if input.CopyID == "" { + return result, validation.NewError("blobs.Client", "AbortCopy", "`input.CopyID` cannot be an empty string.") + } + + req, err := client.AbortCopyPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "AbortCopy", nil, "Failure preparing request") + return + } + + resp, err := client.AbortCopySender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "AbortCopy", resp, "Failure sending request") + return + } + + result, err = client.AbortCopyResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "AbortCopy", resp, "Failure responding to request") + return + } + + return +} + +// AbortCopyPreparer prepares the AbortCopy request. +func (client Client) AbortCopyPreparer(ctx context.Context, accountName, containerName, blobName string, input AbortCopyInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "copy"), + "copyid": autorest.Encode("query", input.CopyID), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-copy-action": "abort", + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// AbortCopySender sends the AbortCopy request. The method will close the +// http.Response Body if it receives an error. +func (client Client) AbortCopySender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// AbortCopyResponder handles the response to the AbortCopy request. The method always +// closes the http.Response Body. +func (client Client) AbortCopyResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-03-28/blob/blobs/copy_and_wait.go b/storage/2018-03-28/blob/blobs/copy_and_wait.go new file mode 100644 index 0000000..a1e7fa4 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/copy_and_wait.go @@ -0,0 +1,41 @@ +package blobs + +import ( + "context" + "fmt" + "time" +) + +// CopyAndWait copies a blob to a destination within the storage account and waits for it to finish copying. +func (client Client) CopyAndWait(ctx context.Context, accountName, containerName, blobName string, input CopyInput, pollingInterval time.Duration) error { + if _, err := client.Copy(ctx, accountName, containerName, blobName, input); err != nil { + return fmt.Errorf("Error copying: %s", err) + } + + for true { + getInput := GetPropertiesInput{ + LeaseID: input.LeaseID, + } + getResult, err := client.GetProperties(ctx, accountName, containerName, blobName, getInput) + if err != nil { + return fmt.Errorf("") + } + + switch getResult.CopyStatus { + case Aborted: + return fmt.Errorf("Copy was aborted: %s", getResult.CopyStatusDescription) + + case Failed: + return fmt.Errorf("Copy failed: %s", getResult.CopyStatusDescription) + + case Success: + return nil + + case Pending: + time.Sleep(pollingInterval) + continue + } + } + + return fmt.Errorf("Unexpected error waiting for the copy to complete") +} diff --git a/storage/2018-03-28/blob/blobs/copy_test.go b/storage/2018-03-28/blob/blobs/copy_test.go new file mode 100644 index 0000000..6a929e7 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/copy_test.go @@ -0,0 +1,148 @@ +package blobs + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-03-28/blob/containers" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestCopyFromExistingFile(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + fileName := "ubuntu.iso" + copiedFileName := "copied.iso" + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.BlobStorage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + containersClient := containers.NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithStorageResourceManagerAuth(containersClient.Client) + + _, err = containersClient.Create(ctx, accountName, containerName, containers.CreateInput{}) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + defer containersClient.Delete(ctx, accountName, containerName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + blobClient := NewWithEnvironment(client.Environment) + blobClient.Client = client.PrepareWithAuthorizer(blobClient.Client, storageAuth) + + t.Logf("[DEBUG] Copying file to Blob Storage..") + copyInput := CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + + refreshInterval := 5 * time.Second + if err := blobClient.CopyAndWait(ctx, accountName, containerName, fileName, copyInput, refreshInterval); err != nil { + t.Fatalf("Error copying: %s", err) + } + + t.Logf("[DEBUG] Duplicating that file..") + copiedInput := CopyInput{ + CopySource: fmt.Sprintf("%s/%s/%s", endpoints.GetBlobEndpoint(blobClient.BaseURI, accountName), containerName, fileName), + } + if err := blobClient.CopyAndWait(ctx, accountName, containerName, copiedFileName, copiedInput, refreshInterval); err != nil { + t.Fatalf("Error duplicating file: %s", err) + } + + t.Logf("[DEBUG] Retrieving Properties for the Original File..") + props, err := blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error getting properties for the original file: %s", err) + } + + t.Logf("[DEBUG] Retrieving Properties for the Copied File..") + copiedProps, err := blobClient.GetProperties(ctx, accountName, containerName, copiedFileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error getting properties for the copied file: %s", err) + } + + if props.ContentLength != copiedProps.ContentLength { + t.Fatalf("Expected the content length to be %d but it was %d", props.ContentLength, copiedProps.ContentLength) + } + + t.Logf("[DEBUG] Deleting copied file..") + if _, err := blobClient.Delete(ctx, accountName, containerName, copiedFileName, DeleteInput{}); err != nil { + t.Fatalf("Error deleting file: %s", err) + } + + t.Logf("[DEBUG] Deleting original file..") + if _, err := blobClient.Delete(ctx, accountName, containerName, fileName, DeleteInput{}); err != nil { + t.Fatalf("Error deleting file: %s", err) + } +} + +func TestCopyFromURL(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + fileName := "ubuntu.iso" + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.BlobStorage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + containersClient := containers.NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithStorageResourceManagerAuth(containersClient.Client) + + _, err = containersClient.Create(ctx, accountName, containerName, containers.CreateInput{}) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + defer containersClient.Delete(ctx, accountName, containerName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + blobClient := NewWithEnvironment(client.Environment) + blobClient.Client = client.PrepareWithAuthorizer(blobClient.Client, storageAuth) + + t.Logf("[DEBUG] Copying file to Blob Storage..") + copyInput := CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + + refreshInterval := 5 * time.Second + if err := blobClient.CopyAndWait(ctx, accountName, containerName, fileName, copyInput, refreshInterval); err != nil { + t.Fatalf("Error copying: %s", err) + } + + t.Logf("[DEBUG] Retrieving Properties..") + props, err := blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error getting properties: %s", err) + } + + if props.ContentLength == 0 { + t.Fatalf("Expected the file to be there but looks like it isn't: %d", props.ContentLength) + } + + t.Logf("[DEBUG] Deleting file..") + if _, err := blobClient.Delete(ctx, accountName, containerName, fileName, DeleteInput{}); err != nil { + t.Fatalf("Error deleting file: %s", err) + } +} diff --git a/storage/2018-03-28/blob/blobs/delete.go b/storage/2018-03-28/blob/blobs/delete.go new file mode 100644 index 0000000..c1c642d --- /dev/null +++ b/storage/2018-03-28/blob/blobs/delete.go @@ -0,0 +1,105 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type DeleteInput struct { + // Should any Snapshots for this Blob also be deleted? + // If the Blob has Snapshots and this is set to False a 409 Conflict will be returned + DeleteSnapshots bool + + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string +} + +// Delete marks the specified blob or snapshot for deletion. The blob is later deleted during garbage collection. +func (client Client) Delete(ctx context.Context, accountName, containerName, blobName string, input DeleteInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "Delete", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "Delete", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "Delete", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "Delete", "`blobName` cannot be an empty string.") + } + + req, err := client.DeletePreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName, containerName, blobName string, input DeleteInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + if input.DeleteSnapshots { + headers["x-ms-delete-snapshots"] = "include" + } + + preparer := autorest.CreatePreparer( + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-03-28/blob/blobs/delete_snapshot.go b/storage/2018-03-28/blob/blobs/delete_snapshot.go new file mode 100644 index 0000000..18c3d4c --- /dev/null +++ b/storage/2018-03-28/blob/blobs/delete_snapshot.go @@ -0,0 +1,108 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type DeleteSnapshotInput struct { + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string + + // The DateTime of the Snapshot which should be marked for Deletion + SnapshotDateTime string +} + +// DeleteSnapshot marks a single Snapshot of a Blob for Deletion based on it's DateTime, which will be deleted during the next Garbage Collection cycle. +func (client Client) DeleteSnapshot(ctx context.Context, accountName, containerName, blobName string, input DeleteSnapshotInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "DeleteSnapshot", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "DeleteSnapshot", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "DeleteSnapshot", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "DeleteSnapshot", "`blobName` cannot be an empty string.") + } + if input.SnapshotDateTime == "" { + return result, validation.NewError("blobs.Client", "DeleteSnapshot", "`input.SnapshotDateTime` cannot be an empty string.") + } + + req, err := client.DeleteSnapshotPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "DeleteSnapshot", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSnapshotSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "DeleteSnapshot", resp, "Failure sending request") + return + } + + result, err = client.DeleteSnapshotResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "DeleteSnapshot", resp, "Failure responding to request") + return + } + + return +} + +// DeleteSnapshotPreparer prepares the DeleteSnapshot request. +func (client Client) DeleteSnapshotPreparer(ctx context.Context, accountName, containerName, blobName string, input DeleteSnapshotInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "snapshot": autorest.Encode("query", input.SnapshotDateTime), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSnapshotSender sends the DeleteSnapshot request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSnapshotSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteSnapshotResponder handles the response to the DeleteSnapshot request. The method always +// closes the http.Response Body. +func (client Client) DeleteSnapshotResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-03-28/blob/blobs/delete_snapshots.go b/storage/2018-03-28/blob/blobs/delete_snapshots.go new file mode 100644 index 0000000..e7e2b66 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/delete_snapshots.go @@ -0,0 +1,99 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type DeleteSnapshotsInput struct { + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string +} + +// DeleteSnapshots marks all Snapshots of a Blob for Deletion, which will be deleted during the next Garbage Collection Cycle. +func (client Client) DeleteSnapshots(ctx context.Context, accountName, containerName, blobName string, input DeleteSnapshotsInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "DeleteSnapshots", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "DeleteSnapshots", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "DeleteSnapshots", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "DeleteSnapshots", "`blobName` cannot be an empty string.") + } + + req, err := client.DeleteSnapshotsPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "DeleteSnapshots", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSnapshotsSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "DeleteSnapshots", resp, "Failure sending request") + return + } + + result, err = client.DeleteSnapshotsResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "DeleteSnapshots", resp, "Failure responding to request") + return + } + + return +} + +// DeleteSnapshotsPreparer prepares the DeleteSnapshots request. +func (client Client) DeleteSnapshotsPreparer(ctx context.Context, accountName, containerName, blobName string, input DeleteSnapshotsInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + // only delete the snapshots but leave the blob as-is + "x-ms-delete-snapshots": "only", + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSnapshotsSender sends the DeleteSnapshots request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSnapshotsSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteSnapshotsResponder handles the response to the DeleteSnapshots request. The method always +// closes the http.Response Body. +func (client Client) DeleteSnapshotsResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-03-28/blob/blobs/get.go b/storage/2018-03-28/blob/blobs/get.go new file mode 100644 index 0000000..fa88081 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/get.go @@ -0,0 +1,116 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetInput struct { + LeaseID *string + StartByte *int64 + EndByte *int64 +} + +type GetResult struct { + autorest.Response + + Contents []byte +} + +// Get reads or downloads a blob from the system, including its metadata and properties. +func (client Client) Get(ctx context.Context, accountName, containerName, blobName string, input GetInput) (result GetResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "Get", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "Get", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "Get", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "Get", "`blobName` cannot be an empty string.") + } + if input.LeaseID != nil && *input.LeaseID == "" { + return result, validation.NewError("blobs.Client", "Get", "`input.LeaseID` should either be specified or nil, not an empty string.") + } + if (input.StartByte != nil && input.EndByte == nil) || input.StartByte == nil && input.EndByte != nil { + return result, validation.NewError("blobs.Client", "Get", "`input.StartByte` and `input.EndByte` must both be specified, or both be nil.") + } + + req, err := client.GetPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Get", nil, "Failure preparing request") + return + } + + resp, err := client.GetSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "Get", resp, "Failure sending request") + return + } + + result, err = client.GetResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Get", resp, "Failure responding to request") + return + } + + return +} + +// GetPreparer prepares the Get request. +func (client Client) GetPreparer(ctx context.Context, accountName, containerName, blobName string, input GetInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.StartByte != nil && input.EndByte != nil { + headers["x-ms-range"] = fmt.Sprintf("bytes=%d-%d", *input.StartByte, *input.EndByte) + } + + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetSender sends the Get request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetResponder handles the response to the Get request. The method always +// closes the http.Response Body. +func (client Client) GetResponder(resp *http.Response) (result GetResult, err error) { + if resp != nil { + result.Contents = make([]byte, resp.ContentLength) + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusPartialContent), + autorest.ByUnmarshallingBytes(&result.Contents), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/blobs/get_block_list.go b/storage/2018-03-28/blob/blobs/get_block_list.go new file mode 100644 index 0000000..9f8120c --- /dev/null +++ b/storage/2018-03-28/blob/blobs/get_block_list.go @@ -0,0 +1,140 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetBlockListInput struct { + BlockListType BlockListType + LeaseID *string +} + +type GetBlockListResult struct { + autorest.Response + + // The size of the blob in bytes + ContentLength *int64 + + // The Content Type of the blob + ContentType string + + // The ETag associated with this blob + ETag string + + // A list of blocks which have been committed + CommittedBlocks CommittedBlocks `xml:"CommittedBlocks,omitempty"` + + // A list of blocks which have not yet been committed + UncommittedBlocks UncommittedBlocks `xml:"UncommittedBlocks,omitempty"` +} + +// GetBlockList retrieves the list of blocks that have been uploaded as part of a block blob. +func (client Client) GetBlockList(ctx context.Context, accountName, containerName, blobName string, input GetBlockListInput) (result GetBlockListResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "GetBlockList", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "GetBlockList", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "GetBlockList", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "GetBlockList", "`blobName` cannot be an empty string.") + } + + req, err := client.GetBlockListPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetBlockList", nil, "Failure preparing request") + return + } + + resp, err := client.GetBlockListSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "GetBlockList", resp, "Failure sending request") + return + } + + result, err = client.GetBlockListResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetBlockList", resp, "Failure responding to request") + return + } + + return +} + +// GetBlockListPreparer prepares the GetBlockList request. +func (client Client) GetBlockListPreparer(ctx context.Context, accountName, containerName, blobName string, input GetBlockListInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "blocklisttype": autorest.Encode("query", string(input.BlockListType)), + "comp": autorest.Encode("query", "blocklist"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetBlockListSender sends the GetBlockList request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetBlockListSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetBlockListResponder handles the response to the GetBlockList request. The method always +// closes the http.Response Body. +func (client Client) GetBlockListResponder(resp *http.Response) (result GetBlockListResult, err error) { + if resp != nil && resp.Header != nil { + result.ContentType = resp.Header.Get("Content-Type") + result.ETag = resp.Header.Get("ETag") + + if v := resp.Header.Get("x-ms-blob-content-length"); v != "" { + i, innerErr := strconv.Atoi(v) + if innerErr != nil { + err = fmt.Errorf("Error parsing %q as an integer: %s", v, innerErr) + return + } + + i64 := int64(i) + result.ContentLength = &i64 + } + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-03-28/blob/blobs/get_page_ranges.go b/storage/2018-03-28/blob/blobs/get_page_ranges.go new file mode 100644 index 0000000..37abf63 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/get_page_ranges.go @@ -0,0 +1,152 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetPageRangesInput struct { + LeaseID *string + + StartByte *int64 + EndByte *int64 +} + +type GetPageRangesResult struct { + autorest.Response + + // The size of the blob in bytes + ContentLength *int64 + + // The Content Type of the blob + ContentType string + + // The ETag associated with this blob + ETag string + + PageRanges []PageRange `xml:"PageRange"` +} + +type PageRange struct { + // The start byte offset for this range, inclusive + Start int64 `xml:"Start"` + + // The end byte offset for this range, inclusive + End int64 `xml:"End"` +} + +// GetPageRanges returns the list of valid page ranges for a page blob or snapshot of a page blob. +func (client Client) GetPageRanges(ctx context.Context, accountName, containerName, blobName string, input GetPageRangesInput) (result GetPageRangesResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "GetPageRanges", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "GetPageRanges", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "GetPageRanges", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "GetPageRanges", "`blobName` cannot be an empty string.") + } + if (input.StartByte != nil && input.EndByte == nil) || input.StartByte == nil && input.EndByte != nil { + return result, validation.NewError("blobs.Client", "GetPageRanges", "`input.StartByte` and `input.EndByte` must both be specified, or both be nil.") + } + + req, err := client.GetPageRangesPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetPageRanges", nil, "Failure preparing request") + return + } + + resp, err := client.GetPageRangesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "GetPageRanges", resp, "Failure sending request") + return + } + + result, err = client.GetPageRangesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetPageRanges", resp, "Failure responding to request") + return + } + + return +} + +// GetPageRangesPreparer prepares the GetPageRanges request. +func (client Client) GetPageRangesPreparer(ctx context.Context, accountName, containerName, blobName string, input GetPageRangesInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "pagelist"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + if input.StartByte != nil && input.EndByte != nil { + headers["x-ms-range"] = fmt.Sprintf("bytes=%d-%d", *input.StartByte, *input.EndByte) + } + + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetPageRangesSender sends the GetPageRanges request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetPageRangesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetPageRangesResponder handles the response to the GetPageRanges request. The method always +// closes the http.Response Body. +func (client Client) GetPageRangesResponder(resp *http.Response) (result GetPageRangesResult, err error) { + if resp != nil && resp.Header != nil { + result.ContentType = resp.Header.Get("Content-Type") + result.ETag = resp.Header.Get("ETag") + + if v := resp.Header.Get("x-ms-blob-content-length"); v != "" { + i, innerErr := strconv.Atoi(v) + if innerErr != nil { + err = fmt.Errorf("Error parsing %q as an integer: %s", v, innerErr) + return + } + + i64 := int64(i) + result.ContentLength = &i64 + } + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-03-28/blob/blobs/incremental_copy_blob.go b/storage/2018-03-28/blob/blobs/incremental_copy_blob.go new file mode 100644 index 0000000..7fb7e6b --- /dev/null +++ b/storage/2018-03-28/blob/blobs/incremental_copy_blob.go @@ -0,0 +1,120 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type IncrementalCopyBlobInput struct { + CopySource string + IfModifiedSince *string + IfUnmodifiedSince *string + IfMatch *string + IfNoneMatch *string +} + +// IncrementalCopyBlob copies a snapshot of the source page blob to a destination page blob. +// The snapshot is copied such that only the differential changes between the previously copied +// snapshot are transferred to the destination. +// The copied snapshots are complete copies of the original snapshot and can be read or copied from as usual. +func (client Client) IncrementalCopyBlob(ctx context.Context, accountName, containerName, blobName string, input IncrementalCopyBlobInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "IncrementalCopyBlob", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "IncrementalCopyBlob", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "IncrementalCopyBlob", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "IncrementalCopyBlob", "`blobName` cannot be an empty string.") + } + if input.CopySource == "" { + return result, validation.NewError("blobs.Client", "IncrementalCopyBlob", "`input.CopySource` cannot be an empty string.") + } + + req, err := client.IncrementalCopyBlobPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "IncrementalCopyBlob", nil, "Failure preparing request") + return + } + + resp, err := client.IncrementalCopyBlobSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "IncrementalCopyBlob", resp, "Failure sending request") + return + } + + result, err = client.IncrementalCopyBlobResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "IncrementalCopyBlob", resp, "Failure responding to request") + return + } + + return +} + +// IncrementalCopyBlobPreparer prepares the IncrementalCopyBlob request. +func (client Client) IncrementalCopyBlobPreparer(ctx context.Context, accountName, containerName, blobName string, input IncrementalCopyBlobInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "incrementalcopy"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-copy-source": input.CopySource, + } + + if input.IfModifiedSince != nil { + headers["If-Modified-Since"] = *input.IfModifiedSince + } + if input.IfUnmodifiedSince != nil { + headers["If-Unmodified-Since"] = *input.IfUnmodifiedSince + } + if input.IfMatch != nil { + headers["If-Match"] = *input.IfMatch + } + if input.IfNoneMatch != nil { + headers["If-None-Match"] = *input.IfNoneMatch + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// IncrementalCopyBlobSender sends the IncrementalCopyBlob request. The method will close the +// http.Response Body if it receives an error. +func (client Client) IncrementalCopyBlobSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// IncrementalCopyBlobResponder handles the response to the IncrementalCopyBlob request. The method always +// closes the http.Response Body. +func (client Client) IncrementalCopyBlobResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-03-28/blob/blobs/lease_acquire.go b/storage/2018-03-28/blob/blobs/lease_acquire.go new file mode 100644 index 0000000..432c1f5 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/lease_acquire.go @@ -0,0 +1,135 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type AcquireLeaseInput struct { + // The ID of the existing Lease, if leased + LeaseID *string + + // Specifies the duration of the lease, in seconds, or negative one (-1) for a lease that never expires. + // A non-infinite lease can be between 15 and 60 seconds + LeaseDuration int + + // The Proposed new ID for the Lease + ProposedLeaseID *string +} + +type AcquireLeaseResult struct { + autorest.Response + + LeaseID string +} + +// AcquireLease establishes and manages a lock on a blob for write and delete operations. +func (client Client) AcquireLease(ctx context.Context, accountName, containerName, blobName string, input AcquireLeaseInput) (result AcquireLeaseResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "AcquireLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "AcquireLease", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "AcquireLease", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "AcquireLease", "`blobName` cannot be an empty string.") + } + if input.LeaseID != nil && *input.LeaseID == "" { + return result, validation.NewError("blobs.Client", "AcquireLease", "`input.LeaseID` cannot be an empty string, if specified.") + } + if input.ProposedLeaseID != nil && *input.ProposedLeaseID == "" { + return result, validation.NewError("blobs.Client", "AcquireLease", "`input.ProposedLeaseID` cannot be an empty string, if specified.") + } + // An infinite lease duration is -1 seconds. A non-infinite lease can be between 15 and 60 seconds + if input.LeaseDuration != -1 && (input.LeaseDuration <= 15 || input.LeaseDuration >= 60) { + return result, validation.NewError("blobs.Client", "AcquireLease", "`input.LeaseDuration` must be -1 (infinite), or between 15 and 60 seconds.") + } + + req, err := client.AcquireLeasePreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "AcquireLease", nil, "Failure preparing request") + return + } + + resp, err := client.AcquireLeaseSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "AcquireLease", resp, "Failure sending request") + return + } + + result, err = client.AcquireLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "AcquireLease", resp, "Failure responding to request") + return + } + + return +} + +// AcquireLeasePreparer prepares the AcquireLease request. +func (client Client) AcquireLeasePreparer(ctx context.Context, accountName, containerName, blobName string, input AcquireLeaseInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "acquire", + "x-ms-lease-duration": input.LeaseDuration, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + if input.ProposedLeaseID != nil { + headers["x-ms-proposed-lease-id"] = input.ProposedLeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// AcquireLeaseSender sends the AcquireLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) AcquireLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// AcquireLeaseResponder handles the response to the AcquireLease request. The method always +// closes the http.Response Body. +func (client Client) AcquireLeaseResponder(resp *http.Response) (result AcquireLeaseResult, err error) { + if resp != nil && resp.Header != nil { + result.LeaseID = resp.Header.Get("x-ms-lease-id") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/blobs/lease_break.go b/storage/2018-03-28/blob/blobs/lease_break.go new file mode 100644 index 0000000..d564204 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/lease_break.go @@ -0,0 +1,124 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type BreakLeaseInput struct { + // For a break operation, proposed duration the lease should continue + // before it is broken, in seconds, between 0 and 60. + // This break period is only used if it is shorter than the time remaining on the lease. + // If longer, the time remaining on the lease is used. + // A new lease will not be available before the break period has expired, + // but the lease may be held for longer than the break period. + // If this header does not appear with a break operation, a fixed-duration lease breaks + // after the remaining lease period elapses, and an infinite lease breaks immediately. + BreakPeriod *int + + LeaseID string +} + +type BreakLeaseResponse struct { + autorest.Response + + // Approximate time remaining in the lease period, in seconds. + // If the break is immediate, 0 is returned. + LeaseTime int +} + +// BreakLease breaks an existing lock on a blob using the LeaseID. +func (client Client) BreakLease(ctx context.Context, accountName, containerName, blobName string, input BreakLeaseInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "BreakLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "BreakLease", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "BreakLease", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "BreakLease", "`blobName` cannot be an empty string.") + } + if input.LeaseID == "" { + return result, validation.NewError("blobs.Client", "BreakLease", "`input.LeaseID` cannot be an empty string.") + } + + req, err := client.BreakLeasePreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "BreakLease", nil, "Failure preparing request") + return + } + + resp, err := client.BreakLeaseSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "BreakLease", resp, "Failure sending request") + return + } + + result, err = client.BreakLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "BreakLease", resp, "Failure responding to request") + return + } + + return +} + +// BreakLeasePreparer prepares the BreakLease request. +func (client Client) BreakLeasePreparer(ctx context.Context, accountName, containerName, blobName string, input BreakLeaseInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "break", + "x-ms-lease-id": input.LeaseID, + } + + if input.BreakPeriod != nil { + headers["x-ms-lease-break-period"] = *input.BreakPeriod + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// BreakLeaseSender sends the BreakLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) BreakLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// BreakLeaseResponder handles the response to the BreakLease request. The method always +// closes the http.Response Body. +func (client Client) BreakLeaseResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/blobs/lease_change.go b/storage/2018-03-28/blob/blobs/lease_change.go new file mode 100644 index 0000000..c57f9db --- /dev/null +++ b/storage/2018-03-28/blob/blobs/lease_change.go @@ -0,0 +1,117 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type ChangeLeaseInput struct { + ExistingLeaseID string + ProposedLeaseID string +} + +type ChangeLeaseResponse struct { + autorest.Response + + LeaseID string +} + +// ChangeLease changes an existing lock on a blob for another lock. +func (client Client) ChangeLease(ctx context.Context, accountName, containerName, blobName string, input ChangeLeaseInput) (result ChangeLeaseResponse, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "ChangeLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "ChangeLease", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "ChangeLease", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "ChangeLease", "`blobName` cannot be an empty string.") + } + if input.ExistingLeaseID == "" { + return result, validation.NewError("blobs.Client", "ChangeLease", "`input.ExistingLeaseID` cannot be an empty string.") + } + if input.ProposedLeaseID == "" { + return result, validation.NewError("blobs.Client", "ChangeLease", "`input.ProposedLeaseID` cannot be an empty string.") + } + + req, err := client.ChangeLeasePreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "ChangeLease", nil, "Failure preparing request") + return + } + + resp, err := client.ChangeLeaseSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "ChangeLease", resp, "Failure sending request") + return + } + + result, err = client.ChangeLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "ChangeLease", resp, "Failure responding to request") + return + } + + return +} + +// ChangeLeasePreparer prepares the ChangeLease request. +func (client Client) ChangeLeasePreparer(ctx context.Context, accountName, containerName, blobName string, input ChangeLeaseInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "change", + "x-ms-lease-id": input.ExistingLeaseID, + "x-ms-proposed-lease-id": input.ProposedLeaseID, + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ChangeLeaseSender sends the ChangeLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ChangeLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ChangeLeaseResponder handles the response to the ChangeLease request. The method always +// closes the http.Response Body. +func (client Client) ChangeLeaseResponder(resp *http.Response) (result ChangeLeaseResponse, err error) { + if resp != nil && resp.Header != nil { + result.LeaseID = resp.Header.Get("x-ms-lease-id") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/blobs/lease_release.go b/storage/2018-03-28/blob/blobs/lease_release.go new file mode 100644 index 0000000..0226cdf --- /dev/null +++ b/storage/2018-03-28/blob/blobs/lease_release.go @@ -0,0 +1,98 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// ReleaseLease releases a lock based on the Lease ID. +func (client Client) ReleaseLease(ctx context.Context, accountName, containerName, blobName, leaseID string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "ReleaseLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "ReleaseLease", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "ReleaseLease", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "ReleaseLease", "`blobName` cannot be an empty string.") + } + if leaseID == "" { + return result, validation.NewError("blobs.Client", "ReleaseLease", "`leaseID` cannot be an empty string.") + } + + req, err := client.ReleaseLeasePreparer(ctx, accountName, containerName, blobName, leaseID) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "ReleaseLease", nil, "Failure preparing request") + return + } + + resp, err := client.ReleaseLeaseSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "ReleaseLease", resp, "Failure sending request") + return + } + + result, err = client.ReleaseLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "ReleaseLease", resp, "Failure responding to request") + return + } + + return +} + +// ReleaseLeasePreparer prepares the ReleaseLease request. +func (client Client) ReleaseLeasePreparer(ctx context.Context, accountName, containerName, blobName, leaseID string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "release", + "x-ms-lease-id": leaseID, + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ReleaseLeaseSender sends the ReleaseLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ReleaseLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ReleaseLeaseResponder handles the response to the ReleaseLease request. The method always +// closes the http.Response Body. +func (client Client) ReleaseLeaseResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/blobs/lease_renew.go b/storage/2018-03-28/blob/blobs/lease_renew.go new file mode 100644 index 0000000..69c495b --- /dev/null +++ b/storage/2018-03-28/blob/blobs/lease_renew.go @@ -0,0 +1,97 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +func (client Client) RenewLease(ctx context.Context, accountName, containerName, blobName, leaseID string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "RenewLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "RenewLease", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "RenewLease", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "RenewLease", "`blobName` cannot be an empty string.") + } + if leaseID == "" { + return result, validation.NewError("blobs.Client", "RenewLease", "`leaseID` cannot be an empty string.") + } + + req, err := client.RenewLeasePreparer(ctx, accountName, containerName, blobName, leaseID) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "RenewLease", nil, "Failure preparing request") + return + } + + resp, err := client.RenewLeaseSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "RenewLease", resp, "Failure sending request") + return + } + + result, err = client.RenewLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "RenewLease", resp, "Failure responding to request") + return + } + + return +} + +// RenewLeasePreparer prepares the RenewLease request. +func (client Client) RenewLeasePreparer(ctx context.Context, accountName, containerName, blobName, leaseID string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "renew", + "x-ms-lease-id": leaseID, + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// RenewLeaseSender sends the RenewLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) RenewLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// RenewLeaseResponder handles the response to the RenewLease request. The method always +// closes the http.Response Body. +func (client Client) RenewLeaseResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/blobs/lease_test.go b/storage/2018-03-28/blob/blobs/lease_test.go new file mode 100644 index 0000000..7196f3a --- /dev/null +++ b/storage/2018-03-28/blob/blobs/lease_test.go @@ -0,0 +1,106 @@ +package blobs + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-03-28/blob/containers" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestLeaseLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + fileName := "ubuntu.iso" + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.BlobStorage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + containersClient := containers.NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithStorageResourceManagerAuth(containersClient.Client) + + _, err = containersClient.Create(ctx, accountName, containerName, containers.CreateInput{}) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + defer containersClient.Delete(ctx, accountName, containerName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + blobClient := NewWithEnvironment(client.Environment) + blobClient.Client = client.PrepareWithAuthorizer(blobClient.Client, storageAuth) + + t.Logf("[DEBUG] Copying file to Blob Storage..") + copyInput := CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + + refreshInterval := 5 * time.Second + if err := blobClient.CopyAndWait(ctx, accountName, containerName, fileName, copyInput, refreshInterval); err != nil { + t.Fatalf("Error copying: %s", err) + } + defer blobClient.Delete(ctx, accountName, containerName, fileName, DeleteInput{}) + + // Test begins here + t.Logf("[DEBUG] Acquiring Lease..") + leaseInput := AcquireLeaseInput{ + LeaseDuration: -1, + } + leaseInfo, err := blobClient.AcquireLease(ctx, accountName, containerName, fileName, leaseInput) + if err != nil { + t.Fatalf("Error acquiring lease: %s", err) + } + t.Logf("[DEBUG] Lease ID: %q", leaseInfo.LeaseID) + + t.Logf("[DEBUG] Changing Lease..") + changeLeaseInput := ChangeLeaseInput{ + ExistingLeaseID: leaseInfo.LeaseID, + ProposedLeaseID: "31f5bb01-cdd9-4166-bcdc-95186076bde0", + } + changeLeaseResult, err := blobClient.ChangeLease(ctx, accountName, containerName, fileName, changeLeaseInput) + if err != nil { + t.Fatalf("Error changing lease: %s", err) + } + t.Logf("[DEBUG] New Lease ID: %q", changeLeaseResult.LeaseID) + + t.Logf("[DEBUG] Releasing Lease..") + if _, err := blobClient.ReleaseLease(ctx, accountName, containerName, fileName, changeLeaseResult.LeaseID); err != nil { + t.Fatalf("Error releasing lease: %s", err) + } + + t.Logf("[DEBUG] Acquiring a new lease..") + leaseInput = AcquireLeaseInput{ + LeaseDuration: 30, + } + leaseInfo, err = blobClient.AcquireLease(ctx, accountName, containerName, fileName, leaseInput) + if err != nil { + t.Fatalf("Error acquiring lease: %s", err) + } + t.Logf("[DEBUG] Lease ID: %q", leaseInfo.LeaseID) + + t.Logf("[DEBUG] Renewing lease..") + if _, err := blobClient.RenewLease(ctx, accountName, containerName, fileName, leaseInfo.LeaseID); err != nil { + t.Fatalf("Error renewing lease: %s", err) + } + + t.Logf("[DEBUG] Breaking lease..") + breakLeaseInput := BreakLeaseInput{ + LeaseID: leaseInfo.LeaseID, + } + if _, err := blobClient.BreakLease(ctx, accountName, containerName, fileName, breakLeaseInput); err != nil { + t.Fatalf("Error breaking lease: %s", err) + } +} diff --git a/storage/2018-03-28/blob/blobs/lifecycle_test.go b/storage/2018-03-28/blob/blobs/lifecycle_test.go new file mode 100644 index 0000000..e0b326e --- /dev/null +++ b/storage/2018-03-28/blob/blobs/lifecycle_test.go @@ -0,0 +1,158 @@ +package blobs + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-03-28/blob/containers" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + fileName := "example.txt" + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.BlobStorage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + containersClient := containers.NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithAuthorizer(containersClient.Client, storageAuth) + + _, err = containersClient.Create(ctx, accountName, containerName, containers.CreateInput{}) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + defer containersClient.Delete(ctx, accountName, containerName) + + blobClient := NewWithEnvironment(client.Environment) + blobClient.Client = client.PrepareWithAuthorizer(blobClient.Client, storageAuth) + + t.Logf("[DEBUG] Copying file to Blob Storage..") + copyInput := CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + + refreshInterval := 5 * time.Second + if err := blobClient.CopyAndWait(ctx, accountName, containerName, fileName, copyInput, refreshInterval); err != nil { + t.Fatalf("Error copying: %s", err) + } + + t.Logf("[DEBUG] Retrieving Blob Properties..") + details, err := blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error retrieving properties: %s", err) + } + + // default value + if details.AccessTier != Hot { + t.Fatalf("Expected the AccessTier to be %q but got %q", Hot, details.AccessTier) + } + if details.BlobType != BlockBlob { + t.Fatalf("Expected BlobType to be %q but got %q", BlockBlob, details.BlobType) + } + if len(details.MetaData) != 0 { + t.Fatalf("Expected there to be no items of metadata but got %d", len(details.MetaData)) + } + + t.Logf("[DEBUG] Checking it's returned in the List API..") + listInput := containers.ListBlobsInput{} + listResult, err := containersClient.ListBlobs(ctx, accountName, containerName, listInput) + if err != nil { + t.Fatalf("Error listing blobs: %s", err) + } + + if len(listResult.Blobs.Blobs) != 1 { + t.Fatalf("Expected there to be 1 blob in the container but got %d", len(listResult.Blobs.Blobs)) + } + + t.Logf("[DEBUG] Setting MetaData..") + metaDataInput := SetMetaDataInput{ + MetaData: map[string]string{ + "hello": "there", + }, + } + if _, err := blobClient.SetMetaData(ctx, accountName, containerName, fileName, metaDataInput); err != nil { + t.Fatalf("Error setting MetaData: %s", err) + } + + t.Logf("[DEBUG] Re-retrieving Blob Properties..") + details, err = blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error re-retrieving properties: %s", err) + } + + // default value + if details.AccessTier != Hot { + t.Fatalf("Expected the AccessTier to be %q but got %q", Hot, details.AccessTier) + } + if details.BlobType != BlockBlob { + t.Fatalf("Expected BlobType to be %q but got %q", BlockBlob, details.BlobType) + } + if len(details.MetaData) != 1 { + t.Fatalf("Expected there to be 1 item of metadata but got %d", len(details.MetaData)) + } + if details.MetaData["hello"] != "there" { + t.Fatalf("Expected `hello` to be `there` but got %q", details.MetaData["there"]) + } + + t.Logf("[DEBUG] Retrieving the Block List..") + getBlockListInput := GetBlockListInput{ + BlockListType: All, + } + blockList, err := blobClient.GetBlockList(ctx, accountName, containerName, fileName, getBlockListInput) + if err != nil { + t.Fatalf("Error retrieving Block List: %s", err) + } + + // since this is a copy from an existing file, all blocks should be present + if len(blockList.CommittedBlocks.Blocks) == 0 { + t.Fatalf("Expected there to be committed blocks but there weren't!") + } + if len(blockList.UncommittedBlocks.Blocks) != 0 { + t.Fatalf("Expected all blocks to be committed but got %d uncommitted blocks", len(blockList.UncommittedBlocks.Blocks)) + } + + t.Logf("[DEBUG] Changing the Access Tiers..") + tiers := []AccessTier{ + Hot, + Cool, + Archive, + } + for _, tier := range tiers { + t.Logf("[DEBUG] Updating the Access Tier to %q..", string(tier)) + if _, err := blobClient.SetTier(ctx, accountName, containerName, fileName, tier); err != nil { + t.Fatalf("Error setting the Access Tier: %s", err) + } + + t.Logf("[DEBUG] Re-retrieving Blob Properties..") + details, err = blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error re-retrieving properties: %s", err) + } + + if details.AccessTier != tier { + t.Fatalf("Expected the AccessTier to be %q but got %q", tier, details.AccessTier) + } + } + + t.Logf("[DEBUG] Deleting Blob") + if _, err := blobClient.Delete(ctx, accountName, containerName, fileName, DeleteInput{}); err != nil { + t.Fatalf("Error deleting Blob: %s", err) + } +} diff --git a/storage/2018-03-28/blob/blobs/metadata_set.go b/storage/2018-03-28/blob/blobs/metadata_set.go new file mode 100644 index 0000000..ec69152 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/metadata_set.go @@ -0,0 +1,113 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type SetMetaDataInput struct { + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string + + // Any metadata which should be added to this blob + MetaData map[string]string +} + +// SetMetaData marks the specified blob or snapshot for deletion. The blob is later deleted during garbage collection. +func (client Client) SetMetaData(ctx context.Context, accountName, containerName, blobName string, input SetMetaDataInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "GetProperties", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "GetProperties", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "GetProperties", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "GetProperties", "`blobName` cannot be an empty string.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("blobs.Client", "GetProperties", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.SetMetaDataPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "SetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.SetMetaDataSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "SetMetaData", resp, "Failure sending request") + return + } + + result, err = client.SetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "SetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// SetMetaDataPreparer prepares the SetMetaData request. +func (client Client) SetMetaDataPreparer(ctx context.Context, accountName, containerName, blobName string, input SetMetaDataInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetMetaDataSender sends the SetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetMetaDataResponder handles the response to the SetMetaData request. The method always +// closes the http.Response Body. +func (client Client) SetMetaDataResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/blobs/models.go b/storage/2018-03-28/blob/blobs/models.go new file mode 100644 index 0000000..d7d83aa --- /dev/null +++ b/storage/2018-03-28/blob/blobs/models.go @@ -0,0 +1,82 @@ +package blobs + +type AccessTier string + +var ( + Archive AccessTier = "Archive" + Cool AccessTier = "Cool" + Hot AccessTier = "Hot" +) + +type ArchiveStatus string + +var ( + None ArchiveStatus = "" + RehydratePendingToCool ArchiveStatus = "rehydrate-pending-to-cool" + RehydratePendingToHot ArchiveStatus = "rehydrate-pending-to-hot" +) + +type BlockListType string + +var ( + All BlockListType = "all" + Committed BlockListType = "committed" + Uncommitted BlockListType = "uncommitted" +) + +type Block struct { + // The base64-encoded Block ID + Name string `xml:"Name"` + + // The size of the Block in Bytes + Size int64 `xml:"Size"` +} + +type BlobType string + +var ( + AppendBlob BlobType = "AppendBlob" + BlockBlob BlobType = "BlockBlob" + PageBlob BlobType = "PageBlob" +) + +type CommittedBlocks struct { + Blocks []Block `xml:"Block"` +} + +type CopyStatus string + +var ( + Aborted CopyStatus = "aborted" + Failed CopyStatus = "failed" + Pending CopyStatus = "pending" + Success CopyStatus = "success" +) + +type LeaseDuration string + +var ( + Fixed LeaseDuration = "fixed" + Infinite LeaseDuration = "infinite" +) + +type LeaseState string + +var ( + Available LeaseState = "available" + Breaking LeaseState = "breaking" + Broken LeaseState = "broken" + Expired LeaseState = "expired" + Leased LeaseState = "leased" +) + +type LeaseStatus string + +var ( + Locked LeaseStatus = "locked" + Unlocked LeaseStatus = "unlocked" +) + +type UncommittedBlocks struct { + Blocks []Block `xml:"Block"` +} diff --git a/storage/2018-03-28/blob/blobs/properties_get.go b/storage/2018-03-28/blob/blobs/properties_get.go new file mode 100644 index 0000000..de7c5fc --- /dev/null +++ b/storage/2018-03-28/blob/blobs/properties_get.go @@ -0,0 +1,310 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetPropertiesInput struct { + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string +} + +type GetPropertiesResult struct { + autorest.Response + + // The tier of page blob on a premium storage account or tier of block blob on blob storage or general purpose v2 account. + AccessTier AccessTier + + // This gives the last time tier was changed on the object. + // This header is returned only if tier on block blob was ever set. + // The date format follows RFC 1123 + AccessTierChangeTime string + + // For page blobs on a premium storage account only. + // If the access tier is not explicitly set on the blob, the tier is inferred based on its content length + // and this header will be returned with true value. + // For block blobs on Blob Storage or general purpose v2 account, if the blob does not have the access tier + // set then we infer the tier from the storage account properties. This header is set only if the block blob + // tier is inferred + AccessTierInferred bool + + // For blob storage or general purpose v2 account. + // If the blob is being rehydrated and is not complete then this header is returned indicating + // that rehydrate is pending and also tells the destination tier + ArchiveStatus ArchiveStatus + + // The number of committed blocks present in the blob. + // This header is returned only for append blobs. + BlobCommittedBlockCount string + + // The current sequence number for a page blob. + // This header is not returned for block blobs or append blobs. + // This header is not returned for block blobs. + BlobSequenceNumber string + + // The blob type. + BlobType BlobType + + // If the Cache-Control request header has previously been set for the blob, that value is returned in this header. + CacheControl string + + // The Content-Disposition response header field conveys additional information about how to process + // the response payload, and also can be used to attach additional metadata. + // For example, if set to attachment, it indicates that the user-agent should not display the response, + // but instead show a Save As dialog. + ContentDisposition string + + // If the Content-Encoding request header has previously been set for the blob, + // that value is returned in this header. + ContentEncoding string + + // If the Content-Language request header has previously been set for the blob, + // that value is returned in this header. + ContentLanguage string + + // The size of the blob in bytes. + // For a page blob, this header returns the value of the x-ms-blob-content-length header stored with the blob. + ContentLength int64 + + // The content type specified for the blob. + // If no content type was specified, the default content type is `application/octet-stream`. + ContentType string + + // If the Content-MD5 header has been set for the blob, this response header is returned so that + // the client can check for message content integrity. + ContentMD5 string + + // Conclusion time of the last attempted Copy Blob operation where this blob was the destination blob. + // This value can specify the time of a completed, aborted, or failed copy attempt. + // This header does not appear if a copy is pending, if this blob has never been the + // destination in a Copy Blob operation, or if this blob has been modified after a concluded Copy Blob + // operation using Set Blob Properties, Put Blob, or Put Block List. + CopyCompletionTime string + + // Included if the blob is incremental copy blob or incremental copy snapshot, if x-ms-copy-status is success. + // Snapshot time of the last successful incremental copy snapshot for this blob + CopyDestinationSnapshot string + + // String identifier for the last attempted Copy Blob operation where this blob was the destination blob. + // This header does not appear if this blob has never been the destination in a Copy Blob operation, + // or if this blob has been modified after a concluded Copy Blob operation using Set Blob Properties, + // Put Blob, or Put Block List. + CopyID string + + // Contains the number of bytes copied and the total bytes in the source in the last attempted + // Copy Blob operation where this blob was the destination blob. + // Can show between 0 and Content-Length bytes copied. + // This header does not appear if this blob has never been the destination in a Copy Blob operation, + // or if this blob has been modified after a concluded Copy Blob operation using Set Blob Properties, + // Put Blob, or Put Block List. + CopyProgress string + + // URL up to 2 KB in length that specifies the source blob used in the last attempted Copy Blob operation + // where this blob was the destination blob. + // This header does not appear if this blob has never been the destination in a Copy Blob operation, + // or if this blob has been modified after a concluded Copy Blob operation using Set Blob Properties, + // Put Blob, or Put Block List + CopySource string + + // State of the copy operation identified by x-ms-copy-id, with these values: + // - success: Copy completed successfully. + // - pending: Copy is in progress. + // Check x-ms-copy-status-description if intermittent, non-fatal errors + // impede copy progress but don’t cause failure. + // - aborted: Copy was ended by Abort Copy Blob. + // - failed: Copy failed. See x-ms- copy-status-description for failure details. + // This header does not appear if this blob has never been the destination in a Copy Blob operation, + // or if this blob has been modified after a completed Copy Blob operation using Set Blob Properties, + // Put Blob, or Put Block List. + CopyStatus CopyStatus + + // Describes cause of fatal or non-fatal copy operation failure. + // This header does not appear if this blob has never been the destination in a Copy Blob operation, + // or if this blob has been modified after a concluded Copy Blob operation using Set Blob Properties, + // Put Blob, or Put Block List. + CopyStatusDescription string + + // The date/time at which the blob was created. The date format follows RFC 1123 + CreationTime string + + // The ETag contains a value that you can use to perform operations conditionally + ETag string + + // Included if the blob is incremental copy blob. + IncrementalCopy bool + + // The date/time that the blob was last modified. The date format follows RFC 1123. + LastModified string + + // When a blob is leased, specifies whether the lease is of infinite or fixed duration + LeaseDuration LeaseDuration + + // The lease state of the blob + LeaseState LeaseState + + LeaseStatus LeaseStatus + + // A set of name-value pairs that correspond to the user-defined metadata associated with this blob + MetaData map[string]string + + // Is the Storage Account encrypted using server-side encryption? This should always return true + ServerEncrypted bool +} + +// GetProperties returns all user-defined metadata, standard HTTP properties, and system properties for the blob +func (client Client) GetProperties(ctx context.Context, accountName, containerName, blobName string, input GetPropertiesInput) (result GetPropertiesResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "GetProperties", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "GetProperties", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "GetProperties", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "GetProperties", "`blobName` cannot be an empty string.") + } + + req, err := client.GetPropertiesPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetProperties", nil, "Failure preparing request") + return + } + + resp, err := client.GetPropertiesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "GetProperties", resp, "Failure sending request") + return + } + + result, err = client.GetPropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetProperties", resp, "Failure responding to request") + return + } + + return +} + +// GetPropertiesPreparer prepares the GetProperties request. +func (client Client) GetPropertiesPreparer(ctx context.Context, accountName, containerName, blobName string, input GetPropertiesInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsHead(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetPropertiesSender sends the GetProperties request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetPropertiesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetPropertiesResponder handles the response to the GetProperties request. The method always +// closes the http.Response Body. +func (client Client) GetPropertiesResponder(resp *http.Response) (result GetPropertiesResult, err error) { + if resp != nil && resp.Header != nil { + result.AccessTier = AccessTier(resp.Header.Get("x-ms-access-tier")) + result.AccessTierChangeTime = resp.Header.Get(" x-ms-access-tier-change-time") + result.ArchiveStatus = ArchiveStatus(resp.Header.Get(" x-ms-archive-status")) + result.BlobCommittedBlockCount = resp.Header.Get("x-ms-blob-committed-block-count") + result.BlobSequenceNumber = resp.Header.Get("x-ms-blob-sequence-number") + result.BlobType = BlobType(resp.Header.Get("x-ms-blob-type")) + result.CacheControl = resp.Header.Get("Cache-Control") + result.ContentDisposition = resp.Header.Get("Content-Disposition") + result.ContentEncoding = resp.Header.Get("Content-Encoding") + result.ContentLanguage = resp.Header.Get("Content-Language") + result.ContentMD5 = resp.Header.Get("Content-MD5") + result.ContentType = resp.Header.Get("Content-Type") + result.CopyCompletionTime = resp.Header.Get("x-ms-copy-completion-time") + result.CopyDestinationSnapshot = resp.Header.Get("x-ms-copy-destination-snapshot") + result.CopyID = resp.Header.Get("x-ms-copy-id") + result.CopyProgress = resp.Header.Get(" x-ms-copy-progress") + result.CopySource = resp.Header.Get("x-ms-copy-source") + result.CopyStatus = CopyStatus(resp.Header.Get("x-ms-copy-status")) + result.CopyStatusDescription = resp.Header.Get("x-ms-copy-status-description") + result.CreationTime = resp.Header.Get("x-ms-creation-time") + result.ETag = resp.Header.Get("Etag") + result.LastModified = resp.Header.Get("Last-Modified") + result.LeaseDuration = LeaseDuration(resp.Header.Get("x-ms-lease-duration")) + result.LeaseState = LeaseState(resp.Header.Get("x-ms-lease-state")) + result.LeaseStatus = LeaseStatus(resp.Header.Get("x-ms-lease-status")) + result.MetaData = metadata.ParseFromHeaders(resp.Header) + + if v := resp.Header.Get("x-ms-access-tier-inferred"); v != "" { + b, innerErr := strconv.ParseBool(v) + if innerErr != nil { + err = fmt.Errorf("Error parsing %q as a bool: %s", v, innerErr) + return + } + + result.AccessTierInferred = b + } + + if v := resp.Header.Get("Content-Length"); v != "" { + i, innerErr := strconv.Atoi(v) + if innerErr != nil { + err = fmt.Errorf("Error parsing %q as an integer: %s", v, innerErr) + } + + result.ContentLength = int64(i) + } + + if v := resp.Header.Get("x-ms-incremental-copy"); v != "" { + b, innerErr := strconv.ParseBool(v) + if innerErr != nil { + err = fmt.Errorf("Error parsing %q as a bool: %s", v, innerErr) + return + } + + result.IncrementalCopy = b + } + + if v := resp.Header.Get("x-ms-server-encrypted"); v != "" { + b, innerErr := strconv.ParseBool(v) + if innerErr != nil { + err = fmt.Errorf("Error parsing %q as a bool: %s", v, innerErr) + return + } + + result.IncrementalCopy = b + } + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-03-28/blob/blobs/properties_set.go b/storage/2018-03-28/blob/blobs/properties_set.go new file mode 100644 index 0000000..a8c0ed8 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/properties_set.go @@ -0,0 +1,156 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type SetPropertiesInput struct { + CacheControl *string + ContentType *string + ContentMD5 *string + ContentEncoding *string + ContentLanguage *string + LeaseID *string + ContentDisposition *string + ContentLength *int64 + SequenceNumberAction *SequenceNumberAction + BlobSequenceNumber *string +} + +type SetPropertiesResult struct { + autorest.Response + + BlobSequenceNumber string + Etag string +} + +// SetProperties sets system properties on the blob. +func (client Client) SetProperties(ctx context.Context, accountName, containerName, blobName string, input SetPropertiesInput) (result SetPropertiesResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "SetProperties", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "SetProperties", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "SetProperties", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "SetProperties", "`blobName` cannot be an empty string.") + } + + req, err := client.SetPropertiesPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "SetProperties", nil, "Failure preparing request") + return + } + + resp, err := client.SetPropertiesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "SetProperties", resp, "Failure sending request") + return + } + + result, err = client.SetPropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "SetProperties", resp, "Failure responding to request") + return + } + + return +} + +type SequenceNumberAction string + +var ( + Increment SequenceNumberAction = "increment" + Max SequenceNumberAction = "max" + Update SequenceNumberAction = "update" +) + +// SetPropertiesPreparer prepares the SetProperties request. +func (client Client) SetPropertiesPreparer(ctx context.Context, accountName, containerName, blobName string, input SetPropertiesInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "properties"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.CacheControl != nil { + headers["x-ms-blob-cache-control"] = *input.CacheControl + } + if input.ContentDisposition != nil { + headers["x-ms-blob-content-disposition"] = *input.ContentDisposition + } + if input.ContentEncoding != nil { + headers["x-ms-blob-content-encoding"] = *input.ContentEncoding + } + if input.ContentLanguage != nil { + headers["x-ms-blob-content-language"] = *input.ContentLanguage + } + if input.ContentMD5 != nil { + headers["x-ms-blob-content-md5"] = *input.ContentMD5 + } + if input.ContentType != nil { + headers["x-ms-blob-content-type"] = *input.ContentType + } + if input.ContentLength != nil { + headers["x-ms-blob-content-length"] = *input.ContentLength + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + if input.SequenceNumberAction != nil { + headers["x-ms-sequence-number-action"] = string(*input.SequenceNumberAction) + } + if input.BlobSequenceNumber != nil { + headers["x-ms-blob-sequence-number"] = *input.BlobSequenceNumber + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetPropertiesSender sends the SetProperties request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetPropertiesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetPropertiesResponder handles the response to the SetProperties request. The method always +// closes the http.Response Body. +func (client Client) SetPropertiesResponder(resp *http.Response) (result SetPropertiesResult, err error) { + if resp != nil && resp.Header != nil { + result.BlobSequenceNumber = resp.Header.Get("x-ms-blob-sequence-number") + result.Etag = resp.Header.Get("Etag") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-03-28/blob/blobs/put_append_blob.go b/storage/2018-03-28/blob/blobs/put_append_blob.go new file mode 100644 index 0000000..ef2c502 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/put_append_blob.go @@ -0,0 +1,134 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type PutAppendBlobInput struct { + CacheControl *string + ContentDisposition *string + ContentEncoding *string + ContentLanguage *string + ContentMD5 *string + ContentType *string + LeaseID *string + MetaData map[string]string +} + +// PutAppendBlob is a wrapper around the Put API call (with a stricter input object) +// which creates a new append blob, or updates the content of an existing blob. +func (client Client) PutAppendBlob(ctx context.Context, accountName, containerName, blobName string, input PutAppendBlobInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutAppendBlob", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutAppendBlob", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutAppendBlob", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutAppendBlob", "`blobName` cannot be an empty string.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("blobs.Client", "PutAppendBlob", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.PutAppendBlobPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutAppendBlob", nil, "Failure preparing request") + return + } + + resp, err := client.PutAppendBlobSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutAppendBlob", resp, "Failure sending request") + return + } + + result, err = client.PutAppendBlobResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutAppendBlob", resp, "Failure responding to request") + return + } + + return +} + +// PutAppendBlobPreparer prepares the PutAppendBlob request. +func (client Client) PutAppendBlobPreparer(ctx context.Context, accountName, containerName, blobName string, input PutAppendBlobInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-blob-type": string(AppendBlob), + "x-ms-version": APIVersion, + + // For a page blob or an append blob, the value of this header must be set to zero, + // as Put Blob is used only to initialize the blob + "Content-Length": 0, + } + + if input.CacheControl != nil { + headers["x-ms-blob-cache-control"] = *input.CacheControl + } + if input.ContentDisposition != nil { + headers["x-ms-blob-content-disposition"] = *input.ContentDisposition + } + if input.ContentEncoding != nil { + headers["x-ms-blob-content-encoding"] = *input.ContentEncoding + } + if input.ContentLanguage != nil { + headers["x-ms-blob-content-language"] = *input.ContentLanguage + } + if input.ContentMD5 != nil { + headers["x-ms-blob-content-md5"] = *input.ContentMD5 + } + if input.ContentType != nil { + headers["x-ms-blob-content-type"] = *input.ContentType + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutAppendBlobSender sends the PutAppendBlob request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutAppendBlobSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutAppendBlobResponder handles the response to the PutAppendBlob request. The method always +// closes the http.Response Body. +func (client Client) PutAppendBlobResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/blobs/put_block.go b/storage/2018-03-28/blob/blobs/put_block.go new file mode 100644 index 0000000..5256013 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/put_block.go @@ -0,0 +1,125 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type PutBlockInput struct { + BlockID string + Content []byte + ContentMD5 *string + LeaseID *string +} + +type PutBlockResult struct { + autorest.Response + + ContentMD5 string +} + +// PutBlock creates a new block to be committed as part of a blob. +func (client Client) PutBlock(ctx context.Context, accountName, containerName, blobName string, input PutBlockInput) (result PutBlockResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutBlock", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutBlock", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutBlock", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutBlock", "`blobName` cannot be an empty string.") + } + if input.BlockID == "" { + return result, validation.NewError("blobs.Client", "PutBlock", "`input.BlockID` cannot be an empty string.") + } + if len(input.Content) == 0 { + return result, validation.NewError("blobs.Client", "PutBlock", "`input.Content` cannot be empty.") + } + + req, err := client.PutBlockPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlock", nil, "Failure preparing request") + return + } + + resp, err := client.PutBlockSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlock", resp, "Failure sending request") + return + } + + result, err = client.PutBlockResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlock", resp, "Failure responding to request") + return + } + + return +} + +// PutBlockPreparer prepares the PutBlock request. +func (client Client) PutBlockPreparer(ctx context.Context, accountName, containerName, blobName string, input PutBlockInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "block"), + "blockid": autorest.Encode("query", input.BlockID), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.ContentMD5 != nil { + headers["x-ms-blob-content-md5"] = *input.ContentMD5 + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers), + autorest.WithBytes(&input.Content)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutBlockSender sends the PutBlock request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutBlockSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutBlockResponder handles the response to the PutBlock request. The method always +// closes the http.Response Body. +func (client Client) PutBlockResponder(resp *http.Response) (result PutBlockResult, err error) { + if resp != nil && resp.Header != nil { + result.ContentMD5 = resp.Header.Get("Content-MD5") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/blobs/put_block_blob.go b/storage/2018-03-28/blob/blobs/put_block_blob.go new file mode 100644 index 0000000..fa29dd3 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/put_block_blob.go @@ -0,0 +1,135 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type PutBlockBlobInput struct { + CacheControl *string + Content []byte + ContentDisposition *string + ContentEncoding *string + ContentLanguage *string + ContentMD5 *string + ContentType *string + LeaseID *string + MetaData map[string]string +} + +// PutBlockBlob is a wrapper around the Put API call (with a stricter input object) +// which creates a new block append blob, or updates the content of an existing block blob. +func (client Client) PutBlockBlob(ctx context.Context, accountName, containerName, blobName string, input PutBlockBlobInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutBlockBlob", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutBlockBlob", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutBlockBlob", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutBlockBlob", "`blobName` cannot be an empty string.") + } + if len(input.Content) == 0 { + return result, validation.NewError("blobs.Client", "PutBlockBlob", "`input.Content` cannot be empty.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("blobs.Client", "PutBlockBlob", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.PutBlockBlobPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockBlob", nil, "Failure preparing request") + return + } + + resp, err := client.PutBlockBlobSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockBlob", resp, "Failure sending request") + return + } + + result, err = client.PutBlockBlobResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockBlob", resp, "Failure responding to request") + return + } + + return +} + +// PutBlockBlobPreparer prepares the PutBlockBlob request. +func (client Client) PutBlockBlobPreparer(ctx context.Context, accountName, containerName, blobName string, input PutBlockBlobInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-blob-type": string(BlockBlob), + "x-ms-version": APIVersion, + } + + if input.CacheControl != nil { + headers["x-ms-blob-cache-control"] = *input.CacheControl + } + if input.ContentDisposition != nil { + headers["x-ms-blob-content-disposition"] = *input.ContentDisposition + } + if input.ContentEncoding != nil { + headers["x-ms-blob-content-encoding"] = *input.ContentEncoding + } + if input.ContentLanguage != nil { + headers["x-ms-blob-content-language"] = *input.ContentLanguage + } + if input.ContentMD5 != nil { + headers["x-ms-blob-content-md5"] = *input.ContentMD5 + } + if input.ContentType != nil { + headers["x-ms-blob-content-type"] = *input.ContentType + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithBytes(&input.Content)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutBlockBlobSender sends the PutBlockBlob request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutBlockBlobSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutBlockBlobResponder handles the response to the PutBlockBlob request. The method always +// closes the http.Response Body. +func (client Client) PutBlockBlobResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/blobs/put_block_blob_file.go b/storage/2018-03-28/blob/blobs/put_block_blob_file.go new file mode 100644 index 0000000..7232e5e --- /dev/null +++ b/storage/2018-03-28/blob/blobs/put_block_blob_file.go @@ -0,0 +1,34 @@ +package blobs + +import ( + "context" + "fmt" + "io" + "os" +) + +// PutBlockBlobFromFile is a helper method which takes a file, and automatically chunks it up, rather than having to do this yourself +func (client Client) PutBlockBlobFromFile(ctx context.Context, accountName, containerName, blobName string, file *os.File, input PutBlockBlobInput) error { + fileInfo, err := file.Stat() + if err != nil { + return fmt.Errorf("Error loading file info: %s", err) + } + + fileSize := fileInfo.Size() + bytes := make([]byte, fileSize) + + _, err = file.ReadAt(bytes, 0) + if err != nil { + if err != io.EOF { + return fmt.Errorf("Error reading bytes: %s", err) + } + } + + input.Content = bytes + + if _, err = client.PutBlockBlob(ctx, accountName, containerName, blobName, input); err != nil { + return fmt.Errorf("Error putting bytes: %s", err) + } + + return nil +} diff --git a/storage/2018-03-28/blob/blobs/put_block_list.go b/storage/2018-03-28/blob/blobs/put_block_list.go new file mode 100644 index 0000000..f805247 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/put_block_list.go @@ -0,0 +1,157 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type BlockList struct { + CommittedBlockIDs []BlockID `xml:"Committed,omitempty"` + UncommittedBlockIDs []BlockID `xml:"Uncommitted,omitempty"` + LatestBlockIDs []BlockID `xml:"Latest,omitempty"` +} + +type BlockID struct { + Value string `xml:",chardata"` +} + +type PutBlockListInput struct { + BlockList BlockList + CacheControl *string + ContentDisposition *string + ContentEncoding *string + ContentLanguage *string + ContentMD5 *string + ContentType *string + MetaData map[string]string + LeaseID *string +} + +type PutBlockListResult struct { + autorest.Response + + ContentMD5 string + ETag string + LastModified string +} + +// PutBlockList writes a blob by specifying the list of block IDs that make up the blob. +// In order to be written as part of a blob, a block must have been successfully written +// to the server in a prior Put Block operation. +func (client Client) PutBlockList(ctx context.Context, accountName, containerName, blobName string, input PutBlockListInput) (result PutBlockListResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutBlockList", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutBlockList", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutBlockList", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutBlockList", "`blobName` cannot be an empty string.") + } + + req, err := client.PutBlockListPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockList", nil, "Failure preparing request") + return + } + + resp, err := client.PutBlockListSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockList", resp, "Failure sending request") + return + } + + result, err = client.PutBlockListResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockList", resp, "Failure responding to request") + return + } + + return +} + +// PutBlockListPreparer prepares the PutBlockList request. +func (client Client) PutBlockListPreparer(ctx context.Context, accountName, containerName, blobName string, input PutBlockListInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "blocklist"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.CacheControl != nil { + headers["x-ms-blob-cache-control"] = *input.CacheControl + } + if input.ContentDisposition != nil { + headers["x-ms-blob-content-disposition"] = *input.ContentDisposition + } + if input.ContentEncoding != nil { + headers["x-ms-blob-content-encoding"] = *input.ContentEncoding + } + if input.ContentLanguage != nil { + headers["x-ms-blob-content-language"] = *input.ContentLanguage + } + if input.ContentMD5 != nil { + headers["x-ms-blob-content-md5"] = *input.ContentMD5 + } + if input.ContentType != nil { + headers["x-ms-blob-content-type"] = *input.ContentType + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers), + autorest.WithXML(input.BlockList)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutBlockListSender sends the PutBlockList request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutBlockListSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutBlockListResponder handles the response to the PutBlockList request. The method always +// closes the http.Response Body. +func (client Client) PutBlockListResponder(resp *http.Response) (result PutBlockListResult, err error) { + if resp != nil && resp.Header != nil { + result.ContentMD5 = resp.Header.Get("Content-MD5") + result.ETag = resp.Header.Get("ETag") + result.LastModified = resp.Header.Get("Last-Modified") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/blobs/put_block_url.go b/storage/2018-03-28/blob/blobs/put_block_url.go new file mode 100644 index 0000000..95ad974 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/put_block_url.go @@ -0,0 +1,129 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type PutBlockFromURLInput struct { + BlockID string + CopySource string + + ContentMD5 *string + LeaseID *string + Range *string +} + +type PutBlockFromURLResult struct { + autorest.Response + ContentMD5 string +} + +// PutBlockFromURL creates a new block to be committed as part of a blob where the contents are read from a URL +func (client Client) PutBlockFromURL(ctx context.Context, accountName, containerName, blobName string, input PutBlockFromURLInput) (result PutBlockFromURLResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutBlockFromURL", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutBlockFromURL", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutBlockFromURL", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutBlockFromURL", "`blobName` cannot be an empty string.") + } + if input.BlockID == "" { + return result, validation.NewError("blobs.Client", "PutBlockFromURL", "`input.BlockID` cannot be an empty string.") + } + if input.CopySource == "" { + return result, validation.NewError("blobs.Client", "PutBlockFromURL", "`input.CopySource` cannot be an empty string.") + } + + req, err := client.PutBlockFromURLPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockFromURL", nil, "Failure preparing request") + return + } + + resp, err := client.PutBlockFromURLSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockFromURL", resp, "Failure sending request") + return + } + + result, err = client.PutBlockFromURLResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockFromURL", resp, "Failure responding to request") + return + } + + return +} + +// PutBlockFromURLPreparer prepares the PutBlockFromURL request. +func (client Client) PutBlockFromURLPreparer(ctx context.Context, accountName, containerName, blobName string, input PutBlockFromURLInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "block"), + "blockid": autorest.Encode("query", input.BlockID), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-copy-source": input.CopySource, + } + + if input.ContentMD5 != nil { + headers["x-ms-source-content-md5"] = *input.ContentMD5 + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + if input.Range != nil { + headers["x-ms-source-range"] = *input.Range + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutBlockFromURLSender sends the PutBlockFromURL request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutBlockFromURLSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutBlockFromURLResponder handles the response to the PutBlockFromURL request. The method always +// closes the http.Response Body. +func (client Client) PutBlockFromURLResponder(resp *http.Response) (result PutBlockFromURLResult, err error) { + if resp != nil && resp.Header != nil { + result.ContentMD5 = resp.Header.Get("Content-MD5") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/blobs/put_page_blob.go b/storage/2018-03-28/blob/blobs/put_page_blob.go new file mode 100644 index 0000000..ad3c878 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/put_page_blob.go @@ -0,0 +1,148 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type PutPageBlobInput struct { + CacheControl *string + ContentDisposition *string + ContentEncoding *string + ContentLanguage *string + ContentMD5 *string + ContentType *string + LeaseID *string + MetaData map[string]string + + BlobContentLengthBytes int64 + BlobSequenceNumber *int64 + AccessTier *AccessTier +} + +// PutPageBlob is a wrapper around the Put API call (with a stricter input object) +// which creates a new block blob, or updates the content of an existing page blob. +func (client Client) PutPageBlob(ctx context.Context, accountName, containerName, blobName string, input PutPageBlobInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutPageBlob", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutPageBlob", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutPageBlob", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutPageBlob", "`blobName` cannot be an empty string.") + } + if input.BlobContentLengthBytes == 0 || input.BlobContentLengthBytes%512 != 0 { + return result, validation.NewError("blobs.Client", "PutPageBlob", "`blobName` must be aligned to a 512-byte boundary.") + } + + req, err := client.PutPageBlobPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageBlob", nil, "Failure preparing request") + return + } + + resp, err := client.PutPageBlobSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageBlob", resp, "Failure sending request") + return + } + + result, err = client.PutPageBlobResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageBlob", resp, "Failure responding to request") + return + } + + return +} + +// PutPageBlobPreparer prepares the PutPageBlob request. +func (client Client) PutPageBlobPreparer(ctx context.Context, accountName, containerName, blobName string, input PutPageBlobInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-blob-type": string(PageBlob), + "x-ms-version": APIVersion, + + // For a page blob or an page blob, the value of this header must be set to zero, + // as Put Blob is used only to initialize the blob + "Content-Length": 0, + + // This header specifies the maximum size for the page blob, up to 8 TB. + // The page blob size must be aligned to a 512-byte boundary. + "x-ms-blob-content-length": input.BlobContentLengthBytes, + } + + if input.AccessTier != nil { + headers["x-ms-access-tier"] = string(*input.AccessTier) + } + if input.BlobSequenceNumber != nil { + headers["x-ms-blob-sequence-number"] = *input.BlobSequenceNumber + } + + if input.CacheControl != nil { + headers["x-ms-blob-cache-control"] = *input.CacheControl + } + if input.ContentDisposition != nil { + headers["x-ms-blob-content-disposition"] = *input.ContentDisposition + } + if input.ContentEncoding != nil { + headers["x-ms-blob-content-encoding"] = *input.ContentEncoding + } + if input.ContentLanguage != nil { + headers["x-ms-blob-content-language"] = *input.ContentLanguage + } + if input.ContentMD5 != nil { + headers["x-ms-blob-content-md5"] = *input.ContentMD5 + } + if input.ContentType != nil { + headers["x-ms-blob-content-type"] = *input.ContentType + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutPageBlobSender sends the PutPageBlob request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutPageBlobSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutPageBlobResponder handles the response to the PutPageBlob request. The method always +// closes the http.Response Body. +func (client Client) PutPageBlobResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/blobs/put_page_clear.go b/storage/2018-03-28/blob/blobs/put_page_clear.go new file mode 100644 index 0000000..59feaa5 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/put_page_clear.go @@ -0,0 +1,113 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type PutPageClearInput struct { + StartByte int64 + EndByte int64 + + LeaseID *string +} + +// PutPageClear clears a range of pages within a page blob. +func (client Client) PutPageClear(ctx context.Context, accountName, containerName, blobName string, input PutPageClearInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutPageClear", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutPageClear", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutPageClear", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutPageClear", "`blobName` cannot be an empty string.") + } + if input.StartByte < 0 { + return result, validation.NewError("blobs.Client", "PutPageClear", "`input.StartByte` must be greater than or equal to 0.") + } + if input.EndByte <= 0 { + return result, validation.NewError("blobs.Client", "PutPageClear", "`input.EndByte` must be greater than 0.") + } + + req, err := client.PutPageClearPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageClear", nil, "Failure preparing request") + return + } + + resp, err := client.PutPageClearSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageClear", resp, "Failure sending request") + return + } + + result, err = client.PutPageClearResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageClear", resp, "Failure responding to request") + return + } + + return +} + +// PutPageClearPreparer prepares the PutPageClear request. +func (client Client) PutPageClearPreparer(ctx context.Context, accountName, containerName, blobName string, input PutPageClearInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "page"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-page-write": "clear", + "x-ms-range": fmt.Sprintf("bytes=%d-%d", input.StartByte, input.EndByte), + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutPageClearSender sends the PutPageClear request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutPageClearSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutPageClearResponder handles the response to the PutPageClear request. The method always +// closes the http.Response Body. +func (client Client) PutPageClearResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/blobs/put_page_update.go b/storage/2018-03-28/blob/blobs/put_page_update.go new file mode 100644 index 0000000..a47e8ca --- /dev/null +++ b/storage/2018-03-28/blob/blobs/put_page_update.go @@ -0,0 +1,163 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type PutPageUpdateInput struct { + StartByte int64 + EndByte int64 + Content []byte + + IfSequenceNumberEQ *string + IfSequenceNumberLE *string + IfSequenceNumberLT *string + IfModifiedSince *string + IfUnmodifiedSince *string + IfMatch *string + IfNoneMatch *string + LeaseID *string +} + +type PutPageUpdateResult struct { + autorest.Response + + BlobSequenceNumber string + ContentMD5 string + LastModified string +} + +// PutPageUpdate writes a range of pages to a page blob. +func (client Client) PutPageUpdate(ctx context.Context, accountName, containerName, blobName string, input PutPageUpdateInput) (result PutPageUpdateResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutPageUpdate", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutPageUpdate", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutPageUpdate", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutPageUpdate", "`blobName` cannot be an empty string.") + } + if input.StartByte < 0 { + return result, validation.NewError("blobs.Client", "PutPageUpdate", "`input.StartByte` must be greater than or equal to 0.") + } + if input.EndByte <= 0 { + return result, validation.NewError("blobs.Client", "PutPageUpdate", "`input.EndByte` must be greater than 0.") + } + + expectedSize := (input.EndByte - input.StartByte) + 1 + actualSize := int64(len(input.Content)) + if expectedSize != actualSize { + return result, validation.NewError("blobs.Client", "PutPageUpdate", fmt.Sprintf("Content Size was defined as %d but got %d.", expectedSize, actualSize)) + } + + req, err := client.PutPageUpdatePreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageUpdate", nil, "Failure preparing request") + return + } + + resp, err := client.PutPageUpdateSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageUpdate", resp, "Failure sending request") + return + } + + result, err = client.PutPageUpdateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageUpdate", resp, "Failure responding to request") + return + } + + return +} + +// PutPageUpdatePreparer prepares the PutPageUpdate request. +func (client Client) PutPageUpdatePreparer(ctx context.Context, accountName, containerName, blobName string, input PutPageUpdateInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "page"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-page-write": "update", + "x-ms-range": fmt.Sprintf("bytes=%d-%d", input.StartByte, input.EndByte), + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + if input.IfSequenceNumberEQ != nil { + headers["x-ms-if-sequence-number-eq"] = *input.IfSequenceNumberEQ + } + if input.IfSequenceNumberLE != nil { + headers["x-ms-if-sequence-number-le"] = *input.IfSequenceNumberLE + } + if input.IfSequenceNumberLT != nil { + headers["x-ms-if-sequence-number-lt"] = *input.IfSequenceNumberLT + } + if input.IfModifiedSince != nil { + headers["If-Modified-Since"] = *input.IfModifiedSince + } + if input.IfUnmodifiedSince != nil { + headers["If-Unmodified-Since"] = *input.IfUnmodifiedSince + } + if input.IfMatch != nil { + headers["If-Match"] = *input.IfMatch + } + if input.IfNoneMatch != nil { + headers["If-None-Match"] = *input.IfNoneMatch + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers), + autorest.WithBytes(&input.Content)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutPageUpdateSender sends the PutPageUpdate request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutPageUpdateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutPageUpdateResponder handles the response to the PutPageUpdate request. The method always +// closes the http.Response Body. +func (client Client) PutPageUpdateResponder(resp *http.Response) (result PutPageUpdateResult, err error) { + if resp != nil && resp.Header != nil { + result.BlobSequenceNumber = resp.Header.Get("x-ms-blob-sequence-number") + result.ContentMD5 = resp.Header.Get("Content-MD5") + result.LastModified = resp.Header.Get("Last-Modified") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/blobs/resource_id.go b/storage/2018-03-28/blob/blobs/resource_id.go new file mode 100644 index 0000000..0f6dddf --- /dev/null +++ b/storage/2018-03-28/blob/blobs/resource_id.go @@ -0,0 +1,56 @@ +package blobs + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given Blob +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, containerName, blobName string) string { + domain := endpoints.GetBlobEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s/%s", domain, containerName, blobName) +} + +type ResourceID struct { + AccountName string + ContainerName string + BlobName string +} + +// ParseResourceID parses the Resource ID and returns an object which can be used +// to interact with the Blob Resource +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://foo.blob.core.windows.net/Bar/example.vhd + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + path := strings.TrimPrefix(uri.Path, "/") + segments := strings.Split(path, "/") + if len(segments) == 0 { + return nil, fmt.Errorf("Expected the path to contain segments but got none") + } + + containerName := segments[0] + blobName := strings.TrimPrefix(path, containerName) + blobName = strings.TrimPrefix(blobName, "/") + return &ResourceID{ + AccountName: *accountName, + ContainerName: containerName, + BlobName: blobName, + }, nil +} diff --git a/storage/2018-03-28/blob/blobs/resource_id_test.go b/storage/2018-03-28/blob/blobs/resource_id_test.go new file mode 100644 index 0000000..bb6cad1 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/resource_id_test.go @@ -0,0 +1,123 @@ +package blobs + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.blob.core.chinacloudapi.cn/container1/blob1.vhd", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.blob.core.cloudapi.de/container1/blob1.vhd", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.blob.core.windows.net/container1/blob1.vhd", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.blob.core.usgovcloudapi.net/container1/blob1.vhd", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "container1", "blob1.vhd") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.blob.core.chinacloudapi.cn/container1/blob1.vhd", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.blob.core.cloudapi.de/container1/blob1.vhd", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.blob.core.windows.net/container1/blob1.vhd", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.blob.core.usgovcloudapi.net/container1/blob1.vhd", + }, + } + t.Logf("[DEBUG] Top Level Files") + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.ContainerName != "container1" { + t.Fatalf("Expected Container Name to be `container1` but got %q", actual.ContainerName) + } + if actual.BlobName != "blob1.vhd" { + t.Fatalf("Expected Blob Name to be `blob1.vhd` but got %q", actual.BlobName) + } + } + + testData = []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.blob.core.chinacloudapi.cn/container1/example/blob1.vhd", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.blob.core.cloudapi.de/container1/example/blob1.vhd", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.blob.core.windows.net/container1/example/blob1.vhd", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.blob.core.usgovcloudapi.net/container1/example/blob1.vhd", + }, + } + t.Logf("[DEBUG] Nested Files") + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.ContainerName != "container1" { + t.Fatalf("Expected Container Name to be `container1` but got %q", actual.ContainerName) + } + if actual.BlobName != "example/blob1.vhd" { + t.Fatalf("Expected Blob Name to be `example/blob1.vhd` but got %q", actual.BlobName) + } + } +} diff --git a/storage/2018-03-28/blob/blobs/set_tier.go b/storage/2018-03-28/blob/blobs/set_tier.go new file mode 100644 index 0000000..dd0f0b8 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/set_tier.go @@ -0,0 +1,93 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// SetTier sets the tier on a blob. +func (client Client) SetTier(ctx context.Context, accountName, containerName, blobName string, tier AccessTier) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "SetTier", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "SetTier", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "SetTier", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "SetTier", "`blobName` cannot be an empty string.") + } + + req, err := client.SetTierPreparer(ctx, accountName, containerName, blobName, tier) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "SetTier", nil, "Failure preparing request") + return + } + + resp, err := client.SetTierSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "SetTier", resp, "Failure sending request") + return + } + + result, err = client.SetTierResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "SetTier", resp, "Failure responding to request") + return + } + + return +} + +// SetTierPreparer prepares the SetTier request. +func (client Client) SetTierPreparer(ctx context.Context, accountName, containerName, blobName string, tier AccessTier) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("path", "tier"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-access-tier": string(tier), + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetTierSender sends the SetTier request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetTierSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetTierResponder handles the response to the SetTier request. The method always +// closes the http.Response Body. +func (client Client) SetTierResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-03-28/blob/blobs/snapshot.go b/storage/2018-03-28/blob/blobs/snapshot.go new file mode 100644 index 0000000..180070b --- /dev/null +++ b/storage/2018-03-28/blob/blobs/snapshot.go @@ -0,0 +1,163 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type SnapshotInput struct { + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string + + // MetaData is a user-defined name-value pair associated with the blob. + // If no name-value pairs are specified, the operation will copy the base blob metadata to the snapshot. + // If one or more name-value pairs are specified, the snapshot is created with the specified metadata, + // and metadata is not copied from the base blob. + MetaData map[string]string + + // A DateTime value which will only snapshot the blob if it has been modified since the specified date/time + // If the base blob has not been modified, the Blob service returns status code 412 (Precondition Failed). + IfModifiedSince *string + + // A DateTime value which will only snapshot the blob if it has not been modified since the specified date/time + // If the base blob has been modified, the Blob service returns status code 412 (Precondition Failed). + IfUnmodifiedSince *string + + // An ETag value to snapshot the blob only if its ETag value matches the value specified. + // If the values do not match, the Blob service returns status code 412 (Precondition Failed). + IfMatch *string + + // An ETag value for this conditional header to snapshot the blob only if its ETag value + // does not match the value specified. + // If the values are identical, the Blob service returns status code 412 (Precondition Failed). + IfNoneMatch *string +} + +type SnapshotResult struct { + autorest.Response + + // The ETag of the snapshot + ETag string + + // A DateTime value that uniquely identifies the snapshot. + // The value of this header indicates the snapshot version, + // and may be used in subsequent requests to access the snapshot. + SnapshotDateTime string +} + +// Snapshot captures a Snapshot of a given Blob +func (client Client) Snapshot(ctx context.Context, accountName, containerName, blobName string, input SnapshotInput) (result SnapshotResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "Snapshot", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "Snapshot", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "Snapshot", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "Snapshot", "`blobName` cannot be an empty string.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("blobs.Client", "Snapshot", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.SnapshotPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Snapshot", nil, "Failure preparing request") + return + } + + resp, err := client.SnapshotSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "Snapshot", resp, "Failure sending request") + return + } + + result, err = client.SnapshotResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Snapshot", resp, "Failure responding to request") + return + } + + return +} + +// SnapshotPreparer prepares the Snapshot request. +func (client Client) SnapshotPreparer(ctx context.Context, accountName, containerName, blobName string, input SnapshotInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "snapshot"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + if input.IfModifiedSince != nil { + headers["If-Modified-Since"] = *input.IfModifiedSince + } + if input.IfUnmodifiedSince != nil { + headers["If-Unmodified-Since"] = *input.IfUnmodifiedSince + } + if input.IfMatch != nil { + headers["If-Match"] = *input.IfMatch + } + if input.IfNoneMatch != nil { + headers["If-None-Match"] = *input.IfNoneMatch + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SnapshotSender sends the Snapshot request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SnapshotSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SnapshotResponder handles the response to the Snapshot request. The method always +// closes the http.Response Body. +func (client Client) SnapshotResponder(resp *http.Response) (result SnapshotResult, err error) { + if resp != nil && resp.Header != nil { + result.ETag = resp.Header.Get("ETag") + result.SnapshotDateTime = resp.Header.Get("x-ms-snapshot") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/blobs/snapshot_get_properties.go b/storage/2018-03-28/blob/blobs/snapshot_get_properties.go new file mode 100644 index 0000000..fe1be63 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/snapshot_get_properties.go @@ -0,0 +1,90 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetSnapshotPropertiesInput struct { + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string + + // The ID of the Snapshot which should be retrieved + SnapshotID string +} + +// GetSnapshotProperties returns all user-defined metadata, standard HTTP properties, and system properties for +// the specified snapshot of a blob +func (client Client) GetSnapshotProperties(ctx context.Context, accountName, containerName, blobName string, input GetSnapshotPropertiesInput) (result GetPropertiesResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "GetSnapshotProperties", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "GetSnapshotProperties", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "GetSnapshotProperties", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "GetSnapshotProperties", "`blobName` cannot be an empty string.") + } + if input.SnapshotID == "" { + return result, validation.NewError("blobs.Client", "GetSnapshotProperties", "`input.SnapshotID` cannot be an empty string.") + } + + req, err := client.GetSnapshotPropertiesPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetSnapshotProperties", nil, "Failure preparing request") + return + } + + // we re-use the GetProperties methods since this is otherwise the same + resp, err := client.GetPropertiesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "GetSnapshotProperties", resp, "Failure sending request") + return + } + + result, err = client.GetPropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetSnapshotProperties", resp, "Failure responding to request") + return + } + + return +} + +// GetSnapshotPreparer prepares the GetSnapshot request. +func (client Client) GetSnapshotPropertiesPreparer(ctx context.Context, accountName, containerName, blobName string, input GetSnapshotPropertiesInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "snapshot": autorest.Encode("query", input.SnapshotID), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsHead(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} diff --git a/storage/2018-03-28/blob/blobs/snapshot_test.go b/storage/2018-03-28/blob/blobs/snapshot_test.go new file mode 100644 index 0000000..c8d7585 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/snapshot_test.go @@ -0,0 +1,159 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-03-28/blob/containers" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestSnapshotLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + fileName := "example.txt" + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.BlobStorage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + containersClient := containers.NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithStorageResourceManagerAuth(containersClient.Client) + + _, err = containersClient.Create(ctx, accountName, containerName, containers.CreateInput{}) + if err != nil { + t.Fatalf("Error creating: %s", err) + } + defer containersClient.Delete(ctx, accountName, containerName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + blobClient := NewWithEnvironment(client.Environment) + blobClient.Client = client.PrepareWithAuthorizer(blobClient.Client, storageAuth) + + t.Logf("[DEBUG] Copying file to Blob Storage..") + copyInput := CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + + refreshInterval := 5 * time.Second + if err := blobClient.CopyAndWait(ctx, accountName, containerName, fileName, copyInput, refreshInterval); err != nil { + t.Fatalf("Error copying: %s", err) + } + + t.Logf("[DEBUG] First Snapshot..") + firstSnapshot, err := blobClient.Snapshot(ctx, accountName, containerName, fileName, SnapshotInput{}) + if err != nil { + t.Fatalf("Error taking first snapshot: %s", err) + } + t.Logf("[DEBUG] First Snapshot ID: %q", firstSnapshot.SnapshotDateTime) + + t.Log("[DEBUG] Waiting 2 seconds..") + time.Sleep(2 * time.Second) + + t.Logf("[DEBUG] Second Snapshot..") + secondSnapshot, err := blobClient.Snapshot(ctx, accountName, containerName, fileName, SnapshotInput{ + MetaData: map[string]string{ + "hello": "world", + }, + }) + if err != nil { + t.Fatalf("Error taking Second snapshot: %s", err) + } + t.Logf("[DEBUG] Second Snapshot ID: %q", secondSnapshot.SnapshotDateTime) + + t.Logf("[DEBUG] Leasing the Blob..") + leaseDetails, err := blobClient.AcquireLease(ctx, accountName, containerName, fileName, AcquireLeaseInput{ + // infinite + LeaseDuration: -1, + }) + if err != nil { + t.Fatalf("Error leasing Blob: %s", err) + } + t.Logf("[DEBUG] Lease ID: %q", leaseDetails.LeaseID) + + t.Logf("[DEBUG] Third Snapshot..") + thirdSnapshot, err := blobClient.Snapshot(ctx, accountName, containerName, fileName, SnapshotInput{ + LeaseID: &leaseDetails.LeaseID, + }) + if err != nil { + t.Fatalf("Error taking Third snapshot: %s", err) + } + t.Logf("[DEBUG] Third Snapshot ID: %q", thirdSnapshot.SnapshotDateTime) + + t.Logf("[DEBUG] Releasing Lease..") + if _, err := blobClient.ReleaseLease(ctx, accountName, containerName, fileName, leaseDetails.LeaseID); err != nil { + t.Fatalf("Error releasing Lease: %s", err) + } + + // get the properties from the blob, which should include the LastModifiedDate + t.Logf("[DEBUG] Retrieving Properties for Blob") + props, err := blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error getting properties: %s", err) + } + + // confirm that the If-Modified-None returns an error + t.Logf("[DEBUG] Third Snapshot..") + fourthSnapshot, err := blobClient.Snapshot(ctx, accountName, containerName, fileName, SnapshotInput{ + LeaseID: &leaseDetails.LeaseID, + IfModifiedSince: &props.LastModified, + }) + if err == nil { + t.Fatalf("Expected an error but didn't get one") + } + if fourthSnapshot.Response.StatusCode != http.StatusPreconditionFailed { + t.Fatalf("Expected the status code to be Precondition Failed but got: %d", fourthSnapshot.Response.StatusCode) + } + + t.Logf("[DEBUG] Retrieving the Second Snapshot Properties..") + getSecondSnapshotInput := GetSnapshotPropertiesInput{ + SnapshotID: secondSnapshot.SnapshotDateTime, + } + if _, err := blobClient.GetSnapshotProperties(ctx, accountName, containerName, fileName, getSecondSnapshotInput); err != nil { + t.Fatalf("Error retrieving properties for the second snapshot: %s", err) + } + + t.Logf("[DEBUG] Deleting the Second Snapshot..") + deleteSnapshotInput := DeleteSnapshotInput{ + SnapshotDateTime: secondSnapshot.SnapshotDateTime, + } + if _, err := blobClient.DeleteSnapshot(ctx, accountName, containerName, fileName, deleteSnapshotInput); err != nil { + t.Fatalf("Error deleting snapshot: %s", err) + } + + t.Logf("[DEBUG] Re-Retrieving the Second Snapshot Properties..") + secondSnapshotProps, err := blobClient.GetSnapshotProperties(ctx, accountName, containerName, fileName, getSecondSnapshotInput) + if err == nil { + t.Fatalf("Expected an error retrieving the snapshot but got none") + } + if secondSnapshotProps.Response.StatusCode != http.StatusNotFound { + t.Fatalf("Expected the status code to be %d but got %q", http.StatusNoContent, secondSnapshotProps.Response.StatusCode) + } + + t.Logf("[DEBUG] Deleting all the snapshots..") + if _, err := blobClient.DeleteSnapshots(ctx, accountName, containerName, fileName, DeleteSnapshotsInput{}); err != nil { + t.Fatalf("Error deleting snapshots: %s", err) + } + + t.Logf("[DEBUG] Deleting the Blob..") + deleteInput := DeleteInput{ + DeleteSnapshots: false, + } + if _, err := blobClient.Delete(ctx, accountName, containerName, fileName, deleteInput); err != nil { + t.Fatalf("Error deleting Blob: %s", err) + } +} diff --git a/storage/2018-03-28/blob/blobs/undelete.go b/storage/2018-03-28/blob/blobs/undelete.go new file mode 100644 index 0000000..9be2f81 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/undelete.go @@ -0,0 +1,92 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Undelete restores the contents and metadata of soft deleted blob and any associated soft deleted snapshots. +func (client Client) Undelete(ctx context.Context, accountName, containerName, blobName string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "Undelete", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "Undelete", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "Undelete", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "Undelete", "`blobName` cannot be an empty string.") + } + + req, err := client.UndeletePreparer(ctx, accountName, containerName, blobName) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Undelete", nil, "Failure preparing request") + return + } + + resp, err := client.UndeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "Undelete", resp, "Failure sending request") + return + } + + result, err = client.UndeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Undelete", resp, "Failure responding to request") + return + } + + return +} + +// UndeletePreparer prepares the Undelete request. +func (client Client) UndeletePreparer(ctx context.Context, accountName, containerName, blobName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("path", "undelete"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// UndeleteSender sends the Undelete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) UndeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// UndeleteResponder handles the response to the Undelete request. The method always +// closes the http.Response Body. +func (client Client) UndeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-03-28/blob/blobs/version.go b/storage/2018-03-28/blob/blobs/version.go new file mode 100644 index 0000000..b1e5fa9 --- /dev/null +++ b/storage/2018-03-28/blob/blobs/version.go @@ -0,0 +1,14 @@ +package blobs + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2018-03-28" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2018-03-28/blob/containers/README.md b/storage/2018-03-28/blob/containers/README.md new file mode 100644 index 0000000..b6e4c8e --- /dev/null +++ b/storage/2018-03-28/blob/containers/README.md @@ -0,0 +1,45 @@ +## Blob Storage Container SDK for API version 2018-03-28 + +This package allows you to interact with the Containers Blob Storage API + +### Supported Authorizers + +* Azure Active Directory (for the Resource Endpoint `https://storage.azure.com`) +* SharedKeyLite (Blob, File & Queue) + +Note: when using the `ListBlobs` operation, only `SharedKeyLite` authentication is supported. + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2018-03-28/blob/containers" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + containerName := "mycontainer" + + storageAuth := autorest.NewSharedKeyLiteAuthorizer(accountName, storageAccountKey) + containersClient := containers.New() + containersClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + createInput := containers.CreateInput{ + AccessLevel: containers.Private, + } + if _, err := containersClient.Create(ctx, accountName, containerName, createInput); err != nil { + return fmt.Errorf("Error creating Container: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2018-03-28/blob/containers/client.go b/storage/2018-03-28/blob/containers/client.go new file mode 100644 index 0000000..7bf4947 --- /dev/null +++ b/storage/2018-03-28/blob/containers/client.go @@ -0,0 +1,34 @@ +package containers + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for Blob Storage Containers. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithBaseURI creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} + +func (client Client) setAccessLevelIntoHeaders(headers map[string]interface{}, level AccessLevel) map[string]interface{} { + // If this header is not included in the request, container data is private to the account owner. + if level != Private { + headers["x-ms-blob-public-access"] = string(level) + } + + return headers +} diff --git a/storage/2018-03-28/blob/containers/create.go b/storage/2018-03-28/blob/containers/create.go new file mode 100644 index 0000000..84c2887 --- /dev/null +++ b/storage/2018-03-28/blob/containers/create.go @@ -0,0 +1,123 @@ +package containers + +import ( + "context" + "fmt" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type CreateInput struct { + // Specifies whether data in the container may be accessed publicly and the level of access + AccessLevel AccessLevel + + // A name-value pair to associate with the container as metadata. + MetaData map[string]string +} + +type CreateResponse struct { + autorest.Response + Error *ErrorResponse `xml:"Error"` +} + +// Create creates a new container under the specified account. +// If the container with the same name already exists, the operation fails. +func (client Client) Create(ctx context.Context, accountName, containerName string, input CreateInput) (result CreateResponse, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "Create", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "Create", "`containerName` cannot be an empty string.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("containers.Client", "Create", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.CreatePreparer(ctx, accountName, containerName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "Create", nil, "Failure preparing request") + return + } + + resp, err := client.CreateSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "Create", resp, "Failure sending request") + return + } + + result, err = client.CreateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "Create", resp, "Failure responding to request") + return + } + + return +} + +// CreatePreparer prepares the Create request. +func (client Client) CreatePreparer(ctx context.Context, accountName string, containerName string, input CreateInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = client.setAccessLevelIntoHeaders(headers, input.AccessLevel) + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CreateSender sends the Create request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CreateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CreateResponder handles the response to the Create request. The method always +// closes the http.Response Body. +func (client Client) CreateResponder(resp *http.Response) (result CreateResponse, err error) { + successfulStatusCodes := []int{ + http.StatusCreated, + } + if autorest.ResponseHasStatusCode(resp, successfulStatusCodes...) { + // when successful there's no response + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(successfulStatusCodes...), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + } else { + // however when there's an error the error's in the response + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(successfulStatusCodes...), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + } + + return +} diff --git a/storage/2018-03-28/blob/containers/delete.go b/storage/2018-03-28/blob/containers/delete.go new file mode 100644 index 0000000..3095829 --- /dev/null +++ b/storage/2018-03-28/blob/containers/delete.go @@ -0,0 +1,85 @@ +package containers + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Delete marks the specified container for deletion. +// The container and any blobs contained within it are later deleted during garbage collection. +func (client Client) Delete(ctx context.Context, accountName, containerName string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "Delete", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "Delete", "`containerName` cannot be an empty string.") + } + + req, err := client.DeletePreparer(ctx, accountName, containerName) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName string, containerName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-03-28/blob/containers/get_properties.go b/storage/2018-03-28/blob/containers/get_properties.go new file mode 100644 index 0000000..1e308da --- /dev/null +++ b/storage/2018-03-28/blob/containers/get_properties.go @@ -0,0 +1,124 @@ +package containers + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// GetProperties returns the properties for this Container without a Lease +func (client Client) GetProperties(ctx context.Context, accountName, containerName string) (ContainerProperties, error) { + // If specified, Get Container Properties only succeeds if the container’s lease is active and matches this ID. + // If there is no active lease or the ID does not match, 412 (Precondition Failed) is returned. + return client.GetPropertiesWithLeaseID(ctx, accountName, containerName, "") +} + +// GetPropertiesWithLeaseID returns the properties for this Container using the specified LeaseID +func (client Client) GetPropertiesWithLeaseID(ctx context.Context, accountName, containerName, leaseID string) (result ContainerProperties, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "GetPropertiesWithLeaseID", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "GetPropertiesWithLeaseID", "`containerName` cannot be an empty string.") + } + + req, err := client.GetPropertiesWithLeaseIDPreparer(ctx, accountName, containerName, leaseID) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "GetProperties", nil, "Failure preparing request") + return + } + + resp, err := client.GetPropertiesWithLeaseIDSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "GetProperties", resp, "Failure sending request") + return + } + + result, err = client.GetPropertiesWithLeaseIDResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "GetProperties", resp, "Failure responding to request") + return + } + + return +} + +// GetPropertiesWithLeaseIDPreparer prepares the GetPropertiesWithLeaseID request. +func (client Client) GetPropertiesWithLeaseIDPreparer(ctx context.Context, accountName, containerName, leaseID string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + // If specified, Get Container Properties only succeeds if the container’s lease is active and matches this ID. + // If there is no active lease or the ID does not match, 412 (Precondition Failed) is returned. + if leaseID != "" { + headers["x-ms-lease-id"] = leaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetPropertiesWithLeaseIDSender sends the GetPropertiesWithLeaseID request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetPropertiesWithLeaseIDSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetPropertiesWithLeaseIDResponder handles the response to the GetPropertiesWithLeaseID request. The method always +// closes the http.Response Body. +func (client Client) GetPropertiesWithLeaseIDResponder(resp *http.Response) (result ContainerProperties, err error) { + if resp != nil { + result.LeaseStatus = LeaseStatus(resp.Header.Get("x-ms-lease-status")) + result.LeaseState = LeaseState(resp.Header.Get("x-ms-lease-state")) + if result.LeaseStatus == Locked { + duration := LeaseDuration(resp.Header.Get("x-ms-lease-duration")) + result.LeaseDuration = &duration + } + + // If this header is not returned in the response, the container is private to the account owner. + accessLevel := resp.Header.Get("x-ms-blob-public-access") + if accessLevel != "" { + result.AccessLevel = AccessLevel(accessLevel) + } else { + result.AccessLevel = Private + } + + // we can't necessarily use strconv.ParseBool here since this could be nil (only in some API versions) + result.HasImmutabilityPolicy = strings.EqualFold(resp.Header.Get("x-ms-has-immutability-policy"), "true") + result.HasLegalHold = strings.EqualFold(resp.Header.Get("x-ms-has-legal-hold"), "true") + + result.MetaData = metadata.ParseFromHeaders(resp.Header) + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/containers/lease_acquire.go b/storage/2018-03-28/blob/containers/lease_acquire.go new file mode 100644 index 0000000..061c863 --- /dev/null +++ b/storage/2018-03-28/blob/containers/lease_acquire.go @@ -0,0 +1,115 @@ +package containers + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type AcquireLeaseInput struct { + // Specifies the duration of the lease, in seconds, or negative one (-1) for a lease that never expires. + // A non-infinite lease can be between 15 and 60 seconds + LeaseDuration int + + ProposedLeaseID string +} + +type AcquireLeaseResponse struct { + autorest.Response + + LeaseID string +} + +// AcquireLease establishes and manages a lock on a container for delete operations. +func (client Client) AcquireLease(ctx context.Context, accountName, containerName string, input AcquireLeaseInput) (result AcquireLeaseResponse, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "AcquireLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "AcquireLease", "`containerName` cannot be an empty string.") + } + // An infinite lease duration is -1 seconds. A non-infinite lease can be between 15 and 60 seconds + if input.LeaseDuration != -1 && (input.LeaseDuration <= 15 || input.LeaseDuration >= 60) { + return result, validation.NewError("containers.Client", "AcquireLease", "`input.LeaseDuration` must be -1 (infinite), or between 15 and 60 seconds.") + } + + req, err := client.AcquireLeasePreparer(ctx, accountName, containerName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "AcquireLease", nil, "Failure preparing request") + return + } + + resp, err := client.AcquireLeaseSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "AcquireLease", resp, "Failure sending request") + return + } + + result, err = client.AcquireLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "AcquireLease", resp, "Failure responding to request") + return + } + + return +} + +// AcquireLeasePreparer prepares the AcquireLease request. +func (client Client) AcquireLeasePreparer(ctx context.Context, accountName string, containerName string, input AcquireLeaseInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + "comp": autorest.Encode("path", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "acquire", + "x-ms-lease-duration": input.LeaseDuration, + } + + if input.ProposedLeaseID != "" { + headers["x-ms-proposed-lease-id"] = input.ProposedLeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// AcquireLeaseSender sends the AcquireLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) AcquireLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// AcquireLeaseResponder handles the response to the AcquireLease request. The method always +// closes the http.Response Body. +func (client Client) AcquireLeaseResponder(resp *http.Response) (result AcquireLeaseResponse, err error) { + if resp != nil { + result.LeaseID = resp.Header.Get("x-ms-lease-id") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/containers/lease_break.go b/storage/2018-03-28/blob/containers/lease_break.go new file mode 100644 index 0000000..08acfb7 --- /dev/null +++ b/storage/2018-03-28/blob/containers/lease_break.go @@ -0,0 +1,129 @@ +package containers + +import ( + "context" + "net/http" + "strconv" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type BreakLeaseInput struct { + // For a break operation, proposed duration the lease should continue + // before it is broken, in seconds, between 0 and 60. + // This break period is only used if it is shorter than the time remaining on the lease. + // If longer, the time remaining on the lease is used. + // A new lease will not be available before the break period has expired, + // but the lease may be held for longer than the break period. + // If this header does not appear with a break operation, a fixed-duration lease breaks + // after the remaining lease period elapses, and an infinite lease breaks immediately. + BreakPeriod *int + + LeaseID string +} + +type BreakLeaseResponse struct { + autorest.Response + + // Approximate time remaining in the lease period, in seconds. + // If the break is immediate, 0 is returned. + LeaseTime int +} + +// BreakLease breaks a lock based on it's Lease ID +func (client Client) BreakLease(ctx context.Context, accountName, containerName string, input BreakLeaseInput) (result BreakLeaseResponse, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "BreakLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "BreakLease", "`containerName` cannot be an empty string.") + } + if input.LeaseID == "" { + return result, validation.NewError("containers.Client", "BreakLease", "`input.LeaseID` cannot be an empty string.") + } + + req, err := client.BreakLeasePreparer(ctx, accountName, containerName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "BreakLease", nil, "Failure preparing request") + return + } + + resp, err := client.BreakLeaseSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "BreakLease", resp, "Failure sending request") + return + } + + result, err = client.BreakLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "BreakLease", resp, "Failure responding to request") + return + } + + return +} + +// BreakLeasePreparer prepares the BreakLease request. +func (client Client) BreakLeasePreparer(ctx context.Context, accountName string, containerName string, input BreakLeaseInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + "comp": autorest.Encode("path", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "break", + "x-ms-lease-id": input.LeaseID, + } + + if input.BreakPeriod != nil { + headers["x-ms-lease-break-period"] = *input.BreakPeriod + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// BreakLeaseSender sends the BreakLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) BreakLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// BreakLeaseResponder handles the response to the BreakLease request. The method always +// closes the http.Response Body. +func (client Client) BreakLeaseResponder(resp *http.Response) (result BreakLeaseResponse, err error) { + if resp != nil { + leaseRaw := resp.Header.Get("x-ms-lease-time") + if leaseRaw != "" { + i, err := strconv.Atoi(leaseRaw) + if err == nil { + result.LeaseTime = i + } + } + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/containers/lease_change.go b/storage/2018-03-28/blob/containers/lease_change.go new file mode 100644 index 0000000..dfbcb13 --- /dev/null +++ b/storage/2018-03-28/blob/containers/lease_change.go @@ -0,0 +1,111 @@ +package containers + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type ChangeLeaseInput struct { + ExistingLeaseID string + ProposedLeaseID string +} + +type ChangeLeaseResponse struct { + autorest.Response + + LeaseID string +} + +// ChangeLease changes the lock from one Lease ID to another Lease ID +func (client Client) ChangeLease(ctx context.Context, accountName, containerName string, input ChangeLeaseInput) (result ChangeLeaseResponse, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "ChangeLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "ChangeLease", "`containerName` cannot be an empty string.") + } + if input.ExistingLeaseID == "" { + return result, validation.NewError("containers.Client", "ChangeLease", "`input.ExistingLeaseID` cannot be an empty string.") + } + if input.ProposedLeaseID == "" { + return result, validation.NewError("containers.Client", "ChangeLease", "`input.ProposedLeaseID` cannot be an empty string.") + } + + req, err := client.ChangeLeasePreparer(ctx, accountName, containerName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "ChangeLease", nil, "Failure preparing request") + return + } + + resp, err := client.ChangeLeaseSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "ChangeLease", resp, "Failure sending request") + return + } + + result, err = client.ChangeLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "ChangeLease", resp, "Failure responding to request") + return + } + + return +} + +// ChangeLeasePreparer prepares the ChangeLease request. +func (client Client) ChangeLeasePreparer(ctx context.Context, accountName string, containerName string, input ChangeLeaseInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + "comp": autorest.Encode("path", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "change", + "x-ms-lease-id": input.ExistingLeaseID, + "x-ms-proposed-lease-id": input.ProposedLeaseID, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ChangeLeaseSender sends the ChangeLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ChangeLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ChangeLeaseResponder handles the response to the ChangeLease request. The method always +// closes the http.Response Body. +func (client Client) ChangeLeaseResponder(resp *http.Response) (result ChangeLeaseResponse, err error) { + if resp != nil { + result.LeaseID = resp.Header.Get("x-ms-lease-id") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/containers/lease_release.go b/storage/2018-03-28/blob/containers/lease_release.go new file mode 100644 index 0000000..fafcf98 --- /dev/null +++ b/storage/2018-03-28/blob/containers/lease_release.go @@ -0,0 +1,92 @@ +package containers + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// ReleaseLease releases the lock based on the Lease ID +func (client Client) ReleaseLease(ctx context.Context, accountName, containerName, leaseID string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "ReleaseLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "ReleaseLease", "`containerName` cannot be an empty string.") + } + if leaseID == "" { + return result, validation.NewError("containers.Client", "ReleaseLease", "`leaseID` cannot be an empty string.") + } + + req, err := client.ReleaseLeasePreparer(ctx, accountName, containerName, leaseID) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "ReleaseLease", nil, "Failure preparing request") + return + } + + resp, err := client.ReleaseLeaseSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "ReleaseLease", resp, "Failure sending request") + return + } + + result, err = client.ReleaseLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "ReleaseLease", resp, "Failure responding to request") + return + } + + return +} + +// ReleaseLeasePreparer prepares the ReleaseLease request. +func (client Client) ReleaseLeasePreparer(ctx context.Context, accountName string, containerName string, leaseID string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + "comp": autorest.Encode("path", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "release", + "x-ms-lease-id": leaseID, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ReleaseLeaseSender sends the ReleaseLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ReleaseLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ReleaseLeaseResponder handles the response to the ReleaseLease request. The method always +// closes the http.Response Body. +func (client Client) ReleaseLeaseResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/containers/lease_renew.go b/storage/2018-03-28/blob/containers/lease_renew.go new file mode 100644 index 0000000..3fe1765 --- /dev/null +++ b/storage/2018-03-28/blob/containers/lease_renew.go @@ -0,0 +1,92 @@ +package containers + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// RenewLease renewes the lock based on the Lease ID +func (client Client) RenewLease(ctx context.Context, accountName, containerName, leaseID string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "RenewLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "RenewLease", "`containerName` cannot be an empty string.") + } + if leaseID == "" { + return result, validation.NewError("containers.Client", "RenewLease", "`leaseID` cannot be an empty string.") + } + + req, err := client.RenewLeasePreparer(ctx, accountName, containerName, leaseID) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "RenewLease", nil, "Failure preparing request") + return + } + + resp, err := client.RenewLeaseSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "RenewLease", resp, "Failure sending request") + return + } + + result, err = client.RenewLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "RenewLease", resp, "Failure responding to request") + return + } + + return +} + +// RenewLeasePreparer prepares the RenewLease request. +func (client Client) RenewLeasePreparer(ctx context.Context, accountName string, containerName string, leaseID string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + "comp": autorest.Encode("path", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "renew", + "x-ms-lease-id": leaseID, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// RenewLeaseSender sends the RenewLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) RenewLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// RenewLeaseResponder handles the response to the RenewLease request. The method always +// closes the http.Response Body. +func (client Client) RenewLeaseResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/containers/lifecycle_test.go b/storage/2018-03-28/blob/containers/lifecycle_test.go new file mode 100644 index 0000000..389c773 --- /dev/null +++ b/storage/2018-03-28/blob/containers/lifecycle_test.go @@ -0,0 +1,174 @@ +package containers + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestContainerLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.BlobStorage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + containersClient := NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithAuthorizer(containersClient.Client, storageAuth) + + // first let's test an empty container + input := CreateInput{} + _, err = containersClient.Create(ctx, accountName, containerName, input) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + + container, err := containersClient.GetProperties(ctx, accountName, containerName) + if err != nil { + t.Fatal(fmt.Errorf("Error retrieving: %s", err)) + } + + if container.AccessLevel != Private { + t.Fatalf("Expected Access Level to be Private but got %q", container.AccessLevel) + } + if len(container.MetaData) != 0 { + t.Fatalf("Expected MetaData to be empty but got: %s", container.MetaData) + } + if container.LeaseStatus != Unlocked { + t.Fatalf("Expected Container Lease to be Unlocked but was: %s", container.LeaseStatus) + } + + // then update the metadata + metaData := map[string]string{ + "dont": "kill-my-vibe", + } + _, err = containersClient.SetMetaData(ctx, accountName, containerName, metaData) + if err != nil { + t.Fatal(fmt.Errorf("Error updating metadata: %s", err)) + } + + // give azure time to replicate + time.Sleep(2 * time.Second) + + // then assert that + container, err = containersClient.GetProperties(ctx, accountName, containerName) + if err != nil { + t.Fatal(fmt.Errorf("Error re-retrieving: %s", err)) + } + if len(container.MetaData) != 1 { + t.Fatalf("Expected 1 item in the metadata but got: %s", container.MetaData) + } + if container.MetaData["dont"] != "kill-my-vibe" { + t.Fatalf("Expected `kill-my-vibe` but got %q", container.MetaData["dont"]) + } + if container.AccessLevel != Private { + t.Fatalf("Expected Access Level to be Private but got %q", container.AccessLevel) + } + if container.LeaseStatus != Unlocked { + t.Fatalf("Expected Container Lease to be Unlocked but was: %s", container.LeaseStatus) + } + + // then update the ACL + _, err = containersClient.SetAccessControl(ctx, accountName, containerName, Blob) + if err != nil { + t.Fatal(fmt.Errorf("Error updating ACL's: %s", err)) + } + + // give azure some time to replicate + time.Sleep(2 * time.Second) + + // then assert that + container, err = containersClient.GetProperties(ctx, accountName, containerName) + if err != nil { + t.Fatal(fmt.Errorf("Error re-retrieving: %s", err)) + } + if container.AccessLevel != Blob { + t.Fatalf("Expected Access Level to be Blob but got %q", container.AccessLevel) + } + if len(container.MetaData) != 1 { + t.Fatalf("Expected 1 item in the metadata but got: %s", container.MetaData) + } + if container.LeaseStatus != Unlocked { + t.Fatalf("Expected Container Lease to be Unlocked but was: %s", container.LeaseStatus) + } + + // acquire a lease for 30s + acquireLeaseInput := AcquireLeaseInput{ + LeaseDuration: 30, + } + acquireLeaseResp, err := containersClient.AcquireLease(ctx, accountName, containerName, acquireLeaseInput) + if err != nil { + t.Fatalf("Error acquiring lease: %s", err) + } + t.Logf("[DEBUG] Lease ID: %s", acquireLeaseResp.LeaseID) + + // we should then be able to update the ID + t.Logf("[DEBUG] Changing lease..") + updateLeaseInput := ChangeLeaseInput{ + ExistingLeaseID: acquireLeaseResp.LeaseID, + ProposedLeaseID: "aaaabbbb-aaaa-bbbb-cccc-aaaabbbbcccc", + } + updateLeaseResp, err := containersClient.ChangeLease(ctx, accountName, containerName, updateLeaseInput) + if err != nil { + t.Fatalf("Error changing lease: %s", err) + } + + // then renew it + _, err = containersClient.RenewLease(ctx, accountName, containerName, updateLeaseResp.LeaseID) + if err != nil { + t.Fatalf("Error renewing lease: %s", err) + } + + // and then give it a timeout + breakPeriod := 20 + breakLeaseInput := BreakLeaseInput{ + LeaseID: updateLeaseResp.LeaseID, + BreakPeriod: &breakPeriod, + } + breakLeaseResp, err := containersClient.BreakLease(ctx, accountName, containerName, breakLeaseInput) + if err != nil { + t.Fatalf("Error breaking lease: %s", err) + } + if breakLeaseResp.LeaseTime == 0 { + t.Fatalf("Lease broke immediately when should have waited: %d", breakLeaseResp.LeaseTime) + } + + // and finally ditch it + _, err = containersClient.ReleaseLease(ctx, accountName, containerName, updateLeaseResp.LeaseID) + if err != nil { + t.Fatalf("Error releasing lease: %s", err) + } + + t.Logf("[DEBUG] Listing blobs in the container..") + listInput := ListBlobsInput{} + listResult, err := containersClient.ListBlobs(ctx, accountName, containerName, listInput) + if err != nil { + t.Fatalf("Error listing blobs: %s", err) + } + + if len(listResult.Blobs.Blobs) != 0 { + t.Fatalf("Expected there to be no blobs in the container but got %d", len(listResult.Blobs.Blobs)) + } + + t.Logf("[DEBUG] Deleting..") + _, err = containersClient.Delete(ctx, accountName, containerName) + if err != nil { + t.Fatal(fmt.Errorf("Error deleting: %s", err)) + } +} diff --git a/storage/2018-03-28/blob/containers/list_blobs.go b/storage/2018-03-28/blob/containers/list_blobs.go new file mode 100644 index 0000000..82797d0 --- /dev/null +++ b/storage/2018-03-28/blob/containers/list_blobs.go @@ -0,0 +1,179 @@ +package containers + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type ListBlobsInput struct { + Delimiter *string + Include *[]Dataset + Marker *string + MaxResults *int + Prefix *string +} + +type ListBlobsResult struct { + autorest.Response + + Delimiter string `xml:"Delimiter"` + Marker string `xml:"Marker"` + MaxResults int `xml:"MaxResults"` + NextMarker *string `xml:"NextMarker,omitempty"` + Prefix string `xml:"Prefix"` + Blobs Blobs `xml:"Blobs"` +} + +type Blobs struct { + Blobs []BlobDetails `xml:"Blob"` + BlobPrefix *BlobPrefix `xml:"BlobPrefix"` +} + +type BlobDetails struct { + Name string `xml:"Name"` + Deleted bool `xml:"Deleted,omitempty"` + MetaData map[string]interface{} `map:"Metadata,omitempty"` + Properties *BlobProperties `xml:"Properties,omitempty"` + Snapshot *string `xml:"Snapshot,omitempty"` +} + +type BlobProperties struct { + AccessTier *string `xml:"AccessTier,omitempty"` + AccessTierInferred *bool `xml:"AccessTierInferred,omitempty"` + AccessTierChangeTime *string `xml:"AccessTierChangeTime,omitempty"` + BlobType *string `xml:"BlobType,omitempty"` + BlobSequenceNumber *string `xml:"x-ms-blob-sequence-number,omitempty"` + CacheControl *string `xml:"Cache-Control,omitempty"` + ContentEncoding *string `xml:"ContentEncoding,omitempty"` + ContentLanguage *string `xml:"Content-Language,omitempty"` + ContentLength *int64 `xml:"Content-Length,omitempty"` + ContentMD5 *string `xml:"Content-MD5,omitempty"` + ContentType *string `xml:"Content-Type,omitempty"` + CopyCompletionTime *string `xml:"CopyCompletionTime,omitempty"` + CopyId *string `xml:"CopyId,omitempty"` + CopyStatus *string `xml:"CopyStatus,omitempty"` + CopySource *string `xml:"CopySource,omitempty"` + CopyProgress *string `xml:"CopyProgress,omitempty"` + CopyStatusDescription *string `xml:"CopyStatusDescription,omitempty"` + CreationTime *string `xml:"CreationTime,omitempty"` + ETag *string `xml:"Etag,omitempty"` + DeletedTime *string `xml:"DeletedTime,omitempty"` + IncrementalCopy *bool `xml:"IncrementalCopy,omitempty"` + LastModified *string `xml:"Last-Modified,omitempty"` + LeaseDuration *string `xml:"LeaseDuration,omitempty"` + LeaseState *string `xml:"LeaseState,omitempty"` + LeaseStatus *string `xml:"LeaseStatus,omitempty"` + RemainingRetentionDays *string `xml:"RemainingRetentionDays,omitempty"` + ServerEncrypted *bool `xml:"ServerEncrypted,omitempty"` +} + +type BlobPrefix struct { + Name string `xml:"Name"` +} + +// ListBlobs lists the blobs matching the specified query within the specified Container +func (client Client) ListBlobs(ctx context.Context, accountName, containerName string, input ListBlobsInput) (result ListBlobsResult, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "ListBlobs", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "ListBlobs", "`containerName` cannot be an empty string.") + } + if input.MaxResults != nil && (*input.MaxResults <= 0 || *input.MaxResults > 5000) { + return result, validation.NewError("containers.Client", "ListBlobs", "`input.MaxResults` can either be nil or between 0 and 5000.") + } + + req, err := client.ListBlobsPreparer(ctx, accountName, containerName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "ListBlobs", nil, "Failure preparing request") + return + } + + resp, err := client.ListBlobsSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "ListBlobs", resp, "Failure sending request") + return + } + + result, err = client.ListBlobsResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "ListBlobs", resp, "Failure responding to request") + return + } + + return +} + +// ListBlobsPreparer prepares the ListBlobs request. +func (client Client) ListBlobsPreparer(ctx context.Context, accountName, containerName string, input ListBlobsInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "list"), + "restype": autorest.Encode("query", "container"), + } + + if input.Delimiter != nil { + queryParameters["delimiter"] = autorest.Encode("query", *input.Delimiter) + } + if input.Include != nil { + vals := make([]string, 0) + for _, v := range *input.Include { + vals = append(vals, string(v)) + } + include := strings.Join(vals, ",") + queryParameters["include"] = autorest.Encode("query", include) + } + if input.Marker != nil { + queryParameters["marker"] = autorest.Encode("query", *input.Marker) + } + if input.MaxResults != nil { + queryParameters["maxresults"] = autorest.Encode("query", *input.MaxResults) + } + if input.Prefix != nil { + queryParameters["prefix"] = autorest.Encode("query", *input.Prefix) + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ListBlobsSender sends the ListBlobs request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ListBlobsSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ListBlobsResponder handles the response to the ListBlobs request. The method always +// closes the http.Response Body. +func (client Client) ListBlobsResponder(resp *http.Response) (result ListBlobsResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/containers/models.go b/storage/2018-03-28/blob/containers/models.go new file mode 100644 index 0000000..adba368 --- /dev/null +++ b/storage/2018-03-28/blob/containers/models.go @@ -0,0 +1,75 @@ +package containers + +import "github.com/Azure/go-autorest/autorest" + +type AccessLevel string + +var ( + // Blob specifies public read access for blobs. + // Blob data within this container can be read via anonymous request, + // but container data is not available. + // Clients cannot enumerate blobs within the container via anonymous request. + Blob AccessLevel = "blob" + + // Container specifies full public read access for container and blob data. + // Clients can enumerate blobs within the container via anonymous request, + // but cannot enumerate containers within the storage account. + Container AccessLevel = "container" + + // Private specifies that container data is private to the account owner + Private AccessLevel = "" +) + +type ContainerProperties struct { + autorest.Response + + AccessLevel AccessLevel + LeaseStatus LeaseStatus + LeaseState LeaseState + LeaseDuration *LeaseDuration + MetaData map[string]string + HasImmutabilityPolicy bool + HasLegalHold bool +} + +type Dataset string + +var ( + Copy Dataset = "copy" + Deleted Dataset = "deleted" + MetaData Dataset = "metadata" + Snapshots Dataset = "snapshots" + UncommittedBlobs Dataset = "uncommittedblobs" +) + +type ErrorResponse struct { + Code *string `xml:"Code"` + Message *string `xml:"Message"` +} + +type LeaseDuration string + +var ( + // If this lease is for a Fixed Duration + Fixed LeaseDuration = "fixed" + + // If this lease is for an Indefinite Duration + Infinite LeaseDuration = "infinite" +) + +type LeaseState string + +var ( + Available LeaseState = "available" + Breaking LeaseState = "breaking" + Broken LeaseState = "broken" + Expired LeaseState = "expired" + Leased LeaseState = "leased" +) + +type LeaseStatus string + +var ( + Locked LeaseStatus = "locked" + Unlocked LeaseStatus = "unlocked" +) diff --git a/storage/2018-03-28/blob/containers/resource_id.go b/storage/2018-03-28/blob/containers/resource_id.go new file mode 100644 index 0000000..a5bfd6e --- /dev/null +++ b/storage/2018-03-28/blob/containers/resource_id.go @@ -0,0 +1,46 @@ +package containers + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given Container +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, containerName string) string { + domain := endpoints.GetBlobEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s", domain, containerName) +} + +type ResourceID struct { + AccountName string + ContainerName string +} + +// ParseResourceID parses the Resource ID and returns an object which can be used +// to interact with the Container Resource +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://foo.blob.core.windows.net/Bar + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + containerName := strings.TrimPrefix(uri.Path, "/") + return &ResourceID{ + AccountName: *accountName, + ContainerName: containerName, + }, nil +} diff --git a/storage/2018-03-28/blob/containers/resource_id_test.go b/storage/2018-03-28/blob/containers/resource_id_test.go new file mode 100644 index 0000000..e27bc9d --- /dev/null +++ b/storage/2018-03-28/blob/containers/resource_id_test.go @@ -0,0 +1,79 @@ +package containers + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.blob.core.chinacloudapi.cn/container1", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.blob.core.cloudapi.de/container1", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.blob.core.windows.net/container1", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.blob.core.usgovcloudapi.net/container1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "container1") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.blob.core.chinacloudapi.cn/container1", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.blob.core.cloudapi.de/container1", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.blob.core.windows.net/container1", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.blob.core.usgovcloudapi.net/container1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected the account name to be `account1` but got %q", actual.AccountName) + } + + if actual.ContainerName != "container1" { + t.Fatalf("Expected the container name to be `container1` but got %q", actual.ContainerName) + } + } +} diff --git a/storage/2018-03-28/blob/containers/set_acl.go b/storage/2018-03-28/blob/containers/set_acl.go new file mode 100644 index 0000000..fcf4e10 --- /dev/null +++ b/storage/2018-03-28/blob/containers/set_acl.go @@ -0,0 +1,100 @@ +package containers + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// SetAccessControl sets the Access Control for a Container without a Lease ID +func (client Client) SetAccessControl(ctx context.Context, accountName, containerName string, level AccessLevel) (autorest.Response, error) { + return client.SetAccessControlWithLeaseID(ctx, accountName, containerName, "", level) +} + +// SetAccessControlWithLeaseID sets the Access Control for a Container using the specified Lease ID +func (client Client) SetAccessControlWithLeaseID(ctx context.Context, accountName, containerName, leaseID string, level AccessLevel) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "SetAccessControl", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "SetAccessControl", "`containerName` cannot be an empty string.") + } + + req, err := client.SetAccessControlWithLeaseIDPreparer(ctx, accountName, containerName, leaseID, level) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "SetAccessControl", nil, "Failure preparing request") + return + } + + resp, err := client.SetAccessControlWithLeaseIDSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "SetAccessControl", resp, "Failure sending request") + return + } + + result, err = client.SetAccessControlWithLeaseIDResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "SetAccessControl", resp, "Failure responding to request") + return + } + + return +} + +// SetAccessControlWithLeaseIDPreparer prepares the SetAccessControlWithLeaseID request. +func (client Client) SetAccessControlWithLeaseIDPreparer(ctx context.Context, accountName, containerName, leaseID string, level AccessLevel) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("path", "acl"), + "restype": autorest.Encode("path", "container"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = client.setAccessLevelIntoHeaders(headers, level) + + // If specified, Get Container Properties only succeeds if the container’s lease is active and matches this ID. + // If there is no active lease or the ID does not match, 412 (Precondition Failed) is returned. + if leaseID != "" { + headers["x-ms-lease-id"] = leaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetAccessControlWithLeaseIDSender sends the SetAccessControlWithLeaseID request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetAccessControlWithLeaseIDSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetAccessControlWithLeaseIDResponder handles the response to the SetAccessControlWithLeaseID request. The method always +// closes the http.Response Body. +func (client Client) SetAccessControlWithLeaseIDResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/containers/set_metadata.go b/storage/2018-03-28/blob/containers/set_metadata.go new file mode 100644 index 0000000..fb9e07f --- /dev/null +++ b/storage/2018-03-28/blob/containers/set_metadata.go @@ -0,0 +1,105 @@ +package containers + +import ( + "context" + "fmt" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// SetMetaData sets the specified MetaData on the Container without a Lease ID +func (client Client) SetMetaData(ctx context.Context, accountName, containerName string, metaData map[string]string) (autorest.Response, error) { + return client.SetMetaDataWithLeaseID(ctx, accountName, containerName, "", metaData) +} + +// SetMetaDataWithLeaseID sets the specified MetaData on the Container using the specified Lease ID +func (client Client) SetMetaDataWithLeaseID(ctx context.Context, accountName, containerName, leaseID string, metaData map[string]string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "SetMetaData", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "SetMetaData", "`containerName` cannot be an empty string.") + } + if err := metadata.Validate(metaData); err != nil { + return result, validation.NewError("containers.Client", "SetMetaData", fmt.Sprintf("`metaData` is not valid: %s.", err)) + } + + req, err := client.SetMetaDataWithLeaseIDPreparer(ctx, accountName, containerName, leaseID, metaData) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "SetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.SetMetaDataWithLeaseIDSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "SetMetaData", resp, "Failure sending request") + return + } + + result, err = client.SetMetaDataWithLeaseIDResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "SetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// SetMetaDataWithLeaseIDPreparer prepares the SetMetaDataWithLeaseID request. +func (client Client) SetMetaDataWithLeaseIDPreparer(ctx context.Context, accountName, containerName, leaseID string, metaData map[string]string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("path", "metadata"), + "restype": autorest.Encode("path", "container"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = metadata.SetIntoHeaders(headers, metaData) + + // If specified, Get Container Properties only succeeds if the container’s lease is active and matches this ID. + // If there is no active lease or the ID does not match, 412 (Precondition Failed) is returned. + if leaseID != "" { + headers["x-ms-lease-id"] = leaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetMetaDataWithLeaseIDSender sends the SetMetaDataWithLeaseID request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetMetaDataWithLeaseIDSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetMetaDataWithLeaseIDResponder handles the response to the SetMetaDataWithLeaseID request. The method always +// closes the http.Response Body. +func (client Client) SetMetaDataWithLeaseIDResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/blob/containers/version.go b/storage/2018-03-28/blob/containers/version.go new file mode 100644 index 0000000..048d103 --- /dev/null +++ b/storage/2018-03-28/blob/containers/version.go @@ -0,0 +1,14 @@ +package containers + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2018-03-28" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2018-03-28/file/directories/README.md b/storage/2018-03-28/file/directories/README.md new file mode 100644 index 0000000..804f11b --- /dev/null +++ b/storage/2018-03-28/file/directories/README.md @@ -0,0 +1,44 @@ +## File Storage Directories SDK for API version 2018-03-28 + +This package allows you to interact with the Directories File Storage API + +### Supported Authorizers + +* Azure Active Directory (for the Resource Endpoint `https://storage.azure.com`) +* SharedKeyLite (Blob, File & Queue) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2018-03-28/file/directories" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + shareName := "myshare" + directoryName := "myfiles" + + storageAuth := autorest.NewSharedKeyLiteAuthorizer(accountName, storageAccountKey) + directoriesClient := directories.New() + directoriesClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + metadata := map[string]string{ + "hello": "world", + } + if _, err := directoriesClient.Create(ctx, accountName, shareName, directoryName, metadata); err != nil { + return fmt.Errorf("Error creating Directory: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2018-03-28/file/directories/client.go b/storage/2018-03-28/file/directories/client.go new file mode 100644 index 0000000..bf2d315 --- /dev/null +++ b/storage/2018-03-28/file/directories/client.go @@ -0,0 +1,25 @@ +package directories + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for File Storage Shares. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithEnvironment creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2018-03-28/file/directories/create.go b/storage/2018-03-28/file/directories/create.go new file mode 100644 index 0000000..93f5c82 --- /dev/null +++ b/storage/2018-03-28/file/directories/create.go @@ -0,0 +1,101 @@ +package directories + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// Create creates a new directory under the specified share or parent directory. +func (client Client) Create(ctx context.Context, accountName, shareName, path string, metaData map[string]string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("directories.Client", "Create", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("directories.Client", "Create", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("directories.Client", "Create", "`shareName` must be a lower-cased string.") + } + if path == "" { + return result, validation.NewError("directories.Client", "Create", "`path` cannot be an empty string.") + } + if err := metadata.Validate(metaData); err != nil { + return result, validation.NewError("directories.Client", "Create", fmt.Sprintf("`metadata` is not valid: %s.", err)) + } + + req, err := client.CreatePreparer(ctx, accountName, shareName, path, metaData) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "Create", nil, "Failure preparing request") + return + } + + resp, err := client.CreateSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "directories.Client", "Create", resp, "Failure sending request") + return + } + + result, err = client.CreateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "Create", resp, "Failure responding to request") + return + } + + return +} + +// CreatePreparer prepares the Create request. +func (client Client) CreatePreparer(ctx context.Context, accountName, shareName, path string, metaData map[string]string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "directory"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = metadata.SetIntoHeaders(headers, metaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CreateSender sends the Create request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CreateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CreateResponder handles the response to the Create request. The method always +// closes the http.Response Body. +func (client Client) CreateResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/directories/delete.go b/storage/2018-03-28/file/directories/delete.go new file mode 100644 index 0000000..9443c25 --- /dev/null +++ b/storage/2018-03-28/file/directories/delete.go @@ -0,0 +1,95 @@ +package directories + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Delete removes the specified empty directory +// Note that the directory must be empty before it can be deleted. +func (client Client) Delete(ctx context.Context, accountName, shareName, path string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("directories.Client", "Delete", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("directories.Client", "Delete", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("directories.Client", "Delete", "`shareName` must be a lower-cased string.") + } + if path == "" { + return result, validation.NewError("directories.Client", "Delete", "`path` cannot be an empty string.") + } + + req, err := client.DeletePreparer(ctx, accountName, shareName, path) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "directories.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName, shareName, path string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "directory"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/directories/get.go b/storage/2018-03-28/file/directories/get.go new file mode 100644 index 0000000..817d680 --- /dev/null +++ b/storage/2018-03-28/file/directories/get.go @@ -0,0 +1,112 @@ +package directories + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetResult struct { + autorest.Response + + // A set of name-value pairs that contain metadata for the directory. + MetaData map[string]string + + // The value of this header is set to true if the directory metadata is completely + // encrypted using the specified algorithm. Otherwise, the value is set to false. + DirectoryMetaDataEncrypted bool +} + +// Get returns all system properties for the specified directory, +// and can also be used to check the existence of a directory. +func (client Client) Get(ctx context.Context, accountName, shareName, path string) (result GetResult, err error) { + if accountName == "" { + return result, validation.NewError("directories.Client", "Get", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("directories.Client", "Get", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("directories.Client", "Get", "`shareName` must be a lower-cased string.") + } + if path == "" { + return result, validation.NewError("directories.Client", "Get", "`path` cannot be an empty string.") + } + + req, err := client.GetPreparer(ctx, accountName, shareName, path) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "Get", nil, "Failure preparing request") + return + } + + resp, err := client.GetSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "directories.Client", "Get", resp, "Failure sending request") + return + } + + result, err = client.GetResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "Get", resp, "Failure responding to request") + return + } + + return +} + +// GetPreparer prepares the Get request. +func (client Client) GetPreparer(ctx context.Context, accountName, shareName, path string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "directory"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetSender sends the Get request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetResponder handles the response to the Get request. The method always +// closes the http.Response Body. +func (client Client) GetResponder(resp *http.Response) (result GetResult, err error) { + if resp != nil && resp.Header != nil { + result.MetaData = metadata.ParseFromHeaders(resp.Header) + result.DirectoryMetaDataEncrypted = strings.EqualFold(resp.Header.Get("x-ms-server-encrypted"), "true") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/directories/lifecycle_test.go b/storage/2018-03-28/file/directories/lifecycle_test.go new file mode 100644 index 0000000..aaf5d47 --- /dev/null +++ b/storage/2018-03-28/file/directories/lifecycle_test.go @@ -0,0 +1,107 @@ +package directories + +import ( + "context" + "fmt" + "log" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-03-28/file/shares" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestDirectoriesLifeCycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + shareName := fmt.Sprintf("share-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + sharesClient := shares.NewWithEnvironment(client.Environment) + sharesClient.Client = client.PrepareWithAuthorizer(sharesClient.Client, storageAuth) + + directoriesClient := NewWithEnvironment(client.Environment) + directoriesClient.Client = client.PrepareWithAuthorizer(directoriesClient.Client, storageAuth) + + input := shares.CreateInput{ + QuotaInGB: 1, + } + _, err = sharesClient.Create(ctx, accountName, shareName, input) + if err != nil { + t.Fatalf("Error creating fileshare: %s", err) + } + defer sharesClient.Delete(ctx, accountName, shareName, true) + + metaData := map[string]string{ + "hello": "world", + } + + log.Printf("[DEBUG] Creating Top Level..") + if _, err := directoriesClient.Create(ctx, accountName, shareName, "hello", metaData); err != nil { + t.Fatalf("Error creating Top Level Directory: %s", err) + } + + log.Printf("[DEBUG] Creating Inner..") + if _, err := directoriesClient.Create(ctx, accountName, shareName, "hello/there", metaData); err != nil { + t.Fatalf("Error creating Inner Directory: %s", err) + } + + log.Printf("[DEBUG] Retrieving share") + innerDir, err := directoriesClient.Get(ctx, accountName, shareName, "hello/there") + if err != nil { + t.Fatalf("Error retrieving Inner Directory: %s", err) + } + + if innerDir.DirectoryMetaDataEncrypted != true { + t.Fatalf("Expected MetaData to be encrypted but got: %t", innerDir.DirectoryMetaDataEncrypted) + } + + if len(innerDir.MetaData) != 1 { + t.Fatalf("Expected MetaData to contain 1 item but got %d", len(innerDir.MetaData)) + } + if innerDir.MetaData["hello"] != "world" { + t.Fatalf("Expected MetaData `hello` to be `world`: %s", innerDir.MetaData["hello"]) + } + + log.Printf("[DEBUG] Setting MetaData") + updatedMetaData := map[string]string{ + "panda": "pops", + } + if _, err := directoriesClient.SetMetaData(ctx, accountName, shareName, "hello/there", updatedMetaData); err != nil { + t.Fatalf("Error updating MetaData: %s", err) + } + + log.Printf("[DEBUG] Retrieving MetaData") + retrievedMetaData, err := directoriesClient.GetMetaData(ctx, accountName, shareName, "hello/there") + if err != nil { + t.Fatalf("Error retrieving the updated metadata: %s", err) + } + if len(retrievedMetaData.MetaData) != 1 { + t.Fatalf("Expected the updated metadata to have 1 item but got %d", len(retrievedMetaData.MetaData)) + } + if retrievedMetaData.MetaData["panda"] != "pops" { + t.Fatalf("Expected the metadata `panda` to be `pops` but got %q", retrievedMetaData.MetaData["panda"]) + } + + t.Logf("[DEBUG] Deleting Inner..") + if _, err := directoriesClient.Delete(ctx, accountName, shareName, "hello/there"); err != nil { + t.Fatalf("Error deleting Inner Directory: %s", err) + } + + t.Logf("[DEBUG] Deleting Top Level..") + if _, err := directoriesClient.Delete(ctx, accountName, shareName, "hello"); err != nil { + t.Fatalf("Error deleting Top Level Directory: %s", err) + } +} diff --git a/storage/2018-03-28/file/directories/metadata_get.go b/storage/2018-03-28/file/directories/metadata_get.go new file mode 100644 index 0000000..173716d --- /dev/null +++ b/storage/2018-03-28/file/directories/metadata_get.go @@ -0,0 +1,106 @@ +package directories + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetMetaDataResult struct { + autorest.Response + + MetaData map[string]string +} + +// GetMetaData returns all user-defined metadata for the specified directory +func (client Client) GetMetaData(ctx context.Context, accountName, shareName, path string) (result GetMetaDataResult, err error) { + if accountName == "" { + return result, validation.NewError("directories.Client", "GetMetaData", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("directories.Client", "GetMetaData", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("directories.Client", "GetMetaData", "`shareName` must be a lower-cased string.") + } + if path == "" { + return result, validation.NewError("directories.Client", "GetMetaData", "`path` cannot be an empty string.") + } + + req, err := client.GetMetaDataPreparer(ctx, accountName, shareName, path) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "GetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.GetMetaDataSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "directories.Client", "GetMetaData", resp, "Failure sending request") + return + } + + result, err = client.GetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "GetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// GetMetaDataPreparer prepares the GetMetaData request. +func (client Client) GetMetaDataPreparer(ctx context.Context, accountName, shareName, path string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "directory"), + "comp": autorest.Encode("query", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetMetaDataSender sends the GetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetMetaDataResponder handles the response to the GetMetaData request. The method always +// closes the http.Response Body. +func (client Client) GetMetaDataResponder(resp *http.Response) (result GetMetaDataResult, err error) { + if resp != nil && resp.Header != nil { + result.MetaData = metadata.ParseFromHeaders(resp.Header) + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/directories/metadata_set.go b/storage/2018-03-28/file/directories/metadata_set.go new file mode 100644 index 0000000..cb13312 --- /dev/null +++ b/storage/2018-03-28/file/directories/metadata_set.go @@ -0,0 +1,102 @@ +package directories + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// SetMetaData updates user defined metadata for the specified directory +func (client Client) SetMetaData(ctx context.Context, accountName, shareName, path string, metaData map[string]string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("directories.Client", "SetMetaData", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("directories.Client", "SetMetaData", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("directories.Client", "SetMetaData", "`shareName` must be a lower-cased string.") + } + if path == "" { + return result, validation.NewError("directories.Client", "SetMetaData", "`path` cannot be an empty string.") + } + if err := metadata.Validate(metaData); err != nil { + return result, validation.NewError("directories.Client", "SetMetaData", fmt.Sprintf("`metaData` is not valid: %s.", err)) + } + + req, err := client.SetMetaDataPreparer(ctx, accountName, shareName, path, metaData) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "SetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.SetMetaDataSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "directories.Client", "SetMetaData", resp, "Failure sending request") + return + } + + result, err = client.SetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "SetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// SetMetaDataPreparer prepares the SetMetaData request. +func (client Client) SetMetaDataPreparer(ctx context.Context, accountName, shareName, path string, metaData map[string]string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "directory"), + "comp": autorest.Encode("query", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = metadata.SetIntoHeaders(headers, metaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetMetaDataSender sends the SetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetMetaDataResponder handles the response to the SetMetaData request. The method always +// closes the http.Response Body. +func (client Client) SetMetaDataResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/directories/resource_id.go b/storage/2018-03-28/file/directories/resource_id.go new file mode 100644 index 0000000..44607c4 --- /dev/null +++ b/storage/2018-03-28/file/directories/resource_id.go @@ -0,0 +1,56 @@ +package directories + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given Directory +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, shareName, directoryName string) string { + domain := endpoints.GetFileEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s/%s", domain, shareName, directoryName) +} + +type ResourceID struct { + AccountName string + DirectoryName string + ShareName string +} + +// ParseResourceID parses the Resource ID into an Object +// which can be used to interact with the Directory within the File Share +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://foo.file.core.windows.net/Bar/Folder + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + path := strings.TrimPrefix(uri.Path, "/") + segments := strings.Split(path, "/") + if len(segments) == 0 { + return nil, fmt.Errorf("Expected the path to contain segments but got none") + } + + shareName := segments[0] + directoryName := strings.TrimPrefix(path, shareName) + directoryName = strings.TrimPrefix(directoryName, "/") + return &ResourceID{ + AccountName: *accountName, + ShareName: shareName, + DirectoryName: directoryName, + }, nil +} diff --git a/storage/2018-03-28/file/directories/resource_id_test.go b/storage/2018-03-28/file/directories/resource_id_test.go new file mode 100644 index 0000000..0be800d --- /dev/null +++ b/storage/2018-03-28/file/directories/resource_id_test.go @@ -0,0 +1,81 @@ +package directories + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.file.core.chinacloudapi.cn/share1/directory1", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.file.core.cloudapi.de/share1/directory1", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.file.core.windows.net/share1/directory1", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.file.core.usgovcloudapi.net/share1/directory1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "share1", "directory1") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.file.core.chinacloudapi.cn/share1/directory1", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.file.core.cloudapi.de/share1/directory1", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.file.core.windows.net/share1/directory1", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.file.core.usgovcloudapi.net/share1/directory1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.ShareName != "share1" { + t.Fatalf("Expected Share Name to be `share1` but got %q", actual.ShareName) + } + if actual.DirectoryName != "directory1" { + t.Fatalf("Expected Directory Name to be `directory1` but got %q", actual.DirectoryName) + } + } +} diff --git a/storage/2018-03-28/file/directories/version.go b/storage/2018-03-28/file/directories/version.go new file mode 100644 index 0000000..7940bde --- /dev/null +++ b/storage/2018-03-28/file/directories/version.go @@ -0,0 +1,14 @@ +package directories + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2018-03-28" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2018-03-28/file/files/README.md b/storage/2018-03-28/file/files/README.md new file mode 100644 index 0000000..240d617 --- /dev/null +++ b/storage/2018-03-28/file/files/README.md @@ -0,0 +1,43 @@ +## File Storage Files SDK for API version 2018-03-28 + +This package allows you to interact with the Files File Storage API + +### Supported Authorizers + +* Azure Active Directory (for the Resource Endpoint `https://storage.azure.com`) +* SharedKeyLite (Blob, File & Queue) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2018-03-28/file/files" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + shareName := "myshare" + directoryName := "myfiles" + fileName := "example.txt" + + storageAuth := autorest.NewSharedKeyLiteAuthorizer(accountName, storageAccountKey) + filesClient := files.New() + filesClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + input := files.CreateInput{} + if _, err := filesClient.Create(ctx, accountName, shareName, directoryName, fileName, input); err != nil { + return fmt.Errorf("Error creating File: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2018-03-28/file/files/client.go b/storage/2018-03-28/file/files/client.go new file mode 100644 index 0000000..ecca815 --- /dev/null +++ b/storage/2018-03-28/file/files/client.go @@ -0,0 +1,25 @@ +package files + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for File Storage Shares. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithEnvironment creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2018-03-28/file/files/copy.go b/storage/2018-03-28/file/files/copy.go new file mode 100644 index 0000000..31768b3 --- /dev/null +++ b/storage/2018-03-28/file/files/copy.go @@ -0,0 +1,132 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type CopyInput struct { + // Specifies the URL of the source file or blob, up to 2 KB in length. + // + // To copy a file to another file within the same storage account, you may use Shared Key to authenticate + // the source file. If you are copying a file from another storage account, or if you are copying a blob from + // the same storage account or another storage account, then you must authenticate the source file or blob using a + // shared access signature. If the source is a public blob, no authentication is required to perform the copy + // operation. A file in a share snapshot can also be specified as a copy source. + CopySource string + + MetaData map[string]string +} + +type CopyResult struct { + autorest.Response + + // The CopyID, which can be passed to AbortCopy to abort the copy. + CopyID string + + // Either `success` or `pending` + CopySuccess string +} + +// Copy copies a blob or file to a destination file within the storage account asynchronously. +func (client Client) Copy(ctx context.Context, accountName, shareName, path, fileName string, input CopyInput) (result CopyResult, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "Copy", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "Copy", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "Copy", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "Copy", "`fileName` cannot be an empty string.") + } + if input.CopySource == "" { + return result, validation.NewError("files.Client", "Copy", "`input.CopySource` cannot be an empty string.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("files.Client", "Copy", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.CopyPreparer(ctx, accountName, shareName, path, fileName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "Copy", nil, "Failure preparing request") + return + } + + resp, err := client.CopySender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "Copy", resp, "Failure sending request") + return + } + + result, err = client.CopyResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "Copy", resp, "Failure responding to request") + return + } + + return +} + +// CopyPreparer prepares the Copy request. +func (client Client) CopyPreparer(ctx context.Context, accountName, shareName, path, fileName string, input CopyInput) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-copy-source": input.CopySource, + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CopySender sends the Copy request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CopySender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CopyResponder handles the response to the Copy request. The method always +// closes the http.Response Body. +func (client Client) CopyResponder(resp *http.Response) (result CopyResult, err error) { + if resp != nil && resp.Header != nil { + result.CopyID = resp.Header.Get("x-ms-copy-id") + result.CopySuccess = resp.Header.Get("x-ms-copy-status") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/files/copy_abort.go b/storage/2018-03-28/file/files/copy_abort.go new file mode 100644 index 0000000..2f09131 --- /dev/null +++ b/storage/2018-03-28/file/files/copy_abort.go @@ -0,0 +1,104 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// AbortCopy aborts a pending Copy File operation, and leaves a destination file with zero length and full metadata +func (client Client) AbortCopy(ctx context.Context, accountName, shareName, path, fileName, copyID string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "AbortCopy", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "AbortCopy", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "AbortCopy", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "AbortCopy", "`fileName` cannot be an empty string.") + } + if copyID == "" { + return result, validation.NewError("files.Client", "AbortCopy", "`copyID` cannot be an empty string.") + } + + req, err := client.AbortCopyPreparer(ctx, accountName, shareName, path, fileName, copyID) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "AbortCopy", nil, "Failure preparing request") + return + } + + resp, err := client.AbortCopySender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "AbortCopy", resp, "Failure sending request") + return + } + + result, err = client.AbortCopyResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "AbortCopy", resp, "Failure responding to request") + return + } + + return +} + +// AbortCopyPreparer prepares the AbortCopy request. +func (client Client) AbortCopyPreparer(ctx context.Context, accountName, shareName, path, fileName, copyID string) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "copy"), + "copyid": autorest.Encode("query", copyID), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-copy-action": "abort", + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// AbortCopySender sends the AbortCopy request. The method will close the +// http.Response Body if it receives an error. +func (client Client) AbortCopySender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// AbortCopyResponder handles the response to the AbortCopy request. The method always +// closes the http.Response Body. +func (client Client) AbortCopyResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/files/copy_wait.go b/storage/2018-03-28/file/files/copy_wait.go new file mode 100644 index 0000000..e6a646b --- /dev/null +++ b/storage/2018-03-28/file/files/copy_wait.go @@ -0,0 +1,55 @@ +package files + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/Azure/go-autorest/autorest" +) + +type CopyAndWaitResult struct { + autorest.Response + + CopyID string +} + +const DefaultCopyPollDuration = 15 * time.Second + +// CopyAndWait is a convenience method which doesn't exist in the API, which copies the file and then waits for the copy to complete +func (client Client) CopyAndWait(ctx context.Context, accountName, shareName, path, fileName string, input CopyInput, pollDuration time.Duration) (result CopyResult, err error) { + copy, e := client.Copy(ctx, accountName, shareName, path, fileName, input) + if err != nil { + result.Response = copy.Response + err = fmt.Errorf("Error copying: %s", e) + return + } + + result.CopyID = copy.CopyID + + // since the API doesn't return a LRO, this is a hack which also polls every 10s, but should be sufficient + for true { + props, e := client.GetProperties(ctx, accountName, shareName, path, fileName) + if e != nil { + result.Response = copy.Response + err = fmt.Errorf("Error waiting for copy: %s", e) + return + } + + switch strings.ToLower(props.CopyStatus) { + case "pending": + time.Sleep(pollDuration) + continue + + case "success": + return + + default: + err = fmt.Errorf("Unexpected CopyState %q", e) + return + } + } + + return +} diff --git a/storage/2018-03-28/file/files/copy_wait_test.go b/storage/2018-03-28/file/files/copy_wait_test.go new file mode 100644 index 0000000..bac351c --- /dev/null +++ b/storage/2018-03-28/file/files/copy_wait_test.go @@ -0,0 +1,129 @@ +package files + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-03-28/file/shares" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestFilesCopyAndWaitFromURL(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + shareName := fmt.Sprintf("share-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + sharesClient := shares.NewWithEnvironment(client.Environment) + sharesClient.Client = client.PrepareWithAuthorizer(sharesClient.Client, storageAuth) + + input := shares.CreateInput{ + QuotaInGB: 10, + } + _, err = sharesClient.Create(ctx, accountName, shareName, input) + if err != nil { + t.Fatalf("Error creating fileshare: %s", err) + } + defer sharesClient.Delete(ctx, accountName, shareName, false) + + filesClient := NewWithEnvironment(client.Environment) + filesClient.Client = client.PrepareWithAuthorizer(filesClient.Client, storageAuth) + + copiedFileName := "ubuntu.iso" + copyInput := CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + + t.Logf("[DEBUG] Copy And Waiting..") + if _, err := filesClient.CopyAndWait(ctx, accountName, shareName, "", copiedFileName, copyInput, DefaultCopyPollDuration); err != nil { + t.Fatalf("Error copy & waiting: %s", err) + } + + t.Logf("[DEBUG] Asserting that the file's ready..") + + props, err := filesClient.GetProperties(ctx, accountName, shareName, "", copiedFileName) + if err != nil { + t.Fatalf("Error retrieving file: %s", err) + } + + if !strings.EqualFold(props.CopyStatus, "success") { + t.Fatalf("Expected the Copy Status to be `Success` but got %q", props.CopyStatus) + } +} + +func TestFilesCopyAndWaitFromBlob(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + shareName := fmt.Sprintf("share-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + sharesClient := shares.NewWithEnvironment(client.Environment) + sharesClient.Client = client.PrepareWithAuthorizer(sharesClient.Client, storageAuth) + + input := shares.CreateInput{ + QuotaInGB: 10, + } + _, err = sharesClient.Create(ctx, accountName, shareName, input) + if err != nil { + t.Fatalf("Error creating fileshare: %s", err) + } + defer sharesClient.Delete(ctx, accountName, shareName, false) + + filesClient := NewWithEnvironment(client.Environment) + filesClient.Client = client.PrepareWithAuthorizer(filesClient.Client, storageAuth) + + originalFileName := "ubuntu.iso" + copiedFileName := "ubuntu-copied.iso" + copyInput := CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + t.Logf("[DEBUG] Copy And Waiting the original file..") + if _, err := filesClient.CopyAndWait(ctx, accountName, shareName, "", originalFileName, copyInput, DefaultCopyPollDuration); err != nil { + t.Fatalf("Error copy & waiting: %s", err) + } + + t.Logf("[DEBUG] Now copying that blob..") + duplicateInput := CopyInput{ + CopySource: fmt.Sprintf("%s/%s/%s", endpoints.GetFileEndpoint(filesClient.BaseURI, accountName), shareName, originalFileName), + } + if _, err := filesClient.CopyAndWait(ctx, accountName, shareName, "", copiedFileName, duplicateInput, DefaultCopyPollDuration); err != nil { + t.Fatalf("Error copying duplicate: %s", err) + } + + t.Logf("[DEBUG] Asserting that the file's ready..") + props, err := filesClient.GetProperties(ctx, accountName, shareName, "", copiedFileName) + if err != nil { + t.Fatalf("Error retrieving file: %s", err) + } + + if !strings.EqualFold(props.CopyStatus, "success") { + t.Fatalf("Expected the Copy Status to be `Success` but got %q", props.CopyStatus) + } +} diff --git a/storage/2018-03-28/file/files/create.go b/storage/2018-03-28/file/files/create.go new file mode 100644 index 0000000..85d4b0b --- /dev/null +++ b/storage/2018-03-28/file/files/create.go @@ -0,0 +1,146 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type CreateInput struct { + // This header specifies the maximum size for the file, up to 1 TiB. + ContentLength int64 + + // The MIME content type of the file + // If not specified, the default type is application/octet-stream. + ContentType *string + + // Specifies which content encodings have been applied to the file. + // This value is returned to the client when the Get File operation is performed + // on the file resource and can be used to decode file content. + ContentEncoding *string + + // Specifies the natural languages used by this resource. + ContentLanguage *string + + // The File service stores this value but does not use or modify it. + CacheControl *string + + // Sets the file's MD5 hash. + ContentMD5 *string + + // Sets the file’s Content-Disposition header. + ContentDisposition *string + + MetaData map[string]string +} + +// Create creates a new file or replaces a file. +func (client Client) Create(ctx context.Context, accountName, shareName, path, fileName string, input CreateInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "Create", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "Create", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "Create", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "Create", "`fileName` cannot be an empty string.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("files.Client", "Create", "`input.MetaData` cannot be an empty string.") + } + + req, err := client.CreatePreparer(ctx, accountName, shareName, path, fileName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "Create", nil, "Failure preparing request") + return + } + + resp, err := client.CreateSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "Create", resp, "Failure sending request") + return + } + + result, err = client.CreateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "Create", resp, "Failure responding to request") + return + } + + return +} + +// CreatePreparer prepares the Create request. +func (client Client) CreatePreparer(ctx context.Context, accountName, shareName, path, fileName string, input CreateInput) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-content-length": input.ContentLength, + "x-ms-type": "file", + } + + if input.ContentDisposition != nil { + headers["x-ms-content-disposition"] = *input.ContentDisposition + } + + if input.ContentEncoding != nil { + headers["x-ms-content-encoding"] = *input.ContentEncoding + } + + if input.ContentMD5 != nil { + headers["x-ms-content-md5"] = *input.ContentMD5 + } + + if input.ContentType != nil { + headers["x-ms-content-type"] = *input.ContentType + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CreateSender sends the Create request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CreateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CreateResponder handles the response to the Create request. The method always +// closes the http.Response Body. +func (client Client) CreateResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/files/delete.go b/storage/2018-03-28/file/files/delete.go new file mode 100644 index 0000000..5debd76 --- /dev/null +++ b/storage/2018-03-28/file/files/delete.go @@ -0,0 +1,94 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Delete immediately deletes the file from the File Share. +func (client Client) Delete(ctx context.Context, accountName, shareName, path, fileName string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "Delete", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "Delete", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "Delete", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "Delete", "`fileName` cannot be an empty string.") + } + + req, err := client.DeletePreparer(ctx, accountName, shareName, path, fileName) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName, shareName, path, fileName string) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/files/lifecycle_test.go b/storage/2018-03-28/file/files/lifecycle_test.go new file mode 100644 index 0000000..ec514a2 --- /dev/null +++ b/storage/2018-03-28/file/files/lifecycle_test.go @@ -0,0 +1,144 @@ +package files + +import ( + "context" + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-03-28/file/shares" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestFilesLifeCycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + shareName := fmt.Sprintf("share-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + sharesClient := shares.NewWithEnvironment(client.Environment) + sharesClient.Client = client.PrepareWithAuthorizer(sharesClient.Client, storageAuth) + + input := shares.CreateInput{ + QuotaInGB: 1, + } + _, err = sharesClient.Create(ctx, accountName, shareName, input) + if err != nil { + t.Fatalf("Error creating fileshare: %s", err) + } + defer sharesClient.Delete(ctx, accountName, shareName, false) + + filesClient := NewWithEnvironment(client.Environment) + filesClient.Client = client.PrepareWithAuthorizer(filesClient.Client, storageAuth) + + fileName := "bled5.png" + contentEncoding := "application/vnd+panda" + + t.Logf("[DEBUG] Creating Top Level File..") + createInput := CreateInput{ + ContentLength: 1024, + ContentEncoding: &contentEncoding, + } + if _, err := filesClient.Create(ctx, accountName, shareName, "", fileName, createInput); err != nil { + t.Fatalf("Error creating Top-Level File: %s", err) + } + + t.Logf("[DEBUG] Retrieving Properties for the Top-Level File..") + file, err := filesClient.GetProperties(ctx, accountName, shareName, "", fileName) + if err != nil { + t.Fatalf("Error retrieving Top-Level File: %s", err) + } + + if *file.ContentLength != 1024 { + t.Fatalf("Expected the Content-Length to be 1024 but got %d", *file.ContentLength) + } + + if file.ContentEncoding != contentEncoding { + t.Fatalf("Expected the Content-Encoding to be %q but got %q", contentEncoding, file.ContentEncoding) + } + + updatedSize := int64(2048) + updatedEncoding := "application/vnd+pandas2" + updatedInput := SetPropertiesInput{ + ContentEncoding: &updatedEncoding, + ContentLength: &updatedSize, + } + if _, err := filesClient.SetProperties(ctx, accountName, shareName, "", fileName, updatedInput); err != nil { + t.Fatalf("Error setting properties: %s", err) + } + + t.Logf("[DEBUG] Re-retrieving Properties for the Top-Level File..") + file, err = filesClient.GetProperties(ctx, accountName, shareName, "", fileName) + if err != nil { + t.Fatalf("Error retrieving Top-Level File: %s", err) + } + + if *file.ContentLength != 2048 { + t.Fatalf("Expected the Content-Length to be 1024 but got %d", *file.ContentLength) + } + + if file.ContentEncoding != updatedEncoding { + t.Fatalf("Expected the Content-Encoding to be %q but got %q", updatedEncoding, file.ContentEncoding) + } + + t.Logf("[DEBUG] Setting MetaData..") + metaData := map[string]string{ + "hello": "there", + } + if _, err := filesClient.SetMetaData(ctx, accountName, shareName, "", fileName, metaData); err != nil { + t.Fatalf("Error setting MetaData: %s", err) + } + + t.Logf("[DEBUG] Retrieving MetaData..") + retrievedMetaData, err := filesClient.GetMetaData(ctx, accountName, shareName, "", fileName) + if err != nil { + t.Fatalf("Error retrieving MetaData: %s", err) + } + if len(retrievedMetaData.MetaData) != 1 { + t.Fatalf("Expected 1 item but got %d", len(retrievedMetaData.MetaData)) + } + if retrievedMetaData.MetaData["hello"] != "there" { + t.Fatalf("Expected `hello` to be `there` but got %q", retrievedMetaData.MetaData["hello"]) + } + + t.Logf("[DEBUG] Re-Setting MetaData..") + metaData = map[string]string{ + "hello": "there", + "second": "thing", + } + if _, err := filesClient.SetMetaData(ctx, accountName, shareName, "", fileName, metaData); err != nil { + t.Fatalf("Error setting MetaData: %s", err) + } + + t.Logf("[DEBUG] Re-Retrieving MetaData..") + retrievedMetaData, err = filesClient.GetMetaData(ctx, accountName, shareName, "", fileName) + if err != nil { + t.Fatalf("Error retrieving MetaData: %s", err) + } + if len(retrievedMetaData.MetaData) != 2 { + t.Fatalf("Expected 2 items but got %d", len(retrievedMetaData.MetaData)) + } + if retrievedMetaData.MetaData["hello"] != "there" { + t.Fatalf("Expected `hello` to be `there` but got %q", retrievedMetaData.MetaData["hello"]) + } + if retrievedMetaData.MetaData["second"] != "thing" { + t.Fatalf("Expected `second` to be `thing` but got %q", retrievedMetaData.MetaData["second"]) + } + + t.Logf("[DEBUG] Deleting Top Level File..") + if _, err := filesClient.Delete(ctx, accountName, shareName, "", fileName); err != nil { + t.Fatalf("Error deleting Top-Level File: %s", err) + } +} diff --git a/storage/2018-03-28/file/files/metadata_get.go b/storage/2018-03-28/file/files/metadata_get.go new file mode 100644 index 0000000..fd62f90 --- /dev/null +++ b/storage/2018-03-28/file/files/metadata_get.go @@ -0,0 +1,111 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetMetaDataResult struct { + autorest.Response + + MetaData map[string]string +} + +// GetMetaData returns the MetaData for the specified File. +func (client Client) GetMetaData(ctx context.Context, accountName, shareName, path, fileName string) (result GetMetaDataResult, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "GetMetaData", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "GetMetaData", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "GetMetaData", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "GetMetaData", "`fileName` cannot be an empty string.") + } + + req, err := client.GetMetaDataPreparer(ctx, accountName, shareName, path, fileName) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "GetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.GetMetaDataSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "GetMetaData", resp, "Failure sending request") + return + } + + result, err = client.GetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "GetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// GetMetaDataPreparer prepares the GetMetaData request. +func (client Client) GetMetaDataPreparer(ctx context.Context, accountName, shareName, path, fileName string) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetMetaDataSender sends the GetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetMetaDataResponder handles the response to the GetMetaData request. The method always +// closes the http.Response Body. +func (client Client) GetMetaDataResponder(resp *http.Response) (result GetMetaDataResult, err error) { + if resp != nil && resp.Header != nil { + result.MetaData = metadata.ParseFromHeaders(resp.Header) + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + //metadata.ByParsingFromHeaders(&result.MetaData), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/files/metadata_set.go b/storage/2018-03-28/file/files/metadata_set.go new file mode 100644 index 0000000..41e3ffc --- /dev/null +++ b/storage/2018-03-28/file/files/metadata_set.go @@ -0,0 +1,105 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// SetMetaData updates the specified File to have the specified MetaData. +func (client Client) SetMetaData(ctx context.Context, accountName, shareName, path, fileName string, metaData map[string]string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "SetMetaData", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "SetMetaData", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "SetMetaData", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "SetMetaData", "`fileName` cannot be an empty string.") + } + if err := metadata.Validate(metaData); err != nil { + return result, validation.NewError("files.Client", "SetMetaData", fmt.Sprintf("`metaData` is not valid: %s.", err)) + } + + req, err := client.SetMetaDataPreparer(ctx, accountName, shareName, path, fileName, metaData) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "SetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.SetMetaDataSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "SetMetaData", resp, "Failure sending request") + return + } + + result, err = client.SetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "SetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// SetMetaDataPreparer prepares the SetMetaData request. +func (client Client) SetMetaDataPreparer(ctx context.Context, accountName, shareName, path, fileName string, metaData map[string]string) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = metadata.SetIntoHeaders(headers, metaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetMetaDataSender sends the SetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetMetaDataResponder handles the response to the SetMetaData request. The method always +// closes the http.Response Body. +func (client Client) SetMetaDataResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/files/properties_get.go b/storage/2018-03-28/file/files/properties_get.go new file mode 100644 index 0000000..c6a0c39 --- /dev/null +++ b/storage/2018-03-28/file/files/properties_get.go @@ -0,0 +1,144 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetResult struct { + autorest.Response + + CacheControl string + ContentDisposition string + ContentEncoding string + ContentLanguage string + ContentLength *int64 + ContentMD5 string + ContentType string + CopyID string + CopyStatus string + CopySource string + CopyProgress string + CopyStatusDescription string + CopyCompletionTime string + Encrypted bool + + MetaData map[string]string +} + +// GetProperties returns the Properties for the specified file +func (client Client) GetProperties(ctx context.Context, accountName, shareName, path, fileName string) (result GetResult, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "GetProperties", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "GetProperties", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "GetProperties", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "GetProperties", "`fileName` cannot be an empty string.") + } + + req, err := client.GetPropertiesPreparer(ctx, accountName, shareName, path, fileName) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "GetProperties", nil, "Failure preparing request") + return + } + + resp, err := client.GetPropertiesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "GetProperties", resp, "Failure sending request") + return + } + + result, err = client.GetPropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "GetProperties", resp, "Failure responding to request") + return + } + + return +} + +// GetPropertiesPreparer prepares the GetProperties request. +func (client Client) GetPropertiesPreparer(ctx context.Context, accountName, shareName, path, fileName string) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsHead(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetPropertiesSender sends the GetProperties request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetPropertiesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetPropertiesResponder handles the response to the GetProperties request. The method always +// closes the http.Response Body. +func (client Client) GetPropertiesResponder(resp *http.Response) (result GetResult, err error) { + if resp != nil && resp.Header != nil { + result.CacheControl = resp.Header.Get("Cache-Control") + result.ContentDisposition = resp.Header.Get("Content-Disposition") + result.ContentEncoding = resp.Header.Get("Content-Encoding") + result.ContentLanguage = resp.Header.Get("Content-Language") + result.ContentMD5 = resp.Header.Get("x-ms-content-md5") + result.ContentType = resp.Header.Get("Content-Type") + result.CopyID = resp.Header.Get("x-ms-copy-id") + result.CopyProgress = resp.Header.Get("x-ms-copy-progress") + result.CopySource = resp.Header.Get("x-ms-copy-source") + result.CopyStatus = resp.Header.Get("x-ms-copy-status") + result.CopyStatusDescription = resp.Header.Get("x-ms-copy-status-description") + result.CopyCompletionTime = resp.Header.Get("x-ms-copy-completion-time") + result.Encrypted = strings.EqualFold(resp.Header.Get("x-ms-server-encrypted"), "true") + result.MetaData = metadata.ParseFromHeaders(resp.Header) + + contentLengthRaw := resp.Header.Get("Content-Length") + if contentLengthRaw != "" { + contentLength, err := strconv.Atoi(contentLengthRaw) + if err != nil { + return result, fmt.Errorf("Error parsing %q for Content-Length as an integer: %s", contentLengthRaw, err) + } + contentLengthI64 := int64(contentLength) + result.ContentLength = &contentLengthI64 + } + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/files/properties_set.go b/storage/2018-03-28/file/files/properties_set.go new file mode 100644 index 0000000..79fffc2 --- /dev/null +++ b/storage/2018-03-28/file/files/properties_set.go @@ -0,0 +1,160 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type SetPropertiesInput struct { + // Resizes a file to the specified size. + // If the specified byte value is less than the current size of the file, + // then all ranges above the specified byte value are cleared. + ContentLength *int64 + + // Modifies the cache control string for the file. + // If this property is not specified on the request, then the property will be cleared for the file. + // Subsequent calls to Get File Properties will not return this property, + // unless it is explicitly set on the file again. + ContentControl *string + + // Sets the file’s Content-Disposition header. + // If this property is not specified on the request, then the property will be cleared for the file. + // Subsequent calls to Get File Properties will not return this property, + // unless it is explicitly set on the file again. + ContentDisposition *string + + // Sets the file's content encoding. + // If this property is not specified on the request, then the property will be cleared for the file. + // Subsequent calls to Get File Properties will not return this property, + // unless it is explicitly set on the file again. + ContentEncoding *string + + // Sets the file's content language. + // If this property is not specified on the request, then the property will be cleared for the file. + // Subsequent calls to Get File Properties will not return this property, + // unless it is explicitly set on the file again. + ContentLanguage *string + + // Sets the file's MD5 hash. + // If this property is not specified on the request, then the property will be cleared for the file. + // Subsequent calls to Get File Properties will not return this property, + // unless it is explicitly set on the file again. + ContentMD5 *string + + // Sets the file's content type. + // If this property is not specified on the request, then the property will be cleared for the file. + // Subsequent calls to Get File Properties will not return this property, + // unless it is explicitly set on the file again. + ContentType *string +} + +// SetProperties sets the specified properties on the specified File +func (client Client) SetProperties(ctx context.Context, accountName, shareName, path, fileName string, input SetPropertiesInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "SetProperties", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "SetProperties", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "SetProperties", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "SetProperties", "`fileName` cannot be an empty string.") + } + + req, err := client.SetPropertiesPreparer(ctx, accountName, shareName, path, fileName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "SetProperties", nil, "Failure preparing request") + return + } + + resp, err := client.SetPropertiesSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "SetProperties", resp, "Failure sending request") + return + } + + result, err = client.SetPropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "SetProperties", resp, "Failure responding to request") + return + } + + return +} + +// SetPropertiesPreparer prepares the SetProperties request. +func (client Client) SetPropertiesPreparer(ctx context.Context, accountName, shareName, path, fileName string, input SetPropertiesInput) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-type": "file", + } + + if input.ContentControl != nil { + headers["x-ms-cache-control"] = *input.ContentControl + } + if input.ContentDisposition != nil { + headers["x-ms-content-disposition"] = *input.ContentDisposition + } + if input.ContentEncoding != nil { + headers["x-ms-content-encoding"] = *input.ContentEncoding + } + if input.ContentLanguage != nil { + headers["x-ms-content-language"] = *input.ContentLanguage + } + if input.ContentLength != nil { + headers["x-ms-content-length"] = *input.ContentLength + } + if input.ContentMD5 != nil { + headers["x-ms-content-md5"] = *input.ContentMD5 + } + if input.ContentType != nil { + headers["x-ms-content-type"] = *input.ContentType + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetPropertiesSender sends the SetProperties request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetPropertiesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetPropertiesResponder handles the response to the SetProperties request. The method always +// closes the http.Response Body. +func (client Client) SetPropertiesResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/files/range_clear.go b/storage/2018-03-28/file/files/range_clear.go new file mode 100644 index 0000000..5d8145f --- /dev/null +++ b/storage/2018-03-28/file/files/range_clear.go @@ -0,0 +1,112 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type ClearByteRangeInput struct { + StartBytes int64 + EndBytes int64 +} + +// ClearByteRange clears the specified Byte Range from within the specified File +func (client Client) ClearByteRange(ctx context.Context, accountName, shareName, path, fileName string, input ClearByteRangeInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "ClearByteRange", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "ClearByteRange", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "ClearByteRange", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "ClearByteRange", "`fileName` cannot be an empty string.") + } + if input.StartBytes < 0 { + return result, validation.NewError("files.Client", "ClearByteRange", "`input.StartBytes` must be greater or equal to 0.") + } + if input.EndBytes <= 0 { + return result, validation.NewError("files.Client", "ClearByteRange", "`input.EndBytes` must be greater than 0.") + } + + req, err := client.ClearByteRangePreparer(ctx, accountName, shareName, path, fileName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "ClearByteRange", nil, "Failure preparing request") + return + } + + resp, err := client.ClearByteRangeSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "ClearByteRange", resp, "Failure sending request") + return + } + + result, err = client.ClearByteRangeResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "ClearByteRange", resp, "Failure responding to request") + return + } + + return +} + +// ClearByteRangePreparer prepares the ClearByteRange request. +func (client Client) ClearByteRangePreparer(ctx context.Context, accountName, shareName, path, fileName string, input ClearByteRangeInput) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "range"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-write": "clear", + "x-ms-range": fmt.Sprintf("bytes=%d-%d", input.StartBytes, input.EndBytes), + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ClearByteRangeSender sends the ClearByteRange request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ClearByteRangeSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ClearByteRangeResponder handles the response to the ClearByteRange request. The method always +// closes the http.Response Body. +func (client Client) ClearByteRangeResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/files/range_get.go b/storage/2018-03-28/file/files/range_get.go new file mode 100644 index 0000000..733d3f5 --- /dev/null +++ b/storage/2018-03-28/file/files/range_get.go @@ -0,0 +1,121 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetByteRangeInput struct { + StartBytes int64 + EndBytes int64 +} + +type GetByteRangeResult struct { + autorest.Response + + Contents []byte +} + +// GetByteRange returns the specified Byte Range from the specified File. +func (client Client) GetByteRange(ctx context.Context, accountName, shareName, path, fileName string, input GetByteRangeInput) (result GetByteRangeResult, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "GetByteRange", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "GetByteRange", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "GetByteRange", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "GetByteRange", "`fileName` cannot be an empty string.") + } + if input.StartBytes < 0 { + return result, validation.NewError("files.Client", "GetByteRange", "`input.StartBytes` must be greater or equal to 0.") + } + if input.EndBytes <= 0 { + return result, validation.NewError("files.Client", "GetByteRange", "`input.EndBytes` must be greater than 0.") + } + expectedBytes := input.EndBytes - input.StartBytes + if expectedBytes < (4 * 1024) { + return result, validation.NewError("files.Client", "GetByteRange", "Requested Byte Range must be at least 4KB.") + } + if expectedBytes > (4 * 1024 * 1024) { + return result, validation.NewError("files.Client", "GetByteRange", "Requested Byte Range must be at most 4MB.") + } + + req, err := client.GetByteRangePreparer(ctx, accountName, shareName, path, fileName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "GetByteRange", nil, "Failure preparing request") + return + } + + resp, err := client.GetByteRangeSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "GetByteRange", resp, "Failure sending request") + return + } + + result, err = client.GetByteRangeResponder(resp, expectedBytes) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "GetByteRange", resp, "Failure responding to request") + return + } + + return +} + +// GetByteRangePreparer prepares the GetByteRange request. +func (client Client) GetByteRangePreparer(ctx context.Context, accountName, shareName, path, fileName string, input GetByteRangeInput) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-range": fmt.Sprintf("bytes=%d-%d", input.StartBytes, input.EndBytes-1), + } + + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetByteRangeSender sends the GetByteRange request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetByteRangeSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetByteRangeResponder handles the response to the GetByteRange request. The method always +// closes the http.Response Body. +func (client Client) GetByteRangeResponder(resp *http.Response, length int64) (result GetByteRangeResult, err error) { + result.Contents = make([]byte, length) + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusPartialContent), + autorest.ByUnmarshallingBytes(&result.Contents), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/files/range_get_file.go b/storage/2018-03-28/file/files/range_get_file.go new file mode 100644 index 0000000..9e5be17 --- /dev/null +++ b/storage/2018-03-28/file/files/range_get_file.go @@ -0,0 +1,128 @@ +package files + +import ( + "context" + "fmt" + "log" + "math" + "runtime" + "sync" + + "github.com/Azure/go-autorest/autorest" +) + +// GetFile is a helper method to download a file by chunking it automatically +func (client Client) GetFile(ctx context.Context, accountName, shareName, path, fileName string, parallelism int) (result autorest.Response, outputBytes []byte, err error) { + + // first look up the file and check out how many bytes it is + file, e := client.GetProperties(ctx, accountName, shareName, path, fileName) + if err != nil { + result = file.Response + err = e + return + } + + if file.ContentLength == nil { + err = fmt.Errorf("Content-Length was nil!") + return + } + + length := int64(*file.ContentLength) + chunkSize := int64(4 * 1024 * 1024) // 4MB + + if chunkSize > length { + chunkSize = length + } + + // then split that up into chunks and retrieve it retrieve it into the 'results' set + chunks := int(math.Ceil(float64(length) / float64(chunkSize))) + workerCount := parallelism * runtime.NumCPU() + if workerCount > chunks { + workerCount = chunks + } + + var waitGroup sync.WaitGroup + waitGroup.Add(workerCount) + + results := make([]*downloadFileChunkResult, chunks) + errors := make(chan error, chunkSize) + + for i := 0; i < chunks; i++ { + go func(i int) { + log.Printf("[DEBUG] Downloading Chunk %d of %d", i+1, chunks) + + dfci := downloadFileChunkInput{ + thisChunk: i, + chunkSize: chunkSize, + fileSize: length, + } + + result, err := client.downloadFileChunk(ctx, accountName, shareName, path, fileName, dfci) + if err != nil { + errors <- err + waitGroup.Done() + return + } + + // if there's no error, we should have bytes, so this is safe + results[i] = result + + waitGroup.Done() + }(i) + } + waitGroup.Wait() + + // TODO: we should switch to hashicorp/multi-error here + if len(errors) > 0 { + err = fmt.Errorf("Error downloading file: %s", <-errors) + return + } + + // then finally put it all together, in order and return it + output := make([]byte, length) + for _, v := range results { + copy(output[v.startBytes:v.endBytes], v.bytes) + } + + outputBytes = output + return +} + +type downloadFileChunkInput struct { + thisChunk int + chunkSize int64 + fileSize int64 +} + +type downloadFileChunkResult struct { + startBytes int64 + endBytes int64 + bytes []byte +} + +func (client Client) downloadFileChunk(ctx context.Context, accountName, shareName, path, fileName string, input downloadFileChunkInput) (*downloadFileChunkResult, error) { + startBytes := input.chunkSize * int64(input.thisChunk) + endBytes := startBytes + input.chunkSize + + // the last chunk may exceed the size of the file + remaining := input.fileSize - startBytes + if input.chunkSize > remaining { + endBytes = startBytes + remaining + } + + getInput := GetByteRangeInput{ + StartBytes: startBytes, + EndBytes: endBytes, + } + result, err := client.GetByteRange(ctx, accountName, shareName, path, fileName, getInput) + if err != nil { + return nil, fmt.Errorf("Error putting bytes: %s", err) + } + + output := downloadFileChunkResult{ + startBytes: startBytes, + endBytes: endBytes, + bytes: result.Contents, + } + return &output, nil +} diff --git a/storage/2018-03-28/file/files/range_get_file_test.go b/storage/2018-03-28/file/files/range_get_file_test.go new file mode 100644 index 0000000..b1d3c59 --- /dev/null +++ b/storage/2018-03-28/file/files/range_get_file_test.go @@ -0,0 +1,108 @@ +package files + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-03-28/file/shares" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestGetSmallFile(t *testing.T) { + // the purpose of this test is to verify that the small, single-chunked file gets downloaded correctly + testGetFile(t, "small-file.png", "image/png") +} + +func TestGetLargeFile(t *testing.T) { + // the purpose of this test is to verify that the large, multi-chunked file gets downloaded correctly + testGetFile(t, "blank-large-file.dmg", "application/x-apple-diskimage") +} + +func testGetFile(t *testing.T, fileName string, contentType string) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + shareName := fmt.Sprintf("share-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + sharesClient := shares.NewWithEnvironment(client.Environment) + sharesClient.Client = client.PrepareWithAuthorizer(sharesClient.Client, storageAuth) + + input := shares.CreateInput{ + QuotaInGB: 10, + } + _, err = sharesClient.Create(ctx, accountName, shareName, input) + if err != nil { + t.Fatalf("Error creating fileshare: %s", err) + } + defer sharesClient.Delete(ctx, accountName, shareName, false) + + filesClient := NewWithEnvironment(client.Environment) + filesClient.Client = client.PrepareWithAuthorizer(filesClient.Client, storageAuth) + + // store files outside of this directory, since they're reused + file, err := os.Open("../../../testdata/" + fileName) + if err != nil { + t.Fatalf("Error opening: %s", err) + } + + info, err := file.Stat() + if err != nil { + t.Fatalf("Error 'stat'-ing: %s", err) + } + + t.Logf("[DEBUG] Creating Top Level File..") + createFileInput := CreateInput{ + ContentLength: info.Size(), + ContentType: &contentType, + } + if _, err := filesClient.Create(ctx, accountName, shareName, "", fileName, createFileInput); err != nil { + t.Fatalf("Error creating Top-Level File: %s", err) + } + + t.Logf("[DEBUG] Uploading File..") + if err := filesClient.PutFile(ctx, accountName, shareName, "", fileName, file, 4); err != nil { + t.Fatalf("Error uploading File: %s", err) + } + + t.Logf("[DEBUG] Downloading file..") + _, downloadedBytes, err := filesClient.GetFile(ctx, accountName, shareName, "", fileName, 4) + if err != nil { + t.Fatalf("Error downloading file: %s", err) + } + + t.Logf("[DEBUG] Asserting the files are the same size..") + expectedBytes := make([]byte, info.Size()) + file.Read(expectedBytes) + if len(expectedBytes) != len(downloadedBytes) { + t.Fatalf("Expected %d bytes but got %d", len(expectedBytes), len(downloadedBytes)) + } + + t.Logf("[DEBUG] Asserting the files are the same content-wise..") + // overkill, but it's this or shasum-ing + for i := int64(0); i < info.Size(); i++ { + if expectedBytes[i] != downloadedBytes[i] { + t.Fatalf("Expected byte %d to be %q but got %q", i, expectedBytes[i], downloadedBytes[i]) + } + } + + t.Logf("[DEBUG] Deleting Top Level File..") + if _, err := filesClient.Delete(ctx, accountName, shareName, "", fileName); err != nil { + t.Fatalf("Error deleting Top-Level File: %s", err) + } + +} diff --git a/storage/2018-03-28/file/files/range_put.go b/storage/2018-03-28/file/files/range_put.go new file mode 100644 index 0000000..77fe101 --- /dev/null +++ b/storage/2018-03-28/file/files/range_put.go @@ -0,0 +1,129 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type PutByteRangeInput struct { + StartBytes int64 + EndBytes int64 + + // Content is the File Contents for the specified range + // which can be at most 4MB + Content []byte +} + +// PutByteRange puts the specified Byte Range in the specified File. +func (client Client) PutByteRange(ctx context.Context, accountName, shareName, path, fileName string, input PutByteRangeInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "PutByteRange", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "PutByteRange", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "PutByteRange", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "PutByteRange", "`fileName` cannot be an empty string.") + } + if input.StartBytes < 0 { + return result, validation.NewError("files.Client", "PutByteRange", "`input.StartBytes` must be greater or equal to 0.") + } + if input.EndBytes <= 0 { + return result, validation.NewError("files.Client", "PutByteRange", "`input.EndBytes` must be greater than 0.") + } + + expectedBytes := input.EndBytes - input.StartBytes + actualBytes := len(input.Content) + if expectedBytes != int64(actualBytes) { + return result, validation.NewError("files.Client", "PutByteRange", fmt.Sprintf("The specified byte-range (%d) didn't match the content size (%d).", expectedBytes, actualBytes)) + } + if expectedBytes < (4 * 1024) { + return result, validation.NewError("files.Client", "PutByteRange", "Specified Byte Range must be at least 4KB.") + } + + if expectedBytes > (4 * 1024 * 1024) { + return result, validation.NewError("files.Client", "PutByteRange", "Specified Byte Range must be at most 4MB.") + } + + req, err := client.PutByteRangePreparer(ctx, accountName, shareName, path, fileName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "PutByteRange", nil, "Failure preparing request") + return + } + + resp, err := client.PutByteRangeSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "PutByteRange", resp, "Failure sending request") + return + } + + result, err = client.PutByteRangeResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "PutByteRange", resp, "Failure responding to request") + return + } + + return +} + +// PutByteRangePreparer prepares the PutByteRange request. +func (client Client) PutByteRangePreparer(ctx context.Context, accountName, shareName, path, fileName string, input PutByteRangeInput) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "range"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-write": "update", + "x-ms-range": fmt.Sprintf("bytes=%d-%d", input.StartBytes, input.EndBytes-1), + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters), + autorest.WithBytes(&input.Content)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutByteRangeSender sends the PutByteRange request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutByteRangeSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutByteRangeResponder handles the response to the PutByteRange request. The method always +// closes the http.Response Body. +func (client Client) PutByteRangeResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/files/range_put_file.go b/storage/2018-03-28/file/files/range_put_file.go new file mode 100644 index 0000000..a39cd37 --- /dev/null +++ b/storage/2018-03-28/file/files/range_put_file.go @@ -0,0 +1,107 @@ +package files + +import ( + "context" + "fmt" + "io" + "log" + "math" + "os" + "runtime" + "sync" + + "github.com/Azure/go-autorest/autorest" +) + +// PutFile is a helper method which takes a file, and automatically chunks it up, rather than having to do this yourself +func (client Client) PutFile(ctx context.Context, accountName, shareName, path, fileName string, file *os.File, parallelism int) error { + fileInfo, err := file.Stat() + if err != nil { + return fmt.Errorf("Error loading file info: %s", err) + } + + fileSize := fileInfo.Size() + chunkSize := 4 * 1024 * 1024 // 4MB + if chunkSize > int(fileSize) { + chunkSize = int(fileSize) + } + chunks := int(math.Ceil(float64(fileSize) / float64(chunkSize*1.0))) + + workerCount := parallelism * runtime.NumCPU() + if workerCount > chunks { + workerCount = chunks + } + + var waitGroup sync.WaitGroup + waitGroup.Add(workerCount) + errors := make(chan error, chunkSize) + + for i := 0; i < chunks; i++ { + go func(i int) { + log.Printf("[DEBUG] Chunk %d of %d", i+1, chunks) + + uci := uploadChunkInput{ + thisChunk: i, + chunkSize: chunkSize, + fileSize: fileSize, + } + + _, err := client.uploadChunk(ctx, accountName, shareName, path, fileName, uci, file) + if err != nil { + errors <- err + waitGroup.Done() + return + } + + waitGroup.Done() + return + }(i) + } + waitGroup.Wait() + + // TODO: we should switch to hashicorp/multi-error here + if len(errors) > 0 { + return fmt.Errorf("Error uploading file: %s", <-errors) + } + + return nil +} + +type uploadChunkInput struct { + thisChunk int + chunkSize int + fileSize int64 +} + +func (client Client) uploadChunk(ctx context.Context, accountName, shareName, path, fileName string, input uploadChunkInput, file *os.File) (result autorest.Response, err error) { + startBytes := int64(input.chunkSize * input.thisChunk) + endBytes := startBytes + int64(input.chunkSize) + + // the last size may exceed the size of the file + remaining := input.fileSize - startBytes + if int64(input.chunkSize) > remaining { + endBytes = startBytes + remaining + } + + bytesToRead := int(endBytes) - int(startBytes) + bytes := make([]byte, bytesToRead) + + _, err = file.ReadAt(bytes, startBytes) + if err != nil { + if err != io.EOF { + return result, fmt.Errorf("Error reading bytes: %s", err) + } + } + + putBytesInput := PutByteRangeInput{ + StartBytes: startBytes, + EndBytes: endBytes, + Content: bytes, + } + result, err = client.PutByteRange(ctx, accountName, shareName, path, fileName, putBytesInput) + if err != nil { + return result, fmt.Errorf("Error putting bytes: %s", err) + } + + return +} diff --git a/storage/2018-03-28/file/files/range_put_file_test.go b/storage/2018-03-28/file/files/range_put_file_test.go new file mode 100644 index 0000000..cfc736f --- /dev/null +++ b/storage/2018-03-28/file/files/range_put_file_test.go @@ -0,0 +1,86 @@ +package files + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-03-28/file/shares" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestPutSmallFile(t *testing.T) { + // the purpose of this test is to ensure that a small file (< 4MB) is a single chunk + testPutFile(t, "small-file.png", "image/png") +} + +func TestPutLargeFile(t *testing.T) { + // the purpose of this test is to ensure that large files (> 4MB) are chunked + testPutFile(t, "blank-large-file.dmg", "application/x-apple-diskimage") +} + +func testPutFile(t *testing.T, fileName string, contentType string) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + shareName := fmt.Sprintf("share-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + sharesClient := shares.NewWithEnvironment(client.Environment) + sharesClient.Client = client.PrepareWithAuthorizer(sharesClient.Client, storageAuth) + + input := shares.CreateInput{ + QuotaInGB: 10, + } + _, err = sharesClient.Create(ctx, accountName, shareName, input) + if err != nil { + t.Fatalf("Error creating fileshare: %s", err) + } + defer sharesClient.Delete(ctx, accountName, shareName, false) + + filesClient := NewWithEnvironment(client.Environment) + filesClient.Client = client.PrepareWithAuthorizer(filesClient.Client, storageAuth) + + // store files outside of this directory, since they're reused + file, err := os.Open("../../../testdata/" + fileName) + if err != nil { + t.Fatalf("Error opening: %s", err) + } + + info, err := file.Stat() + if err != nil { + t.Fatalf("Error 'stat'-ing: %s", err) + } + + t.Logf("[DEBUG] Creating Top Level File..") + createFileInput := CreateInput{ + ContentLength: info.Size(), + ContentType: &contentType, + } + if _, err := filesClient.Create(ctx, accountName, shareName, "", fileName, createFileInput); err != nil { + t.Fatalf("Error creating Top-Level File: %s", err) + } + + t.Logf("[DEBUG] Uploading File..") + if err := filesClient.PutFile(ctx, accountName, shareName, "", fileName, file, 4); err != nil { + t.Fatalf("Error uploading File: %s", err) + } + + t.Logf("[DEBUG] Deleting Top Level File..") + if _, err := filesClient.Delete(ctx, accountName, shareName, "", fileName); err != nil { + t.Fatalf("Error deleting Top-Level File: %s", err) + } +} diff --git a/storage/2018-03-28/file/files/ranges_list.go b/storage/2018-03-28/file/files/ranges_list.go new file mode 100644 index 0000000..ea309f9 --- /dev/null +++ b/storage/2018-03-28/file/files/ranges_list.go @@ -0,0 +1,114 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type ListRangesResult struct { + autorest.Response + + Ranges []Range `xml:"Range"` +} + +type Range struct { + Start string `xml:"Start"` + End string `xml:"End"` +} + +// ListRanges returns the list of valid ranges for the specified File. +func (client Client) ListRanges(ctx context.Context, accountName, shareName, path, fileName string) (result ListRangesResult, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "ListRanges", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "ListRanges", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "ListRanges", "`shareName` must be a lower-cased string.") + } + if path == "" { + return result, validation.NewError("files.Client", "ListRanges", "`path` cannot be an empty string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "ListRanges", "`fileName` cannot be an empty string.") + } + + req, err := client.ListRangesPreparer(ctx, accountName, shareName, path, fileName) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "ListRanges", nil, "Failure preparing request") + return + } + + resp, err := client.ListRangesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "ListRanges", resp, "Failure sending request") + return + } + + result, err = client.ListRangesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "ListRanges", resp, "Failure responding to request") + return + } + + return +} + +// ListRangesPreparer prepares the ListRanges request. +func (client Client) ListRangesPreparer(ctx context.Context, accountName, shareName, path, fileName string) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "rangelist"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ListRangesSender sends the ListRanges request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ListRangesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ListRangesResponder handles the response to the ListRanges request. The method always +// closes the http.Response Body. +func (client Client) ListRangesResponder(resp *http.Response) (result ListRangesResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/files/resource_id.go b/storage/2018-03-28/file/files/resource_id.go new file mode 100644 index 0000000..ed1208d --- /dev/null +++ b/storage/2018-03-28/file/files/resource_id.go @@ -0,0 +1,64 @@ +package files + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given File +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, shareName, directoryName, filePath string) string { + domain := endpoints.GetFileEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s/%s/%s", domain, shareName, directoryName, filePath) +} + +type ResourceID struct { + AccountName string + DirectoryName string + FileName string + ShareName string +} + +// ParseResourceID parses the specified Resource ID and returns an object +// which can be used to interact with Files within a Storage Share. +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://account1.file.core.chinacloudapi.cn/share1/directory1/file1.txt + // example: https://account1.file.core.chinacloudapi.cn/share1/directory1/directory2/file1.txt + + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + path := strings.TrimPrefix(uri.Path, "/") + segments := strings.Split(path, "/") + if len(segments) == 0 { + return nil, fmt.Errorf("Expected the path to contain segments but got none") + } + + shareName := segments[0] + fileName := segments[len(segments)-1] + + directoryName := strings.TrimPrefix(path, shareName) + directoryName = strings.TrimPrefix(directoryName, "/") + directoryName = strings.TrimSuffix(directoryName, fileName) + directoryName = strings.TrimSuffix(directoryName, "/") + return &ResourceID{ + AccountName: *accountName, + ShareName: shareName, + DirectoryName: directoryName, + FileName: fileName, + }, nil +} diff --git a/storage/2018-03-28/file/files/resource_id_test.go b/storage/2018-03-28/file/files/resource_id_test.go new file mode 100644 index 0000000..1b521ac --- /dev/null +++ b/storage/2018-03-28/file/files/resource_id_test.go @@ -0,0 +1,131 @@ +package files + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.file.core.chinacloudapi.cn/share1/directory1/file1.txt", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.file.core.cloudapi.de/share1/directory1/file1.txt", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.file.core.windows.net/share1/directory1/file1.txt", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.file.core.usgovcloudapi.net/share1/directory1/file1.txt", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "share1", "directory1", "file1.txt") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.file.core.chinacloudapi.cn/share1/directory1/file1.txt", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.file.core.cloudapi.de/share1/directory1/file1.txt", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.file.core.windows.net/share1/directory1/file1.txt", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.file.core.usgovcloudapi.net/share1/directory1/file1.txt", + }, + } + + t.Logf("[DEBUG] Top Level Files") + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.ShareName != "share1" { + t.Fatalf("Expected Share Name to be `share1` but got %q", actual.ShareName) + } + if actual.DirectoryName != "directory1" { + t.Fatalf("Expected Directory Name to be `directory1` but got %q", actual.DirectoryName) + } + if actual.FileName != "file1.txt" { + t.Fatalf("Expected File Name to be `file1.txt` but got %q", actual.FileName) + } + } + + testData = []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.file.core.chinacloudapi.cn/share1/directory1/directory2/file1.txt", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.file.core.cloudapi.de/share1/directory1/directory2/file1.txt", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.file.core.windows.net/share1/directory1/directory2/file1.txt", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.file.core.usgovcloudapi.net/share1/directory1/directory2/file1.txt", + }, + } + + t.Logf("[DEBUG] Nested Files") + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.ShareName != "share1" { + t.Fatalf("Expected Share Name to be `share1` but got %q", actual.ShareName) + } + if actual.DirectoryName != "directory1/directory2" { + t.Fatalf("Expected Directory Name to be `directory1/directory2` but got %q", actual.DirectoryName) + } + if actual.FileName != "file1.txt" { + t.Fatalf("Expected File Name to be `file1.txt` but got %q", actual.FileName) + } + } +} diff --git a/storage/2018-03-28/file/files/version.go b/storage/2018-03-28/file/files/version.go new file mode 100644 index 0000000..0f66afa --- /dev/null +++ b/storage/2018-03-28/file/files/version.go @@ -0,0 +1,14 @@ +package files + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2018-03-28" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2018-03-28/file/shares/README.md b/storage/2018-03-28/file/shares/README.md new file mode 100644 index 0000000..cba86e8 --- /dev/null +++ b/storage/2018-03-28/file/shares/README.md @@ -0,0 +1,43 @@ +## File Storage Shares SDK for API version 2018-03-28 + +This package allows you to interact with the Shares File Storage API + +### Supported Authorizers + +* Azure Active Directory (for the Resource Endpoint `https://storage.azure.com`) +* SharedKeyLite (Blob, File & Queue) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2018-03-28/file/shares" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + shareName := "myshare" + + storageAuth := autorest.NewSharedKeyLiteAuthorizer(accountName, storageAccountKey) + sharesClient := shares.New() + sharesClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + input := shares.CreateInput{ + QuotaInGB: 2, + } + if _, err := sharesClient.Create(ctx, accountName, shareName, input); err != nil { + return fmt.Errorf("Error creating Share: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2018-03-28/file/shares/acl_get.go b/storage/2018-03-28/file/shares/acl_get.go new file mode 100644 index 0000000..ea6ff4c --- /dev/null +++ b/storage/2018-03-28/file/shares/acl_get.go @@ -0,0 +1,98 @@ +package shares + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetACLResult struct { + autorest.Response + + SignedIdentifiers []SignedIdentifier `xml:"SignedIdentifier"` +} + +// GetACL get the Access Control List for the specified Storage Share +func (client Client) GetACL(ctx context.Context, accountName, shareName string) (result GetACLResult, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "GetACL", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "GetACL", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "GetACL", "`shareName` must be a lower-cased string.") + } + + req, err := client.GetACLPreparer(ctx, accountName, shareName) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetACL", nil, "Failure preparing request") + return + } + + resp, err := client.GetACLSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "GetACL", resp, "Failure sending request") + return + } + + result, err = client.GetACLResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetACL", resp, "Failure responding to request") + return + } + + return +} + +// GetACLPreparer prepares the GetACL request. +func (client Client) GetACLPreparer(ctx context.Context, accountName, shareName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + "comp": autorest.Encode("query", "acl"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetACLSender sends the GetACL request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetACLSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetACLResponder handles the response to the GetACL request. The method always +// closes the http.Response Body. +func (client Client) GetACLResponder(resp *http.Response) (result GetACLResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/shares/acl_set.go b/storage/2018-03-28/file/shares/acl_set.go new file mode 100644 index 0000000..18d1788 --- /dev/null +++ b/storage/2018-03-28/file/shares/acl_set.go @@ -0,0 +1,103 @@ +package shares + +import ( + "context" + "encoding/xml" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type setAcl struct { + SignedIdentifiers []SignedIdentifier `xml:"SignedIdentifier"` + + XMLName xml.Name `xml:"SignedIdentifiers"` +} + +// SetACL sets the specified Access Control List on the specified Storage Share +func (client Client) SetACL(ctx context.Context, accountName, shareName string, acls []SignedIdentifier) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "SetACL", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "SetACL", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "SetACL", "`shareName` must be a lower-cased string.") + } + + req, err := client.SetACLPreparer(ctx, accountName, shareName, acls) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "SetACL", nil, "Failure preparing request") + return + } + + resp, err := client.SetACLSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "SetACL", resp, "Failure sending request") + return + } + + result, err = client.SetACLResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "SetACL", resp, "Failure responding to request") + return + } + + return +} + +// SetACLPreparer prepares the SetACL request. +func (client Client) SetACLPreparer(ctx context.Context, accountName, shareName string, acls []SignedIdentifier) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + "comp": autorest.Encode("query", "acl"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + input := setAcl{ + SignedIdentifiers: acls, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers), + autorest.WithXML(&input)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetACLSender sends the SetACL request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetACLSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetACLResponder handles the response to the SetACL request. The method always +// closes the http.Response Body. +func (client Client) SetACLResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/shares/client.go b/storage/2018-03-28/file/shares/client.go new file mode 100644 index 0000000..4f3a6f9 --- /dev/null +++ b/storage/2018-03-28/file/shares/client.go @@ -0,0 +1,25 @@ +package shares + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for File Storage Shares. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithEnvironment creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2018-03-28/file/shares/create.go b/storage/2018-03-28/file/shares/create.go new file mode 100644 index 0000000..84fd40d --- /dev/null +++ b/storage/2018-03-28/file/shares/create.go @@ -0,0 +1,109 @@ +package shares + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type CreateInput struct { + // Specifies the maximum size of the share, in gigabytes. + // Must be greater than 0, and less than or equal to 5TB (5120). + QuotaInGB int + + MetaData map[string]string +} + +// Create creates the specified Storage Share within the specified Storage Account +func (client Client) Create(ctx context.Context, accountName, shareName string, input CreateInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "Create", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "Create", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "Create", "`shareName` must be a lower-cased string.") + } + if input.QuotaInGB <= 0 || input.QuotaInGB > 5120 { + return result, validation.NewError("shares.Client", "Create", "`input.QuotaInGB` must be greater than 0, and less than/equal to 5TB (5120 GB)") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("shares.Client", "Create", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.CreatePreparer(ctx, accountName, shareName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "Create", nil, "Failure preparing request") + return + } + + resp, err := client.CreateSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "Create", resp, "Failure sending request") + return + } + + result, err = client.CreateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "Create", resp, "Failure responding to request") + return + } + + return +} + +// CreatePreparer prepares the Create request. +func (client Client) CreatePreparer(ctx context.Context, accountName, shareName string, input CreateInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "share"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-share-quota": input.QuotaInGB, + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CreateSender sends the Create request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CreateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CreateResponder handles the response to the Create request. The method always +// closes the http.Response Body. +func (client Client) CreateResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/shares/delete.go b/storage/2018-03-28/file/shares/delete.go new file mode 100644 index 0000000..70ef985 --- /dev/null +++ b/storage/2018-03-28/file/shares/delete.go @@ -0,0 +1,94 @@ +package shares + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Delete deletes the specified Storage Share from within a Storage Account +func (client Client) Delete(ctx context.Context, accountName, shareName string, deleteSnapshots bool) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "Delete", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "Delete", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "Delete", "`shareName` must be a lower-cased string.") + } + + req, err := client.DeletePreparer(ctx, accountName, shareName, deleteSnapshots) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName, shareName string, deleteSnapshots bool) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "share"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if deleteSnapshots { + headers["x-ms-delete-snapshots"] = "include" + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/shares/lifecycle_test.go b/storage/2018-03-28/file/shares/lifecycle_test.go new file mode 100644 index 0000000..fbab96d --- /dev/null +++ b/storage/2018-03-28/file/shares/lifecycle_test.go @@ -0,0 +1,152 @@ +package shares + +import ( + "context" + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestSharesLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + shareName := fmt.Sprintf("share-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + sharesClient := NewWithEnvironment(client.Environment) + sharesClient.Client = client.PrepareWithAuthorizer(sharesClient.Client, storageAuth) + + input := CreateInput{ + QuotaInGB: 1, + } + _, err = sharesClient.Create(ctx, accountName, shareName, input) + if err != nil { + t.Fatalf("Error creating fileshare: %s", err) + } + + snapshot, err := sharesClient.CreateSnapshot(ctx, accountName, shareName, CreateSnapshotInput{}) + if err != nil { + t.Fatalf("Error taking snapshot: %s", err) + } + t.Logf("Snapshot Date Time: %s", snapshot.SnapshotDateTime) + + snapshotDetails, err := sharesClient.GetSnapshot(ctx, accountName, shareName, snapshot.SnapshotDateTime) + if err != nil { + t.Fatalf("Error retrieving snapshot: %s", err) + } + + t.Logf("MetaData: %s", snapshotDetails.MetaData) + + _, err = sharesClient.DeleteSnapshot(ctx, accountName, shareName, snapshot.SnapshotDateTime) + if err != nil { + t.Fatalf("Error deleting snapshot: %s", err) + } + + stats, err := sharesClient.GetStats(ctx, accountName, shareName) + if err != nil { + t.Fatalf("Error retrieving stats: %s", err) + } + + if stats.ShareUsageBytes != 0 { + t.Fatalf("Expected `stats.ShareUsageBytes` to be 0 but got: %d", stats.ShareUsageBytes) + } + + share, err := sharesClient.GetProperties(ctx, accountName, shareName) + if err != nil { + t.Fatalf("Error retrieving share: %s", err) + } + if share.ShareQuota != 1 { + t.Fatalf("Expected Quota to be 1 but got: %d", share.ShareQuota) + } + + _, err = sharesClient.SetProperties(ctx, accountName, shareName, 5) + if err != nil { + t.Fatalf("Error updating quota: %s", err) + } + + share, err = sharesClient.GetProperties(ctx, accountName, shareName) + if err != nil { + t.Fatalf("Error retrieving share: %s", err) + } + if share.ShareQuota != 5 { + t.Fatalf("Expected Quota to be 5 but got: %d", share.ShareQuota) + } + + updatedMetaData := map[string]string{ + "hello": "world", + } + _, err = sharesClient.SetMetaData(ctx, accountName, shareName, updatedMetaData) + if err != nil { + t.Fatalf("Erorr setting metadata: %s", err) + } + + result, err := sharesClient.GetMetaData(ctx, accountName, shareName) + if err != nil { + t.Fatalf("Error retrieving metadata: %s", err) + } + + if result.MetaData["hello"] != "world" { + t.Fatalf("Expected metadata `hello` to be `world` but got: %q", result.MetaData["hello"]) + } + if len(result.MetaData) != 1 { + t.Fatalf("Expected metadata to be 1 item but got: %s", result.MetaData) + } + + acls, err := sharesClient.GetACL(ctx, accountName, shareName) + if err != nil { + t.Fatalf("Error retrieving ACL's: %s", err) + } + if len(acls.SignedIdentifiers) != 0 { + t.Fatalf("Expected 0 identifiers but got %d", len(acls.SignedIdentifiers)) + } + + updatedAcls := []SignedIdentifier{ + { + Id: "abc123", + AccessPolicy: AccessPolicy{ + Start: "2020-07-01T08:49:37.0000000Z", + Expiry: "2020-07-01T09:49:37.0000000Z", + Permission: "rwd", + }, + }, + { + Id: "bcd234", + AccessPolicy: AccessPolicy{ + Start: "2020-07-01T08:49:37.0000000Z", + Expiry: "2020-07-01T09:49:37.0000000Z", + Permission: "rwd", + }, + }, + } + _, err = sharesClient.SetACL(ctx, accountName, shareName, updatedAcls) + if err != nil { + t.Fatalf("Error setting ACL's: %s", err) + } + + acls, err = sharesClient.GetACL(ctx, accountName, shareName) + if err != nil { + t.Fatalf("Error retrieving ACL's: %s", err) + } + if len(acls.SignedIdentifiers) != 2 { + t.Fatalf("Expected 2 identifiers but got %d", len(acls.SignedIdentifiers)) + } + + _, err = sharesClient.Delete(ctx, accountName, shareName, false) + if err != nil { + t.Fatalf("Error deleting Share: %s", err) + } +} diff --git a/storage/2018-03-28/file/shares/metadata_get.go b/storage/2018-03-28/file/shares/metadata_get.go new file mode 100644 index 0000000..9fa4d9f --- /dev/null +++ b/storage/2018-03-28/file/shares/metadata_get.go @@ -0,0 +1,102 @@ +package shares + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetMetaDataResult struct { + autorest.Response + + MetaData map[string]string +} + +// GetMetaData returns the MetaData associated with the specified Storage Share +func (client Client) GetMetaData(ctx context.Context, accountName, shareName string) (result GetMetaDataResult, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "GetMetaData", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "GetMetaData", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "GetMetaData", "`shareName` must be a lower-cased string.") + } + + req, err := client.GetMetaDataPreparer(ctx, accountName, shareName) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.GetMetaDataSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "GetMetaData", resp, "Failure sending request") + return + } + + result, err = client.GetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// GetMetaDataPreparer prepares the GetMetaData request. +func (client Client) GetMetaDataPreparer(ctx context.Context, accountName, shareName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + "comp": autorest.Encode("query", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetMetaDataSender sends the GetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetMetaDataResponder handles the response to the GetMetaData request. The method always +// closes the http.Response Body. +func (client Client) GetMetaDataResponder(resp *http.Response) (result GetMetaDataResult, err error) { + if resp.Header != nil { + result.MetaData = metadata.ParseFromHeaders(resp.Header) + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/shares/metadata_set.go b/storage/2018-03-28/file/shares/metadata_set.go new file mode 100644 index 0000000..7e64e60 --- /dev/null +++ b/storage/2018-03-28/file/shares/metadata_set.go @@ -0,0 +1,97 @@ +package shares + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// SetMetaData sets the MetaData on the specified Storage Share +func (client Client) SetMetaData(ctx context.Context, accountName, shareName string, metaData map[string]string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "SetMetaData", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "SetMetaData", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "SetMetaData", "`shareName` must be a lower-cased string.") + } + if err := metadata.Validate(metaData); err != nil { + return result, validation.NewError("shares.Client", "SetMetaData", fmt.Sprintf("`metadata` is not valid: %s.", err)) + } + + req, err := client.SetMetaDataPreparer(ctx, accountName, shareName, metaData) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "SetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.SetMetaDataSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "SetMetaData", resp, "Failure sending request") + return + } + + result, err = client.SetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "SetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// SetMetaDataPreparer prepares the SetMetaData request. +func (client Client) SetMetaDataPreparer(ctx context.Context, accountName, shareName string, metaData map[string]string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + "comp": autorest.Encode("query", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + headers = metadata.SetIntoHeaders(headers, metaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetPropertiesSetMetaDataSender sends the SetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetMetaDataResponder handles the response to the SetMetaData request. The method always +// closes the http.Response Body. +func (client Client) SetMetaDataResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/shares/models.go b/storage/2018-03-28/file/shares/models.go new file mode 100644 index 0000000..31ef7c2 --- /dev/null +++ b/storage/2018-03-28/file/shares/models.go @@ -0,0 +1,12 @@ +package shares + +type SignedIdentifier struct { + Id string `xml:"Id"` + AccessPolicy AccessPolicy `xml:"AccessPolicy"` +} + +type AccessPolicy struct { + Start string `xml:"Start"` + Expiry string `xml:"Expiry"` + Permission string `xml:"Permission"` +} diff --git a/storage/2018-03-28/file/shares/properties_get.go b/storage/2018-03-28/file/shares/properties_get.go new file mode 100644 index 0000000..80e26a4 --- /dev/null +++ b/storage/2018-03-28/file/shares/properties_get.go @@ -0,0 +1,111 @@ +package shares + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetPropertiesResult struct { + autorest.Response + + MetaData map[string]string + ShareQuota int +} + +// GetProperties returns the properties about the specified Storage Share +func (client Client) GetProperties(ctx context.Context, accountName, shareName string) (result GetPropertiesResult, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "GetProperties", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "GetProperties", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "GetProperties", "`shareName` must be a lower-cased string.") + } + + req, err := client.GetPropertiesPreparer(ctx, accountName, shareName) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetProperties", nil, "Failure preparing request") + return + } + + resp, err := client.GetPropertiesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "GetProperties", resp, "Failure sending request") + return + } + + result, err = client.GetPropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetProperties", resp, "Failure responding to request") + return + } + + return +} + +// GetPropertiesPreparer prepares the GetProperties request. +func (client Client) GetPropertiesPreparer(ctx context.Context, accountName, shareName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetPropertiesSender sends the GetProperties request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetPropertiesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetPropertiesResponder handles the response to the GetProperties request. The method always +// closes the http.Response Body. +func (client Client) GetPropertiesResponder(resp *http.Response) (result GetPropertiesResult, err error) { + if resp.Header != nil { + result.MetaData = metadata.ParseFromHeaders(resp.Header) + + quotaRaw := resp.Header.Get("x-ms-share-quota") + quota, e := strconv.Atoi(quotaRaw) + if e != nil { + return result, fmt.Errorf("Error converting %q to an integer: %s", quotaRaw, err) + } + result.ShareQuota = quota + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/shares/properties_set.go b/storage/2018-03-28/file/shares/properties_set.go new file mode 100644 index 0000000..4553e5e --- /dev/null +++ b/storage/2018-03-28/file/shares/properties_set.go @@ -0,0 +1,95 @@ +package shares + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// SetProperties lets you update the Quota for the specified Storage Share +func (client Client) SetProperties(ctx context.Context, accountName, shareName string, newQuotaGB int) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "SetProperties", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "SetProperties", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "SetProperties", "`shareName` must be a lower-cased string.") + } + if newQuotaGB <= 0 || newQuotaGB > 5120 { + return result, validation.NewError("shares.Client", "SetProperties", "`newQuotaGB` must be greater than 0, and less than/equal to 5TB (5120 GB)") + } + + req, err := client.SetPropertiesPreparer(ctx, accountName, shareName, newQuotaGB) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "SetProperties", nil, "Failure preparing request") + return + } + + resp, err := client.SetPropertiesSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "SetProperties", resp, "Failure sending request") + return + } + + result, err = client.SetPropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "SetProperties", resp, "Failure responding to request") + return + } + + return +} + +// SetPropertiesPreparer prepares the SetProperties request. +func (client Client) SetPropertiesPreparer(ctx context.Context, accountName, shareName string, quotaGB int) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + "comp": autorest.Encode("query", "properties"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-share-quota": quotaGB, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetPropertiesSender sends the SetProperties request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetPropertiesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetPropertiesResponder handles the response to the SetProperties request. The method always +// closes the http.Response Body. +func (client Client) SetPropertiesResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/shares/resource_id.go b/storage/2018-03-28/file/shares/resource_id.go new file mode 100644 index 0000000..bfdcbfd --- /dev/null +++ b/storage/2018-03-28/file/shares/resource_id.go @@ -0,0 +1,46 @@ +package shares + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given File Share +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, shareName string) string { + domain := endpoints.GetFileEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s", domain, shareName) +} + +type ResourceID struct { + AccountName string + ShareName string +} + +// ParseResourceID parses the specified Resource ID and returns an object +// which can be used to interact with the Storage Shares SDK +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://foo.file.core.windows.net/Bar + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + shareName := strings.TrimPrefix(uri.Path, "/") + return &ResourceID{ + AccountName: *accountName, + ShareName: shareName, + }, nil +} diff --git a/storage/2018-03-28/file/shares/resource_id_test.go b/storage/2018-03-28/file/shares/resource_id_test.go new file mode 100644 index 0000000..1b7eea3 --- /dev/null +++ b/storage/2018-03-28/file/shares/resource_id_test.go @@ -0,0 +1,79 @@ +package shares + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.file.core.chinacloudapi.cn/share1", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.file.core.cloudapi.de/share1", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.file.core.windows.net/share1", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.file.core.usgovcloudapi.net/share1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "share1") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.file.core.chinacloudapi.cn/share1", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.file.core.cloudapi.de/share1", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.file.core.windows.net/share1", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.file.core.usgovcloudapi.net/share1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected the account name to be `account1` but got %q", actual.AccountName) + } + + if actual.ShareName != "share1" { + t.Fatalf("Expected the share name to be `share1` but got %q", actual.ShareName) + } + } +} diff --git a/storage/2018-03-28/file/shares/snapshot_create.go b/storage/2018-03-28/file/shares/snapshot_create.go new file mode 100644 index 0000000..0ded38b --- /dev/null +++ b/storage/2018-03-28/file/shares/snapshot_create.go @@ -0,0 +1,115 @@ +package shares + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type CreateSnapshotInput struct { + MetaData map[string]string +} + +type CreateSnapshotResult struct { + autorest.Response + + // This header is a DateTime value that uniquely identifies the share snapshot. + // The value of this header may be used in subsequent requests to access the share snapshot. + // This value is opaque. + SnapshotDateTime string +} + +// CreateSnapshot creates a read-only snapshot of the share +// A share can support creation of 200 share snapshots. Attempting to create more than 200 share snapshots fails with 409 (Conflict). +// Attempting to create a share snapshot while a previous Snapshot Share operation is in progress fails with 409 (Conflict). +func (client Client) CreateSnapshot(ctx context.Context, accountName, shareName string, input CreateSnapshotInput) (result CreateSnapshotResult, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "CreateSnapshot", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "CreateSnapshot", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "CreateSnapshot", "`shareName` must be a lower-cased string.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("shares.Client", "CreateSnapshot", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.CreateSnapshotPreparer(ctx, accountName, shareName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "CreateSnapshot", nil, "Failure preparing request") + return + } + + resp, err := client.CreateSnapshotSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "CreateSnapshot", resp, "Failure sending request") + return + } + + result, err = client.CreateSnapshotResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "CreateSnapshot", resp, "Failure responding to request") + return + } + + return +} + +// CreateSnapshotPreparer prepares the CreateSnapshot request. +func (client Client) CreateSnapshotPreparer(ctx context.Context, accountName, shareName string, input CreateSnapshotInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "snapshot"), + "restype": autorest.Encode("query", "share"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CreateSnapshotSender sends the CreateSnapshot request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CreateSnapshotSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CreateSnapshotResponder handles the response to the CreateSnapshot request. The method always +// closes the http.Response Body. +func (client Client) CreateSnapshotResponder(resp *http.Response) (result CreateSnapshotResult, err error) { + result.SnapshotDateTime = resp.Header.Get("x-ms-snapshot") + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/shares/snapshot_delete.go b/storage/2018-03-28/file/shares/snapshot_delete.go new file mode 100644 index 0000000..1f5d665 --- /dev/null +++ b/storage/2018-03-28/file/shares/snapshot_delete.go @@ -0,0 +1,94 @@ +package shares + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// DeleteSnapshot deletes the specified Snapshot of a Storage Share +func (client Client) DeleteSnapshot(ctx context.Context, accountName, shareName string, shareSnapshot string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "DeleteSnapshot", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "DeleteSnapshot", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "DeleteSnapshot", "`shareName` must be a lower-cased string.") + } + if shareSnapshot == "" { + return result, validation.NewError("shares.Client", "DeleteSnapshot", "`shareSnapshot` cannot be an empty string.") + } + + req, err := client.DeleteSnapshotPreparer(ctx, accountName, shareName, shareSnapshot) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "DeleteSnapshot", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSnapshotSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "DeleteSnapshot", resp, "Failure sending request") + return + } + + result, err = client.DeleteSnapshotResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "DeleteSnapshot", resp, "Failure responding to request") + return + } + + return +} + +// DeleteSnapshotPreparer prepares the DeleteSnapshot request. +func (client Client) DeleteSnapshotPreparer(ctx context.Context, accountName, shareName string, shareSnapshot string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "share"), + "sharesnapshot": autorest.Encode("query", shareSnapshot), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSnapshotSender sends the DeleteSnapshot request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSnapshotSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteSnapshotResponder handles the response to the DeleteSnapshot request. The method always +// closes the http.Response Body. +func (client Client) DeleteSnapshotResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/shares/snapshot_get.go b/storage/2018-03-28/file/shares/snapshot_get.go new file mode 100644 index 0000000..2cf5f16 --- /dev/null +++ b/storage/2018-03-28/file/shares/snapshot_get.go @@ -0,0 +1,105 @@ +package shares + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetSnapshotPropertiesResult struct { + autorest.Response + + MetaData map[string]string +} + +// GetSnapshot gets information about the specified Snapshot of the specified Storage Share +func (client Client) GetSnapshot(ctx context.Context, accountName, shareName, snapshotShare string) (result GetSnapshotPropertiesResult, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "GetSnapshot", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "GetSnapshot", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "GetSnapshot", "`shareName` must be a lower-cased string.") + } + if snapshotShare == "" { + return result, validation.NewError("shares.Client", "GetSnapshot", "`snapshotShare` cannot be an empty string.") + } + + req, err := client.GetSnapshotPreparer(ctx, accountName, shareName, snapshotShare) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetSnapshot", nil, "Failure preparing request") + return + } + + resp, err := client.GetSnapshotSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "GetSnapshot", resp, "Failure sending request") + return + } + + result, err = client.GetSnapshotResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetSnapshot", resp, "Failure responding to request") + return + } + + return +} + +// GetSnapshotPreparer prepares the GetSnapshot request. +func (client Client) GetSnapshotPreparer(ctx context.Context, accountName, shareName, snapshotShare string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + "snapshot": autorest.Encode("query", snapshotShare), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetSnapshotSender sends the GetSnapshot request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetSnapshotSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetSnapshotResponder handles the response to the GetSnapshot request. The method always +// closes the http.Response Body. +func (client Client) GetSnapshotResponder(resp *http.Response) (result GetSnapshotPropertiesResult, err error) { + if resp.Header != nil { + result.MetaData = metadata.ParseFromHeaders(resp.Header) + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/shares/stats.go b/storage/2018-03-28/file/shares/stats.go new file mode 100644 index 0000000..3539ecc --- /dev/null +++ b/storage/2018-03-28/file/shares/stats.go @@ -0,0 +1,100 @@ +package shares + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetStatsResult struct { + autorest.Response + + // The approximate size of the data stored on the share. + // Note that this value may not include all recently created or recently resized files. + ShareUsageBytes int64 `xml:"ShareUsageBytes"` +} + +// GetStats returns information about the specified Storage Share +func (client Client) GetStats(ctx context.Context, accountName, shareName string) (result GetStatsResult, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "GetStats", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "GetStats", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "GetStats", "`shareName` must be a lower-cased string.") + } + + req, err := client.GetStatsPreparer(ctx, accountName, shareName) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetStats", nil, "Failure preparing request") + return + } + + resp, err := client.GetStatsSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "GetStats", resp, "Failure sending request") + return + } + + result, err = client.GetStatsResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetStats", resp, "Failure responding to request") + return + } + + return +} + +// GetStatsPreparer prepares the GetStats request. +func (client Client) GetStatsPreparer(ctx context.Context, accountName, shareName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + "comp": autorest.Encode("query", "stats"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetStatsSender sends the GetStats request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetStatsSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetStatsResponder handles the response to the GetStats request. The method always +// closes the http.Response Body. +func (client Client) GetStatsResponder(resp *http.Response) (result GetStatsResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/file/shares/version.go b/storage/2018-03-28/file/shares/version.go new file mode 100644 index 0000000..88cc6c4 --- /dev/null +++ b/storage/2018-03-28/file/shares/version.go @@ -0,0 +1,14 @@ +package shares + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2018-03-28" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2018-03-28/queue/messages/README.md b/storage/2018-03-28/queue/messages/README.md new file mode 100644 index 0000000..635cbb8 --- /dev/null +++ b/storage/2018-03-28/queue/messages/README.md @@ -0,0 +1,43 @@ +## Queue Storage Messages SDK for API version 2018-03-28 + +This package allows you to interact with the Messages Queue Storage API + +### Supported Authorizers + +* Azure Active Directory (for the Resource Endpoint `https://storage.azure.com`) +* SharedKeyLite (Blob, File & Queue) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2018-03-28/queue/messages" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + queueName := "myqueue" + + storageAuth := autorest.NewSharedKeyLiteAuthorizer(accountName, storageAccountKey) + messagesClient := messages.New() + messagesClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + input := messages.PutInput{ + Message: "hello", + } + if _, err := messagesClient.Put(ctx, accountName, queueName, input); err != nil { + return fmt.Errorf("Error creating Message: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2018-03-28/queue/messages/client.go b/storage/2018-03-28/queue/messages/client.go new file mode 100644 index 0000000..08b1801 --- /dev/null +++ b/storage/2018-03-28/queue/messages/client.go @@ -0,0 +1,25 @@ +package messages + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for Messages. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithEnvironment creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2018-03-28/queue/messages/delete.go b/storage/2018-03-28/queue/messages/delete.go new file mode 100644 index 0000000..1ec0e1a --- /dev/null +++ b/storage/2018-03-28/queue/messages/delete.go @@ -0,0 +1,97 @@ +package messages + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Delete deletes a specific message +func (client Client) Delete(ctx context.Context, accountName, queueName, messageID, popReceipt string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("messages.Client", "Delete", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("messages.Client", "Delete", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("messages.Client", "Delete", "`queueName` must be a lower-cased string.") + } + if messageID == "" { + return result, validation.NewError("messages.Client", "Delete", "`messageID` cannot be an empty string.") + } + if popReceipt == "" { + return result, validation.NewError("messages.Client", "Delete", "`popReceipt` cannot be an empty string.") + } + + req, err := client.DeletePreparer(ctx, accountName, queueName, messageID, popReceipt) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "messages.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName, queueName, messageID, popReceipt string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + "messageID": autorest.Encode("path", messageID), + } + + queryParameters := map[string]interface{}{ + "popreceipt": autorest.Encode("query", popReceipt), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}/messages/{messageID}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/queue/messages/get.go b/storage/2018-03-28/queue/messages/get.go new file mode 100644 index 0000000..4edeb6d --- /dev/null +++ b/storage/2018-03-28/queue/messages/get.go @@ -0,0 +1,112 @@ +package messages + +import ( + "context" + "net/http" + "strings" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetInput struct { + // VisibilityTimeout specifies the new visibility timeout value, in seconds, relative to server time. + // The new value must be larger than or equal to 0, and cannot be larger than 7 days. + VisibilityTimeout *int +} + +// Get retrieves one or more messages from the front of the queue +func (client Client) Get(ctx context.Context, accountName, queueName string, numberOfMessages int, input GetInput) (result QueueMessagesListResult, err error) { + if accountName == "" { + return result, validation.NewError("messages.Client", "Get", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("messages.Client", "Get", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("messages.Client", "Get", "`queueName` must be a lower-cased string.") + } + if numberOfMessages < 1 || numberOfMessages > 32 { + return result, validation.NewError("messages.Client", "Get", "`numberOfMessages` must be between 1 and 32.") + } + if input.VisibilityTimeout != nil { + t := *input.VisibilityTimeout + maxTime := (time.Hour * 24 * 7).Seconds() + if t < 1 || t < int(maxTime) { + return result, validation.NewError("messages.Client", "Get", "`input.VisibilityTimeout` must be larger than or equal to 1 second, and cannot be larger than 7 days.") + } + } + + req, err := client.GetPreparer(ctx, accountName, queueName, numberOfMessages, input) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Get", nil, "Failure preparing request") + return + } + + resp, err := client.GetSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "messages.Client", "Get", resp, "Failure sending request") + return + } + + result, err = client.GetResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Get", resp, "Failure responding to request") + return + } + + return +} + +// GetPreparer prepares the Get request. +func (client Client) GetPreparer(ctx context.Context, accountName, queueName string, numberOfMessages int, input GetInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + } + + queryParameters := map[string]interface{}{ + "numofmessages": autorest.Encode("query", numberOfMessages), + } + + if input.VisibilityTimeout != nil { + queryParameters["visibilitytimeout"] = autorest.Encode("query", *input.VisibilityTimeout) + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}/messages", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetSender sends the Get request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetResponder handles the response to the Get request. The method always +// closes the http.Response Body. +func (client Client) GetResponder(resp *http.Response) (result QueueMessagesListResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + autorest.ByUnmarshallingXML(&result), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/queue/messages/lifecycle_test.go b/storage/2018-03-28/queue/messages/lifecycle_test.go new file mode 100644 index 0000000..6d13ae0 --- /dev/null +++ b/storage/2018-03-28/queue/messages/lifecycle_test.go @@ -0,0 +1,95 @@ +package messages + +import ( + "context" + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-03-28/queue/queues" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestLifeCycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + queueName := fmt.Sprintf("queue-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + queuesClient := queues.NewWithEnvironment(client.Environment) + queuesClient.Client = client.PrepareWithStorageResourceManagerAuth(queuesClient.Client) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + messagesClient := NewWithEnvironment(client.Environment) + messagesClient.Client = client.PrepareWithAuthorizer(messagesClient.Client, storageAuth) + + _, err = queuesClient.Create(ctx, accountName, queueName, map[string]string{}) + if err != nil { + t.Fatalf("Error creating queue: %s", err) + } + defer queuesClient.Delete(ctx, accountName, queueName) + + input := PutInput{ + Message: "ohhai", + } + putResp, err := messagesClient.Put(ctx, accountName, queueName, input) + if err != nil { + t.Fatalf("Error putting message in queue: %s", err) + } + + messageId := (*putResp.QueueMessages)[0].MessageId + popReceipt := (*putResp.QueueMessages)[0].PopReceipt + + _, err = messagesClient.Update(ctx, accountName, queueName, messageId, UpdateInput{ + PopReceipt: popReceipt, + Message: "Updated message", + VisibilityTimeout: 65, + }) + if err != nil { + t.Fatalf("Error updating: %s", err) + } + + for i := 0; i < 5; i++ { + input := PutInput{ + Message: fmt.Sprintf("Message %d", i), + } + _, err := messagesClient.Put(ctx, accountName, queueName, input) + if err != nil { + t.Fatalf("Error putting message %d in queue: %s", i, err) + } + } + + peakedMessages, err := messagesClient.Peek(ctx, accountName, queueName, 3) + if err != nil { + t.Fatalf("Error peaking messages: %s", err) + } + + for _, v := range *peakedMessages.QueueMessages { + t.Logf("Message: %q", v.MessageId) + } + + retrievedMessages, err := messagesClient.Get(ctx, accountName, queueName, 6, GetInput{}) + if err != nil { + t.Fatalf("Error retrieving messages: %s", err) + } + + for _, v := range *retrievedMessages.QueueMessages { + t.Logf("Message: %q", v.MessageId) + + _, err = messagesClient.Delete(ctx, accountName, queueName, v.MessageId, v.PopReceipt) + if err != nil { + t.Fatalf("Error deleting message from queue: %s", err) + } + } +} diff --git a/storage/2018-03-28/queue/messages/models.go b/storage/2018-03-28/queue/messages/models.go new file mode 100644 index 0000000..67815a8 --- /dev/null +++ b/storage/2018-03-28/queue/messages/models.go @@ -0,0 +1,21 @@ +package messages + +import "github.com/Azure/go-autorest/autorest" + +type QueueMessage struct { + MessageText string `xml:"MessageText"` +} + +type QueueMessagesListResult struct { + autorest.Response + + QueueMessages *[]QueueMessageResponse `xml:"QueueMessage"` +} + +type QueueMessageResponse struct { + MessageId string `xml:"MessageId"` + InsertionTime string `xml:"InsertionTime"` + ExpirationTime string `xml:"ExpirationTime"` + PopReceipt string `xml:"PopReceipt"` + TimeNextVisible string `xml:"TimeNextVisible"` +} diff --git a/storage/2018-03-28/queue/messages/peek.go b/storage/2018-03-28/queue/messages/peek.go new file mode 100644 index 0000000..7288bd5 --- /dev/null +++ b/storage/2018-03-28/queue/messages/peek.go @@ -0,0 +1,95 @@ +package messages + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Peek retrieves one or more messages from the front of the queue, but doesn't alter the visibility of the messages +func (client Client) Peek(ctx context.Context, accountName, queueName string, numberOfMessages int) (result QueueMessagesListResult, err error) { + if accountName == "" { + return result, validation.NewError("messages.Client", "Peek", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("messages.Client", "Peek", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("messages.Client", "Peek", "`queueName` must be a lower-cased string.") + } + if numberOfMessages < 1 || numberOfMessages > 32 { + return result, validation.NewError("messages.Client", "Peek", "`numberOfMessages` must be between 1 and 32.") + } + + req, err := client.PeekPreparer(ctx, accountName, queueName, numberOfMessages) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Peek", nil, "Failure preparing request") + return + } + + resp, err := client.PeekSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "messages.Client", "Peek", resp, "Failure sending request") + return + } + + result, err = client.PeekResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Peek", resp, "Failure responding to request") + return + } + + return +} + +// PeekPreparer prepares the Peek request. +func (client Client) PeekPreparer(ctx context.Context, accountName, queueName string, numberOfMessages int) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + } + + queryParameters := map[string]interface{}{ + "numofmessages": autorest.Encode("query", numberOfMessages), + "peekonly": autorest.Encode("query", true), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}/messages", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PeekSender sends the Peek request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PeekSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PeekResponder handles the response to the Peek request. The method always +// closes the http.Response Body. +func (client Client) PeekResponder(resp *http.Response) (result QueueMessagesListResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + autorest.ByUnmarshallingXML(&result), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/queue/messages/put.go b/storage/2018-03-28/queue/messages/put.go new file mode 100644 index 0000000..612b4a1 --- /dev/null +++ b/storage/2018-03-28/queue/messages/put.go @@ -0,0 +1,120 @@ +package messages + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type PutInput struct { + // A message must be in a format that can be included in an XML request with UTF-8 encoding. + // The encoded message can be up to 64 KB in size. + Message string + + // The maximum time-to-live can be any positive number, + // as well as -1 indicating that the message does not expire. + // If this parameter is omitted, the default time-to-live is 7 days. + MessageTtl *int + + // Specifies the new visibility timeout value, in seconds, relative to server time. + // The new value must be larger than or equal to 0, and cannot be larger than 7 days. + // The visibility timeout of a message cannot be set to a value later than the expiry time. + // visibilitytimeout should be set to a value smaller than the time-to-live value. + // If not specified, the default value is 0. + VisibilityTimeout *int +} + +// Put adds a new message to the back of the message queue +func (client Client) Put(ctx context.Context, accountName, queueName string, input PutInput) (result QueueMessagesListResult, err error) { + if accountName == "" { + return result, validation.NewError("messages.Client", "Put", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("messages.Client", "Put", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("messages.Client", "Put", "`queueName` must be a lower-cased string.") + } + + req, err := client.PutPreparer(ctx, accountName, queueName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Put", nil, "Failure preparing request") + return + } + + resp, err := client.PutSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "messages.Client", "Put", resp, "Failure sending request") + return + } + + result, err = client.PutResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Put", resp, "Failure responding to request") + return + } + + return +} + +// PutPreparer prepares the Put request. +func (client Client) PutPreparer(ctx context.Context, accountName, queueName string, input PutInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + } + + queryParameters := map[string]interface{}{} + + if input.MessageTtl != nil { + queryParameters["messagettl"] = autorest.Encode("path", *input.MessageTtl) + } + + if input.VisibilityTimeout != nil { + queryParameters["visibilitytimeout"] = autorest.Encode("path", *input.VisibilityTimeout) + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + body := QueueMessage{ + MessageText: input.Message, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPost(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}/messages", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithXML(body), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutSender sends the Put request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutResponder handles the response to the Put request. The method always +// closes the http.Response Body. +func (client Client) PutResponder(resp *http.Response) (result QueueMessagesListResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + autorest.ByUnmarshallingXML(&result), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/queue/messages/resource_id.go b/storage/2018-03-28/queue/messages/resource_id.go new file mode 100644 index 0000000..7ece98a --- /dev/null +++ b/storage/2018-03-28/queue/messages/resource_id.go @@ -0,0 +1,56 @@ +package messages + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given Message within a Queue +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, queueName, messageID string) string { + domain := endpoints.GetQueueEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s/messages/%s", domain, queueName, messageID) +} + +type ResourceID struct { + AccountName string + QueueName string + MessageID string +} + +// ParseResourceID parses the specified Resource ID and returns an object +// which can be used to interact with the Message within a Queue +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://account1.queue.core.chinacloudapi.cn/queue1/messages/message1 + + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + path := strings.TrimPrefix(uri.Path, "/") + segments := strings.Split(path, "/") + if len(segments) != 3 { + return nil, fmt.Errorf("Expected the path to contain 3 segments but got %d", len(segments)) + } + + queueName := segments[0] + messageID := segments[2] + return &ResourceID{ + AccountName: *accountName, + MessageID: messageID, + QueueName: queueName, + }, nil +} diff --git a/storage/2018-03-28/queue/messages/resource_id_test.go b/storage/2018-03-28/queue/messages/resource_id_test.go new file mode 100644 index 0000000..5053279 --- /dev/null +++ b/storage/2018-03-28/queue/messages/resource_id_test.go @@ -0,0 +1,81 @@ +package messages + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.queue.core.chinacloudapi.cn/queue1/messages/message1", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.queue.core.cloudapi.de/queue1/messages/message1", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.queue.core.windows.net/queue1/messages/message1", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.queue.core.usgovcloudapi.net/queue1/messages/message1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "queue1", "message1") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.queue.core.chinacloudapi.cn/queue1/messages/message1", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.queue.core.cloudapi.de/queue1/messages/message1", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.queue.core.windows.net/queue1/messages/message1", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.queue.core.usgovcloudapi.net/queue1/messages/message1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.QueueName != "queue1" { + t.Fatalf("Expected Queue Name to be `queue1` but got %q", actual.QueueName) + } + if actual.MessageID != "message1" { + t.Fatalf("Expected Message ID to be `message1` but got %q", actual.MessageID) + } + } +} diff --git a/storage/2018-03-28/queue/messages/update.go b/storage/2018-03-28/queue/messages/update.go new file mode 100644 index 0000000..fb10fad --- /dev/null +++ b/storage/2018-03-28/queue/messages/update.go @@ -0,0 +1,115 @@ +package messages + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type UpdateInput struct { + // A message must be in a format that can be included in an XML request with UTF-8 encoding. + // The encoded message can be up to 64 KB in size. + Message string + + // Specifies the valid pop receipt value required to modify this message. + PopReceipt string + + // Specifies the new visibility timeout value, in seconds, relative to server time. + // The new value must be larger than or equal to 0, and cannot be larger than 7 days. + // The visibility timeout of a message cannot be set to a value later than the expiry time. + // A message can be updated until it has been deleted or has expired. + VisibilityTimeout int +} + +// Update updates an existing message based on it's Pop Receipt +func (client Client) Update(ctx context.Context, accountName, queueName string, messageID string, input UpdateInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("messages.Client", "Update", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("messages.Client", "Update", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("messages.Client", "Update", "`queueName` must be a lower-cased string.") + } + if input.PopReceipt == "" { + return result, validation.NewError("messages.Client", "Update", "`input.PopReceipt` cannot be an empty string.") + } + + req, err := client.UpdatePreparer(ctx, accountName, queueName, messageID, input) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Update", nil, "Failure preparing request") + return + } + + resp, err := client.UpdateSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "messages.Client", "Update", resp, "Failure sending request") + return + } + + result, err = client.UpdateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Update", resp, "Failure responding to request") + return + } + + return +} + +// UpdatePreparer prepares the Update request. +func (client Client) UpdatePreparer(ctx context.Context, accountName, queueName string, messageID string, input UpdateInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + "messageID": autorest.Encode("path", messageID), + } + + queryParameters := map[string]interface{}{ + "popreceipt": autorest.Encode("query", input.PopReceipt), + "visibilitytimeout": autorest.Encode("query", input.VisibilityTimeout), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + body := QueueMessage{ + MessageText: input.Message, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}/messages/{messageID}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithXML(body), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// UpdateSender sends the Update request. The method will close the +// http.Response Body if it receives an error. +func (client Client) UpdateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// UpdateResponder handles the response to the Update request. The method always +// closes the http.Response Body. +func (client Client) UpdateResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/queue/messages/version.go b/storage/2018-03-28/queue/messages/version.go new file mode 100644 index 0000000..4717045 --- /dev/null +++ b/storage/2018-03-28/queue/messages/version.go @@ -0,0 +1,14 @@ +package messages + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2018-03-28" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2018-03-28/queue/queues/README.md b/storage/2018-03-28/queue/queues/README.md new file mode 100644 index 0000000..2d62998 --- /dev/null +++ b/storage/2018-03-28/queue/queues/README.md @@ -0,0 +1,43 @@ +## Queue Storage Queues SDK for API version 2018-03-28 + +This package allows you to interact with the Queues Queue Storage API + +### Supported Authorizers + +* Azure Active Directory (for the Resource Endpoint `https://storage.azure.com`) +* SharedKeyLite (Blob, File & Queue) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2018-03-28/queue/queues" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + queueName := "myqueue" + + storageAuth := autorest.NewSharedKeyLiteAuthorizer(accountName, storageAccountKey) + queuesClient := queues.New() + queuesClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + metadata := map[string]string{ + "hello": "world", + } + if _, err := queuesClient.Create(ctx, accountName, queueName, metadata); err != nil { + return fmt.Errorf("Error creating Queue: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2018-03-28/queue/queues/client.go b/storage/2018-03-28/queue/queues/client.go new file mode 100644 index 0000000..2f80085 --- /dev/null +++ b/storage/2018-03-28/queue/queues/client.go @@ -0,0 +1,25 @@ +package queues + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for Queue Storage Shares. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithEnvironment creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2018-03-28/queue/queues/create.go b/storage/2018-03-28/queue/queues/create.go new file mode 100644 index 0000000..f18910a --- /dev/null +++ b/storage/2018-03-28/queue/queues/create.go @@ -0,0 +1,92 @@ +package queues + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// Create creates the specified Queue within the specified Storage Account +func (client Client) Create(ctx context.Context, accountName, queueName string, metaData map[string]string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("queues.Client", "Create", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("queues.Client", "Create", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("queues.Client", "Create", "`queueName` must be a lower-cased string.") + } + if err := metadata.Validate(metaData); err != nil { + return result, validation.NewError("queues.Client", "Create", fmt.Sprintf("`metadata` is not valid: %s.", err)) + } + + req, err := client.CreatePreparer(ctx, accountName, queueName, metaData) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "Create", nil, "Failure preparing request") + return + } + + resp, err := client.CreateSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "queues.Client", "Create", resp, "Failure sending request") + return + } + + result, err = client.CreateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "Create", resp, "Failure responding to request") + return + } + + return +} + +// CreatePreparer prepares the Create request. +func (client Client) CreatePreparer(ctx context.Context, accountName string, queueName string, metaData map[string]string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = metadata.SetIntoHeaders(headers, metaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CreateSender sends the Create request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CreateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CreateResponder handles the response to the Create request. The method always +// closes the http.Response Body. +func (client Client) CreateResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/queue/queues/delete.go b/storage/2018-03-28/queue/queues/delete.go new file mode 100644 index 0000000..5f70595 --- /dev/null +++ b/storage/2018-03-28/queue/queues/delete.go @@ -0,0 +1,85 @@ +package queues + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Delete deletes the specified Queue within the specified Storage Account +func (client Client) Delete(ctx context.Context, accountName, queueName string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("queues.Client", "Delete", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("queues.Client", "Delete", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("queues.Client", "Delete", "`queueName` must be a lower-cased string.") + } + + req, err := client.DeletePreparer(ctx, accountName, queueName) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "queues.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName string, queueName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/queue/queues/lifecycle_test.go b/storage/2018-03-28/queue/queues/lifecycle_test.go new file mode 100644 index 0000000..ff720f6 --- /dev/null +++ b/storage/2018-03-28/queue/queues/lifecycle_test.go @@ -0,0 +1,155 @@ +package queues + +import ( + "context" + "fmt" + "log" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestQueuesLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + queueName := fmt.Sprintf("queue-%d", testhelpers.RandomInt()) + + _, err = client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + queuesClient := NewWithEnvironment(client.Environment) + queuesClient.Client = client.PrepareWithStorageResourceManagerAuth(queuesClient.Client) + + // first let's test an empty container + _, err = queuesClient.Create(ctx, accountName, queueName, map[string]string{}) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + + // then let's retrieve it to ensure there's no metadata.. + resp, err := queuesClient.GetMetaData(ctx, accountName, queueName) + if err != nil { + t.Fatalf("Error retrieving MetaData: %s", err) + } + if len(resp.MetaData) != 0 { + t.Fatalf("Expected no MetaData but got: %s", err) + } + + // then let's add some.. + updatedMetaData := map[string]string{ + "band": "panic", + "boots": "the-overpass", + } + _, err = queuesClient.SetMetaData(ctx, accountName, queueName, updatedMetaData) + if err != nil { + t.Fatalf("Error setting MetaData: %s", err) + } + + resp, err = queuesClient.GetMetaData(ctx, accountName, queueName) + if err != nil { + t.Fatalf("Error re-retrieving MetaData: %s", err) + } + + if len(resp.MetaData) != 2 { + t.Fatalf("Expected metadata to have 2 items but got: %s", resp.MetaData) + } + if resp.MetaData["band"] != "panic" { + t.Fatalf("Expected `band` to be `panic` but got: %s", resp.MetaData["band"]) + } + if resp.MetaData["boots"] != "the-overpass" { + t.Fatalf("Expected `boots` to be `the-overpass` but got: %s", resp.MetaData["boots"]) + } + + // and woo let's remove it again + _, err = queuesClient.SetMetaData(ctx, accountName, queueName, map[string]string{}) + if err != nil { + t.Fatalf("Error setting MetaData: %s", err) + } + + resp, err = queuesClient.GetMetaData(ctx, accountName, queueName) + if err != nil { + t.Fatalf("Error retrieving MetaData: %s", err) + } + if len(resp.MetaData) != 0 { + t.Fatalf("Expected no MetaData but got: %s", err) + } + + // set some properties + props := StorageServiceProperties{ + Logging: &LoggingConfig{ + Version: "1.0", + Delete: true, + Read: true, + Write: true, + RetentionPolicy: RetentionPolicy{ + Enabled: true, + Days: 7, + }, + }, + Cors: &Cors{ + CorsRule: CorsRule{ + AllowedMethods: "GET,PUT", + AllowedOrigins: "http://www.example.com", + ExposedHeaders: "x-tempo-*", + AllowedHeaders: "x-tempo-*", + MaxAgeInSeconds: 500, + }, + }, + HourMetrics: &MetricsConfig{ + Version: "1.0", + Enabled: false, + RetentionPolicy: RetentionPolicy{ + Enabled: true, + Days: 7, + }, + }, + MinuteMetrics: &MetricsConfig{ + Version: "1.0", + Enabled: false, + RetentionPolicy: RetentionPolicy{ + Enabled: true, + Days: 7, + }, + }, + } + _, err = queuesClient.SetServiceProperties(ctx, accountName, props) + if err != nil { + t.Fatalf("SetServiceProperties failed: %s", err) + } + + properties, err := queuesClient.GetServiceProperties(ctx, accountName) + if err != nil { + t.Fatalf("GetServiceProperties failed: %s", err) + } + + if properties.Cors.CorsRule.AllowedMethods != "GET,PUT" { + t.Fatalf("CORS Methods weren't set!") + } + + if properties.HourMetrics.Enabled { + t.Fatalf("HourMetrics were enabled when they shouldn't be!") + } + + if properties.MinuteMetrics.Enabled { + t.Fatalf("MinuteMetrics were enabled when they shouldn't be!") + } + + if !properties.Logging.Write { + t.Fatalf("Logging Write's was not enabled when they should be!") + } + + log.Printf("[DEBUG] Deleting..") + _, err = queuesClient.Delete(ctx, accountName, queueName) + if err != nil { + t.Fatal(fmt.Errorf("Error deleting: %s", err)) + } +} diff --git a/storage/2018-03-28/queue/queues/metadata_get.go b/storage/2018-03-28/queue/queues/metadata_get.go new file mode 100644 index 0000000..9c230b6 --- /dev/null +++ b/storage/2018-03-28/queue/queues/metadata_get.go @@ -0,0 +1,101 @@ +package queues + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetMetaDataResult struct { + autorest.Response + + MetaData map[string]string +} + +// GetMetaData returns the metadata for this Queue +func (client Client) GetMetaData(ctx context.Context, accountName, queueName string) (result GetMetaDataResult, err error) { + if accountName == "" { + return result, validation.NewError("queues.Client", "GetMetaData", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("queues.Client", "GetMetaData", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("queues.Client", "GetMetaData", "`queueName` must be a lower-cased string.") + } + + req, err := client.GetMetaDataPreparer(ctx, accountName, queueName) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "GetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.GetMetaDataSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "queues.Client", "GetMetaData", resp, "Failure sending request") + return + } + + result, err = client.GetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "GetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// GetMetaDataPreparer prepares the GetMetaData request. +func (client Client) GetMetaDataPreparer(ctx context.Context, accountName, queueName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("path", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetMetaDataSender sends the GetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetMetaDataResponder handles the response to the GetMetaData request. The method always +// closes the http.Response Body. +func (client Client) GetMetaDataResponder(resp *http.Response) (result GetMetaDataResult, err error) { + if resp != nil { + result.MetaData = metadata.ParseFromHeaders(resp.Header) + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/queue/queues/metadata_set.go b/storage/2018-03-28/queue/queues/metadata_set.go new file mode 100644 index 0000000..51154a5 --- /dev/null +++ b/storage/2018-03-28/queue/queues/metadata_set.go @@ -0,0 +1,97 @@ +package queues + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// SetMetaData returns the metadata for this Queue +func (client Client) SetMetaData(ctx context.Context, accountName, queueName string, metaData map[string]string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("queues.Client", "SetMetaData", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("queues.Client", "SetMetaData", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("queues.Client", "SetMetaData", "`queueName` must be a lower-cased string.") + } + if err := metadata.Validate(metaData); err != nil { + return result, validation.NewError("queues.Client", "SetMetaData", fmt.Sprintf("`metadata` is not valid: %s.", err)) + } + + req, err := client.SetMetaDataPreparer(ctx, accountName, queueName, metaData) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "SetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.SetMetaDataSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "queues.Client", "SetMetaData", resp, "Failure sending request") + return + } + + result, err = client.SetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "SetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// SetMetaDataPreparer prepares the SetMetaData request. +func (client Client) SetMetaDataPreparer(ctx context.Context, accountName, queueName string, metaData map[string]string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("path", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = metadata.SetIntoHeaders(headers, metaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetMetaDataSender sends the SetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetMetaDataResponder handles the response to the SetMetaData request. The method always +// closes the http.Response Body. +func (client Client) SetMetaDataResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/queue/queues/models.go b/storage/2018-03-28/queue/queues/models.go new file mode 100644 index 0000000..89c2380 --- /dev/null +++ b/storage/2018-03-28/queue/queues/models.go @@ -0,0 +1,42 @@ +package queues + +type StorageServiceProperties struct { + Logging *LoggingConfig `xml:"Logging,omitempty"` + HourMetrics *MetricsConfig `xml:"HourMetrics,omitempty"` + MinuteMetrics *MetricsConfig `xml:"MinuteMetrics,omitempty"` + Cors *Cors `xml:"Cors,omitempty"` +} + +type LoggingConfig struct { + Version string `xml:"Version"` + Delete bool `xml:"Delete"` + Read bool `xml:"Read"` + Write bool `xml:"Write"` + RetentionPolicy RetentionPolicy `xml:"RetentionPolicy"` +} + +type MetricsConfig struct { + Version string `xml:"Version"` + Enabled bool `xml:"Enabled"` + RetentionPolicy RetentionPolicy `xml:"RetentionPolicy"` + + // Element IncludeAPIs is only expected when Metrics is enabled + IncludeAPIs *bool `xml:"IncludeAPIs,omitempty"` +} + +type RetentionPolicy struct { + Enabled bool `xml:"Enabled"` + Days int `xml:"Days"` +} + +type Cors struct { + CorsRule CorsRule `xml:"CorsRule"` +} + +type CorsRule struct { + AllowedOrigins string `xml:"AllowedOrigins"` + AllowedMethods string `xml:"AllowedMethods"` + AllowedHeaders string `xml:"AllowedHeaders` + ExposedHeaders string `xml:"ExposedHeaders"` + MaxAgeInSeconds int `xml:"MaxAgeInSeconds"` +} diff --git a/storage/2018-03-28/queue/queues/properties_get.go b/storage/2018-03-28/queue/queues/properties_get.go new file mode 100644 index 0000000..9d17fb2 --- /dev/null +++ b/storage/2018-03-28/queue/queues/properties_get.go @@ -0,0 +1,85 @@ +package queues + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type StorageServicePropertiesResponse struct { + StorageServiceProperties + autorest.Response +} + +// SetServiceProperties gets the properties for this queue +func (client Client) GetServiceProperties(ctx context.Context, accountName string) (result StorageServicePropertiesResponse, err error) { + if accountName == "" { + return result, validation.NewError("queues.Client", "SetServiceProperties", "`accountName` cannot be an empty string.") + } + + req, err := client.GetServicePropertiesPreparer(ctx, accountName) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "SetServiceProperties", nil, "Failure preparing request") + return + } + + resp, err := client.GetServicePropertiesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "queues.Client", "SetServiceProperties", resp, "Failure sending request") + return + } + + result, err = client.GetServicePropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "SetServiceProperties", resp, "Failure responding to request") + return + } + + return +} + +// GetServicePropertiesPreparer prepares the GetServiceProperties request. +func (client Client) GetServicePropertiesPreparer(ctx context.Context, accountName string) (*http.Request, error) { + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("path", "properties"), + "restype": autorest.Encode("path", "service"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetServicePropertiesSender sends the GetServiceProperties request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetServicePropertiesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetServicePropertiesResponder handles the response to the GetServiceProperties request. The method always +// closes the http.Response Body. +func (client Client) GetServicePropertiesResponder(resp *http.Response) (result StorageServicePropertiesResponse, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/queue/queues/properties_set.go b/storage/2018-03-28/queue/queues/properties_set.go new file mode 100644 index 0000000..d6f6392 --- /dev/null +++ b/storage/2018-03-28/queue/queues/properties_set.go @@ -0,0 +1,80 @@ +package queues + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// SetServiceProperties sets the properties for this queue +func (client Client) SetServiceProperties(ctx context.Context, accountName string, properties StorageServiceProperties) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("queues.Client", "SetServiceProperties", "`accountName` cannot be an empty string.") + } + + req, err := client.SetServicePropertiesPreparer(ctx, accountName, properties) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "SetServiceProperties", nil, "Failure preparing request") + return + } + + resp, err := client.SetServicePropertiesSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "queues.Client", "SetServiceProperties", resp, "Failure sending request") + return + } + + result, err = client.SetServicePropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "SetServiceProperties", resp, "Failure responding to request") + return + } + + return +} + +// SetServicePropertiesPreparer prepares the SetServiceProperties request. +func (client Client) SetServicePropertiesPreparer(ctx context.Context, accountName string, properties StorageServiceProperties) (*http.Request, error) { + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("path", "properties"), + "restype": autorest.Encode("path", "service"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithQueryParameters(queryParameters), + autorest.WithXML(properties), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetServicePropertiesSender sends the SetServiceProperties request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetServicePropertiesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetServicePropertiesResponder handles the response to the SetServiceProperties request. The method always +// closes the http.Response Body. +func (client Client) SetServicePropertiesResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/queue/queues/resource_id.go b/storage/2018-03-28/queue/queues/resource_id.go new file mode 100644 index 0000000..ee28b8b --- /dev/null +++ b/storage/2018-03-28/queue/queues/resource_id.go @@ -0,0 +1,46 @@ +package queues + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given Queue +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, queueName string) string { + domain := endpoints.GetQueueEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s", domain, queueName) +} + +type ResourceID struct { + AccountName string + QueueName string +} + +// ParseResourceID parses the Resource ID and returns an Object which +// can be used to interact with a Queue within a Storage Account +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://foo.queue.core.windows.net/Bar + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + queueName := strings.TrimPrefix(uri.Path, "/") + return &ResourceID{ + AccountName: *accountName, + QueueName: queueName, + }, nil +} diff --git a/storage/2018-03-28/queue/queues/resource_id_test.go b/storage/2018-03-28/queue/queues/resource_id_test.go new file mode 100644 index 0000000..89323d7 --- /dev/null +++ b/storage/2018-03-28/queue/queues/resource_id_test.go @@ -0,0 +1,79 @@ +package queues + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.queue.core.chinacloudapi.cn/queue1", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.queue.core.cloudapi.de/queue1", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.queue.core.windows.net/queue1", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.queue.core.usgovcloudapi.net/queue1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "queue1") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.queue.core.chinacloudapi.cn/queue1", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.queue.core.cloudapi.de/queue1", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.queue.core.windows.net/queue1", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.queue.core.usgovcloudapi.net/queue1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected the account name to be `account1` but got %q", actual.AccountName) + } + + if actual.QueueName != "queue1" { + t.Fatalf("Expected the queue name to be `queue1` but got %q", actual.QueueName) + } + } +} diff --git a/storage/2018-03-28/queue/queues/version.go b/storage/2018-03-28/queue/queues/version.go new file mode 100644 index 0000000..98ef659 --- /dev/null +++ b/storage/2018-03-28/queue/queues/version.go @@ -0,0 +1,14 @@ +package queues + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2018-03-28" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2018-03-28/table/entities/README.md b/storage/2018-03-28/table/entities/README.md new file mode 100644 index 0000000..ff72d35 --- /dev/null +++ b/storage/2018-03-28/table/entities/README.md @@ -0,0 +1,48 @@ +## Table Storage Entities SDK for API version 2018-03-28 + +This package allows you to interact with the Entities Table Storage API + +### Supported Authorizers + +* SharedKeyLite (Table) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2018-03-28/table/entities" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + tableName := "mytable" + + storageAuth := autorest.NewSharedKeyLiteTableAuthorizer(accountName, storageAccountKey) + entitiesClient := entities.New() + entitiesClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + input := entities.InsertEntityInput{ + PartitionKey: "abc", + RowKey: "123", + MetaDataLevel: entities.NoMetaData, + Entity: map[string]interface{}{ + "title": "Don't Kill My Vibe", + "artist": "Sigrid", + }, + } + if _, err := entitiesClient.Insert(ctx, accountName, tableName, input); err != nil { + return fmt.Errorf("Error creating Entity: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2018-03-28/table/entities/client.go b/storage/2018-03-28/table/entities/client.go new file mode 100644 index 0000000..17e9d75 --- /dev/null +++ b/storage/2018-03-28/table/entities/client.go @@ -0,0 +1,25 @@ +package entities + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for Table Storage Shares. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithEnvironment creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2018-03-28/table/entities/delete.go b/storage/2018-03-28/table/entities/delete.go new file mode 100644 index 0000000..83e9188 --- /dev/null +++ b/storage/2018-03-28/table/entities/delete.go @@ -0,0 +1,99 @@ +package entities + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type DeleteEntityInput struct { + // When inserting an entity into a table, you must specify values for the PartitionKey and RowKey system properties. + // Together, these properties form the primary key and must be unique within the table. + // Both the PartitionKey and RowKey values must be string values; each key value may be up to 64 KB in size. + // If you are using an integer value for the key value, you should convert the integer to a fixed-width string, + // because they are canonically sorted. For example, you should convert the value 1 to 0000001 to ensure proper sorting. + RowKey string + PartitionKey string +} + +// Delete deletes an existing entity in a table. +func (client Client) Delete(ctx context.Context, accountName, tableName string, input DeleteEntityInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("entities.Client", "Delete", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("entities.Client", "Delete", "`tableName` cannot be an empty string.") + } + if input.PartitionKey == "" { + return result, validation.NewError("entities.Client", "Delete", "`input.PartitionKey` cannot be an empty string.") + } + if input.RowKey == "" { + return result, validation.NewError("entities.Client", "Delete", "`input.RowKey` cannot be an empty string.") + } + + req, err := client.DeletePreparer(ctx, accountName, tableName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "entities.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName, tableName string, input DeleteEntityInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + "partitionKey": autorest.Encode("path", input.PartitionKey), + "rowKey": autorest.Encode("path", input.RowKey), + } + + headers := map[string]interface{}{ + // TODO: support for eTags + "If-Match": "*", + } + + preparer := autorest.CreatePreparer( + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}(PartitionKey='{partitionKey}', RowKey='{rowKey}')", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/table/entities/get.go b/storage/2018-03-28/table/entities/get.go new file mode 100644 index 0000000..bdb4018 --- /dev/null +++ b/storage/2018-03-28/table/entities/get.go @@ -0,0 +1,108 @@ +package entities + +import ( + "context" + "fmt" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetEntityInput struct { + PartitionKey string + RowKey string + + // The Level of MetaData which should be returned + MetaDataLevel MetaDataLevel +} + +type GetEntityResult struct { + autorest.Response + + Entity map[string]interface{} +} + +// Get queries entities in a table and includes the $filter and $select options. +func (client Client) Get(ctx context.Context, accountName, tableName string, input GetEntityInput) (result GetEntityResult, err error) { + if accountName == "" { + return result, validation.NewError("entities.Client", "Get", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("entities.Client", "Get", "`tableName` cannot be an empty string.") + } + if input.PartitionKey == "" { + return result, validation.NewError("entities.Client", "Get", "`input.PartitionKey` cannot be an empty string.") + } + if input.RowKey == "" { + return result, validation.NewError("entities.Client", "Get", "`input.RowKey` cannot be an empty string.") + } + + req, err := client.GetPreparer(ctx, accountName, tableName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Get", nil, "Failure preparing request") + return + } + + resp, err := client.GetSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "entities.Client", "Get", resp, "Failure sending request") + return + } + + result, err = client.GetResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Get", resp, "Failure responding to request") + return + } + + return +} + +// GetPreparer prepares the Get request. +func (client Client) GetPreparer(ctx context.Context, accountName, tableName string, input GetEntityInput) (*http.Request, error) { + + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + "partitionKey": autorest.Encode("path", input.PartitionKey), + "rowKey": autorest.Encode("path", input.RowKey), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "Accept": fmt.Sprintf("application/json;odata=%s", input.MetaDataLevel), + "DataServiceVersion": "3.0;NetFx", + "MaxDataServiceVersion": "3.0;NetFx", + } + + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}(PartitionKey='{partitionKey}',RowKey='{rowKey}')", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetSender sends the Get request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetResponder handles the response to the Get request. The method always +// closes the http.Response Body. +func (client Client) GetResponder(resp *http.Response) (result GetEntityResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingJSON(&result.Entity), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/table/entities/insert.go b/storage/2018-03-28/table/entities/insert.go new file mode 100644 index 0000000..92b05ce --- /dev/null +++ b/storage/2018-03-28/table/entities/insert.go @@ -0,0 +1,112 @@ +package entities + +import ( + "context" + "fmt" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type InsertEntityInput struct { + // The level of MetaData provided for this Entity + MetaDataLevel MetaDataLevel + + // The Entity which should be inserted, by default all values are strings + // To explicitly type a property, specify the appropriate OData data type by setting + // the m:type attribute within the property definition + Entity map[string]interface{} + + // When inserting an entity into a table, you must specify values for the PartitionKey and RowKey system properties. + // Together, these properties form the primary key and must be unique within the table. + // Both the PartitionKey and RowKey values must be string values; each key value may be up to 64 KB in size. + // If you are using an integer value for the key value, you should convert the integer to a fixed-width string, + // because they are canonically sorted. For example, you should convert the value 1 to 0000001 to ensure proper sorting. + RowKey string + PartitionKey string +} + +// Insert inserts a new entity into a table. +func (client Client) Insert(ctx context.Context, accountName, tableName string, input InsertEntityInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("entities.Client", "Insert", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("entities.Client", "Insert", "`tableName` cannot be an empty string.") + } + if input.PartitionKey == "" { + return result, validation.NewError("entities.Client", "Insert", "`input.PartitionKey` cannot be an empty string.") + } + if input.RowKey == "" { + return result, validation.NewError("entities.Client", "Insert", "`input.RowKey` cannot be an empty string.") + } + + req, err := client.InsertPreparer(ctx, accountName, tableName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Insert", nil, "Failure preparing request") + return + } + + resp, err := client.InsertSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "entities.Client", "Insert", resp, "Failure sending request") + return + } + + result, err = client.InsertResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Insert", resp, "Failure responding to request") + return + } + + return +} + +// InsertPreparer prepares the Insert request. +func (client Client) InsertPreparer(ctx context.Context, accountName, tableName string, input InsertEntityInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "Accept": fmt.Sprintf("application/json;odata=%s", input.MetaDataLevel), + "Prefer": "return-no-content", + } + + input.Entity["PartitionKey"] = input.PartitionKey + input.Entity["RowKey"] = input.RowKey + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/json"), + autorest.AsPost(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}", pathParameters), + autorest.WithJSON(input.Entity), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// InsertSender sends the Insert request. The method will close the +// http.Response Body if it receives an error. +func (client Client) InsertSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// InsertResponder handles the response to the Insert request. The method always +// closes the http.Response Body. +func (client Client) InsertResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/table/entities/insert_or_merge.go b/storage/2018-03-28/table/entities/insert_or_merge.go new file mode 100644 index 0000000..1fb4ed3 --- /dev/null +++ b/storage/2018-03-28/table/entities/insert_or_merge.go @@ -0,0 +1,108 @@ +package entities + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type InsertOrMergeEntityInput struct { + // The Entity which should be inserted, by default all values are strings + // To explicitly type a property, specify the appropriate OData data type by setting + // the m:type attribute within the property definition + Entity map[string]interface{} + + // When inserting an entity into a table, you must specify values for the PartitionKey and RowKey system properties. + // Together, these properties form the primary key and must be unique within the table. + // Both the PartitionKey and RowKey values must be string values; each key value may be up to 64 KB in size. + // If you are using an integer value for the key value, you should convert the integer to a fixed-width string, + // because they are canonically sorted. For example, you should convert the value 1 to 0000001 to ensure proper sorting. + RowKey string + PartitionKey string +} + +// InsertOrMerge updates an existing entity or inserts a new entity if it does not exist in the table. +// Because this operation can insert or update an entity, it is also known as an upsert operation. +func (client Client) InsertOrMerge(ctx context.Context, accountName, tableName string, input InsertOrMergeEntityInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("entities.Client", "InsertOrMerge", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("entities.Client", "InsertOrMerge", "`tableName` cannot be an empty string.") + } + if input.PartitionKey == "" { + return result, validation.NewError("entities.Client", "InsertOrMerge", "`input.PartitionKey` cannot be an empty string.") + } + if input.RowKey == "" { + return result, validation.NewError("entities.Client", "InsertOrMerge", "`input.RowKey` cannot be an empty string.") + } + + req, err := client.InsertOrMergePreparer(ctx, accountName, tableName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "InsertOrMerge", nil, "Failure preparing request") + return + } + + resp, err := client.InsertOrMergeSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "entities.Client", "InsertOrMerge", resp, "Failure sending request") + return + } + + result, err = client.InsertOrMergeResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "InsertOrMerge", resp, "Failure responding to request") + return + } + + return +} + +// InsertOrMergePreparer prepares the InsertOrMerge request. +func (client Client) InsertOrMergePreparer(ctx context.Context, accountName, tableName string, input InsertOrMergeEntityInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + "partitionKey": autorest.Encode("path", input.PartitionKey), + "rowKey": autorest.Encode("path", input.RowKey), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "Accept": "application/json", + "Prefer": "return-no-content", + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/json"), + autorest.AsMerge(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}(PartitionKey='{partitionKey}', RowKey='{rowKey}')", pathParameters), + autorest.WithJSON(input.Entity), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// InsertOrMergeSender sends the InsertOrMerge request. The method will close the +// http.Response Body if it receives an error. +func (client Client) InsertOrMergeSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// InsertOrMergeResponder handles the response to the InsertOrMerge request. The method always +// closes the http.Response Body. +func (client Client) InsertOrMergeResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/table/entities/insert_or_replace.go b/storage/2018-03-28/table/entities/insert_or_replace.go new file mode 100644 index 0000000..036ba5d --- /dev/null +++ b/storage/2018-03-28/table/entities/insert_or_replace.go @@ -0,0 +1,108 @@ +package entities + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type InsertOrReplaceEntityInput struct { + // The Entity which should be inserted, by default all values are strings + // To explicitly type a property, specify the appropriate OData data type by setting + // the m:type attribute within the property definition + Entity map[string]interface{} + + // When inserting an entity into a table, you must specify values for the PartitionKey and RowKey system properties. + // Together, these properties form the primary key and must be unique within the table. + // Both the PartitionKey and RowKey values must be string values; each key value may be up to 64 KB in size. + // If you are using an integer value for the key value, you should convert the integer to a fixed-width string, + // because they are canonically sorted. For example, you should convert the value 1 to 0000001 to ensure proper sorting. + RowKey string + PartitionKey string +} + +// InsertOrReplace replaces an existing entity or inserts a new entity if it does not exist in the table. +// Because this operation can insert or update an entity, it is also known as an upsert operation. +func (client Client) InsertOrReplace(ctx context.Context, accountName, tableName string, input InsertOrReplaceEntityInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("entities.Client", "InsertOrReplace", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("entities.Client", "InsertOrReplace", "`tableName` cannot be an empty string.") + } + if input.PartitionKey == "" { + return result, validation.NewError("entities.Client", "InsertOrReplace", "`input.PartitionKey` cannot be an empty string.") + } + if input.RowKey == "" { + return result, validation.NewError("entities.Client", "InsertOrReplace", "`input.RowKey` cannot be an empty string.") + } + + req, err := client.InsertOrReplacePreparer(ctx, accountName, tableName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "InsertOrReplace", nil, "Failure preparing request") + return + } + + resp, err := client.InsertOrReplaceSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "entities.Client", "InsertOrReplace", resp, "Failure sending request") + return + } + + result, err = client.InsertOrReplaceResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "InsertOrReplace", resp, "Failure responding to request") + return + } + + return +} + +// InsertOrReplacePreparer prepares the InsertOrReplace request. +func (client Client) InsertOrReplacePreparer(ctx context.Context, accountName, tableName string, input InsertOrReplaceEntityInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + "partitionKey": autorest.Encode("path", input.PartitionKey), + "rowKey": autorest.Encode("path", input.RowKey), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "Accept": "application/json", + "Prefer": "return-no-content", + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/json"), + autorest.AsMerge(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}(PartitionKey='{partitionKey}', RowKey='{rowKey}')", pathParameters), + autorest.WithJSON(input.Entity), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// InsertOrReplaceSender sends the InsertOrReplace request. The method will close the +// http.Response Body if it receives an error. +func (client Client) InsertOrReplaceSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// InsertOrReplaceResponder handles the response to the InsertOrReplace request. The method always +// closes the http.Response Body. +func (client Client) InsertOrReplaceResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/table/entities/lifecycle_test.go b/storage/2018-03-28/table/entities/lifecycle_test.go new file mode 100644 index 0000000..9f15722 --- /dev/null +++ b/storage/2018-03-28/table/entities/lifecycle_test.go @@ -0,0 +1,135 @@ +package entities + +import ( + "context" + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-03-28/table/tables" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestEntitiesLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + tableName := fmt.Sprintf("table%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteTableAuthorizer(accountName, testData.StorageAccountKey) + tablesClient := tables.NewWithEnvironment(client.Environment) + tablesClient.Client = client.PrepareWithAuthorizer(tablesClient.Client, storageAuth) + + t.Logf("[DEBUG] Creating Table..") + if _, err := tablesClient.Create(ctx, accountName, tableName); err != nil { + t.Fatalf("Error creating Table %q: %s", tableName, err) + } + defer tablesClient.Delete(ctx, accountName, tableName) + + entitiesClient := NewWithEnvironment(client.Environment) + entitiesClient.Client = client.PrepareWithAuthorizer(entitiesClient.Client, storageAuth) + + partitionKey := "hello" + rowKey := "there" + + t.Logf("[DEBUG] Inserting..") + insertInput := InsertEntityInput{ + MetaDataLevel: NoMetaData, + PartitionKey: partitionKey, + RowKey: rowKey, + Entity: map[string]interface{}{ + "hello": "world", + }, + } + if _, err := entitiesClient.Insert(ctx, accountName, tableName, insertInput); err != nil { + t.Logf("Error retrieving: %s", err) + } + + t.Logf("[DEBUG] Insert or Merging..") + insertOrMergeInput := InsertOrMergeEntityInput{ + PartitionKey: partitionKey, + RowKey: rowKey, + Entity: map[string]interface{}{ + "hello": "ther88e", + }, + } + if _, err := entitiesClient.InsertOrMerge(ctx, accountName, tableName, insertOrMergeInput); err != nil { + t.Logf("Error insert/merging: %s", err) + } + + t.Logf("[DEBUG] Insert or Replacing..") + insertOrReplaceInput := InsertOrReplaceEntityInput{ + PartitionKey: partitionKey, + RowKey: rowKey, + Entity: map[string]interface{}{ + "hello": "pandas", + }, + } + if _, err := entitiesClient.InsertOrReplace(ctx, accountName, tableName, insertOrReplaceInput); err != nil { + t.Logf("Error inserting/replacing: %s", err) + } + + t.Logf("[DEBUG] Querying..") + queryInput := QueryEntitiesInput{ + MetaDataLevel: NoMetaData, + } + results, err := entitiesClient.Query(ctx, accountName, tableName, queryInput) + if err != nil { + t.Logf("Error querying: %s", err) + } + + if len(results.Entities) != 1 { + t.Fatalf("Expected 1 item but got %d", len(results.Entities)) + } + + for _, v := range results.Entities { + thisPartitionKey := v["PartitionKey"].(string) + thisRowKey := v["RowKey"].(string) + if partitionKey != thisPartitionKey { + t.Fatalf("Expected Partition Key to be %q but got %q", partitionKey, thisPartitionKey) + } + if rowKey != thisRowKey { + t.Fatalf("Expected Partition Key to be %q but got %q", rowKey, thisRowKey) + } + } + + t.Logf("[DEBUG] Retrieving..") + getInput := GetEntityInput{ + MetaDataLevel: MinimalMetaData, + PartitionKey: partitionKey, + RowKey: rowKey, + } + getResults, err := entitiesClient.Get(ctx, accountName, tableName, getInput) + if err != nil { + t.Logf("Error querying: %s", err) + } + + partitionKey2 := getResults.Entity["PartitionKey"].(string) + rowKey2 := getResults.Entity["RowKey"].(string) + if partitionKey2 != partitionKey { + t.Fatalf("Expected Partition Key to be %q but got %q", partitionKey, partitionKey2) + } + if rowKey2 != rowKey { + t.Fatalf("Expected Row Key to be %q but got %q", rowKey, rowKey2) + } + + t.Logf("[DEBUG] Deleting..") + deleteInput := DeleteEntityInput{ + PartitionKey: partitionKey, + RowKey: rowKey, + } + if _, err := entitiesClient.Delete(ctx, accountName, tableName, deleteInput); err != nil { + t.Logf("Error deleting: %s", err) + } +} diff --git a/storage/2018-03-28/table/entities/models.go b/storage/2018-03-28/table/entities/models.go new file mode 100644 index 0000000..e3c6ccc --- /dev/null +++ b/storage/2018-03-28/table/entities/models.go @@ -0,0 +1,9 @@ +package entities + +type MetaDataLevel string + +var ( + NoMetaData MetaDataLevel = "nometadata" + MinimalMetaData MetaDataLevel = "minimalmetadata" + FullMetaData MetaDataLevel = "fullmetadata" +) diff --git a/storage/2018-03-28/table/entities/query.go b/storage/2018-03-28/table/entities/query.go new file mode 100644 index 0000000..a768b83 --- /dev/null +++ b/storage/2018-03-28/table/entities/query.go @@ -0,0 +1,155 @@ +package entities + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type QueryEntitiesInput struct { + // An optional OData filter + Filter *string + + // An optional comma-separated + PropertyNamesToSelect *[]string + + PartitionKey string + RowKey string + + // The Level of MetaData which should be returned + MetaDataLevel MetaDataLevel + + // The Next Partition Key used to load data from a previous point + NextPartitionKey *string + + // The Next Row Key used to load data from a previous point + NextRowKey *string +} + +type QueryEntitiesResult struct { + autorest.Response + + NextPartitionKey string + NextRowKey string + + MetaData string `json:"odata.metadata,omitempty"` + Entities []map[string]interface{} `json:"value"` +} + +// Query queries entities in a table and includes the $filter and $select options. +func (client Client) Query(ctx context.Context, accountName, tableName string, input QueryEntitiesInput) (result QueryEntitiesResult, err error) { + if accountName == "" { + return result, validation.NewError("entities.Client", "Query", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("entities.Client", "Query", "`tableName` cannot be an empty string.") + } + + req, err := client.QueryPreparer(ctx, accountName, tableName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Query", nil, "Failure preparing request") + return + } + + resp, err := client.QuerySender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "entities.Client", "Query", resp, "Failure sending request") + return + } + + result, err = client.QueryResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Query", resp, "Failure responding to request") + return + } + + return +} + +// QueryPreparer prepares the Query request. +func (client Client) QueryPreparer(ctx context.Context, accountName, tableName string, input QueryEntitiesInput) (*http.Request, error) { + + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + "additionalParameters": "", + } + + //PartitionKey='',RowKey='' + additionalParams := make([]string, 0) + if input.PartitionKey != "" { + additionalParams = append(additionalParams, fmt.Sprintf("PartitionKey='%s'", input.PartitionKey)) + } + if input.RowKey != "" { + additionalParams = append(additionalParams, fmt.Sprintf("RowKey='%s'", input.RowKey)) + } + if len(additionalParams) > 0 { + pathParameters["additionalParameters"] = autorest.Encode("path", strings.Join(additionalParams, ",")) + } + + queryParameters := map[string]interface{}{} + + if input.Filter != nil { + queryParameters["filter"] = autorest.Encode("query", input.Filter) + } + + if input.PropertyNamesToSelect != nil { + queryParameters["$select"] = autorest.Encode("query", strings.Join(*input.PropertyNamesToSelect, ",")) + } + + if input.NextPartitionKey != nil { + queryParameters["NextPartitionKey"] = *input.NextPartitionKey + } + + if input.NextRowKey != nil { + queryParameters["NextRowKey"] = *input.NextRowKey + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "Accept": fmt.Sprintf("application/json;odata=%s", input.MetaDataLevel), + "DataServiceVersion": "3.0;NetFx", + "MaxDataServiceVersion": "3.0;NetFx", + } + + // GET /myaccount/Customers()?$filter=(Rating%20ge%203)%20and%20(Rating%20le%206)&$select=PartitionKey,RowKey,Address,CustomerSince + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}({additionalParameters})", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// QuerySender sends the Query request. The method will close the +// http.Response Body if it receives an error. +func (client Client) QuerySender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// QueryResponder handles the response to the Query request. The method always +// closes the http.Response Body. +func (client Client) QueryResponder(resp *http.Response) (result QueryEntitiesResult, err error) { + if resp != nil && resp.Header != nil { + result.NextPartitionKey = resp.Header.Get("x-ms-continuation-NextPartitionKey") + result.NextRowKey = resp.Header.Get("x-ms-continuation-NextRowKey") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingJSON(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/table/entities/resource_id.go b/storage/2018-03-28/table/entities/resource_id.go new file mode 100644 index 0000000..59366a2 --- /dev/null +++ b/storage/2018-03-28/table/entities/resource_id.go @@ -0,0 +1,91 @@ +package entities + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given Entity +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, tableName, partitionKey, rowKey string) string { + domain := endpoints.GetTableEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s(PartitionKey='%s',RowKey='%s')", domain, tableName, partitionKey, rowKey) +} + +type ResourceID struct { + AccountName string + TableName string + PartitionKey string + RowKey string +} + +// ParseResourceID parses the specified Resource ID and returns an object which +// can be used to look up the specified Entity within the specified Table +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://account1.table.core.chinacloudapi.cn/table1(PartitionKey='partition1',RowKey='row1') + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + // assume there a `Table('')` + path := strings.TrimPrefix(uri.Path, "/") + if !strings.Contains(uri.Path, "(") || !strings.HasSuffix(uri.Path, ")") { + return nil, fmt.Errorf("Expected the Table Name to be in the format `tables(PartitionKey='',RowKey='')` but got %q", path) + } + + // NOTE: honestly this could probably be a RegEx, but this seemed like the simplest way to + // allow these two fields to be specified in either order + indexOfBracket := strings.IndexByte(path, '(') + tableName := path[0:indexOfBracket] + + // trim off the brackets + temp := strings.TrimPrefix(path, fmt.Sprintf("%s(", tableName)) + temp = strings.TrimSuffix(temp, ")") + + dictionary := strings.Split(temp, ",") + partitionKey := "" + rowKey := "" + for _, v := range dictionary { + split := strings.Split(v, "=") + if len(split) != 2 { + return nil, fmt.Errorf("Expected 2 segments but got %d for %q", len(split), v) + } + + key := split[0] + value := strings.TrimSuffix(strings.TrimPrefix(split[1], "'"), "'") + if strings.EqualFold(key, "PartitionKey") { + partitionKey = value + } else if strings.EqualFold(key, "RowKey") { + rowKey = value + } else { + return nil, fmt.Errorf("Unexpected Key %q", key) + } + } + + if partitionKey == "" { + return nil, fmt.Errorf("Expected a PartitionKey but didn't get one") + } + if rowKey == "" { + return nil, fmt.Errorf("Expected a RowKey but didn't get one") + } + + return &ResourceID{ + AccountName: *accountName, + TableName: tableName, + PartitionKey: partitionKey, + RowKey: rowKey, + }, nil +} diff --git a/storage/2018-03-28/table/entities/resource_id_test.go b/storage/2018-03-28/table/entities/resource_id_test.go new file mode 100644 index 0000000..e85af79 --- /dev/null +++ b/storage/2018-03-28/table/entities/resource_id_test.go @@ -0,0 +1,84 @@ +package entities + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.table.core.chinacloudapi.cn/table1(PartitionKey='partition1',RowKey='row1')", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.table.core.cloudapi.de/table1(PartitionKey='partition1',RowKey='row1')", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.table.core.windows.net/table1(PartitionKey='partition1',RowKey='row1')", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.table.core.usgovcloudapi.net/table1(PartitionKey='partition1',RowKey='row1')", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "table1", "partition1", "row1") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.table.core.chinacloudapi.cn/table1(PartitionKey='partition1',RowKey='row1')", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.table.core.cloudapi.de/table1(PartitionKey='partition1',RowKey='row1')", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.table.core.windows.net/table1(PartitionKey='partition1',RowKey='row1')", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.table.core.usgovcloudapi.net/table1(PartitionKey='partition1',RowKey='row1')", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.TableName != "table1" { + t.Fatalf("Expected Table Name to be `table1` but got %q", actual.TableName) + } + if actual.PartitionKey != "partition1" { + t.Fatalf("Expected Partition Key to be `partition1` but got %q", actual.PartitionKey) + } + if actual.RowKey != "row1" { + t.Fatalf("Expected Row Key to be `row1` but got %q", actual.RowKey) + } + } +} diff --git a/storage/2018-03-28/table/entities/version.go b/storage/2018-03-28/table/entities/version.go new file mode 100644 index 0000000..064c70c --- /dev/null +++ b/storage/2018-03-28/table/entities/version.go @@ -0,0 +1,14 @@ +package entities + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2018-03-28" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2018-03-28/table/tables/README.md b/storage/2018-03-28/table/tables/README.md new file mode 100644 index 0000000..ca03d8f --- /dev/null +++ b/storage/2018-03-28/table/tables/README.md @@ -0,0 +1,39 @@ +## Table Storage Tables SDK for API version 2018-03-28 + +This package allows you to interact with the Tables Table Storage API + +### Supported Authorizers + +* SharedKeyLite (Table) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2018-03-28/table/tables" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + tableName := "mytable" + + storageAuth := autorest.NewSharedKeyLiteTableAuthorizer(accountName, storageAccountKey) + tablesClient := tables.New() + tablesClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + if _, err := tablesClient.Insert(ctx, accountName, tableName); err != nil { + return fmt.Errorf("Error creating Table: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2018-03-28/table/tables/acl_get.go b/storage/2018-03-28/table/tables/acl_get.go new file mode 100644 index 0000000..0ef0000 --- /dev/null +++ b/storage/2018-03-28/table/tables/acl_get.go @@ -0,0 +1,93 @@ +package tables + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetACLResult struct { + autorest.Response + + SignedIdentifiers []SignedIdentifier `xml:"SignedIdentifier"` +} + +// GetACL returns the Access Control List for the specified Table +func (client Client) GetACL(ctx context.Context, accountName, tableName string) (result GetACLResult, err error) { + if accountName == "" { + return result, validation.NewError("tables.Client", "GetACL", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("tables.Client", "GetACL", "`tableName` cannot be an empty string.") + } + + req, err := client.GetACLPreparer(ctx, accountName, tableName) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "GetACL", nil, "Failure preparing request") + return + } + + resp, err := client.GetACLSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "tables.Client", "GetACL", resp, "Failure sending request") + return + } + + result, err = client.GetACLResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "GetACL", resp, "Failure responding to request") + return + } + + return +} + +// GetACLPreparer prepares the GetACL request. +func (client Client) GetACLPreparer(ctx context.Context, accountName, tableName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "acl"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetACLSender sends the GetACL request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetACLSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetACLResponder handles the response to the GetACL request. The method always +// closes the http.Response Body. +func (client Client) GetACLResponder(resp *http.Response) (result GetACLResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/table/tables/acl_set.go b/storage/2018-03-28/table/tables/acl_set.go new file mode 100644 index 0000000..c26bffc --- /dev/null +++ b/storage/2018-03-28/table/tables/acl_set.go @@ -0,0 +1,98 @@ +package tables + +import ( + "context" + "encoding/xml" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type setAcl struct { + SignedIdentifiers []SignedIdentifier `xml:"SignedIdentifier"` + + XMLName xml.Name `xml:"SignedIdentifiers"` +} + +// SetACL sets the specified Access Control List for the specified Table +func (client Client) SetACL(ctx context.Context, accountName, tableName string, acls []SignedIdentifier) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("tables.Client", "SetACL", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("tables.Client", "SetACL", "`tableName` cannot be an empty string.") + } + + req, err := client.SetACLPreparer(ctx, accountName, tableName, acls) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "SetACL", nil, "Failure preparing request") + return + } + + resp, err := client.SetACLSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "tables.Client", "SetACL", resp, "Failure sending request") + return + } + + result, err = client.SetACLResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "SetACL", resp, "Failure responding to request") + return + } + + return +} + +// SetACLPreparer prepares the SetACL request. +func (client Client) SetACLPreparer(ctx context.Context, accountName, tableName string, acls []SignedIdentifier) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "acl"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + input := setAcl{ + SignedIdentifiers: acls, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers), + autorest.WithXML(&input)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetACLSender sends the SetACL request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetACLSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetACLResponder handles the response to the SetACL request. The method always +// closes the http.Response Body. +func (client Client) SetACLResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/table/tables/client.go b/storage/2018-03-28/table/tables/client.go new file mode 100644 index 0000000..56724b9 --- /dev/null +++ b/storage/2018-03-28/table/tables/client.go @@ -0,0 +1,25 @@ +package tables + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for Table Storage Shares. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithEnvironment creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2018-03-28/table/tables/create.go b/storage/2018-03-28/table/tables/create.go new file mode 100644 index 0000000..561f574 --- /dev/null +++ b/storage/2018-03-28/table/tables/create.go @@ -0,0 +1,90 @@ +package tables + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type createTableRequest struct { + TableName string `json:"TableName"` +} + +// Create creates a new table in the storage account. +func (client Client) Create(ctx context.Context, accountName, tableName string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("tables.Client", "Create", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("tables.Client", "Create", "`tableName` cannot be an empty string.") + } + + req, err := client.CreatePreparer(ctx, accountName, tableName) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Create", nil, "Failure preparing request") + return + } + + resp, err := client.CreateSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "tables.Client", "Create", resp, "Failure sending request") + return + } + + result, err = client.CreateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Create", resp, "Failure responding to request") + return + } + + return +} + +// CreatePreparer prepares the Create request. +func (client Client) CreatePreparer(ctx context.Context, accountName, tableName string) (*http.Request, error) { + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + // NOTE: we could support returning metadata here, but it doesn't appear to be directly useful + // vs making a request using the Get methods as-necessary? + "Accept": "application/json;odata=nometadata", + "Prefer": "return-no-content", + } + + body := createTableRequest{ + TableName: tableName, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/json"), + autorest.AsPost(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPath("/Tables"), + autorest.WithJSON(body), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CreateSender sends the Create request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CreateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CreateResponder handles the response to the Create request. The method always +// closes the http.Response Body. +func (client Client) CreateResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/table/tables/delete.go b/storage/2018-03-28/table/tables/delete.go new file mode 100644 index 0000000..5b5ec86 --- /dev/null +++ b/storage/2018-03-28/table/tables/delete.go @@ -0,0 +1,79 @@ +package tables + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Delete deletes the specified table and any data it contains. +func (client Client) Delete(ctx context.Context, accountName, tableName string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("tables.Client", "Delete", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("tables.Client", "Delete", "`tableName` cannot be an empty string.") + } + + req, err := client.DeletePreparer(ctx, accountName, tableName) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "tables.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName, tableName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + } + + // NOTE: whilst the API documentation says that API Version is Optional + // apparently specifying it causes an "invalid content type" to always be returned + // as such we omit it here :shrug: + + preparer := autorest.CreatePreparer( + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/Tables('{tableName}')", pathParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/table/tables/exists.go b/storage/2018-03-28/table/tables/exists.go new file mode 100644 index 0000000..b3a2718 --- /dev/null +++ b/storage/2018-03-28/table/tables/exists.go @@ -0,0 +1,80 @@ +package tables + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Exists checks that the specified table exists +func (client Client) Exists(ctx context.Context, accountName, tableName string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("tables.Client", "Exists", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("tables.Client", "Exists", "`tableName` cannot be an empty string.") + } + + req, err := client.ExistsPreparer(ctx, accountName, tableName) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Exists", nil, "Failure preparing request") + return + } + + resp, err := client.ExistsSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "tables.Client", "Exists", resp, "Failure sending request") + return + } + + result, err = client.ExistsResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Exists", resp, "Failure responding to request") + return + } + + return +} + +// ExistsPreparer prepares the Exists request. +func (client Client) ExistsPreparer(ctx context.Context, accountName, tableName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + } + + // NOTE: whilst the API documentation says that API Version is Optional + // apparently specifying it causes an "invalid content type" to always be returned + // as such we omit it here :shrug: + + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.AsContentType("application/xml"), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/Tables('{tableName}')", pathParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ExistsSender sends the Exists request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ExistsSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ExistsResponder handles the response to the Exists request. The method always +// closes the http.Response Body. +func (client Client) ExistsResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/table/tables/lifecycle_test.go b/storage/2018-03-28/table/tables/lifecycle_test.go new file mode 100644 index 0000000..74ab0fe --- /dev/null +++ b/storage/2018-03-28/table/tables/lifecycle_test.go @@ -0,0 +1,112 @@ +package tables + +import ( + "context" + "fmt" + "log" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestTablesLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + tableName := fmt.Sprintf("table%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteTableAuthorizer(accountName, testData.StorageAccountKey) + tablesClient := NewWithEnvironment(client.Environment) + tablesClient.Client = client.PrepareWithAuthorizer(tablesClient.Client, storageAuth) + + t.Logf("[DEBUG] Creating Table..") + if _, err := tablesClient.Create(ctx, accountName, tableName); err != nil { + t.Fatalf("Error creating Table %q: %s", tableName, err) + } + + // first look it up directly and confirm it's there + t.Logf("[DEBUG] Checking if Table exists..") + if _, err := tablesClient.Exists(ctx, accountName, tableName); err != nil { + t.Fatalf("Error checking if Table %q exists: %s", tableName, err) + } + + // then confirm it exists in the Query too + t.Logf("[DEBUG] Querying for Tables..") + result, err := tablesClient.Query(ctx, accountName, NoMetaData) + if err != nil { + t.Fatalf("Error retrieving Tables: %s", err) + } + found := false + for _, v := range result.Tables { + log.Printf("[DEBUG] Table: %q", v.TableName) + + if v.TableName == tableName { + found = true + } + } + if !found { + t.Fatalf("%q was not found in the Query response!", tableName) + } + + t.Logf("[DEBUG] Setting ACL's for Table %q..", tableName) + acls := []SignedIdentifier{ + { + Id: "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=", + AccessPolicy: AccessPolicy{ + Permission: "raud", + Start: "2020-11-26T08:49:37.0000000Z", + Expiry: "2020-11-27T08:49:37.0000000Z", + }, + }, + } + if _, err := tablesClient.SetACL(ctx, accountName, tableName, acls); err != nil { + t.Fatalf("Error setting ACLs: %s", err) + } + + t.Logf("[DEBUG] Retrieving ACL's for Table %q..", tableName) + retrievedACLs, err := tablesClient.GetACL(ctx, accountName, tableName) + if err != nil { + t.Fatalf("Error retrieving ACLs: %s", err) + } + + if len(retrievedACLs.SignedIdentifiers) != len(acls) { + t.Fatalf("Expected %d but got %q ACLs", len(retrievedACLs.SignedIdentifiers), len(acls)) + } + + for i, retrievedAcl := range retrievedACLs.SignedIdentifiers { + expectedAcl := acls[i] + + if retrievedAcl.Id != expectedAcl.Id { + t.Fatalf("Expected ID to be %q but got %q", retrievedAcl.Id, expectedAcl.Id) + } + + if retrievedAcl.AccessPolicy.Start != expectedAcl.AccessPolicy.Start { + t.Fatalf("Expected Start to be %q but got %q", retrievedAcl.AccessPolicy.Start, expectedAcl.AccessPolicy.Start) + } + + if retrievedAcl.AccessPolicy.Expiry != expectedAcl.AccessPolicy.Expiry { + t.Fatalf("Expected Expiry to be %q but got %q", retrievedAcl.AccessPolicy.Expiry, expectedAcl.AccessPolicy.Expiry) + } + + if retrievedAcl.AccessPolicy.Permission != expectedAcl.AccessPolicy.Permission { + t.Fatalf("Expected Permission to be %q but got %q", retrievedAcl.AccessPolicy.Permission, expectedAcl.AccessPolicy.Permission) + } + } + + t.Logf("[DEBUG] Deleting Table %q..", tableName) + if _, err := tablesClient.Delete(ctx, accountName, tableName); err != nil { + t.Fatalf("Error deleting %q: %s", tableName, err) + } +} diff --git a/storage/2018-03-28/table/tables/models.go b/storage/2018-03-28/table/tables/models.go new file mode 100644 index 0000000..d7c382a --- /dev/null +++ b/storage/2018-03-28/table/tables/models.go @@ -0,0 +1,29 @@ +package tables + +type MetaDataLevel string + +var ( + NoMetaData MetaDataLevel = "nometadata" + MinimalMetaData MetaDataLevel = "minimalmetadata" + FullMetaData MetaDataLevel = "fullmetadata" +) + +type GetResultItem struct { + TableName string `json:"TableName"` + + // Optional, depending on the MetaData Level + ODataType string `json:"odata.type,omitempty"` + ODataID string `json:"odata.id,omitEmpty"` + ODataEditLink string `json:"odata.editLink,omitEmpty"` +} + +type SignedIdentifier struct { + Id string `xml:"Id"` + AccessPolicy AccessPolicy `xml:"AccessPolicy"` +} + +type AccessPolicy struct { + Start string `xml:"Start"` + Expiry string `xml:"Expiry"` + Permission string `xml:"Permission"` +} diff --git a/storage/2018-03-28/table/tables/query.go b/storage/2018-03-28/table/tables/query.go new file mode 100644 index 0000000..475370f --- /dev/null +++ b/storage/2018-03-28/table/tables/query.go @@ -0,0 +1,87 @@ +package tables + +import ( + "context" + "fmt" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetResult struct { + autorest.Response + + MetaData string `json:"odata.metadata,omitempty"` + Tables []GetResultItem `json:"value"` +} + +// Query returns a list of tables under the specified account. +func (client Client) Query(ctx context.Context, accountName string, metaDataLevel MetaDataLevel) (result GetResult, err error) { + if accountName == "" { + return result, validation.NewError("tables.Client", "Query", "`accountName` cannot be an empty string.") + } + + req, err := client.QueryPreparer(ctx, accountName, metaDataLevel) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Query", nil, "Failure preparing request") + return + } + + resp, err := client.QuerySender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "tables.Client", "Query", resp, "Failure sending request") + return + } + + result, err = client.QueryResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Query", resp, "Failure responding to request") + return + } + + return +} + +// QueryPreparer prepares the Query request. +func (client Client) QueryPreparer(ctx context.Context, accountName string, metaDataLevel MetaDataLevel) (*http.Request, error) { + // NOTE: whilst this supports ContinuationTokens and 'Top' + // it appears that 'Skip' returns a '501 Not Implemented' + // as such, we intentionally don't support those right now + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "Accept": fmt.Sprintf("application/json;odata=%s", metaDataLevel), + } + + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPath("/Tables"), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// QuerySender sends the Query request. The method will close the +// http.Response Body if it receives an error. +func (client Client) QuerySender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// QueryResponder handles the response to the Query request. The method always +// closes the http.Response Body. +func (client Client) QueryResponder(resp *http.Response) (result GetResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingJSON(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-03-28/table/tables/resource_id.go b/storage/2018-03-28/table/tables/resource_id.go new file mode 100644 index 0000000..1052317 --- /dev/null +++ b/storage/2018-03-28/table/tables/resource_id.go @@ -0,0 +1,54 @@ +package tables + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given Table +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, tableName string) string { + domain := endpoints.GetTableEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/Tables('%s')", domain, tableName) +} + +type ResourceID struct { + AccountName string + TableName string +} + +// ParseResourceID parses the Resource ID and returns an object which +// can be used to interact with the Table within the specified Storage Account +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://foo.table.core.windows.net/Table('foo') + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + // assume there a `Table('')` + path := strings.TrimPrefix(uri.Path, "/") + if !strings.HasPrefix(path, "Tables('") || !strings.HasSuffix(path, "')") { + return nil, fmt.Errorf("Expected the Table Name to be in the format `Tables('name')` but got %q", path) + } + + // strip off the `Table('')` + tableName := strings.TrimPrefix(uri.Path, "/Tables('") + tableName = strings.TrimSuffix(tableName, "')") + return &ResourceID{ + AccountName: *accountName, + TableName: tableName, + }, nil +} diff --git a/storage/2018-03-28/table/tables/resource_id_test.go b/storage/2018-03-28/table/tables/resource_id_test.go new file mode 100644 index 0000000..5557f81 --- /dev/null +++ b/storage/2018-03-28/table/tables/resource_id_test.go @@ -0,0 +1,78 @@ +package tables + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.table.core.chinacloudapi.cn/Tables('table1')", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.table.core.cloudapi.de/Tables('table1')", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.table.core.windows.net/Tables('table1')", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.table.core.usgovcloudapi.net/Tables('table1')", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "table1") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.table.core.chinacloudapi.cn/Tables('table1')", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.table.core.cloudapi.de/Tables('table1')", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.table.core.windows.net/Tables('table1')", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.table.core.usgovcloudapi.net/Tables('table1')", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.TableName != "table1" { + t.Fatalf("Expected Table Name to be `table1` but got %q", actual.TableName) + } + } +} diff --git a/storage/2018-03-28/table/tables/version.go b/storage/2018-03-28/table/tables/version.go new file mode 100644 index 0000000..83a36d2 --- /dev/null +++ b/storage/2018-03-28/table/tables/version.go @@ -0,0 +1,14 @@ +package tables + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2018-03-28" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2018-11-09/README.md b/storage/2018-11-09/README.md new file mode 100644 index 0000000..01dbd7f --- /dev/null +++ b/storage/2018-11-09/README.md @@ -0,0 +1,25 @@ +# Storage API Version 2018-11-09 + +The following API's are supported by this SDK - more information about each SDK can be found within the README in each package. + +## Blob Storage + +- [Blobs API](blob/blobs) +- [Containers API](blob/containers) + +## File Storage + +- [Directories API](file/directories) +- [Files API](file/files) +- [Shares API](file/shares) + +## Queue Storage + +- [Queues API](queue/queues) +- [Messages API](queue/messages) + +## Table Storage + +- [Entities API](table/entities) +- [Tables API](table/tables) + diff --git a/storage/2018-11-09/blob/blobs/README.md b/storage/2018-11-09/blob/blobs/README.md new file mode 100644 index 0000000..c246993 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/README.md @@ -0,0 +1,46 @@ +## Blob Storage Blobs SDK for API version 2018-11-09 + +This package allows you to interact with the Blobs Blob Storage API + +### Supported Authorizers + +* Azure Active Directory (for the Resource Endpoint `https://storage.azure.com`) +* SharedKeyLite (Blob, File & Queue) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/blobs" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + containerName := "mycontainer" + fileName := "example-large-file.iso" + + storageAuth := autorest.NewSharedKeyLiteAuthorizer(accountName, storageAccountKey) + blobClient := blobs.New() + blobClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + copyInput := blobs.CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + refreshInterval := 5 * time.Second + if err := blobClient.CopyAndWait(ctx, accountName, containerName, fileName, copyInput, refreshInterval); err != nil { + return fmt.Errorf("Error copying: %s", err) + } + + return nil +} + +``` \ No newline at end of file diff --git a/storage/2018-11-09/blob/blobs/append_block.go b/storage/2018-11-09/blob/blobs/append_block.go new file mode 100644 index 0000000..7fed86a --- /dev/null +++ b/storage/2018-11-09/blob/blobs/append_block.go @@ -0,0 +1,170 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type AppendBlockInput struct { + + // A number indicating the byte offset to compare. + // Append Block will succeed only if the append position is equal to this number. + // If it is not, the request will fail with an AppendPositionConditionNotMet + // error (HTTP status code 412 – Precondition Failed) + BlobConditionAppendPosition *int64 + + // The max length in bytes permitted for the append blob. + // If the Append Block operation would cause the blob to exceed that limit or if the blob size + // is already greater than the value specified in this header, the request will fail with + // an MaxBlobSizeConditionNotMet error (HTTP status code 412 – Precondition Failed). + BlobConditionMaxSize *int64 + + // The Bytes which should be appended to the end of this Append Blob. + Content []byte + + // An MD5 hash of the block content. + // This hash is used to verify the integrity of the block during transport. + // When this header is specified, the storage service compares the hash of the content + // that has arrived with this header value. + // + // Note that this MD5 hash is not stored with the blob. + // If the two hashes do not match, the operation will fail with error code 400 (Bad Request). + ContentMD5 *string + + // Required if the blob has an active lease. + // To perform this operation on a blob with an active lease, specify the valid lease ID for this header. + LeaseID *string +} + +type AppendBlockResult struct { + autorest.Response + + BlobAppendOffset string + BlobCommittedBlockCount int64 + ContentMD5 string + ETag string + LastModified string +} + +// AppendBlock commits a new block of data to the end of an existing append blob. +func (client Client) AppendBlock(ctx context.Context, accountName, containerName, blobName string, input AppendBlockInput) (result AppendBlockResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "AppendBlock", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "AppendBlock", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "AppendBlock", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "AppendBlock", "`blobName` cannot be an empty string.") + } + if len(input.Content) > (4 * 1024 * 1024) { + return result, validation.NewError("files.Client", "PutByteRange", "`input.Content` must be at most 4MB.") + } + + req, err := client.AppendBlockPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "AppendBlock", nil, "Failure preparing request") + return + } + + resp, err := client.AppendBlockSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "AppendBlock", resp, "Failure sending request") + return + } + + result, err = client.AppendBlockResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "AppendBlock", resp, "Failure responding to request") + return + } + + return +} + +// AppendBlockPreparer prepares the AppendBlock request. +func (client Client) AppendBlockPreparer(ctx context.Context, accountName, containerName, blobName string, input AppendBlockInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "appendblock"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.BlobConditionAppendPosition != nil { + headers["x-ms-blob-condition-appendpos"] = *input.BlobConditionAppendPosition + } + if input.BlobConditionMaxSize != nil { + headers["x-ms-blob-condition-maxsize"] = *input.BlobConditionMaxSize + } + if input.ContentMD5 != nil { + headers["x-ms-blob-content-md5"] = *input.ContentMD5 + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers), + autorest.WithBytes(&input.Content)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// AppendBlockSender sends the AppendBlock request. The method will close the +// http.Response Body if it receives an error. +func (client Client) AppendBlockSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// AppendBlockResponder handles the response to the AppendBlock request. The method always +// closes the http.Response Body. +func (client Client) AppendBlockResponder(resp *http.Response) (result AppendBlockResult, err error) { + if resp != nil && resp.Header != nil { + result.BlobAppendOffset = resp.Header.Get("x-ms-blob-append-offset") + result.ContentMD5 = resp.Header.Get("ETag") + result.ETag = resp.Header.Get("ETag") + result.LastModified = resp.Header.Get("Last-Modified") + + if v := resp.Header.Get("x-ms-blob-committed-block-count"); v != "" { + i, innerErr := strconv.Atoi(v) + if innerErr != nil { + err = fmt.Errorf("Error parsing %q as an integer: %s", v, innerErr) + return + } + + result.BlobCommittedBlockCount = int64(i) + } + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/blobs/blob_append_test.go b/storage/2018-11-09/blob/blobs/blob_append_test.go new file mode 100644 index 0000000..6eb34a0 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/blob_append_test.go @@ -0,0 +1,155 @@ +package blobs + +import ( + "context" + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/containers" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestAppendBlobLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + fileName := "append-blob.txt" + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.BlobStorage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + containersClient := containers.NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithStorageResourceManagerAuth(containersClient.Client) + + _, err = containersClient.Create(ctx, accountName, containerName, containers.CreateInput{}) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + defer containersClient.Delete(ctx, accountName, containerName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + blobClient := NewWithEnvironment(client.Environment) + blobClient.Client = client.PrepareWithAuthorizer(blobClient.Client, storageAuth) + + t.Logf("[DEBUG] Putting Append Blob..") + if _, err := blobClient.PutAppendBlob(ctx, accountName, containerName, fileName, PutAppendBlobInput{}); err != nil { + t.Fatalf("Error putting append blob: %s", err) + } + + t.Logf("[DEBUG] Retrieving Properties..") + props, err := blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error retrieving properties: %s", err) + } + if props.ContentLength != 0 { + t.Fatalf("Expected Content-Length to be 0 but it was %d", props.ContentLength) + } + + t.Logf("[DEBUG] Appending First Block..") + appendInput := AppendBlockInput{ + Content: []byte{ + 12, + 48, + 93, + 76, + 29, + 10, + }, + } + if _, err := blobClient.AppendBlock(ctx, accountName, containerName, fileName, appendInput); err != nil { + t.Fatalf("Error appending first block: %s", err) + } + + t.Logf("[DEBUG] Re-Retrieving Properties..") + props, err = blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error retrieving properties: %s", err) + } + if props.ContentLength != 6 { + t.Fatalf("Expected Content-Length to be 6 but it was %d", props.ContentLength) + } + + t.Logf("[DEBUG] Appending Second Block..") + appendInput = AppendBlockInput{ + Content: []byte{ + 92, + 62, + 64, + 47, + 83, + 77, + }, + } + if _, err := blobClient.AppendBlock(ctx, accountName, containerName, fileName, appendInput); err != nil { + t.Fatalf("Error appending Second block: %s", err) + } + + t.Logf("[DEBUG] Re-Retrieving Properties..") + props, err = blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error retrieving properties: %s", err) + } + if props.ContentLength != 12 { + t.Fatalf("Expected Content-Length to be 12 but it was %d", props.ContentLength) + } + + t.Logf("[DEBUG] Acquiring Lease..") + leaseDetails, err := blobClient.AcquireLease(ctx, accountName, containerName, fileName, AcquireLeaseInput{ + LeaseDuration: -1, + }) + if err != nil { + t.Fatalf("Error acquiring Lease: %s", err) + } + t.Logf("[DEBUG] Lease ID is %q", leaseDetails.LeaseID) + + t.Logf("[DEBUG] Appending Third Block..") + appendInput = AppendBlockInput{ + Content: []byte{ + 64, + 35, + 28, + 93, + 11, + 23, + }, + LeaseID: &leaseDetails.LeaseID, + } + if _, err := blobClient.AppendBlock(ctx, accountName, containerName, fileName, appendInput); err != nil { + t.Fatalf("Error appending Third block: %s", err) + } + + t.Logf("[DEBUG] Re-Retrieving Properties..") + props, err = blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{ + LeaseID: &leaseDetails.LeaseID, + }) + if err != nil { + t.Fatalf("Error retrieving properties: %s", err) + } + if props.ContentLength != 18 { + t.Fatalf("Expected Content-Length to be 18 but it was %d", props.ContentLength) + } + + t.Logf("[DEBUG] Breaking Lease..") + breakLeaseInput := BreakLeaseInput{ + LeaseID: leaseDetails.LeaseID, + } + if _, err := blobClient.BreakLease(ctx, accountName, containerName, fileName, breakLeaseInput); err != nil { + t.Fatalf("Error breaking lease: %s", err) + } + + t.Logf("[DEBUG] Deleting Lease..") + if _, err := blobClient.Delete(ctx, accountName, containerName, fileName, DeleteInput{}); err != nil { + t.Fatalf("Error deleting: %s", err) + } +} diff --git a/storage/2018-11-09/blob/blobs/blob_page_test.go b/storage/2018-11-09/blob/blobs/blob_page_test.go new file mode 100644 index 0000000..6b1efa9 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/blob_page_test.go @@ -0,0 +1,89 @@ +package blobs + +import ( + "context" + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/containers" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestPageBlobLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + fileName := "append-blob.txt" + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.StorageV2) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + containersClient := containers.NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithStorageResourceManagerAuth(containersClient.Client) + + _, err = containersClient.Create(ctx, accountName, containerName, containers.CreateInput{}) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + defer containersClient.Delete(ctx, accountName, containerName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + blobClient := NewWithEnvironment(client.Environment) + blobClient.Client = client.PrepareWithAuthorizer(blobClient.Client, storageAuth) + + t.Logf("[DEBUG] Putting Page Blob..") + fileSize := int64(10240000) + if _, err := blobClient.PutPageBlob(ctx, accountName, containerName, fileName, PutPageBlobInput{ + BlobContentLengthBytes: fileSize, + }); err != nil { + t.Fatalf("Error putting page blob: %s", err) + } + + t.Logf("[DEBUG] Retrieving Properties..") + props, err := blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error retrieving properties: %s", err) + } + if props.ContentLength != fileSize { + t.Fatalf("Expected Content-Length to be %d but it was %d", fileSize, props.ContentLength) + } + + for iteration := 1; iteration <= 3; iteration++ { + t.Logf("[DEBUG] Putting Page %d of 3..", iteration) + byteArray := func() []byte { + o := make([]byte, 0) + + for i := 0; i < 512; i++ { + o = append(o, byte(i)) + } + + return o + }() + startByte := int64(512 * iteration) + endByte := int64(startByte + 511) + putPageInput := PutPageUpdateInput{ + StartByte: startByte, + EndByte: endByte, + Content: byteArray, + } + if _, err := blobClient.PutPageUpdate(ctx, accountName, containerName, fileName, putPageInput); err != nil { + t.Fatalf("Error putting page: %s", err) + } + } + + t.Logf("[DEBUG] Deleting..") + if _, err := blobClient.Delete(ctx, accountName, containerName, fileName, DeleteInput{}); err != nil { + t.Fatalf("Error deleting: %s", err) + } +} diff --git a/storage/2018-11-09/blob/blobs/client.go b/storage/2018-11-09/blob/blobs/client.go new file mode 100644 index 0000000..db20391 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/client.go @@ -0,0 +1,25 @@ +package blobs + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for Blob Storage Blobs. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithBaseURI creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2018-11-09/blob/blobs/copy.go b/storage/2018-11-09/blob/blobs/copy.go new file mode 100644 index 0000000..febaab5 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/copy.go @@ -0,0 +1,235 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type CopyInput struct { + // Specifies the name of the source blob or file. + // Beginning with version 2012-02-12, this value may be a URL of up to 2 KB in length that specifies a blob. + // The value should be URL-encoded as it would appear in a request URI. + // A source blob in the same storage account can be authenticated via Shared Key. + // However, if the source is a blob in another account, + // the source blob must either be public or must be authenticated via a shared access signature. + // If the source blob is public, no authentication is required to perform the copy operation. + // + // Beginning with version 2015-02-21, the source object may be a file in the Azure File service. + // If the source object is a file that is to be copied to a blob, then the source file must be authenticated + // using a shared access signature, whether it resides in the same account or in a different account. + // + // Only storage accounts created on or after June 7th, 2012 allow the Copy Blob operation to + // copy from another storage account. + CopySource string + + // The ID of the Lease + // Required if the destination blob has an active lease. + // The lease ID specified for this header must match the lease ID of the destination blob. + // If the request does not include the lease ID or it is not valid, + // the operation fails with status code 412 (Precondition Failed). + // + // If this header is specified and the destination blob does not currently have an active lease, + // the operation will also fail with status code 412 (Precondition Failed). + LeaseID *string + + // The ID of the Lease on the Source Blob + // Specify to perform the Copy Blob operation only if the lease ID matches the active lease ID of the source blob. + SourceLeaseID *string + + // For page blobs on a premium account only. Specifies the tier to be set on the target blob + AccessTier *AccessTier + + // A user-defined name-value pair associated with the blob. + // If no name-value pairs are specified, the operation will copy the metadata from the source blob or + // file to the destination blob. + // If one or more name-value pairs are specified, the destination blob is created with the specified metadata, + // and metadata is not copied from the source blob or file. + MetaData map[string]string + + // An ETag value. + // Specify an ETag value for this conditional header to copy the blob only if the specified + // ETag value matches the ETag value for an existing destination blob. + // If the ETag for the destination blob does not match the ETag specified for If-Match, + // the Blob service returns status code 412 (Precondition Failed). + IfMatch *string + + // An ETag value, or the wildcard character (*). + // Specify an ETag value for this conditional header to copy the blob only if the specified + // ETag value does not match the ETag value for the destination blob. + // Specify the wildcard character (*) to perform the operation only if the destination blob does not exist. + // If the specified condition isn't met, the Blob service returns status code 412 (Precondition Failed). + IfNoneMatch *string + + // A DateTime value. + // Specify this conditional header to copy the blob only if the destination blob + // has been modified since the specified date/time. + // If the destination blob has not been modified, the Blob service returns status code 412 (Precondition Failed). + IfModifiedSince *string + + // A DateTime value. + // Specify this conditional header to copy the blob only if the destination blob + // has not been modified since the specified date/time. + // If the destination blob has been modified, the Blob service returns status code 412 (Precondition Failed). + IfUnmodifiedSince *string + + // An ETag value. + // Specify this conditional header to copy the source blob only if its ETag matches the value specified. + // If the ETag values do not match, the Blob service returns status code 412 (Precondition Failed). + // This cannot be specified if the source is an Azure File. + SourceIfMatch *string + + // An ETag value. + // Specify this conditional header to copy the blob only if its ETag does not match the value specified. + // If the values are identical, the Blob service returns status code 412 (Precondition Failed). + // This cannot be specified if the source is an Azure File. + SourceIfNoneMatch *string + + // A DateTime value. + // Specify this conditional header to copy the blob only if the source blob has been modified + // since the specified date/time. + // If the source blob has not been modified, the Blob service returns status code 412 (Precondition Failed). + // This cannot be specified if the source is an Azure File. + SourceIfModifiedSince *string + + // A DateTime value. + // Specify this conditional header to copy the blob only if the source blob has not been modified + // since the specified date/time. + // If the source blob has been modified, the Blob service returns status code 412 (Precondition Failed). + // This header cannot be specified if the source is an Azure File. + SourceIfUnmodifiedSince *string +} + +type CopyResult struct { + autorest.Response + + CopyID string + CopyStatus string +} + +// Copy copies a blob to a destination within the storage account asynchronously. +func (client Client) Copy(ctx context.Context, accountName, containerName, blobName string, input CopyInput) (result CopyResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "Copy", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "Copy", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "Copy", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "Copy", "`blobName` cannot be an empty string.") + } + if input.CopySource == "" { + return result, validation.NewError("blobs.Client", "Copy", "`input.CopySource` cannot be an empty string.") + } + + req, err := client.CopyPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Copy", nil, "Failure preparing request") + return + } + + resp, err := client.CopySender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "Copy", resp, "Failure sending request") + return + } + + result, err = client.CopyResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Copy", resp, "Failure responding to request") + return + } + + return +} + +// CopyPreparer prepares the Copy request. +func (client Client) CopyPreparer(ctx context.Context, accountName, containerName, blobName string, input CopyInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-copy-source": autorest.Encode("header", input.CopySource), + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + if input.SourceLeaseID != nil { + headers["x-ms-source-lease-id"] = *input.SourceLeaseID + } + if input.AccessTier != nil { + headers["x-ms-access-tier"] = string(*input.AccessTier) + } + + if input.IfMatch != nil { + headers["If-Match"] = *input.IfMatch + } + if input.IfNoneMatch != nil { + headers["If-None-Match"] = *input.IfNoneMatch + } + if input.IfUnmodifiedSince != nil { + headers["If-Unmodified-Since"] = *input.IfUnmodifiedSince + } + if input.IfModifiedSince != nil { + headers["If-Modified-Since"] = *input.IfModifiedSince + } + + if input.SourceIfMatch != nil { + headers["x-ms-source-if-match"] = *input.SourceIfMatch + } + if input.SourceIfNoneMatch != nil { + headers["x-ms-source-if-none-match"] = *input.SourceIfNoneMatch + } + if input.SourceIfModifiedSince != nil { + headers["x-ms-source-if-modified-since"] = *input.SourceIfModifiedSince + } + if input.SourceIfUnmodifiedSince != nil { + headers["x-ms-source-if-unmodified-since"] = *input.SourceIfUnmodifiedSince + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CopySender sends the Copy request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CopySender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CopyResponder handles the response to the Copy request. The method always +// closes the http.Response Body. +func (client Client) CopyResponder(resp *http.Response) (result CopyResult, err error) { + if resp != nil && resp.Header != nil { + result.CopyID = resp.Header.Get("x-ms-copy-id") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-11-09/blob/blobs/copy_abort.go b/storage/2018-11-09/blob/blobs/copy_abort.go new file mode 100644 index 0000000..a992ff1 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/copy_abort.go @@ -0,0 +1,110 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type AbortCopyInput struct { + // The Copy ID which should be aborted + CopyID string + + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string +} + +// AbortCopy aborts a pending Copy Blob operation, and leaves a destination blob with zero length and full metadata. +func (client Client) AbortCopy(ctx context.Context, accountName, containerName, blobName string, input AbortCopyInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "AbortCopy", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "AbortCopy", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "AbortCopy", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "AbortCopy", "`blobName` cannot be an empty string.") + } + if input.CopyID == "" { + return result, validation.NewError("blobs.Client", "AbortCopy", "`input.CopyID` cannot be an empty string.") + } + + req, err := client.AbortCopyPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "AbortCopy", nil, "Failure preparing request") + return + } + + resp, err := client.AbortCopySender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "AbortCopy", resp, "Failure sending request") + return + } + + result, err = client.AbortCopyResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "AbortCopy", resp, "Failure responding to request") + return + } + + return +} + +// AbortCopyPreparer prepares the AbortCopy request. +func (client Client) AbortCopyPreparer(ctx context.Context, accountName, containerName, blobName string, input AbortCopyInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "copy"), + "copyid": autorest.Encode("query", input.CopyID), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-copy-action": "abort", + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// AbortCopySender sends the AbortCopy request. The method will close the +// http.Response Body if it receives an error. +func (client Client) AbortCopySender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// AbortCopyResponder handles the response to the AbortCopy request. The method always +// closes the http.Response Body. +func (client Client) AbortCopyResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-11-09/blob/blobs/copy_and_wait.go b/storage/2018-11-09/blob/blobs/copy_and_wait.go new file mode 100644 index 0000000..a1e7fa4 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/copy_and_wait.go @@ -0,0 +1,41 @@ +package blobs + +import ( + "context" + "fmt" + "time" +) + +// CopyAndWait copies a blob to a destination within the storage account and waits for it to finish copying. +func (client Client) CopyAndWait(ctx context.Context, accountName, containerName, blobName string, input CopyInput, pollingInterval time.Duration) error { + if _, err := client.Copy(ctx, accountName, containerName, blobName, input); err != nil { + return fmt.Errorf("Error copying: %s", err) + } + + for true { + getInput := GetPropertiesInput{ + LeaseID: input.LeaseID, + } + getResult, err := client.GetProperties(ctx, accountName, containerName, blobName, getInput) + if err != nil { + return fmt.Errorf("") + } + + switch getResult.CopyStatus { + case Aborted: + return fmt.Errorf("Copy was aborted: %s", getResult.CopyStatusDescription) + + case Failed: + return fmt.Errorf("Copy failed: %s", getResult.CopyStatusDescription) + + case Success: + return nil + + case Pending: + time.Sleep(pollingInterval) + continue + } + } + + return fmt.Errorf("Unexpected error waiting for the copy to complete") +} diff --git a/storage/2018-11-09/blob/blobs/copy_test.go b/storage/2018-11-09/blob/blobs/copy_test.go new file mode 100644 index 0000000..4c2a7d7 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/copy_test.go @@ -0,0 +1,148 @@ +package blobs + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/containers" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestCopyFromExistingFile(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + fileName := "ubuntu.iso" + copiedFileName := "copied.iso" + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.BlobStorage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + containersClient := containers.NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithStorageResourceManagerAuth(containersClient.Client) + + _, err = containersClient.Create(ctx, accountName, containerName, containers.CreateInput{}) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + defer containersClient.Delete(ctx, accountName, containerName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + blobClient := NewWithEnvironment(client.Environment) + blobClient.Client = client.PrepareWithAuthorizer(blobClient.Client, storageAuth) + + t.Logf("[DEBUG] Copying file to Blob Storage..") + copyInput := CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + + refreshInterval := 5 * time.Second + if err := blobClient.CopyAndWait(ctx, accountName, containerName, fileName, copyInput, refreshInterval); err != nil { + t.Fatalf("Error copying: %s", err) + } + + t.Logf("[DEBUG] Duplicating that file..") + copiedInput := CopyInput{ + CopySource: fmt.Sprintf("%s/%s/%s", endpoints.GetBlobEndpoint(blobClient.BaseURI, accountName), containerName, fileName), + } + if err := blobClient.CopyAndWait(ctx, accountName, containerName, copiedFileName, copiedInput, refreshInterval); err != nil { + t.Fatalf("Error duplicating file: %s", err) + } + + t.Logf("[DEBUG] Retrieving Properties for the Original File..") + props, err := blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error getting properties for the original file: %s", err) + } + + t.Logf("[DEBUG] Retrieving Properties for the Copied File..") + copiedProps, err := blobClient.GetProperties(ctx, accountName, containerName, copiedFileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error getting properties for the copied file: %s", err) + } + + if props.ContentLength != copiedProps.ContentLength { + t.Fatalf("Expected the content length to be %d but it was %d", props.ContentLength, copiedProps.ContentLength) + } + + t.Logf("[DEBUG] Deleting copied file..") + if _, err := blobClient.Delete(ctx, accountName, containerName, copiedFileName, DeleteInput{}); err != nil { + t.Fatalf("Error deleting file: %s", err) + } + + t.Logf("[DEBUG] Deleting original file..") + if _, err := blobClient.Delete(ctx, accountName, containerName, fileName, DeleteInput{}); err != nil { + t.Fatalf("Error deleting file: %s", err) + } +} + +func TestCopyFromURL(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + fileName := "ubuntu.iso" + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.BlobStorage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + containersClient := containers.NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithStorageResourceManagerAuth(containersClient.Client) + + _, err = containersClient.Create(ctx, accountName, containerName, containers.CreateInput{}) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + defer containersClient.Delete(ctx, accountName, containerName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + blobClient := NewWithEnvironment(client.Environment) + blobClient.Client = client.PrepareWithAuthorizer(blobClient.Client, storageAuth) + + t.Logf("[DEBUG] Copying file to Blob Storage..") + copyInput := CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + + refreshInterval := 5 * time.Second + if err := blobClient.CopyAndWait(ctx, accountName, containerName, fileName, copyInput, refreshInterval); err != nil { + t.Fatalf("Error copying: %s", err) + } + + t.Logf("[DEBUG] Retrieving Properties..") + props, err := blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error getting properties: %s", err) + } + + if props.ContentLength == 0 { + t.Fatalf("Expected the file to be there but looks like it isn't: %d", props.ContentLength) + } + + t.Logf("[DEBUG] Deleting file..") + if _, err := blobClient.Delete(ctx, accountName, containerName, fileName, DeleteInput{}); err != nil { + t.Fatalf("Error deleting file: %s", err) + } +} diff --git a/storage/2018-11-09/blob/blobs/delete.go b/storage/2018-11-09/blob/blobs/delete.go new file mode 100644 index 0000000..c1c642d --- /dev/null +++ b/storage/2018-11-09/blob/blobs/delete.go @@ -0,0 +1,105 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type DeleteInput struct { + // Should any Snapshots for this Blob also be deleted? + // If the Blob has Snapshots and this is set to False a 409 Conflict will be returned + DeleteSnapshots bool + + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string +} + +// Delete marks the specified blob or snapshot for deletion. The blob is later deleted during garbage collection. +func (client Client) Delete(ctx context.Context, accountName, containerName, blobName string, input DeleteInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "Delete", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "Delete", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "Delete", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "Delete", "`blobName` cannot be an empty string.") + } + + req, err := client.DeletePreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName, containerName, blobName string, input DeleteInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + if input.DeleteSnapshots { + headers["x-ms-delete-snapshots"] = "include" + } + + preparer := autorest.CreatePreparer( + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-11-09/blob/blobs/delete_snapshot.go b/storage/2018-11-09/blob/blobs/delete_snapshot.go new file mode 100644 index 0000000..18c3d4c --- /dev/null +++ b/storage/2018-11-09/blob/blobs/delete_snapshot.go @@ -0,0 +1,108 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type DeleteSnapshotInput struct { + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string + + // The DateTime of the Snapshot which should be marked for Deletion + SnapshotDateTime string +} + +// DeleteSnapshot marks a single Snapshot of a Blob for Deletion based on it's DateTime, which will be deleted during the next Garbage Collection cycle. +func (client Client) DeleteSnapshot(ctx context.Context, accountName, containerName, blobName string, input DeleteSnapshotInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "DeleteSnapshot", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "DeleteSnapshot", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "DeleteSnapshot", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "DeleteSnapshot", "`blobName` cannot be an empty string.") + } + if input.SnapshotDateTime == "" { + return result, validation.NewError("blobs.Client", "DeleteSnapshot", "`input.SnapshotDateTime` cannot be an empty string.") + } + + req, err := client.DeleteSnapshotPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "DeleteSnapshot", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSnapshotSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "DeleteSnapshot", resp, "Failure sending request") + return + } + + result, err = client.DeleteSnapshotResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "DeleteSnapshot", resp, "Failure responding to request") + return + } + + return +} + +// DeleteSnapshotPreparer prepares the DeleteSnapshot request. +func (client Client) DeleteSnapshotPreparer(ctx context.Context, accountName, containerName, blobName string, input DeleteSnapshotInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "snapshot": autorest.Encode("query", input.SnapshotDateTime), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSnapshotSender sends the DeleteSnapshot request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSnapshotSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteSnapshotResponder handles the response to the DeleteSnapshot request. The method always +// closes the http.Response Body. +func (client Client) DeleteSnapshotResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-11-09/blob/blobs/delete_snapshots.go b/storage/2018-11-09/blob/blobs/delete_snapshots.go new file mode 100644 index 0000000..e7e2b66 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/delete_snapshots.go @@ -0,0 +1,99 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type DeleteSnapshotsInput struct { + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string +} + +// DeleteSnapshots marks all Snapshots of a Blob for Deletion, which will be deleted during the next Garbage Collection Cycle. +func (client Client) DeleteSnapshots(ctx context.Context, accountName, containerName, blobName string, input DeleteSnapshotsInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "DeleteSnapshots", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "DeleteSnapshots", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "DeleteSnapshots", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "DeleteSnapshots", "`blobName` cannot be an empty string.") + } + + req, err := client.DeleteSnapshotsPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "DeleteSnapshots", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSnapshotsSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "DeleteSnapshots", resp, "Failure sending request") + return + } + + result, err = client.DeleteSnapshotsResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "DeleteSnapshots", resp, "Failure responding to request") + return + } + + return +} + +// DeleteSnapshotsPreparer prepares the DeleteSnapshots request. +func (client Client) DeleteSnapshotsPreparer(ctx context.Context, accountName, containerName, blobName string, input DeleteSnapshotsInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + // only delete the snapshots but leave the blob as-is + "x-ms-delete-snapshots": "only", + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSnapshotsSender sends the DeleteSnapshots request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSnapshotsSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteSnapshotsResponder handles the response to the DeleteSnapshots request. The method always +// closes the http.Response Body. +func (client Client) DeleteSnapshotsResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-11-09/blob/blobs/get.go b/storage/2018-11-09/blob/blobs/get.go new file mode 100644 index 0000000..fa88081 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/get.go @@ -0,0 +1,116 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetInput struct { + LeaseID *string + StartByte *int64 + EndByte *int64 +} + +type GetResult struct { + autorest.Response + + Contents []byte +} + +// Get reads or downloads a blob from the system, including its metadata and properties. +func (client Client) Get(ctx context.Context, accountName, containerName, blobName string, input GetInput) (result GetResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "Get", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "Get", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "Get", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "Get", "`blobName` cannot be an empty string.") + } + if input.LeaseID != nil && *input.LeaseID == "" { + return result, validation.NewError("blobs.Client", "Get", "`input.LeaseID` should either be specified or nil, not an empty string.") + } + if (input.StartByte != nil && input.EndByte == nil) || input.StartByte == nil && input.EndByte != nil { + return result, validation.NewError("blobs.Client", "Get", "`input.StartByte` and `input.EndByte` must both be specified, or both be nil.") + } + + req, err := client.GetPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Get", nil, "Failure preparing request") + return + } + + resp, err := client.GetSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "Get", resp, "Failure sending request") + return + } + + result, err = client.GetResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Get", resp, "Failure responding to request") + return + } + + return +} + +// GetPreparer prepares the Get request. +func (client Client) GetPreparer(ctx context.Context, accountName, containerName, blobName string, input GetInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.StartByte != nil && input.EndByte != nil { + headers["x-ms-range"] = fmt.Sprintf("bytes=%d-%d", *input.StartByte, *input.EndByte) + } + + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetSender sends the Get request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetResponder handles the response to the Get request. The method always +// closes the http.Response Body. +func (client Client) GetResponder(resp *http.Response) (result GetResult, err error) { + if resp != nil { + result.Contents = make([]byte, resp.ContentLength) + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusPartialContent), + autorest.ByUnmarshallingBytes(&result.Contents), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/blobs/get_block_list.go b/storage/2018-11-09/blob/blobs/get_block_list.go new file mode 100644 index 0000000..9f8120c --- /dev/null +++ b/storage/2018-11-09/blob/blobs/get_block_list.go @@ -0,0 +1,140 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetBlockListInput struct { + BlockListType BlockListType + LeaseID *string +} + +type GetBlockListResult struct { + autorest.Response + + // The size of the blob in bytes + ContentLength *int64 + + // The Content Type of the blob + ContentType string + + // The ETag associated with this blob + ETag string + + // A list of blocks which have been committed + CommittedBlocks CommittedBlocks `xml:"CommittedBlocks,omitempty"` + + // A list of blocks which have not yet been committed + UncommittedBlocks UncommittedBlocks `xml:"UncommittedBlocks,omitempty"` +} + +// GetBlockList retrieves the list of blocks that have been uploaded as part of a block blob. +func (client Client) GetBlockList(ctx context.Context, accountName, containerName, blobName string, input GetBlockListInput) (result GetBlockListResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "GetBlockList", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "GetBlockList", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "GetBlockList", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "GetBlockList", "`blobName` cannot be an empty string.") + } + + req, err := client.GetBlockListPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetBlockList", nil, "Failure preparing request") + return + } + + resp, err := client.GetBlockListSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "GetBlockList", resp, "Failure sending request") + return + } + + result, err = client.GetBlockListResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetBlockList", resp, "Failure responding to request") + return + } + + return +} + +// GetBlockListPreparer prepares the GetBlockList request. +func (client Client) GetBlockListPreparer(ctx context.Context, accountName, containerName, blobName string, input GetBlockListInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "blocklisttype": autorest.Encode("query", string(input.BlockListType)), + "comp": autorest.Encode("query", "blocklist"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetBlockListSender sends the GetBlockList request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetBlockListSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetBlockListResponder handles the response to the GetBlockList request. The method always +// closes the http.Response Body. +func (client Client) GetBlockListResponder(resp *http.Response) (result GetBlockListResult, err error) { + if resp != nil && resp.Header != nil { + result.ContentType = resp.Header.Get("Content-Type") + result.ETag = resp.Header.Get("ETag") + + if v := resp.Header.Get("x-ms-blob-content-length"); v != "" { + i, innerErr := strconv.Atoi(v) + if innerErr != nil { + err = fmt.Errorf("Error parsing %q as an integer: %s", v, innerErr) + return + } + + i64 := int64(i) + result.ContentLength = &i64 + } + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-11-09/blob/blobs/get_page_ranges.go b/storage/2018-11-09/blob/blobs/get_page_ranges.go new file mode 100644 index 0000000..37abf63 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/get_page_ranges.go @@ -0,0 +1,152 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetPageRangesInput struct { + LeaseID *string + + StartByte *int64 + EndByte *int64 +} + +type GetPageRangesResult struct { + autorest.Response + + // The size of the blob in bytes + ContentLength *int64 + + // The Content Type of the blob + ContentType string + + // The ETag associated with this blob + ETag string + + PageRanges []PageRange `xml:"PageRange"` +} + +type PageRange struct { + // The start byte offset for this range, inclusive + Start int64 `xml:"Start"` + + // The end byte offset for this range, inclusive + End int64 `xml:"End"` +} + +// GetPageRanges returns the list of valid page ranges for a page blob or snapshot of a page blob. +func (client Client) GetPageRanges(ctx context.Context, accountName, containerName, blobName string, input GetPageRangesInput) (result GetPageRangesResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "GetPageRanges", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "GetPageRanges", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "GetPageRanges", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "GetPageRanges", "`blobName` cannot be an empty string.") + } + if (input.StartByte != nil && input.EndByte == nil) || input.StartByte == nil && input.EndByte != nil { + return result, validation.NewError("blobs.Client", "GetPageRanges", "`input.StartByte` and `input.EndByte` must both be specified, or both be nil.") + } + + req, err := client.GetPageRangesPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetPageRanges", nil, "Failure preparing request") + return + } + + resp, err := client.GetPageRangesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "GetPageRanges", resp, "Failure sending request") + return + } + + result, err = client.GetPageRangesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetPageRanges", resp, "Failure responding to request") + return + } + + return +} + +// GetPageRangesPreparer prepares the GetPageRanges request. +func (client Client) GetPageRangesPreparer(ctx context.Context, accountName, containerName, blobName string, input GetPageRangesInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "pagelist"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + if input.StartByte != nil && input.EndByte != nil { + headers["x-ms-range"] = fmt.Sprintf("bytes=%d-%d", *input.StartByte, *input.EndByte) + } + + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetPageRangesSender sends the GetPageRanges request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetPageRangesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetPageRangesResponder handles the response to the GetPageRanges request. The method always +// closes the http.Response Body. +func (client Client) GetPageRangesResponder(resp *http.Response) (result GetPageRangesResult, err error) { + if resp != nil && resp.Header != nil { + result.ContentType = resp.Header.Get("Content-Type") + result.ETag = resp.Header.Get("ETag") + + if v := resp.Header.Get("x-ms-blob-content-length"); v != "" { + i, innerErr := strconv.Atoi(v) + if innerErr != nil { + err = fmt.Errorf("Error parsing %q as an integer: %s", v, innerErr) + return + } + + i64 := int64(i) + result.ContentLength = &i64 + } + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-11-09/blob/blobs/incremental_copy_blob.go b/storage/2018-11-09/blob/blobs/incremental_copy_blob.go new file mode 100644 index 0000000..7fb7e6b --- /dev/null +++ b/storage/2018-11-09/blob/blobs/incremental_copy_blob.go @@ -0,0 +1,120 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type IncrementalCopyBlobInput struct { + CopySource string + IfModifiedSince *string + IfUnmodifiedSince *string + IfMatch *string + IfNoneMatch *string +} + +// IncrementalCopyBlob copies a snapshot of the source page blob to a destination page blob. +// The snapshot is copied such that only the differential changes between the previously copied +// snapshot are transferred to the destination. +// The copied snapshots are complete copies of the original snapshot and can be read or copied from as usual. +func (client Client) IncrementalCopyBlob(ctx context.Context, accountName, containerName, blobName string, input IncrementalCopyBlobInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "IncrementalCopyBlob", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "IncrementalCopyBlob", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "IncrementalCopyBlob", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "IncrementalCopyBlob", "`blobName` cannot be an empty string.") + } + if input.CopySource == "" { + return result, validation.NewError("blobs.Client", "IncrementalCopyBlob", "`input.CopySource` cannot be an empty string.") + } + + req, err := client.IncrementalCopyBlobPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "IncrementalCopyBlob", nil, "Failure preparing request") + return + } + + resp, err := client.IncrementalCopyBlobSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "IncrementalCopyBlob", resp, "Failure sending request") + return + } + + result, err = client.IncrementalCopyBlobResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "IncrementalCopyBlob", resp, "Failure responding to request") + return + } + + return +} + +// IncrementalCopyBlobPreparer prepares the IncrementalCopyBlob request. +func (client Client) IncrementalCopyBlobPreparer(ctx context.Context, accountName, containerName, blobName string, input IncrementalCopyBlobInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "incrementalcopy"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-copy-source": input.CopySource, + } + + if input.IfModifiedSince != nil { + headers["If-Modified-Since"] = *input.IfModifiedSince + } + if input.IfUnmodifiedSince != nil { + headers["If-Unmodified-Since"] = *input.IfUnmodifiedSince + } + if input.IfMatch != nil { + headers["If-Match"] = *input.IfMatch + } + if input.IfNoneMatch != nil { + headers["If-None-Match"] = *input.IfNoneMatch + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// IncrementalCopyBlobSender sends the IncrementalCopyBlob request. The method will close the +// http.Response Body if it receives an error. +func (client Client) IncrementalCopyBlobSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// IncrementalCopyBlobResponder handles the response to the IncrementalCopyBlob request. The method always +// closes the http.Response Body. +func (client Client) IncrementalCopyBlobResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-11-09/blob/blobs/lease_acquire.go b/storage/2018-11-09/blob/blobs/lease_acquire.go new file mode 100644 index 0000000..432c1f5 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/lease_acquire.go @@ -0,0 +1,135 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type AcquireLeaseInput struct { + // The ID of the existing Lease, if leased + LeaseID *string + + // Specifies the duration of the lease, in seconds, or negative one (-1) for a lease that never expires. + // A non-infinite lease can be between 15 and 60 seconds + LeaseDuration int + + // The Proposed new ID for the Lease + ProposedLeaseID *string +} + +type AcquireLeaseResult struct { + autorest.Response + + LeaseID string +} + +// AcquireLease establishes and manages a lock on a blob for write and delete operations. +func (client Client) AcquireLease(ctx context.Context, accountName, containerName, blobName string, input AcquireLeaseInput) (result AcquireLeaseResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "AcquireLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "AcquireLease", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "AcquireLease", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "AcquireLease", "`blobName` cannot be an empty string.") + } + if input.LeaseID != nil && *input.LeaseID == "" { + return result, validation.NewError("blobs.Client", "AcquireLease", "`input.LeaseID` cannot be an empty string, if specified.") + } + if input.ProposedLeaseID != nil && *input.ProposedLeaseID == "" { + return result, validation.NewError("blobs.Client", "AcquireLease", "`input.ProposedLeaseID` cannot be an empty string, if specified.") + } + // An infinite lease duration is -1 seconds. A non-infinite lease can be between 15 and 60 seconds + if input.LeaseDuration != -1 && (input.LeaseDuration <= 15 || input.LeaseDuration >= 60) { + return result, validation.NewError("blobs.Client", "AcquireLease", "`input.LeaseDuration` must be -1 (infinite), or between 15 and 60 seconds.") + } + + req, err := client.AcquireLeasePreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "AcquireLease", nil, "Failure preparing request") + return + } + + resp, err := client.AcquireLeaseSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "AcquireLease", resp, "Failure sending request") + return + } + + result, err = client.AcquireLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "AcquireLease", resp, "Failure responding to request") + return + } + + return +} + +// AcquireLeasePreparer prepares the AcquireLease request. +func (client Client) AcquireLeasePreparer(ctx context.Context, accountName, containerName, blobName string, input AcquireLeaseInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "acquire", + "x-ms-lease-duration": input.LeaseDuration, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + if input.ProposedLeaseID != nil { + headers["x-ms-proposed-lease-id"] = input.ProposedLeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// AcquireLeaseSender sends the AcquireLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) AcquireLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// AcquireLeaseResponder handles the response to the AcquireLease request. The method always +// closes the http.Response Body. +func (client Client) AcquireLeaseResponder(resp *http.Response) (result AcquireLeaseResult, err error) { + if resp != nil && resp.Header != nil { + result.LeaseID = resp.Header.Get("x-ms-lease-id") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/blobs/lease_break.go b/storage/2018-11-09/blob/blobs/lease_break.go new file mode 100644 index 0000000..d564204 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/lease_break.go @@ -0,0 +1,124 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type BreakLeaseInput struct { + // For a break operation, proposed duration the lease should continue + // before it is broken, in seconds, between 0 and 60. + // This break period is only used if it is shorter than the time remaining on the lease. + // If longer, the time remaining on the lease is used. + // A new lease will not be available before the break period has expired, + // but the lease may be held for longer than the break period. + // If this header does not appear with a break operation, a fixed-duration lease breaks + // after the remaining lease period elapses, and an infinite lease breaks immediately. + BreakPeriod *int + + LeaseID string +} + +type BreakLeaseResponse struct { + autorest.Response + + // Approximate time remaining in the lease period, in seconds. + // If the break is immediate, 0 is returned. + LeaseTime int +} + +// BreakLease breaks an existing lock on a blob using the LeaseID. +func (client Client) BreakLease(ctx context.Context, accountName, containerName, blobName string, input BreakLeaseInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "BreakLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "BreakLease", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "BreakLease", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "BreakLease", "`blobName` cannot be an empty string.") + } + if input.LeaseID == "" { + return result, validation.NewError("blobs.Client", "BreakLease", "`input.LeaseID` cannot be an empty string.") + } + + req, err := client.BreakLeasePreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "BreakLease", nil, "Failure preparing request") + return + } + + resp, err := client.BreakLeaseSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "BreakLease", resp, "Failure sending request") + return + } + + result, err = client.BreakLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "BreakLease", resp, "Failure responding to request") + return + } + + return +} + +// BreakLeasePreparer prepares the BreakLease request. +func (client Client) BreakLeasePreparer(ctx context.Context, accountName, containerName, blobName string, input BreakLeaseInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "break", + "x-ms-lease-id": input.LeaseID, + } + + if input.BreakPeriod != nil { + headers["x-ms-lease-break-period"] = *input.BreakPeriod + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// BreakLeaseSender sends the BreakLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) BreakLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// BreakLeaseResponder handles the response to the BreakLease request. The method always +// closes the http.Response Body. +func (client Client) BreakLeaseResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/blobs/lease_change.go b/storage/2018-11-09/blob/blobs/lease_change.go new file mode 100644 index 0000000..c57f9db --- /dev/null +++ b/storage/2018-11-09/blob/blobs/lease_change.go @@ -0,0 +1,117 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type ChangeLeaseInput struct { + ExistingLeaseID string + ProposedLeaseID string +} + +type ChangeLeaseResponse struct { + autorest.Response + + LeaseID string +} + +// ChangeLease changes an existing lock on a blob for another lock. +func (client Client) ChangeLease(ctx context.Context, accountName, containerName, blobName string, input ChangeLeaseInput) (result ChangeLeaseResponse, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "ChangeLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "ChangeLease", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "ChangeLease", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "ChangeLease", "`blobName` cannot be an empty string.") + } + if input.ExistingLeaseID == "" { + return result, validation.NewError("blobs.Client", "ChangeLease", "`input.ExistingLeaseID` cannot be an empty string.") + } + if input.ProposedLeaseID == "" { + return result, validation.NewError("blobs.Client", "ChangeLease", "`input.ProposedLeaseID` cannot be an empty string.") + } + + req, err := client.ChangeLeasePreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "ChangeLease", nil, "Failure preparing request") + return + } + + resp, err := client.ChangeLeaseSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "ChangeLease", resp, "Failure sending request") + return + } + + result, err = client.ChangeLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "ChangeLease", resp, "Failure responding to request") + return + } + + return +} + +// ChangeLeasePreparer prepares the ChangeLease request. +func (client Client) ChangeLeasePreparer(ctx context.Context, accountName, containerName, blobName string, input ChangeLeaseInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "change", + "x-ms-lease-id": input.ExistingLeaseID, + "x-ms-proposed-lease-id": input.ProposedLeaseID, + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ChangeLeaseSender sends the ChangeLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ChangeLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ChangeLeaseResponder handles the response to the ChangeLease request. The method always +// closes the http.Response Body. +func (client Client) ChangeLeaseResponder(resp *http.Response) (result ChangeLeaseResponse, err error) { + if resp != nil && resp.Header != nil { + result.LeaseID = resp.Header.Get("x-ms-lease-id") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/blobs/lease_release.go b/storage/2018-11-09/blob/blobs/lease_release.go new file mode 100644 index 0000000..0226cdf --- /dev/null +++ b/storage/2018-11-09/blob/blobs/lease_release.go @@ -0,0 +1,98 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// ReleaseLease releases a lock based on the Lease ID. +func (client Client) ReleaseLease(ctx context.Context, accountName, containerName, blobName, leaseID string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "ReleaseLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "ReleaseLease", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "ReleaseLease", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "ReleaseLease", "`blobName` cannot be an empty string.") + } + if leaseID == "" { + return result, validation.NewError("blobs.Client", "ReleaseLease", "`leaseID` cannot be an empty string.") + } + + req, err := client.ReleaseLeasePreparer(ctx, accountName, containerName, blobName, leaseID) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "ReleaseLease", nil, "Failure preparing request") + return + } + + resp, err := client.ReleaseLeaseSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "ReleaseLease", resp, "Failure sending request") + return + } + + result, err = client.ReleaseLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "ReleaseLease", resp, "Failure responding to request") + return + } + + return +} + +// ReleaseLeasePreparer prepares the ReleaseLease request. +func (client Client) ReleaseLeasePreparer(ctx context.Context, accountName, containerName, blobName, leaseID string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "release", + "x-ms-lease-id": leaseID, + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ReleaseLeaseSender sends the ReleaseLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ReleaseLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ReleaseLeaseResponder handles the response to the ReleaseLease request. The method always +// closes the http.Response Body. +func (client Client) ReleaseLeaseResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/blobs/lease_renew.go b/storage/2018-11-09/blob/blobs/lease_renew.go new file mode 100644 index 0000000..69c495b --- /dev/null +++ b/storage/2018-11-09/blob/blobs/lease_renew.go @@ -0,0 +1,97 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +func (client Client) RenewLease(ctx context.Context, accountName, containerName, blobName, leaseID string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "RenewLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "RenewLease", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "RenewLease", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "RenewLease", "`blobName` cannot be an empty string.") + } + if leaseID == "" { + return result, validation.NewError("blobs.Client", "RenewLease", "`leaseID` cannot be an empty string.") + } + + req, err := client.RenewLeasePreparer(ctx, accountName, containerName, blobName, leaseID) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "RenewLease", nil, "Failure preparing request") + return + } + + resp, err := client.RenewLeaseSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "RenewLease", resp, "Failure sending request") + return + } + + result, err = client.RenewLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "RenewLease", resp, "Failure responding to request") + return + } + + return +} + +// RenewLeasePreparer prepares the RenewLease request. +func (client Client) RenewLeasePreparer(ctx context.Context, accountName, containerName, blobName, leaseID string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "renew", + "x-ms-lease-id": leaseID, + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// RenewLeaseSender sends the RenewLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) RenewLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// RenewLeaseResponder handles the response to the RenewLease request. The method always +// closes the http.Response Body. +func (client Client) RenewLeaseResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/blobs/lease_test.go b/storage/2018-11-09/blob/blobs/lease_test.go new file mode 100644 index 0000000..7600ab8 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/lease_test.go @@ -0,0 +1,106 @@ +package blobs + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/containers" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestLeaseLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + fileName := "ubuntu.iso" + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.BlobStorage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + containersClient := containers.NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithStorageResourceManagerAuth(containersClient.Client) + + _, err = containersClient.Create(ctx, accountName, containerName, containers.CreateInput{}) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + defer containersClient.Delete(ctx, accountName, containerName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + blobClient := NewWithEnvironment(client.Environment) + blobClient.Client = client.PrepareWithAuthorizer(blobClient.Client, storageAuth) + + t.Logf("[DEBUG] Copying file to Blob Storage..") + copyInput := CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + + refreshInterval := 5 * time.Second + if err := blobClient.CopyAndWait(ctx, accountName, containerName, fileName, copyInput, refreshInterval); err != nil { + t.Fatalf("Error copying: %s", err) + } + defer blobClient.Delete(ctx, accountName, containerName, fileName, DeleteInput{}) + + // Test begins here + t.Logf("[DEBUG] Acquiring Lease..") + leaseInput := AcquireLeaseInput{ + LeaseDuration: -1, + } + leaseInfo, err := blobClient.AcquireLease(ctx, accountName, containerName, fileName, leaseInput) + if err != nil { + t.Fatalf("Error acquiring lease: %s", err) + } + t.Logf("[DEBUG] Lease ID: %q", leaseInfo.LeaseID) + + t.Logf("[DEBUG] Changing Lease..") + changeLeaseInput := ChangeLeaseInput{ + ExistingLeaseID: leaseInfo.LeaseID, + ProposedLeaseID: "31f5bb01-cdd9-4166-bcdc-95186076bde0", + } + changeLeaseResult, err := blobClient.ChangeLease(ctx, accountName, containerName, fileName, changeLeaseInput) + if err != nil { + t.Fatalf("Error changing lease: %s", err) + } + t.Logf("[DEBUG] New Lease ID: %q", changeLeaseResult.LeaseID) + + t.Logf("[DEBUG] Releasing Lease..") + if _, err := blobClient.ReleaseLease(ctx, accountName, containerName, fileName, changeLeaseResult.LeaseID); err != nil { + t.Fatalf("Error releasing lease: %s", err) + } + + t.Logf("[DEBUG] Acquiring a new lease..") + leaseInput = AcquireLeaseInput{ + LeaseDuration: 30, + } + leaseInfo, err = blobClient.AcquireLease(ctx, accountName, containerName, fileName, leaseInput) + if err != nil { + t.Fatalf("Error acquiring lease: %s", err) + } + t.Logf("[DEBUG] Lease ID: %q", leaseInfo.LeaseID) + + t.Logf("[DEBUG] Renewing lease..") + if _, err := blobClient.RenewLease(ctx, accountName, containerName, fileName, leaseInfo.LeaseID); err != nil { + t.Fatalf("Error renewing lease: %s", err) + } + + t.Logf("[DEBUG] Breaking lease..") + breakLeaseInput := BreakLeaseInput{ + LeaseID: leaseInfo.LeaseID, + } + if _, err := blobClient.BreakLease(ctx, accountName, containerName, fileName, breakLeaseInput); err != nil { + t.Fatalf("Error breaking lease: %s", err) + } +} diff --git a/storage/2018-11-09/blob/blobs/lifecycle_test.go b/storage/2018-11-09/blob/blobs/lifecycle_test.go new file mode 100644 index 0000000..3456978 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/lifecycle_test.go @@ -0,0 +1,158 @@ +package blobs + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/containers" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + fileName := "example.txt" + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.BlobStorage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + containersClient := containers.NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithAuthorizer(containersClient.Client, storageAuth) + + _, err = containersClient.Create(ctx, accountName, containerName, containers.CreateInput{}) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + defer containersClient.Delete(ctx, accountName, containerName) + + blobClient := NewWithEnvironment(client.Environment) + blobClient.Client = client.PrepareWithAuthorizer(blobClient.Client, storageAuth) + + t.Logf("[DEBUG] Copying file to Blob Storage..") + copyInput := CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + + refreshInterval := 5 * time.Second + if err := blobClient.CopyAndWait(ctx, accountName, containerName, fileName, copyInput, refreshInterval); err != nil { + t.Fatalf("Error copying: %s", err) + } + + t.Logf("[DEBUG] Retrieving Blob Properties..") + details, err := blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error retrieving properties: %s", err) + } + + // default value + if details.AccessTier != Hot { + t.Fatalf("Expected the AccessTier to be %q but got %q", Hot, details.AccessTier) + } + if details.BlobType != BlockBlob { + t.Fatalf("Expected BlobType to be %q but got %q", BlockBlob, details.BlobType) + } + if len(details.MetaData) != 0 { + t.Fatalf("Expected there to be no items of metadata but got %d", len(details.MetaData)) + } + + t.Logf("[DEBUG] Checking it's returned in the List API..") + listInput := containers.ListBlobsInput{} + listResult, err := containersClient.ListBlobs(ctx, accountName, containerName, listInput) + if err != nil { + t.Fatalf("Error listing blobs: %s", err) + } + + if len(listResult.Blobs.Blobs) != 1 { + t.Fatalf("Expected there to be 1 blob in the container but got %d", len(listResult.Blobs.Blobs)) + } + + t.Logf("[DEBUG] Setting MetaData..") + metaDataInput := SetMetaDataInput{ + MetaData: map[string]string{ + "hello": "there", + }, + } + if _, err := blobClient.SetMetaData(ctx, accountName, containerName, fileName, metaDataInput); err != nil { + t.Fatalf("Error setting MetaData: %s", err) + } + + t.Logf("[DEBUG] Re-retrieving Blob Properties..") + details, err = blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error re-retrieving properties: %s", err) + } + + // default value + if details.AccessTier != Hot { + t.Fatalf("Expected the AccessTier to be %q but got %q", Hot, details.AccessTier) + } + if details.BlobType != BlockBlob { + t.Fatalf("Expected BlobType to be %q but got %q", BlockBlob, details.BlobType) + } + if len(details.MetaData) != 1 { + t.Fatalf("Expected there to be 1 item of metadata but got %d", len(details.MetaData)) + } + if details.MetaData["hello"] != "there" { + t.Fatalf("Expected `hello` to be `there` but got %q", details.MetaData["there"]) + } + + t.Logf("[DEBUG] Retrieving the Block List..") + getBlockListInput := GetBlockListInput{ + BlockListType: All, + } + blockList, err := blobClient.GetBlockList(ctx, accountName, containerName, fileName, getBlockListInput) + if err != nil { + t.Fatalf("Error retrieving Block List: %s", err) + } + + // since this is a copy from an existing file, all blocks should be present + if len(blockList.CommittedBlocks.Blocks) == 0 { + t.Fatalf("Expected there to be committed blocks but there weren't!") + } + if len(blockList.UncommittedBlocks.Blocks) != 0 { + t.Fatalf("Expected all blocks to be committed but got %d uncommitted blocks", len(blockList.UncommittedBlocks.Blocks)) + } + + t.Logf("[DEBUG] Changing the Access Tiers..") + tiers := []AccessTier{ + Hot, + Cool, + Archive, + } + for _, tier := range tiers { + t.Logf("[DEBUG] Updating the Access Tier to %q..", string(tier)) + if _, err := blobClient.SetTier(ctx, accountName, containerName, fileName, tier); err != nil { + t.Fatalf("Error setting the Access Tier: %s", err) + } + + t.Logf("[DEBUG] Re-retrieving Blob Properties..") + details, err = blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error re-retrieving properties: %s", err) + } + + if details.AccessTier != tier { + t.Fatalf("Expected the AccessTier to be %q but got %q", tier, details.AccessTier) + } + } + + t.Logf("[DEBUG] Deleting Blob") + if _, err := blobClient.Delete(ctx, accountName, containerName, fileName, DeleteInput{}); err != nil { + t.Fatalf("Error deleting Blob: %s", err) + } +} diff --git a/storage/2018-11-09/blob/blobs/metadata_set.go b/storage/2018-11-09/blob/blobs/metadata_set.go new file mode 100644 index 0000000..ec69152 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/metadata_set.go @@ -0,0 +1,113 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type SetMetaDataInput struct { + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string + + // Any metadata which should be added to this blob + MetaData map[string]string +} + +// SetMetaData marks the specified blob or snapshot for deletion. The blob is later deleted during garbage collection. +func (client Client) SetMetaData(ctx context.Context, accountName, containerName, blobName string, input SetMetaDataInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "GetProperties", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "GetProperties", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "GetProperties", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "GetProperties", "`blobName` cannot be an empty string.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("blobs.Client", "GetProperties", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.SetMetaDataPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "SetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.SetMetaDataSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "SetMetaData", resp, "Failure sending request") + return + } + + result, err = client.SetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "SetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// SetMetaDataPreparer prepares the SetMetaData request. +func (client Client) SetMetaDataPreparer(ctx context.Context, accountName, containerName, blobName string, input SetMetaDataInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetMetaDataSender sends the SetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetMetaDataResponder handles the response to the SetMetaData request. The method always +// closes the http.Response Body. +func (client Client) SetMetaDataResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/blobs/models.go b/storage/2018-11-09/blob/blobs/models.go new file mode 100644 index 0000000..d7d83aa --- /dev/null +++ b/storage/2018-11-09/blob/blobs/models.go @@ -0,0 +1,82 @@ +package blobs + +type AccessTier string + +var ( + Archive AccessTier = "Archive" + Cool AccessTier = "Cool" + Hot AccessTier = "Hot" +) + +type ArchiveStatus string + +var ( + None ArchiveStatus = "" + RehydratePendingToCool ArchiveStatus = "rehydrate-pending-to-cool" + RehydratePendingToHot ArchiveStatus = "rehydrate-pending-to-hot" +) + +type BlockListType string + +var ( + All BlockListType = "all" + Committed BlockListType = "committed" + Uncommitted BlockListType = "uncommitted" +) + +type Block struct { + // The base64-encoded Block ID + Name string `xml:"Name"` + + // The size of the Block in Bytes + Size int64 `xml:"Size"` +} + +type BlobType string + +var ( + AppendBlob BlobType = "AppendBlob" + BlockBlob BlobType = "BlockBlob" + PageBlob BlobType = "PageBlob" +) + +type CommittedBlocks struct { + Blocks []Block `xml:"Block"` +} + +type CopyStatus string + +var ( + Aborted CopyStatus = "aborted" + Failed CopyStatus = "failed" + Pending CopyStatus = "pending" + Success CopyStatus = "success" +) + +type LeaseDuration string + +var ( + Fixed LeaseDuration = "fixed" + Infinite LeaseDuration = "infinite" +) + +type LeaseState string + +var ( + Available LeaseState = "available" + Breaking LeaseState = "breaking" + Broken LeaseState = "broken" + Expired LeaseState = "expired" + Leased LeaseState = "leased" +) + +type LeaseStatus string + +var ( + Locked LeaseStatus = "locked" + Unlocked LeaseStatus = "unlocked" +) + +type UncommittedBlocks struct { + Blocks []Block `xml:"Block"` +} diff --git a/storage/2018-11-09/blob/blobs/properties_get.go b/storage/2018-11-09/blob/blobs/properties_get.go new file mode 100644 index 0000000..de7c5fc --- /dev/null +++ b/storage/2018-11-09/blob/blobs/properties_get.go @@ -0,0 +1,310 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetPropertiesInput struct { + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string +} + +type GetPropertiesResult struct { + autorest.Response + + // The tier of page blob on a premium storage account or tier of block blob on blob storage or general purpose v2 account. + AccessTier AccessTier + + // This gives the last time tier was changed on the object. + // This header is returned only if tier on block blob was ever set. + // The date format follows RFC 1123 + AccessTierChangeTime string + + // For page blobs on a premium storage account only. + // If the access tier is not explicitly set on the blob, the tier is inferred based on its content length + // and this header will be returned with true value. + // For block blobs on Blob Storage or general purpose v2 account, if the blob does not have the access tier + // set then we infer the tier from the storage account properties. This header is set only if the block blob + // tier is inferred + AccessTierInferred bool + + // For blob storage or general purpose v2 account. + // If the blob is being rehydrated and is not complete then this header is returned indicating + // that rehydrate is pending and also tells the destination tier + ArchiveStatus ArchiveStatus + + // The number of committed blocks present in the blob. + // This header is returned only for append blobs. + BlobCommittedBlockCount string + + // The current sequence number for a page blob. + // This header is not returned for block blobs or append blobs. + // This header is not returned for block blobs. + BlobSequenceNumber string + + // The blob type. + BlobType BlobType + + // If the Cache-Control request header has previously been set for the blob, that value is returned in this header. + CacheControl string + + // The Content-Disposition response header field conveys additional information about how to process + // the response payload, and also can be used to attach additional metadata. + // For example, if set to attachment, it indicates that the user-agent should not display the response, + // but instead show a Save As dialog. + ContentDisposition string + + // If the Content-Encoding request header has previously been set for the blob, + // that value is returned in this header. + ContentEncoding string + + // If the Content-Language request header has previously been set for the blob, + // that value is returned in this header. + ContentLanguage string + + // The size of the blob in bytes. + // For a page blob, this header returns the value of the x-ms-blob-content-length header stored with the blob. + ContentLength int64 + + // The content type specified for the blob. + // If no content type was specified, the default content type is `application/octet-stream`. + ContentType string + + // If the Content-MD5 header has been set for the blob, this response header is returned so that + // the client can check for message content integrity. + ContentMD5 string + + // Conclusion time of the last attempted Copy Blob operation where this blob was the destination blob. + // This value can specify the time of a completed, aborted, or failed copy attempt. + // This header does not appear if a copy is pending, if this blob has never been the + // destination in a Copy Blob operation, or if this blob has been modified after a concluded Copy Blob + // operation using Set Blob Properties, Put Blob, or Put Block List. + CopyCompletionTime string + + // Included if the blob is incremental copy blob or incremental copy snapshot, if x-ms-copy-status is success. + // Snapshot time of the last successful incremental copy snapshot for this blob + CopyDestinationSnapshot string + + // String identifier for the last attempted Copy Blob operation where this blob was the destination blob. + // This header does not appear if this blob has never been the destination in a Copy Blob operation, + // or if this blob has been modified after a concluded Copy Blob operation using Set Blob Properties, + // Put Blob, or Put Block List. + CopyID string + + // Contains the number of bytes copied and the total bytes in the source in the last attempted + // Copy Blob operation where this blob was the destination blob. + // Can show between 0 and Content-Length bytes copied. + // This header does not appear if this blob has never been the destination in a Copy Blob operation, + // or if this blob has been modified after a concluded Copy Blob operation using Set Blob Properties, + // Put Blob, or Put Block List. + CopyProgress string + + // URL up to 2 KB in length that specifies the source blob used in the last attempted Copy Blob operation + // where this blob was the destination blob. + // This header does not appear if this blob has never been the destination in a Copy Blob operation, + // or if this blob has been modified after a concluded Copy Blob operation using Set Blob Properties, + // Put Blob, or Put Block List + CopySource string + + // State of the copy operation identified by x-ms-copy-id, with these values: + // - success: Copy completed successfully. + // - pending: Copy is in progress. + // Check x-ms-copy-status-description if intermittent, non-fatal errors + // impede copy progress but don’t cause failure. + // - aborted: Copy was ended by Abort Copy Blob. + // - failed: Copy failed. See x-ms- copy-status-description for failure details. + // This header does not appear if this blob has never been the destination in a Copy Blob operation, + // or if this blob has been modified after a completed Copy Blob operation using Set Blob Properties, + // Put Blob, or Put Block List. + CopyStatus CopyStatus + + // Describes cause of fatal or non-fatal copy operation failure. + // This header does not appear if this blob has never been the destination in a Copy Blob operation, + // or if this blob has been modified after a concluded Copy Blob operation using Set Blob Properties, + // Put Blob, or Put Block List. + CopyStatusDescription string + + // The date/time at which the blob was created. The date format follows RFC 1123 + CreationTime string + + // The ETag contains a value that you can use to perform operations conditionally + ETag string + + // Included if the blob is incremental copy blob. + IncrementalCopy bool + + // The date/time that the blob was last modified. The date format follows RFC 1123. + LastModified string + + // When a blob is leased, specifies whether the lease is of infinite or fixed duration + LeaseDuration LeaseDuration + + // The lease state of the blob + LeaseState LeaseState + + LeaseStatus LeaseStatus + + // A set of name-value pairs that correspond to the user-defined metadata associated with this blob + MetaData map[string]string + + // Is the Storage Account encrypted using server-side encryption? This should always return true + ServerEncrypted bool +} + +// GetProperties returns all user-defined metadata, standard HTTP properties, and system properties for the blob +func (client Client) GetProperties(ctx context.Context, accountName, containerName, blobName string, input GetPropertiesInput) (result GetPropertiesResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "GetProperties", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "GetProperties", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "GetProperties", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "GetProperties", "`blobName` cannot be an empty string.") + } + + req, err := client.GetPropertiesPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetProperties", nil, "Failure preparing request") + return + } + + resp, err := client.GetPropertiesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "GetProperties", resp, "Failure sending request") + return + } + + result, err = client.GetPropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetProperties", resp, "Failure responding to request") + return + } + + return +} + +// GetPropertiesPreparer prepares the GetProperties request. +func (client Client) GetPropertiesPreparer(ctx context.Context, accountName, containerName, blobName string, input GetPropertiesInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsHead(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetPropertiesSender sends the GetProperties request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetPropertiesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetPropertiesResponder handles the response to the GetProperties request. The method always +// closes the http.Response Body. +func (client Client) GetPropertiesResponder(resp *http.Response) (result GetPropertiesResult, err error) { + if resp != nil && resp.Header != nil { + result.AccessTier = AccessTier(resp.Header.Get("x-ms-access-tier")) + result.AccessTierChangeTime = resp.Header.Get(" x-ms-access-tier-change-time") + result.ArchiveStatus = ArchiveStatus(resp.Header.Get(" x-ms-archive-status")) + result.BlobCommittedBlockCount = resp.Header.Get("x-ms-blob-committed-block-count") + result.BlobSequenceNumber = resp.Header.Get("x-ms-blob-sequence-number") + result.BlobType = BlobType(resp.Header.Get("x-ms-blob-type")) + result.CacheControl = resp.Header.Get("Cache-Control") + result.ContentDisposition = resp.Header.Get("Content-Disposition") + result.ContentEncoding = resp.Header.Get("Content-Encoding") + result.ContentLanguage = resp.Header.Get("Content-Language") + result.ContentMD5 = resp.Header.Get("Content-MD5") + result.ContentType = resp.Header.Get("Content-Type") + result.CopyCompletionTime = resp.Header.Get("x-ms-copy-completion-time") + result.CopyDestinationSnapshot = resp.Header.Get("x-ms-copy-destination-snapshot") + result.CopyID = resp.Header.Get("x-ms-copy-id") + result.CopyProgress = resp.Header.Get(" x-ms-copy-progress") + result.CopySource = resp.Header.Get("x-ms-copy-source") + result.CopyStatus = CopyStatus(resp.Header.Get("x-ms-copy-status")) + result.CopyStatusDescription = resp.Header.Get("x-ms-copy-status-description") + result.CreationTime = resp.Header.Get("x-ms-creation-time") + result.ETag = resp.Header.Get("Etag") + result.LastModified = resp.Header.Get("Last-Modified") + result.LeaseDuration = LeaseDuration(resp.Header.Get("x-ms-lease-duration")) + result.LeaseState = LeaseState(resp.Header.Get("x-ms-lease-state")) + result.LeaseStatus = LeaseStatus(resp.Header.Get("x-ms-lease-status")) + result.MetaData = metadata.ParseFromHeaders(resp.Header) + + if v := resp.Header.Get("x-ms-access-tier-inferred"); v != "" { + b, innerErr := strconv.ParseBool(v) + if innerErr != nil { + err = fmt.Errorf("Error parsing %q as a bool: %s", v, innerErr) + return + } + + result.AccessTierInferred = b + } + + if v := resp.Header.Get("Content-Length"); v != "" { + i, innerErr := strconv.Atoi(v) + if innerErr != nil { + err = fmt.Errorf("Error parsing %q as an integer: %s", v, innerErr) + } + + result.ContentLength = int64(i) + } + + if v := resp.Header.Get("x-ms-incremental-copy"); v != "" { + b, innerErr := strconv.ParseBool(v) + if innerErr != nil { + err = fmt.Errorf("Error parsing %q as a bool: %s", v, innerErr) + return + } + + result.IncrementalCopy = b + } + + if v := resp.Header.Get("x-ms-server-encrypted"); v != "" { + b, innerErr := strconv.ParseBool(v) + if innerErr != nil { + err = fmt.Errorf("Error parsing %q as a bool: %s", v, innerErr) + return + } + + result.IncrementalCopy = b + } + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-11-09/blob/blobs/properties_set.go b/storage/2018-11-09/blob/blobs/properties_set.go new file mode 100644 index 0000000..a8c0ed8 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/properties_set.go @@ -0,0 +1,156 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type SetPropertiesInput struct { + CacheControl *string + ContentType *string + ContentMD5 *string + ContentEncoding *string + ContentLanguage *string + LeaseID *string + ContentDisposition *string + ContentLength *int64 + SequenceNumberAction *SequenceNumberAction + BlobSequenceNumber *string +} + +type SetPropertiesResult struct { + autorest.Response + + BlobSequenceNumber string + Etag string +} + +// SetProperties sets system properties on the blob. +func (client Client) SetProperties(ctx context.Context, accountName, containerName, blobName string, input SetPropertiesInput) (result SetPropertiesResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "SetProperties", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "SetProperties", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "SetProperties", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "SetProperties", "`blobName` cannot be an empty string.") + } + + req, err := client.SetPropertiesPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "SetProperties", nil, "Failure preparing request") + return + } + + resp, err := client.SetPropertiesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "SetProperties", resp, "Failure sending request") + return + } + + result, err = client.SetPropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "SetProperties", resp, "Failure responding to request") + return + } + + return +} + +type SequenceNumberAction string + +var ( + Increment SequenceNumberAction = "increment" + Max SequenceNumberAction = "max" + Update SequenceNumberAction = "update" +) + +// SetPropertiesPreparer prepares the SetProperties request. +func (client Client) SetPropertiesPreparer(ctx context.Context, accountName, containerName, blobName string, input SetPropertiesInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "properties"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.CacheControl != nil { + headers["x-ms-blob-cache-control"] = *input.CacheControl + } + if input.ContentDisposition != nil { + headers["x-ms-blob-content-disposition"] = *input.ContentDisposition + } + if input.ContentEncoding != nil { + headers["x-ms-blob-content-encoding"] = *input.ContentEncoding + } + if input.ContentLanguage != nil { + headers["x-ms-blob-content-language"] = *input.ContentLanguage + } + if input.ContentMD5 != nil { + headers["x-ms-blob-content-md5"] = *input.ContentMD5 + } + if input.ContentType != nil { + headers["x-ms-blob-content-type"] = *input.ContentType + } + if input.ContentLength != nil { + headers["x-ms-blob-content-length"] = *input.ContentLength + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + if input.SequenceNumberAction != nil { + headers["x-ms-sequence-number-action"] = string(*input.SequenceNumberAction) + } + if input.BlobSequenceNumber != nil { + headers["x-ms-blob-sequence-number"] = *input.BlobSequenceNumber + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetPropertiesSender sends the SetProperties request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetPropertiesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetPropertiesResponder handles the response to the SetProperties request. The method always +// closes the http.Response Body. +func (client Client) SetPropertiesResponder(resp *http.Response) (result SetPropertiesResult, err error) { + if resp != nil && resp.Header != nil { + result.BlobSequenceNumber = resp.Header.Get("x-ms-blob-sequence-number") + result.Etag = resp.Header.Get("Etag") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-11-09/blob/blobs/put_append_blob.go b/storage/2018-11-09/blob/blobs/put_append_blob.go new file mode 100644 index 0000000..ef2c502 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/put_append_blob.go @@ -0,0 +1,134 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type PutAppendBlobInput struct { + CacheControl *string + ContentDisposition *string + ContentEncoding *string + ContentLanguage *string + ContentMD5 *string + ContentType *string + LeaseID *string + MetaData map[string]string +} + +// PutAppendBlob is a wrapper around the Put API call (with a stricter input object) +// which creates a new append blob, or updates the content of an existing blob. +func (client Client) PutAppendBlob(ctx context.Context, accountName, containerName, blobName string, input PutAppendBlobInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutAppendBlob", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutAppendBlob", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutAppendBlob", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutAppendBlob", "`blobName` cannot be an empty string.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("blobs.Client", "PutAppendBlob", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.PutAppendBlobPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutAppendBlob", nil, "Failure preparing request") + return + } + + resp, err := client.PutAppendBlobSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutAppendBlob", resp, "Failure sending request") + return + } + + result, err = client.PutAppendBlobResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutAppendBlob", resp, "Failure responding to request") + return + } + + return +} + +// PutAppendBlobPreparer prepares the PutAppendBlob request. +func (client Client) PutAppendBlobPreparer(ctx context.Context, accountName, containerName, blobName string, input PutAppendBlobInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-blob-type": string(AppendBlob), + "x-ms-version": APIVersion, + + // For a page blob or an append blob, the value of this header must be set to zero, + // as Put Blob is used only to initialize the blob + "Content-Length": 0, + } + + if input.CacheControl != nil { + headers["x-ms-blob-cache-control"] = *input.CacheControl + } + if input.ContentDisposition != nil { + headers["x-ms-blob-content-disposition"] = *input.ContentDisposition + } + if input.ContentEncoding != nil { + headers["x-ms-blob-content-encoding"] = *input.ContentEncoding + } + if input.ContentLanguage != nil { + headers["x-ms-blob-content-language"] = *input.ContentLanguage + } + if input.ContentMD5 != nil { + headers["x-ms-blob-content-md5"] = *input.ContentMD5 + } + if input.ContentType != nil { + headers["x-ms-blob-content-type"] = *input.ContentType + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutAppendBlobSender sends the PutAppendBlob request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutAppendBlobSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutAppendBlobResponder handles the response to the PutAppendBlob request. The method always +// closes the http.Response Body. +func (client Client) PutAppendBlobResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/blobs/put_block.go b/storage/2018-11-09/blob/blobs/put_block.go new file mode 100644 index 0000000..5256013 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/put_block.go @@ -0,0 +1,125 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type PutBlockInput struct { + BlockID string + Content []byte + ContentMD5 *string + LeaseID *string +} + +type PutBlockResult struct { + autorest.Response + + ContentMD5 string +} + +// PutBlock creates a new block to be committed as part of a blob. +func (client Client) PutBlock(ctx context.Context, accountName, containerName, blobName string, input PutBlockInput) (result PutBlockResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutBlock", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutBlock", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutBlock", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutBlock", "`blobName` cannot be an empty string.") + } + if input.BlockID == "" { + return result, validation.NewError("blobs.Client", "PutBlock", "`input.BlockID` cannot be an empty string.") + } + if len(input.Content) == 0 { + return result, validation.NewError("blobs.Client", "PutBlock", "`input.Content` cannot be empty.") + } + + req, err := client.PutBlockPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlock", nil, "Failure preparing request") + return + } + + resp, err := client.PutBlockSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlock", resp, "Failure sending request") + return + } + + result, err = client.PutBlockResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlock", resp, "Failure responding to request") + return + } + + return +} + +// PutBlockPreparer prepares the PutBlock request. +func (client Client) PutBlockPreparer(ctx context.Context, accountName, containerName, blobName string, input PutBlockInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "block"), + "blockid": autorest.Encode("query", input.BlockID), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.ContentMD5 != nil { + headers["x-ms-blob-content-md5"] = *input.ContentMD5 + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers), + autorest.WithBytes(&input.Content)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutBlockSender sends the PutBlock request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutBlockSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutBlockResponder handles the response to the PutBlock request. The method always +// closes the http.Response Body. +func (client Client) PutBlockResponder(resp *http.Response) (result PutBlockResult, err error) { + if resp != nil && resp.Header != nil { + result.ContentMD5 = resp.Header.Get("Content-MD5") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/blobs/put_block_blob.go b/storage/2018-11-09/blob/blobs/put_block_blob.go new file mode 100644 index 0000000..fa29dd3 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/put_block_blob.go @@ -0,0 +1,135 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type PutBlockBlobInput struct { + CacheControl *string + Content []byte + ContentDisposition *string + ContentEncoding *string + ContentLanguage *string + ContentMD5 *string + ContentType *string + LeaseID *string + MetaData map[string]string +} + +// PutBlockBlob is a wrapper around the Put API call (with a stricter input object) +// which creates a new block append blob, or updates the content of an existing block blob. +func (client Client) PutBlockBlob(ctx context.Context, accountName, containerName, blobName string, input PutBlockBlobInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutBlockBlob", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutBlockBlob", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutBlockBlob", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutBlockBlob", "`blobName` cannot be an empty string.") + } + if len(input.Content) == 0 { + return result, validation.NewError("blobs.Client", "PutBlockBlob", "`input.Content` cannot be empty.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("blobs.Client", "PutBlockBlob", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.PutBlockBlobPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockBlob", nil, "Failure preparing request") + return + } + + resp, err := client.PutBlockBlobSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockBlob", resp, "Failure sending request") + return + } + + result, err = client.PutBlockBlobResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockBlob", resp, "Failure responding to request") + return + } + + return +} + +// PutBlockBlobPreparer prepares the PutBlockBlob request. +func (client Client) PutBlockBlobPreparer(ctx context.Context, accountName, containerName, blobName string, input PutBlockBlobInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-blob-type": string(BlockBlob), + "x-ms-version": APIVersion, + } + + if input.CacheControl != nil { + headers["x-ms-blob-cache-control"] = *input.CacheControl + } + if input.ContentDisposition != nil { + headers["x-ms-blob-content-disposition"] = *input.ContentDisposition + } + if input.ContentEncoding != nil { + headers["x-ms-blob-content-encoding"] = *input.ContentEncoding + } + if input.ContentLanguage != nil { + headers["x-ms-blob-content-language"] = *input.ContentLanguage + } + if input.ContentMD5 != nil { + headers["x-ms-blob-content-md5"] = *input.ContentMD5 + } + if input.ContentType != nil { + headers["x-ms-blob-content-type"] = *input.ContentType + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithBytes(&input.Content)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutBlockBlobSender sends the PutBlockBlob request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutBlockBlobSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutBlockBlobResponder handles the response to the PutBlockBlob request. The method always +// closes the http.Response Body. +func (client Client) PutBlockBlobResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/blobs/put_block_blob_file.go b/storage/2018-11-09/blob/blobs/put_block_blob_file.go new file mode 100644 index 0000000..7232e5e --- /dev/null +++ b/storage/2018-11-09/blob/blobs/put_block_blob_file.go @@ -0,0 +1,34 @@ +package blobs + +import ( + "context" + "fmt" + "io" + "os" +) + +// PutBlockBlobFromFile is a helper method which takes a file, and automatically chunks it up, rather than having to do this yourself +func (client Client) PutBlockBlobFromFile(ctx context.Context, accountName, containerName, blobName string, file *os.File, input PutBlockBlobInput) error { + fileInfo, err := file.Stat() + if err != nil { + return fmt.Errorf("Error loading file info: %s", err) + } + + fileSize := fileInfo.Size() + bytes := make([]byte, fileSize) + + _, err = file.ReadAt(bytes, 0) + if err != nil { + if err != io.EOF { + return fmt.Errorf("Error reading bytes: %s", err) + } + } + + input.Content = bytes + + if _, err = client.PutBlockBlob(ctx, accountName, containerName, blobName, input); err != nil { + return fmt.Errorf("Error putting bytes: %s", err) + } + + return nil +} diff --git a/storage/2018-11-09/blob/blobs/put_block_list.go b/storage/2018-11-09/blob/blobs/put_block_list.go new file mode 100644 index 0000000..f805247 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/put_block_list.go @@ -0,0 +1,157 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type BlockList struct { + CommittedBlockIDs []BlockID `xml:"Committed,omitempty"` + UncommittedBlockIDs []BlockID `xml:"Uncommitted,omitempty"` + LatestBlockIDs []BlockID `xml:"Latest,omitempty"` +} + +type BlockID struct { + Value string `xml:",chardata"` +} + +type PutBlockListInput struct { + BlockList BlockList + CacheControl *string + ContentDisposition *string + ContentEncoding *string + ContentLanguage *string + ContentMD5 *string + ContentType *string + MetaData map[string]string + LeaseID *string +} + +type PutBlockListResult struct { + autorest.Response + + ContentMD5 string + ETag string + LastModified string +} + +// PutBlockList writes a blob by specifying the list of block IDs that make up the blob. +// In order to be written as part of a blob, a block must have been successfully written +// to the server in a prior Put Block operation. +func (client Client) PutBlockList(ctx context.Context, accountName, containerName, blobName string, input PutBlockListInput) (result PutBlockListResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutBlockList", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutBlockList", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutBlockList", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutBlockList", "`blobName` cannot be an empty string.") + } + + req, err := client.PutBlockListPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockList", nil, "Failure preparing request") + return + } + + resp, err := client.PutBlockListSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockList", resp, "Failure sending request") + return + } + + result, err = client.PutBlockListResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockList", resp, "Failure responding to request") + return + } + + return +} + +// PutBlockListPreparer prepares the PutBlockList request. +func (client Client) PutBlockListPreparer(ctx context.Context, accountName, containerName, blobName string, input PutBlockListInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "blocklist"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.CacheControl != nil { + headers["x-ms-blob-cache-control"] = *input.CacheControl + } + if input.ContentDisposition != nil { + headers["x-ms-blob-content-disposition"] = *input.ContentDisposition + } + if input.ContentEncoding != nil { + headers["x-ms-blob-content-encoding"] = *input.ContentEncoding + } + if input.ContentLanguage != nil { + headers["x-ms-blob-content-language"] = *input.ContentLanguage + } + if input.ContentMD5 != nil { + headers["x-ms-blob-content-md5"] = *input.ContentMD5 + } + if input.ContentType != nil { + headers["x-ms-blob-content-type"] = *input.ContentType + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers), + autorest.WithXML(input.BlockList)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutBlockListSender sends the PutBlockList request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutBlockListSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutBlockListResponder handles the response to the PutBlockList request. The method always +// closes the http.Response Body. +func (client Client) PutBlockListResponder(resp *http.Response) (result PutBlockListResult, err error) { + if resp != nil && resp.Header != nil { + result.ContentMD5 = resp.Header.Get("Content-MD5") + result.ETag = resp.Header.Get("ETag") + result.LastModified = resp.Header.Get("Last-Modified") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/blobs/put_block_url.go b/storage/2018-11-09/blob/blobs/put_block_url.go new file mode 100644 index 0000000..95ad974 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/put_block_url.go @@ -0,0 +1,129 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type PutBlockFromURLInput struct { + BlockID string + CopySource string + + ContentMD5 *string + LeaseID *string + Range *string +} + +type PutBlockFromURLResult struct { + autorest.Response + ContentMD5 string +} + +// PutBlockFromURL creates a new block to be committed as part of a blob where the contents are read from a URL +func (client Client) PutBlockFromURL(ctx context.Context, accountName, containerName, blobName string, input PutBlockFromURLInput) (result PutBlockFromURLResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutBlockFromURL", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutBlockFromURL", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutBlockFromURL", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutBlockFromURL", "`blobName` cannot be an empty string.") + } + if input.BlockID == "" { + return result, validation.NewError("blobs.Client", "PutBlockFromURL", "`input.BlockID` cannot be an empty string.") + } + if input.CopySource == "" { + return result, validation.NewError("blobs.Client", "PutBlockFromURL", "`input.CopySource` cannot be an empty string.") + } + + req, err := client.PutBlockFromURLPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockFromURL", nil, "Failure preparing request") + return + } + + resp, err := client.PutBlockFromURLSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockFromURL", resp, "Failure sending request") + return + } + + result, err = client.PutBlockFromURLResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutBlockFromURL", resp, "Failure responding to request") + return + } + + return +} + +// PutBlockFromURLPreparer prepares the PutBlockFromURL request. +func (client Client) PutBlockFromURLPreparer(ctx context.Context, accountName, containerName, blobName string, input PutBlockFromURLInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "block"), + "blockid": autorest.Encode("query", input.BlockID), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-copy-source": input.CopySource, + } + + if input.ContentMD5 != nil { + headers["x-ms-source-content-md5"] = *input.ContentMD5 + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + if input.Range != nil { + headers["x-ms-source-range"] = *input.Range + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutBlockFromURLSender sends the PutBlockFromURL request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutBlockFromURLSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutBlockFromURLResponder handles the response to the PutBlockFromURL request. The method always +// closes the http.Response Body. +func (client Client) PutBlockFromURLResponder(resp *http.Response) (result PutBlockFromURLResult, err error) { + if resp != nil && resp.Header != nil { + result.ContentMD5 = resp.Header.Get("Content-MD5") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/blobs/put_page_blob.go b/storage/2018-11-09/blob/blobs/put_page_blob.go new file mode 100644 index 0000000..ad3c878 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/put_page_blob.go @@ -0,0 +1,148 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type PutPageBlobInput struct { + CacheControl *string + ContentDisposition *string + ContentEncoding *string + ContentLanguage *string + ContentMD5 *string + ContentType *string + LeaseID *string + MetaData map[string]string + + BlobContentLengthBytes int64 + BlobSequenceNumber *int64 + AccessTier *AccessTier +} + +// PutPageBlob is a wrapper around the Put API call (with a stricter input object) +// which creates a new block blob, or updates the content of an existing page blob. +func (client Client) PutPageBlob(ctx context.Context, accountName, containerName, blobName string, input PutPageBlobInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutPageBlob", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutPageBlob", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutPageBlob", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutPageBlob", "`blobName` cannot be an empty string.") + } + if input.BlobContentLengthBytes == 0 || input.BlobContentLengthBytes%512 != 0 { + return result, validation.NewError("blobs.Client", "PutPageBlob", "`blobName` must be aligned to a 512-byte boundary.") + } + + req, err := client.PutPageBlobPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageBlob", nil, "Failure preparing request") + return + } + + resp, err := client.PutPageBlobSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageBlob", resp, "Failure sending request") + return + } + + result, err = client.PutPageBlobResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageBlob", resp, "Failure responding to request") + return + } + + return +} + +// PutPageBlobPreparer prepares the PutPageBlob request. +func (client Client) PutPageBlobPreparer(ctx context.Context, accountName, containerName, blobName string, input PutPageBlobInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + headers := map[string]interface{}{ + "x-ms-blob-type": string(PageBlob), + "x-ms-version": APIVersion, + + // For a page blob or an page blob, the value of this header must be set to zero, + // as Put Blob is used only to initialize the blob + "Content-Length": 0, + + // This header specifies the maximum size for the page blob, up to 8 TB. + // The page blob size must be aligned to a 512-byte boundary. + "x-ms-blob-content-length": input.BlobContentLengthBytes, + } + + if input.AccessTier != nil { + headers["x-ms-access-tier"] = string(*input.AccessTier) + } + if input.BlobSequenceNumber != nil { + headers["x-ms-blob-sequence-number"] = *input.BlobSequenceNumber + } + + if input.CacheControl != nil { + headers["x-ms-blob-cache-control"] = *input.CacheControl + } + if input.ContentDisposition != nil { + headers["x-ms-blob-content-disposition"] = *input.ContentDisposition + } + if input.ContentEncoding != nil { + headers["x-ms-blob-content-encoding"] = *input.ContentEncoding + } + if input.ContentLanguage != nil { + headers["x-ms-blob-content-language"] = *input.ContentLanguage + } + if input.ContentMD5 != nil { + headers["x-ms-blob-content-md5"] = *input.ContentMD5 + } + if input.ContentType != nil { + headers["x-ms-blob-content-type"] = *input.ContentType + } + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutPageBlobSender sends the PutPageBlob request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutPageBlobSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutPageBlobResponder handles the response to the PutPageBlob request. The method always +// closes the http.Response Body. +func (client Client) PutPageBlobResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/blobs/put_page_clear.go b/storage/2018-11-09/blob/blobs/put_page_clear.go new file mode 100644 index 0000000..59feaa5 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/put_page_clear.go @@ -0,0 +1,113 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type PutPageClearInput struct { + StartByte int64 + EndByte int64 + + LeaseID *string +} + +// PutPageClear clears a range of pages within a page blob. +func (client Client) PutPageClear(ctx context.Context, accountName, containerName, blobName string, input PutPageClearInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutPageClear", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutPageClear", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutPageClear", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutPageClear", "`blobName` cannot be an empty string.") + } + if input.StartByte < 0 { + return result, validation.NewError("blobs.Client", "PutPageClear", "`input.StartByte` must be greater than or equal to 0.") + } + if input.EndByte <= 0 { + return result, validation.NewError("blobs.Client", "PutPageClear", "`input.EndByte` must be greater than 0.") + } + + req, err := client.PutPageClearPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageClear", nil, "Failure preparing request") + return + } + + resp, err := client.PutPageClearSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageClear", resp, "Failure sending request") + return + } + + result, err = client.PutPageClearResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageClear", resp, "Failure responding to request") + return + } + + return +} + +// PutPageClearPreparer prepares the PutPageClear request. +func (client Client) PutPageClearPreparer(ctx context.Context, accountName, containerName, blobName string, input PutPageClearInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "page"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-page-write": "clear", + "x-ms-range": fmt.Sprintf("bytes=%d-%d", input.StartByte, input.EndByte), + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutPageClearSender sends the PutPageClear request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutPageClearSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutPageClearResponder handles the response to the PutPageClear request. The method always +// closes the http.Response Body. +func (client Client) PutPageClearResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/blobs/put_page_update.go b/storage/2018-11-09/blob/blobs/put_page_update.go new file mode 100644 index 0000000..a47e8ca --- /dev/null +++ b/storage/2018-11-09/blob/blobs/put_page_update.go @@ -0,0 +1,163 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type PutPageUpdateInput struct { + StartByte int64 + EndByte int64 + Content []byte + + IfSequenceNumberEQ *string + IfSequenceNumberLE *string + IfSequenceNumberLT *string + IfModifiedSince *string + IfUnmodifiedSince *string + IfMatch *string + IfNoneMatch *string + LeaseID *string +} + +type PutPageUpdateResult struct { + autorest.Response + + BlobSequenceNumber string + ContentMD5 string + LastModified string +} + +// PutPageUpdate writes a range of pages to a page blob. +func (client Client) PutPageUpdate(ctx context.Context, accountName, containerName, blobName string, input PutPageUpdateInput) (result PutPageUpdateResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "PutPageUpdate", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "PutPageUpdate", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "PutPageUpdate", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "PutPageUpdate", "`blobName` cannot be an empty string.") + } + if input.StartByte < 0 { + return result, validation.NewError("blobs.Client", "PutPageUpdate", "`input.StartByte` must be greater than or equal to 0.") + } + if input.EndByte <= 0 { + return result, validation.NewError("blobs.Client", "PutPageUpdate", "`input.EndByte` must be greater than 0.") + } + + expectedSize := (input.EndByte - input.StartByte) + 1 + actualSize := int64(len(input.Content)) + if expectedSize != actualSize { + return result, validation.NewError("blobs.Client", "PutPageUpdate", fmt.Sprintf("Content Size was defined as %d but got %d.", expectedSize, actualSize)) + } + + req, err := client.PutPageUpdatePreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageUpdate", nil, "Failure preparing request") + return + } + + resp, err := client.PutPageUpdateSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageUpdate", resp, "Failure sending request") + return + } + + result, err = client.PutPageUpdateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "PutPageUpdate", resp, "Failure responding to request") + return + } + + return +} + +// PutPageUpdatePreparer prepares the PutPageUpdate request. +func (client Client) PutPageUpdatePreparer(ctx context.Context, accountName, containerName, blobName string, input PutPageUpdateInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "page"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-page-write": "update", + "x-ms-range": fmt.Sprintf("bytes=%d-%d", input.StartByte, input.EndByte), + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + if input.IfSequenceNumberEQ != nil { + headers["x-ms-if-sequence-number-eq"] = *input.IfSequenceNumberEQ + } + if input.IfSequenceNumberLE != nil { + headers["x-ms-if-sequence-number-le"] = *input.IfSequenceNumberLE + } + if input.IfSequenceNumberLT != nil { + headers["x-ms-if-sequence-number-lt"] = *input.IfSequenceNumberLT + } + if input.IfModifiedSince != nil { + headers["If-Modified-Since"] = *input.IfModifiedSince + } + if input.IfUnmodifiedSince != nil { + headers["If-Unmodified-Since"] = *input.IfUnmodifiedSince + } + if input.IfMatch != nil { + headers["If-Match"] = *input.IfMatch + } + if input.IfNoneMatch != nil { + headers["If-None-Match"] = *input.IfNoneMatch + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers), + autorest.WithBytes(&input.Content)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutPageUpdateSender sends the PutPageUpdate request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutPageUpdateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutPageUpdateResponder handles the response to the PutPageUpdate request. The method always +// closes the http.Response Body. +func (client Client) PutPageUpdateResponder(resp *http.Response) (result PutPageUpdateResult, err error) { + if resp != nil && resp.Header != nil { + result.BlobSequenceNumber = resp.Header.Get("x-ms-blob-sequence-number") + result.ContentMD5 = resp.Header.Get("Content-MD5") + result.LastModified = resp.Header.Get("Last-Modified") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/blobs/resource_id.go b/storage/2018-11-09/blob/blobs/resource_id.go new file mode 100644 index 0000000..0f6dddf --- /dev/null +++ b/storage/2018-11-09/blob/blobs/resource_id.go @@ -0,0 +1,56 @@ +package blobs + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given Blob +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, containerName, blobName string) string { + domain := endpoints.GetBlobEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s/%s", domain, containerName, blobName) +} + +type ResourceID struct { + AccountName string + ContainerName string + BlobName string +} + +// ParseResourceID parses the Resource ID and returns an object which can be used +// to interact with the Blob Resource +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://foo.blob.core.windows.net/Bar/example.vhd + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + path := strings.TrimPrefix(uri.Path, "/") + segments := strings.Split(path, "/") + if len(segments) == 0 { + return nil, fmt.Errorf("Expected the path to contain segments but got none") + } + + containerName := segments[0] + blobName := strings.TrimPrefix(path, containerName) + blobName = strings.TrimPrefix(blobName, "/") + return &ResourceID{ + AccountName: *accountName, + ContainerName: containerName, + BlobName: blobName, + }, nil +} diff --git a/storage/2018-11-09/blob/blobs/resource_id_test.go b/storage/2018-11-09/blob/blobs/resource_id_test.go new file mode 100644 index 0000000..bb6cad1 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/resource_id_test.go @@ -0,0 +1,123 @@ +package blobs + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.blob.core.chinacloudapi.cn/container1/blob1.vhd", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.blob.core.cloudapi.de/container1/blob1.vhd", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.blob.core.windows.net/container1/blob1.vhd", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.blob.core.usgovcloudapi.net/container1/blob1.vhd", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "container1", "blob1.vhd") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.blob.core.chinacloudapi.cn/container1/blob1.vhd", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.blob.core.cloudapi.de/container1/blob1.vhd", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.blob.core.windows.net/container1/blob1.vhd", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.blob.core.usgovcloudapi.net/container1/blob1.vhd", + }, + } + t.Logf("[DEBUG] Top Level Files") + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.ContainerName != "container1" { + t.Fatalf("Expected Container Name to be `container1` but got %q", actual.ContainerName) + } + if actual.BlobName != "blob1.vhd" { + t.Fatalf("Expected Blob Name to be `blob1.vhd` but got %q", actual.BlobName) + } + } + + testData = []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.blob.core.chinacloudapi.cn/container1/example/blob1.vhd", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.blob.core.cloudapi.de/container1/example/blob1.vhd", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.blob.core.windows.net/container1/example/blob1.vhd", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.blob.core.usgovcloudapi.net/container1/example/blob1.vhd", + }, + } + t.Logf("[DEBUG] Nested Files") + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.ContainerName != "container1" { + t.Fatalf("Expected Container Name to be `container1` but got %q", actual.ContainerName) + } + if actual.BlobName != "example/blob1.vhd" { + t.Fatalf("Expected Blob Name to be `example/blob1.vhd` but got %q", actual.BlobName) + } + } +} diff --git a/storage/2018-11-09/blob/blobs/set_tier.go b/storage/2018-11-09/blob/blobs/set_tier.go new file mode 100644 index 0000000..dd0f0b8 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/set_tier.go @@ -0,0 +1,93 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// SetTier sets the tier on a blob. +func (client Client) SetTier(ctx context.Context, accountName, containerName, blobName string, tier AccessTier) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "SetTier", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "SetTier", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "SetTier", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "SetTier", "`blobName` cannot be an empty string.") + } + + req, err := client.SetTierPreparer(ctx, accountName, containerName, blobName, tier) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "SetTier", nil, "Failure preparing request") + return + } + + resp, err := client.SetTierSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "SetTier", resp, "Failure sending request") + return + } + + result, err = client.SetTierResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "SetTier", resp, "Failure responding to request") + return + } + + return +} + +// SetTierPreparer prepares the SetTier request. +func (client Client) SetTierPreparer(ctx context.Context, accountName, containerName, blobName string, tier AccessTier) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("path", "tier"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-access-tier": string(tier), + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetTierSender sends the SetTier request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetTierSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetTierResponder handles the response to the SetTier request. The method always +// closes the http.Response Body. +func (client Client) SetTierResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-11-09/blob/blobs/snapshot.go b/storage/2018-11-09/blob/blobs/snapshot.go new file mode 100644 index 0000000..180070b --- /dev/null +++ b/storage/2018-11-09/blob/blobs/snapshot.go @@ -0,0 +1,163 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type SnapshotInput struct { + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string + + // MetaData is a user-defined name-value pair associated with the blob. + // If no name-value pairs are specified, the operation will copy the base blob metadata to the snapshot. + // If one or more name-value pairs are specified, the snapshot is created with the specified metadata, + // and metadata is not copied from the base blob. + MetaData map[string]string + + // A DateTime value which will only snapshot the blob if it has been modified since the specified date/time + // If the base blob has not been modified, the Blob service returns status code 412 (Precondition Failed). + IfModifiedSince *string + + // A DateTime value which will only snapshot the blob if it has not been modified since the specified date/time + // If the base blob has been modified, the Blob service returns status code 412 (Precondition Failed). + IfUnmodifiedSince *string + + // An ETag value to snapshot the blob only if its ETag value matches the value specified. + // If the values do not match, the Blob service returns status code 412 (Precondition Failed). + IfMatch *string + + // An ETag value for this conditional header to snapshot the blob only if its ETag value + // does not match the value specified. + // If the values are identical, the Blob service returns status code 412 (Precondition Failed). + IfNoneMatch *string +} + +type SnapshotResult struct { + autorest.Response + + // The ETag of the snapshot + ETag string + + // A DateTime value that uniquely identifies the snapshot. + // The value of this header indicates the snapshot version, + // and may be used in subsequent requests to access the snapshot. + SnapshotDateTime string +} + +// Snapshot captures a Snapshot of a given Blob +func (client Client) Snapshot(ctx context.Context, accountName, containerName, blobName string, input SnapshotInput) (result SnapshotResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "Snapshot", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "Snapshot", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "Snapshot", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "Snapshot", "`blobName` cannot be an empty string.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("blobs.Client", "Snapshot", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.SnapshotPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Snapshot", nil, "Failure preparing request") + return + } + + resp, err := client.SnapshotSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "Snapshot", resp, "Failure sending request") + return + } + + result, err = client.SnapshotResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Snapshot", resp, "Failure responding to request") + return + } + + return +} + +// SnapshotPreparer prepares the Snapshot request. +func (client Client) SnapshotPreparer(ctx context.Context, accountName, containerName, blobName string, input SnapshotInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "snapshot"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + if input.IfModifiedSince != nil { + headers["If-Modified-Since"] = *input.IfModifiedSince + } + if input.IfUnmodifiedSince != nil { + headers["If-Unmodified-Since"] = *input.IfUnmodifiedSince + } + if input.IfMatch != nil { + headers["If-Match"] = *input.IfMatch + } + if input.IfNoneMatch != nil { + headers["If-None-Match"] = *input.IfNoneMatch + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SnapshotSender sends the Snapshot request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SnapshotSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SnapshotResponder handles the response to the Snapshot request. The method always +// closes the http.Response Body. +func (client Client) SnapshotResponder(resp *http.Response) (result SnapshotResult, err error) { + if resp != nil && resp.Header != nil { + result.ETag = resp.Header.Get("ETag") + result.SnapshotDateTime = resp.Header.Get("x-ms-snapshot") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/blobs/snapshot_get_properties.go b/storage/2018-11-09/blob/blobs/snapshot_get_properties.go new file mode 100644 index 0000000..fe1be63 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/snapshot_get_properties.go @@ -0,0 +1,90 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetSnapshotPropertiesInput struct { + // The ID of the Lease + // This must be specified if a Lease is present on the Blob, else a 403 is returned + LeaseID *string + + // The ID of the Snapshot which should be retrieved + SnapshotID string +} + +// GetSnapshotProperties returns all user-defined metadata, standard HTTP properties, and system properties for +// the specified snapshot of a blob +func (client Client) GetSnapshotProperties(ctx context.Context, accountName, containerName, blobName string, input GetSnapshotPropertiesInput) (result GetPropertiesResult, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "GetSnapshotProperties", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "GetSnapshotProperties", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "GetSnapshotProperties", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "GetSnapshotProperties", "`blobName` cannot be an empty string.") + } + if input.SnapshotID == "" { + return result, validation.NewError("blobs.Client", "GetSnapshotProperties", "`input.SnapshotID` cannot be an empty string.") + } + + req, err := client.GetSnapshotPropertiesPreparer(ctx, accountName, containerName, blobName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetSnapshotProperties", nil, "Failure preparing request") + return + } + + // we re-use the GetProperties methods since this is otherwise the same + resp, err := client.GetPropertiesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "GetSnapshotProperties", resp, "Failure sending request") + return + } + + result, err = client.GetPropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "GetSnapshotProperties", resp, "Failure responding to request") + return + } + + return +} + +// GetSnapshotPreparer prepares the GetSnapshot request. +func (client Client) GetSnapshotPropertiesPreparer(ctx context.Context, accountName, containerName, blobName string, input GetSnapshotPropertiesInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "snapshot": autorest.Encode("query", input.SnapshotID), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if input.LeaseID != nil { + headers["x-ms-lease-id"] = *input.LeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsHead(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} diff --git a/storage/2018-11-09/blob/blobs/snapshot_test.go b/storage/2018-11-09/blob/blobs/snapshot_test.go new file mode 100644 index 0000000..d8c6ae5 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/snapshot_test.go @@ -0,0 +1,159 @@ +package blobs + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/containers" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestSnapshotLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + fileName := "example.txt" + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.BlobStorage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + containersClient := containers.NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithStorageResourceManagerAuth(containersClient.Client) + + _, err = containersClient.Create(ctx, accountName, containerName, containers.CreateInput{}) + if err != nil { + t.Fatalf("Error creating: %s", err) + } + defer containersClient.Delete(ctx, accountName, containerName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + blobClient := NewWithEnvironment(client.Environment) + blobClient.Client = client.PrepareWithAuthorizer(blobClient.Client, storageAuth) + + t.Logf("[DEBUG] Copying file to Blob Storage..") + copyInput := CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + + refreshInterval := 5 * time.Second + if err := blobClient.CopyAndWait(ctx, accountName, containerName, fileName, copyInput, refreshInterval); err != nil { + t.Fatalf("Error copying: %s", err) + } + + t.Logf("[DEBUG] First Snapshot..") + firstSnapshot, err := blobClient.Snapshot(ctx, accountName, containerName, fileName, SnapshotInput{}) + if err != nil { + t.Fatalf("Error taking first snapshot: %s", err) + } + t.Logf("[DEBUG] First Snapshot ID: %q", firstSnapshot.SnapshotDateTime) + + t.Log("[DEBUG] Waiting 2 seconds..") + time.Sleep(2 * time.Second) + + t.Logf("[DEBUG] Second Snapshot..") + secondSnapshot, err := blobClient.Snapshot(ctx, accountName, containerName, fileName, SnapshotInput{ + MetaData: map[string]string{ + "hello": "world", + }, + }) + if err != nil { + t.Fatalf("Error taking Second snapshot: %s", err) + } + t.Logf("[DEBUG] Second Snapshot ID: %q", secondSnapshot.SnapshotDateTime) + + t.Logf("[DEBUG] Leasing the Blob..") + leaseDetails, err := blobClient.AcquireLease(ctx, accountName, containerName, fileName, AcquireLeaseInput{ + // infinite + LeaseDuration: -1, + }) + if err != nil { + t.Fatalf("Error leasing Blob: %s", err) + } + t.Logf("[DEBUG] Lease ID: %q", leaseDetails.LeaseID) + + t.Logf("[DEBUG] Third Snapshot..") + thirdSnapshot, err := blobClient.Snapshot(ctx, accountName, containerName, fileName, SnapshotInput{ + LeaseID: &leaseDetails.LeaseID, + }) + if err != nil { + t.Fatalf("Error taking Third snapshot: %s", err) + } + t.Logf("[DEBUG] Third Snapshot ID: %q", thirdSnapshot.SnapshotDateTime) + + t.Logf("[DEBUG] Releasing Lease..") + if _, err := blobClient.ReleaseLease(ctx, accountName, containerName, fileName, leaseDetails.LeaseID); err != nil { + t.Fatalf("Error releasing Lease: %s", err) + } + + // get the properties from the blob, which should include the LastModifiedDate + t.Logf("[DEBUG] Retrieving Properties for Blob") + props, err := blobClient.GetProperties(ctx, accountName, containerName, fileName, GetPropertiesInput{}) + if err != nil { + t.Fatalf("Error getting properties: %s", err) + } + + // confirm that the If-Modified-None returns an error + t.Logf("[DEBUG] Third Snapshot..") + fourthSnapshot, err := blobClient.Snapshot(ctx, accountName, containerName, fileName, SnapshotInput{ + LeaseID: &leaseDetails.LeaseID, + IfModifiedSince: &props.LastModified, + }) + if err == nil { + t.Fatalf("Expected an error but didn't get one") + } + if fourthSnapshot.Response.StatusCode != http.StatusPreconditionFailed { + t.Fatalf("Expected the status code to be Precondition Failed but got: %d", fourthSnapshot.Response.StatusCode) + } + + t.Logf("[DEBUG] Retrieving the Second Snapshot Properties..") + getSecondSnapshotInput := GetSnapshotPropertiesInput{ + SnapshotID: secondSnapshot.SnapshotDateTime, + } + if _, err := blobClient.GetSnapshotProperties(ctx, accountName, containerName, fileName, getSecondSnapshotInput); err != nil { + t.Fatalf("Error retrieving properties for the second snapshot: %s", err) + } + + t.Logf("[DEBUG] Deleting the Second Snapshot..") + deleteSnapshotInput := DeleteSnapshotInput{ + SnapshotDateTime: secondSnapshot.SnapshotDateTime, + } + if _, err := blobClient.DeleteSnapshot(ctx, accountName, containerName, fileName, deleteSnapshotInput); err != nil { + t.Fatalf("Error deleting snapshot: %s", err) + } + + t.Logf("[DEBUG] Re-Retrieving the Second Snapshot Properties..") + secondSnapshotProps, err := blobClient.GetSnapshotProperties(ctx, accountName, containerName, fileName, getSecondSnapshotInput) + if err == nil { + t.Fatalf("Expected an error retrieving the snapshot but got none") + } + if secondSnapshotProps.Response.StatusCode != http.StatusNotFound { + t.Fatalf("Expected the status code to be %d but got %q", http.StatusNoContent, secondSnapshotProps.Response.StatusCode) + } + + t.Logf("[DEBUG] Deleting all the snapshots..") + if _, err := blobClient.DeleteSnapshots(ctx, accountName, containerName, fileName, DeleteSnapshotsInput{}); err != nil { + t.Fatalf("Error deleting snapshots: %s", err) + } + + t.Logf("[DEBUG] Deleting the Blob..") + deleteInput := DeleteInput{ + DeleteSnapshots: false, + } + if _, err := blobClient.Delete(ctx, accountName, containerName, fileName, deleteInput); err != nil { + t.Fatalf("Error deleting Blob: %s", err) + } +} diff --git a/storage/2018-11-09/blob/blobs/undelete.go b/storage/2018-11-09/blob/blobs/undelete.go new file mode 100644 index 0000000..9be2f81 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/undelete.go @@ -0,0 +1,92 @@ +package blobs + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Undelete restores the contents and metadata of soft deleted blob and any associated soft deleted snapshots. +func (client Client) Undelete(ctx context.Context, accountName, containerName, blobName string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("blobs.Client", "Undelete", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("blobs.Client", "Undelete", "`containerName` cannot be an empty string.") + } + if strings.ToLower(containerName) != containerName { + return result, validation.NewError("blobs.Client", "Undelete", "`containerName` must be a lower-cased string.") + } + if blobName == "" { + return result, validation.NewError("blobs.Client", "Undelete", "`blobName` cannot be an empty string.") + } + + req, err := client.UndeletePreparer(ctx, accountName, containerName, blobName) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Undelete", nil, "Failure preparing request") + return + } + + resp, err := client.UndeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "blobs.Client", "Undelete", resp, "Failure sending request") + return + } + + result, err = client.UndeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "blobs.Client", "Undelete", resp, "Failure responding to request") + return + } + + return +} + +// UndeletePreparer prepares the Undelete request. +func (client Client) UndeletePreparer(ctx context.Context, accountName, containerName, blobName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + "blobName": autorest.Encode("path", blobName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("path", "undelete"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}/{blobName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// UndeleteSender sends the Undelete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) UndeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// UndeleteResponder handles the response to the Undelete request. The method always +// closes the http.Response Body. +func (client Client) UndeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-11-09/blob/blobs/version.go b/storage/2018-11-09/blob/blobs/version.go new file mode 100644 index 0000000..ad61a57 --- /dev/null +++ b/storage/2018-11-09/blob/blobs/version.go @@ -0,0 +1,14 @@ +package blobs + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2018-11-09" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2018-11-09/blob/containers/README.md b/storage/2018-11-09/blob/containers/README.md new file mode 100644 index 0000000..37d2878 --- /dev/null +++ b/storage/2018-11-09/blob/containers/README.md @@ -0,0 +1,45 @@ +## Blob Storage Container SDK for API version 2018-11-09 + +This package allows you to interact with the Containers Blob Storage API + +### Supported Authorizers + +* Azure Active Directory (for the Resource Endpoint `https://storage.azure.com`) +* SharedKeyLite (Blob, File & Queue) + +Note: when using the `ListBlobs` operation, only `SharedKeyLite` authentication is supported. + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/containers" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + containerName := "mycontainer" + + storageAuth := autorest.NewSharedKeyLiteAuthorizer(accountName, storageAccountKey) + containersClient := containers.New() + containersClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + createInput := containers.CreateInput{ + AccessLevel: containers.Private, + } + if _, err := containersClient.Create(ctx, accountName, containerName, createInput); err != nil { + return fmt.Errorf("Error creating Container: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2018-11-09/blob/containers/client.go b/storage/2018-11-09/blob/containers/client.go new file mode 100644 index 0000000..7bf4947 --- /dev/null +++ b/storage/2018-11-09/blob/containers/client.go @@ -0,0 +1,34 @@ +package containers + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for Blob Storage Containers. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithBaseURI creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} + +func (client Client) setAccessLevelIntoHeaders(headers map[string]interface{}, level AccessLevel) map[string]interface{} { + // If this header is not included in the request, container data is private to the account owner. + if level != Private { + headers["x-ms-blob-public-access"] = string(level) + } + + return headers +} diff --git a/storage/2018-11-09/blob/containers/create.go b/storage/2018-11-09/blob/containers/create.go new file mode 100644 index 0000000..84c2887 --- /dev/null +++ b/storage/2018-11-09/blob/containers/create.go @@ -0,0 +1,123 @@ +package containers + +import ( + "context" + "fmt" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type CreateInput struct { + // Specifies whether data in the container may be accessed publicly and the level of access + AccessLevel AccessLevel + + // A name-value pair to associate with the container as metadata. + MetaData map[string]string +} + +type CreateResponse struct { + autorest.Response + Error *ErrorResponse `xml:"Error"` +} + +// Create creates a new container under the specified account. +// If the container with the same name already exists, the operation fails. +func (client Client) Create(ctx context.Context, accountName, containerName string, input CreateInput) (result CreateResponse, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "Create", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "Create", "`containerName` cannot be an empty string.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("containers.Client", "Create", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.CreatePreparer(ctx, accountName, containerName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "Create", nil, "Failure preparing request") + return + } + + resp, err := client.CreateSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "Create", resp, "Failure sending request") + return + } + + result, err = client.CreateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "Create", resp, "Failure responding to request") + return + } + + return +} + +// CreatePreparer prepares the Create request. +func (client Client) CreatePreparer(ctx context.Context, accountName string, containerName string, input CreateInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = client.setAccessLevelIntoHeaders(headers, input.AccessLevel) + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CreateSender sends the Create request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CreateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CreateResponder handles the response to the Create request. The method always +// closes the http.Response Body. +func (client Client) CreateResponder(resp *http.Response) (result CreateResponse, err error) { + successfulStatusCodes := []int{ + http.StatusCreated, + } + if autorest.ResponseHasStatusCode(resp, successfulStatusCodes...) { + // when successful there's no response + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(successfulStatusCodes...), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + } else { + // however when there's an error the error's in the response + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(successfulStatusCodes...), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + } + + return +} diff --git a/storage/2018-11-09/blob/containers/delete.go b/storage/2018-11-09/blob/containers/delete.go new file mode 100644 index 0000000..3095829 --- /dev/null +++ b/storage/2018-11-09/blob/containers/delete.go @@ -0,0 +1,85 @@ +package containers + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Delete marks the specified container for deletion. +// The container and any blobs contained within it are later deleted during garbage collection. +func (client Client) Delete(ctx context.Context, accountName, containerName string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "Delete", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "Delete", "`containerName` cannot be an empty string.") + } + + req, err := client.DeletePreparer(ctx, accountName, containerName) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName string, containerName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + return +} diff --git a/storage/2018-11-09/blob/containers/get_properties.go b/storage/2018-11-09/blob/containers/get_properties.go new file mode 100644 index 0000000..1e308da --- /dev/null +++ b/storage/2018-11-09/blob/containers/get_properties.go @@ -0,0 +1,124 @@ +package containers + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// GetProperties returns the properties for this Container without a Lease +func (client Client) GetProperties(ctx context.Context, accountName, containerName string) (ContainerProperties, error) { + // If specified, Get Container Properties only succeeds if the container’s lease is active and matches this ID. + // If there is no active lease or the ID does not match, 412 (Precondition Failed) is returned. + return client.GetPropertiesWithLeaseID(ctx, accountName, containerName, "") +} + +// GetPropertiesWithLeaseID returns the properties for this Container using the specified LeaseID +func (client Client) GetPropertiesWithLeaseID(ctx context.Context, accountName, containerName, leaseID string) (result ContainerProperties, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "GetPropertiesWithLeaseID", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "GetPropertiesWithLeaseID", "`containerName` cannot be an empty string.") + } + + req, err := client.GetPropertiesWithLeaseIDPreparer(ctx, accountName, containerName, leaseID) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "GetProperties", nil, "Failure preparing request") + return + } + + resp, err := client.GetPropertiesWithLeaseIDSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "GetProperties", resp, "Failure sending request") + return + } + + result, err = client.GetPropertiesWithLeaseIDResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "GetProperties", resp, "Failure responding to request") + return + } + + return +} + +// GetPropertiesWithLeaseIDPreparer prepares the GetPropertiesWithLeaseID request. +func (client Client) GetPropertiesWithLeaseIDPreparer(ctx context.Context, accountName, containerName, leaseID string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + // If specified, Get Container Properties only succeeds if the container’s lease is active and matches this ID. + // If there is no active lease or the ID does not match, 412 (Precondition Failed) is returned. + if leaseID != "" { + headers["x-ms-lease-id"] = leaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetPropertiesWithLeaseIDSender sends the GetPropertiesWithLeaseID request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetPropertiesWithLeaseIDSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetPropertiesWithLeaseIDResponder handles the response to the GetPropertiesWithLeaseID request. The method always +// closes the http.Response Body. +func (client Client) GetPropertiesWithLeaseIDResponder(resp *http.Response) (result ContainerProperties, err error) { + if resp != nil { + result.LeaseStatus = LeaseStatus(resp.Header.Get("x-ms-lease-status")) + result.LeaseState = LeaseState(resp.Header.Get("x-ms-lease-state")) + if result.LeaseStatus == Locked { + duration := LeaseDuration(resp.Header.Get("x-ms-lease-duration")) + result.LeaseDuration = &duration + } + + // If this header is not returned in the response, the container is private to the account owner. + accessLevel := resp.Header.Get("x-ms-blob-public-access") + if accessLevel != "" { + result.AccessLevel = AccessLevel(accessLevel) + } else { + result.AccessLevel = Private + } + + // we can't necessarily use strconv.ParseBool here since this could be nil (only in some API versions) + result.HasImmutabilityPolicy = strings.EqualFold(resp.Header.Get("x-ms-has-immutability-policy"), "true") + result.HasLegalHold = strings.EqualFold(resp.Header.Get("x-ms-has-legal-hold"), "true") + + result.MetaData = metadata.ParseFromHeaders(resp.Header) + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/containers/lease_acquire.go b/storage/2018-11-09/blob/containers/lease_acquire.go new file mode 100644 index 0000000..061c863 --- /dev/null +++ b/storage/2018-11-09/blob/containers/lease_acquire.go @@ -0,0 +1,115 @@ +package containers + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type AcquireLeaseInput struct { + // Specifies the duration of the lease, in seconds, or negative one (-1) for a lease that never expires. + // A non-infinite lease can be between 15 and 60 seconds + LeaseDuration int + + ProposedLeaseID string +} + +type AcquireLeaseResponse struct { + autorest.Response + + LeaseID string +} + +// AcquireLease establishes and manages a lock on a container for delete operations. +func (client Client) AcquireLease(ctx context.Context, accountName, containerName string, input AcquireLeaseInput) (result AcquireLeaseResponse, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "AcquireLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "AcquireLease", "`containerName` cannot be an empty string.") + } + // An infinite lease duration is -1 seconds. A non-infinite lease can be between 15 and 60 seconds + if input.LeaseDuration != -1 && (input.LeaseDuration <= 15 || input.LeaseDuration >= 60) { + return result, validation.NewError("containers.Client", "AcquireLease", "`input.LeaseDuration` must be -1 (infinite), or between 15 and 60 seconds.") + } + + req, err := client.AcquireLeasePreparer(ctx, accountName, containerName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "AcquireLease", nil, "Failure preparing request") + return + } + + resp, err := client.AcquireLeaseSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "AcquireLease", resp, "Failure sending request") + return + } + + result, err = client.AcquireLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "AcquireLease", resp, "Failure responding to request") + return + } + + return +} + +// AcquireLeasePreparer prepares the AcquireLease request. +func (client Client) AcquireLeasePreparer(ctx context.Context, accountName string, containerName string, input AcquireLeaseInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + "comp": autorest.Encode("path", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "acquire", + "x-ms-lease-duration": input.LeaseDuration, + } + + if input.ProposedLeaseID != "" { + headers["x-ms-proposed-lease-id"] = input.ProposedLeaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// AcquireLeaseSender sends the AcquireLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) AcquireLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// AcquireLeaseResponder handles the response to the AcquireLease request. The method always +// closes the http.Response Body. +func (client Client) AcquireLeaseResponder(resp *http.Response) (result AcquireLeaseResponse, err error) { + if resp != nil { + result.LeaseID = resp.Header.Get("x-ms-lease-id") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/containers/lease_break.go b/storage/2018-11-09/blob/containers/lease_break.go new file mode 100644 index 0000000..08acfb7 --- /dev/null +++ b/storage/2018-11-09/blob/containers/lease_break.go @@ -0,0 +1,129 @@ +package containers + +import ( + "context" + "net/http" + "strconv" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type BreakLeaseInput struct { + // For a break operation, proposed duration the lease should continue + // before it is broken, in seconds, between 0 and 60. + // This break period is only used if it is shorter than the time remaining on the lease. + // If longer, the time remaining on the lease is used. + // A new lease will not be available before the break period has expired, + // but the lease may be held for longer than the break period. + // If this header does not appear with a break operation, a fixed-duration lease breaks + // after the remaining lease period elapses, and an infinite lease breaks immediately. + BreakPeriod *int + + LeaseID string +} + +type BreakLeaseResponse struct { + autorest.Response + + // Approximate time remaining in the lease period, in seconds. + // If the break is immediate, 0 is returned. + LeaseTime int +} + +// BreakLease breaks a lock based on it's Lease ID +func (client Client) BreakLease(ctx context.Context, accountName, containerName string, input BreakLeaseInput) (result BreakLeaseResponse, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "BreakLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "BreakLease", "`containerName` cannot be an empty string.") + } + if input.LeaseID == "" { + return result, validation.NewError("containers.Client", "BreakLease", "`input.LeaseID` cannot be an empty string.") + } + + req, err := client.BreakLeasePreparer(ctx, accountName, containerName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "BreakLease", nil, "Failure preparing request") + return + } + + resp, err := client.BreakLeaseSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "BreakLease", resp, "Failure sending request") + return + } + + result, err = client.BreakLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "BreakLease", resp, "Failure responding to request") + return + } + + return +} + +// BreakLeasePreparer prepares the BreakLease request. +func (client Client) BreakLeasePreparer(ctx context.Context, accountName string, containerName string, input BreakLeaseInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + "comp": autorest.Encode("path", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "break", + "x-ms-lease-id": input.LeaseID, + } + + if input.BreakPeriod != nil { + headers["x-ms-lease-break-period"] = *input.BreakPeriod + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// BreakLeaseSender sends the BreakLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) BreakLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// BreakLeaseResponder handles the response to the BreakLease request. The method always +// closes the http.Response Body. +func (client Client) BreakLeaseResponder(resp *http.Response) (result BreakLeaseResponse, err error) { + if resp != nil { + leaseRaw := resp.Header.Get("x-ms-lease-time") + if leaseRaw != "" { + i, err := strconv.Atoi(leaseRaw) + if err == nil { + result.LeaseTime = i + } + } + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/containers/lease_change.go b/storage/2018-11-09/blob/containers/lease_change.go new file mode 100644 index 0000000..dfbcb13 --- /dev/null +++ b/storage/2018-11-09/blob/containers/lease_change.go @@ -0,0 +1,111 @@ +package containers + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type ChangeLeaseInput struct { + ExistingLeaseID string + ProposedLeaseID string +} + +type ChangeLeaseResponse struct { + autorest.Response + + LeaseID string +} + +// ChangeLease changes the lock from one Lease ID to another Lease ID +func (client Client) ChangeLease(ctx context.Context, accountName, containerName string, input ChangeLeaseInput) (result ChangeLeaseResponse, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "ChangeLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "ChangeLease", "`containerName` cannot be an empty string.") + } + if input.ExistingLeaseID == "" { + return result, validation.NewError("containers.Client", "ChangeLease", "`input.ExistingLeaseID` cannot be an empty string.") + } + if input.ProposedLeaseID == "" { + return result, validation.NewError("containers.Client", "ChangeLease", "`input.ProposedLeaseID` cannot be an empty string.") + } + + req, err := client.ChangeLeasePreparer(ctx, accountName, containerName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "ChangeLease", nil, "Failure preparing request") + return + } + + resp, err := client.ChangeLeaseSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "ChangeLease", resp, "Failure sending request") + return + } + + result, err = client.ChangeLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "ChangeLease", resp, "Failure responding to request") + return + } + + return +} + +// ChangeLeasePreparer prepares the ChangeLease request. +func (client Client) ChangeLeasePreparer(ctx context.Context, accountName string, containerName string, input ChangeLeaseInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + "comp": autorest.Encode("path", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "change", + "x-ms-lease-id": input.ExistingLeaseID, + "x-ms-proposed-lease-id": input.ProposedLeaseID, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ChangeLeaseSender sends the ChangeLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ChangeLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ChangeLeaseResponder handles the response to the ChangeLease request. The method always +// closes the http.Response Body. +func (client Client) ChangeLeaseResponder(resp *http.Response) (result ChangeLeaseResponse, err error) { + if resp != nil { + result.LeaseID = resp.Header.Get("x-ms-lease-id") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/containers/lease_release.go b/storage/2018-11-09/blob/containers/lease_release.go new file mode 100644 index 0000000..fafcf98 --- /dev/null +++ b/storage/2018-11-09/blob/containers/lease_release.go @@ -0,0 +1,92 @@ +package containers + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// ReleaseLease releases the lock based on the Lease ID +func (client Client) ReleaseLease(ctx context.Context, accountName, containerName, leaseID string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "ReleaseLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "ReleaseLease", "`containerName` cannot be an empty string.") + } + if leaseID == "" { + return result, validation.NewError("containers.Client", "ReleaseLease", "`leaseID` cannot be an empty string.") + } + + req, err := client.ReleaseLeasePreparer(ctx, accountName, containerName, leaseID) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "ReleaseLease", nil, "Failure preparing request") + return + } + + resp, err := client.ReleaseLeaseSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "ReleaseLease", resp, "Failure sending request") + return + } + + result, err = client.ReleaseLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "ReleaseLease", resp, "Failure responding to request") + return + } + + return +} + +// ReleaseLeasePreparer prepares the ReleaseLease request. +func (client Client) ReleaseLeasePreparer(ctx context.Context, accountName string, containerName string, leaseID string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + "comp": autorest.Encode("path", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "release", + "x-ms-lease-id": leaseID, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ReleaseLeaseSender sends the ReleaseLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ReleaseLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ReleaseLeaseResponder handles the response to the ReleaseLease request. The method always +// closes the http.Response Body. +func (client Client) ReleaseLeaseResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/containers/lease_renew.go b/storage/2018-11-09/blob/containers/lease_renew.go new file mode 100644 index 0000000..3fe1765 --- /dev/null +++ b/storage/2018-11-09/blob/containers/lease_renew.go @@ -0,0 +1,92 @@ +package containers + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// RenewLease renewes the lock based on the Lease ID +func (client Client) RenewLease(ctx context.Context, accountName, containerName, leaseID string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "RenewLease", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "RenewLease", "`containerName` cannot be an empty string.") + } + if leaseID == "" { + return result, validation.NewError("containers.Client", "RenewLease", "`leaseID` cannot be an empty string.") + } + + req, err := client.RenewLeasePreparer(ctx, accountName, containerName, leaseID) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "RenewLease", nil, "Failure preparing request") + return + } + + resp, err := client.RenewLeaseSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "RenewLease", resp, "Failure sending request") + return + } + + result, err = client.RenewLeaseResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "RenewLease", resp, "Failure responding to request") + return + } + + return +} + +// RenewLeasePreparer prepares the RenewLease request. +func (client Client) RenewLeasePreparer(ctx context.Context, accountName string, containerName string, leaseID string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "container"), + "comp": autorest.Encode("path", "lease"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-lease-action": "renew", + "x-ms-lease-id": leaseID, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// RenewLeaseSender sends the RenewLease request. The method will close the +// http.Response Body if it receives an error. +func (client Client) RenewLeaseSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// RenewLeaseResponder handles the response to the RenewLease request. The method always +// closes the http.Response Body. +func (client Client) RenewLeaseResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/containers/lifecycle_test.go b/storage/2018-11-09/blob/containers/lifecycle_test.go new file mode 100644 index 0000000..389c773 --- /dev/null +++ b/storage/2018-11-09/blob/containers/lifecycle_test.go @@ -0,0 +1,174 @@ +package containers + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestContainerLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + containerName := fmt.Sprintf("cont-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.BlobStorage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + containersClient := NewWithEnvironment(client.Environment) + containersClient.Client = client.PrepareWithAuthorizer(containersClient.Client, storageAuth) + + // first let's test an empty container + input := CreateInput{} + _, err = containersClient.Create(ctx, accountName, containerName, input) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + + container, err := containersClient.GetProperties(ctx, accountName, containerName) + if err != nil { + t.Fatal(fmt.Errorf("Error retrieving: %s", err)) + } + + if container.AccessLevel != Private { + t.Fatalf("Expected Access Level to be Private but got %q", container.AccessLevel) + } + if len(container.MetaData) != 0 { + t.Fatalf("Expected MetaData to be empty but got: %s", container.MetaData) + } + if container.LeaseStatus != Unlocked { + t.Fatalf("Expected Container Lease to be Unlocked but was: %s", container.LeaseStatus) + } + + // then update the metadata + metaData := map[string]string{ + "dont": "kill-my-vibe", + } + _, err = containersClient.SetMetaData(ctx, accountName, containerName, metaData) + if err != nil { + t.Fatal(fmt.Errorf("Error updating metadata: %s", err)) + } + + // give azure time to replicate + time.Sleep(2 * time.Second) + + // then assert that + container, err = containersClient.GetProperties(ctx, accountName, containerName) + if err != nil { + t.Fatal(fmt.Errorf("Error re-retrieving: %s", err)) + } + if len(container.MetaData) != 1 { + t.Fatalf("Expected 1 item in the metadata but got: %s", container.MetaData) + } + if container.MetaData["dont"] != "kill-my-vibe" { + t.Fatalf("Expected `kill-my-vibe` but got %q", container.MetaData["dont"]) + } + if container.AccessLevel != Private { + t.Fatalf("Expected Access Level to be Private but got %q", container.AccessLevel) + } + if container.LeaseStatus != Unlocked { + t.Fatalf("Expected Container Lease to be Unlocked but was: %s", container.LeaseStatus) + } + + // then update the ACL + _, err = containersClient.SetAccessControl(ctx, accountName, containerName, Blob) + if err != nil { + t.Fatal(fmt.Errorf("Error updating ACL's: %s", err)) + } + + // give azure some time to replicate + time.Sleep(2 * time.Second) + + // then assert that + container, err = containersClient.GetProperties(ctx, accountName, containerName) + if err != nil { + t.Fatal(fmt.Errorf("Error re-retrieving: %s", err)) + } + if container.AccessLevel != Blob { + t.Fatalf("Expected Access Level to be Blob but got %q", container.AccessLevel) + } + if len(container.MetaData) != 1 { + t.Fatalf("Expected 1 item in the metadata but got: %s", container.MetaData) + } + if container.LeaseStatus != Unlocked { + t.Fatalf("Expected Container Lease to be Unlocked but was: %s", container.LeaseStatus) + } + + // acquire a lease for 30s + acquireLeaseInput := AcquireLeaseInput{ + LeaseDuration: 30, + } + acquireLeaseResp, err := containersClient.AcquireLease(ctx, accountName, containerName, acquireLeaseInput) + if err != nil { + t.Fatalf("Error acquiring lease: %s", err) + } + t.Logf("[DEBUG] Lease ID: %s", acquireLeaseResp.LeaseID) + + // we should then be able to update the ID + t.Logf("[DEBUG] Changing lease..") + updateLeaseInput := ChangeLeaseInput{ + ExistingLeaseID: acquireLeaseResp.LeaseID, + ProposedLeaseID: "aaaabbbb-aaaa-bbbb-cccc-aaaabbbbcccc", + } + updateLeaseResp, err := containersClient.ChangeLease(ctx, accountName, containerName, updateLeaseInput) + if err != nil { + t.Fatalf("Error changing lease: %s", err) + } + + // then renew it + _, err = containersClient.RenewLease(ctx, accountName, containerName, updateLeaseResp.LeaseID) + if err != nil { + t.Fatalf("Error renewing lease: %s", err) + } + + // and then give it a timeout + breakPeriod := 20 + breakLeaseInput := BreakLeaseInput{ + LeaseID: updateLeaseResp.LeaseID, + BreakPeriod: &breakPeriod, + } + breakLeaseResp, err := containersClient.BreakLease(ctx, accountName, containerName, breakLeaseInput) + if err != nil { + t.Fatalf("Error breaking lease: %s", err) + } + if breakLeaseResp.LeaseTime == 0 { + t.Fatalf("Lease broke immediately when should have waited: %d", breakLeaseResp.LeaseTime) + } + + // and finally ditch it + _, err = containersClient.ReleaseLease(ctx, accountName, containerName, updateLeaseResp.LeaseID) + if err != nil { + t.Fatalf("Error releasing lease: %s", err) + } + + t.Logf("[DEBUG] Listing blobs in the container..") + listInput := ListBlobsInput{} + listResult, err := containersClient.ListBlobs(ctx, accountName, containerName, listInput) + if err != nil { + t.Fatalf("Error listing blobs: %s", err) + } + + if len(listResult.Blobs.Blobs) != 0 { + t.Fatalf("Expected there to be no blobs in the container but got %d", len(listResult.Blobs.Blobs)) + } + + t.Logf("[DEBUG] Deleting..") + _, err = containersClient.Delete(ctx, accountName, containerName) + if err != nil { + t.Fatal(fmt.Errorf("Error deleting: %s", err)) + } +} diff --git a/storage/2018-11-09/blob/containers/list_blobs.go b/storage/2018-11-09/blob/containers/list_blobs.go new file mode 100644 index 0000000..82797d0 --- /dev/null +++ b/storage/2018-11-09/blob/containers/list_blobs.go @@ -0,0 +1,179 @@ +package containers + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type ListBlobsInput struct { + Delimiter *string + Include *[]Dataset + Marker *string + MaxResults *int + Prefix *string +} + +type ListBlobsResult struct { + autorest.Response + + Delimiter string `xml:"Delimiter"` + Marker string `xml:"Marker"` + MaxResults int `xml:"MaxResults"` + NextMarker *string `xml:"NextMarker,omitempty"` + Prefix string `xml:"Prefix"` + Blobs Blobs `xml:"Blobs"` +} + +type Blobs struct { + Blobs []BlobDetails `xml:"Blob"` + BlobPrefix *BlobPrefix `xml:"BlobPrefix"` +} + +type BlobDetails struct { + Name string `xml:"Name"` + Deleted bool `xml:"Deleted,omitempty"` + MetaData map[string]interface{} `map:"Metadata,omitempty"` + Properties *BlobProperties `xml:"Properties,omitempty"` + Snapshot *string `xml:"Snapshot,omitempty"` +} + +type BlobProperties struct { + AccessTier *string `xml:"AccessTier,omitempty"` + AccessTierInferred *bool `xml:"AccessTierInferred,omitempty"` + AccessTierChangeTime *string `xml:"AccessTierChangeTime,omitempty"` + BlobType *string `xml:"BlobType,omitempty"` + BlobSequenceNumber *string `xml:"x-ms-blob-sequence-number,omitempty"` + CacheControl *string `xml:"Cache-Control,omitempty"` + ContentEncoding *string `xml:"ContentEncoding,omitempty"` + ContentLanguage *string `xml:"Content-Language,omitempty"` + ContentLength *int64 `xml:"Content-Length,omitempty"` + ContentMD5 *string `xml:"Content-MD5,omitempty"` + ContentType *string `xml:"Content-Type,omitempty"` + CopyCompletionTime *string `xml:"CopyCompletionTime,omitempty"` + CopyId *string `xml:"CopyId,omitempty"` + CopyStatus *string `xml:"CopyStatus,omitempty"` + CopySource *string `xml:"CopySource,omitempty"` + CopyProgress *string `xml:"CopyProgress,omitempty"` + CopyStatusDescription *string `xml:"CopyStatusDescription,omitempty"` + CreationTime *string `xml:"CreationTime,omitempty"` + ETag *string `xml:"Etag,omitempty"` + DeletedTime *string `xml:"DeletedTime,omitempty"` + IncrementalCopy *bool `xml:"IncrementalCopy,omitempty"` + LastModified *string `xml:"Last-Modified,omitempty"` + LeaseDuration *string `xml:"LeaseDuration,omitempty"` + LeaseState *string `xml:"LeaseState,omitempty"` + LeaseStatus *string `xml:"LeaseStatus,omitempty"` + RemainingRetentionDays *string `xml:"RemainingRetentionDays,omitempty"` + ServerEncrypted *bool `xml:"ServerEncrypted,omitempty"` +} + +type BlobPrefix struct { + Name string `xml:"Name"` +} + +// ListBlobs lists the blobs matching the specified query within the specified Container +func (client Client) ListBlobs(ctx context.Context, accountName, containerName string, input ListBlobsInput) (result ListBlobsResult, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "ListBlobs", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "ListBlobs", "`containerName` cannot be an empty string.") + } + if input.MaxResults != nil && (*input.MaxResults <= 0 || *input.MaxResults > 5000) { + return result, validation.NewError("containers.Client", "ListBlobs", "`input.MaxResults` can either be nil or between 0 and 5000.") + } + + req, err := client.ListBlobsPreparer(ctx, accountName, containerName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "ListBlobs", nil, "Failure preparing request") + return + } + + resp, err := client.ListBlobsSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "ListBlobs", resp, "Failure sending request") + return + } + + result, err = client.ListBlobsResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "ListBlobs", resp, "Failure responding to request") + return + } + + return +} + +// ListBlobsPreparer prepares the ListBlobs request. +func (client Client) ListBlobsPreparer(ctx context.Context, accountName, containerName string, input ListBlobsInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "list"), + "restype": autorest.Encode("query", "container"), + } + + if input.Delimiter != nil { + queryParameters["delimiter"] = autorest.Encode("query", *input.Delimiter) + } + if input.Include != nil { + vals := make([]string, 0) + for _, v := range *input.Include { + vals = append(vals, string(v)) + } + include := strings.Join(vals, ",") + queryParameters["include"] = autorest.Encode("query", include) + } + if input.Marker != nil { + queryParameters["marker"] = autorest.Encode("query", *input.Marker) + } + if input.MaxResults != nil { + queryParameters["maxresults"] = autorest.Encode("query", *input.MaxResults) + } + if input.Prefix != nil { + queryParameters["prefix"] = autorest.Encode("query", *input.Prefix) + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ListBlobsSender sends the ListBlobs request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ListBlobsSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ListBlobsResponder handles the response to the ListBlobs request. The method always +// closes the http.Response Body. +func (client Client) ListBlobsResponder(resp *http.Response) (result ListBlobsResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/containers/models.go b/storage/2018-11-09/blob/containers/models.go new file mode 100644 index 0000000..adba368 --- /dev/null +++ b/storage/2018-11-09/blob/containers/models.go @@ -0,0 +1,75 @@ +package containers + +import "github.com/Azure/go-autorest/autorest" + +type AccessLevel string + +var ( + // Blob specifies public read access for blobs. + // Blob data within this container can be read via anonymous request, + // but container data is not available. + // Clients cannot enumerate blobs within the container via anonymous request. + Blob AccessLevel = "blob" + + // Container specifies full public read access for container and blob data. + // Clients can enumerate blobs within the container via anonymous request, + // but cannot enumerate containers within the storage account. + Container AccessLevel = "container" + + // Private specifies that container data is private to the account owner + Private AccessLevel = "" +) + +type ContainerProperties struct { + autorest.Response + + AccessLevel AccessLevel + LeaseStatus LeaseStatus + LeaseState LeaseState + LeaseDuration *LeaseDuration + MetaData map[string]string + HasImmutabilityPolicy bool + HasLegalHold bool +} + +type Dataset string + +var ( + Copy Dataset = "copy" + Deleted Dataset = "deleted" + MetaData Dataset = "metadata" + Snapshots Dataset = "snapshots" + UncommittedBlobs Dataset = "uncommittedblobs" +) + +type ErrorResponse struct { + Code *string `xml:"Code"` + Message *string `xml:"Message"` +} + +type LeaseDuration string + +var ( + // If this lease is for a Fixed Duration + Fixed LeaseDuration = "fixed" + + // If this lease is for an Indefinite Duration + Infinite LeaseDuration = "infinite" +) + +type LeaseState string + +var ( + Available LeaseState = "available" + Breaking LeaseState = "breaking" + Broken LeaseState = "broken" + Expired LeaseState = "expired" + Leased LeaseState = "leased" +) + +type LeaseStatus string + +var ( + Locked LeaseStatus = "locked" + Unlocked LeaseStatus = "unlocked" +) diff --git a/storage/2018-11-09/blob/containers/resource_id.go b/storage/2018-11-09/blob/containers/resource_id.go new file mode 100644 index 0000000..a5bfd6e --- /dev/null +++ b/storage/2018-11-09/blob/containers/resource_id.go @@ -0,0 +1,46 @@ +package containers + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given Container +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, containerName string) string { + domain := endpoints.GetBlobEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s", domain, containerName) +} + +type ResourceID struct { + AccountName string + ContainerName string +} + +// ParseResourceID parses the Resource ID and returns an object which can be used +// to interact with the Container Resource +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://foo.blob.core.windows.net/Bar + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + containerName := strings.TrimPrefix(uri.Path, "/") + return &ResourceID{ + AccountName: *accountName, + ContainerName: containerName, + }, nil +} diff --git a/storage/2018-11-09/blob/containers/resource_id_test.go b/storage/2018-11-09/blob/containers/resource_id_test.go new file mode 100644 index 0000000..e27bc9d --- /dev/null +++ b/storage/2018-11-09/blob/containers/resource_id_test.go @@ -0,0 +1,79 @@ +package containers + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.blob.core.chinacloudapi.cn/container1", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.blob.core.cloudapi.de/container1", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.blob.core.windows.net/container1", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.blob.core.usgovcloudapi.net/container1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "container1") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.blob.core.chinacloudapi.cn/container1", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.blob.core.cloudapi.de/container1", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.blob.core.windows.net/container1", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.blob.core.usgovcloudapi.net/container1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected the account name to be `account1` but got %q", actual.AccountName) + } + + if actual.ContainerName != "container1" { + t.Fatalf("Expected the container name to be `container1` but got %q", actual.ContainerName) + } + } +} diff --git a/storage/2018-11-09/blob/containers/set_acl.go b/storage/2018-11-09/blob/containers/set_acl.go new file mode 100644 index 0000000..fcf4e10 --- /dev/null +++ b/storage/2018-11-09/blob/containers/set_acl.go @@ -0,0 +1,100 @@ +package containers + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// SetAccessControl sets the Access Control for a Container without a Lease ID +func (client Client) SetAccessControl(ctx context.Context, accountName, containerName string, level AccessLevel) (autorest.Response, error) { + return client.SetAccessControlWithLeaseID(ctx, accountName, containerName, "", level) +} + +// SetAccessControlWithLeaseID sets the Access Control for a Container using the specified Lease ID +func (client Client) SetAccessControlWithLeaseID(ctx context.Context, accountName, containerName, leaseID string, level AccessLevel) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "SetAccessControl", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "SetAccessControl", "`containerName` cannot be an empty string.") + } + + req, err := client.SetAccessControlWithLeaseIDPreparer(ctx, accountName, containerName, leaseID, level) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "SetAccessControl", nil, "Failure preparing request") + return + } + + resp, err := client.SetAccessControlWithLeaseIDSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "SetAccessControl", resp, "Failure sending request") + return + } + + result, err = client.SetAccessControlWithLeaseIDResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "SetAccessControl", resp, "Failure responding to request") + return + } + + return +} + +// SetAccessControlWithLeaseIDPreparer prepares the SetAccessControlWithLeaseID request. +func (client Client) SetAccessControlWithLeaseIDPreparer(ctx context.Context, accountName, containerName, leaseID string, level AccessLevel) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("path", "acl"), + "restype": autorest.Encode("path", "container"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = client.setAccessLevelIntoHeaders(headers, level) + + // If specified, Get Container Properties only succeeds if the container’s lease is active and matches this ID. + // If there is no active lease or the ID does not match, 412 (Precondition Failed) is returned. + if leaseID != "" { + headers["x-ms-lease-id"] = leaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetAccessControlWithLeaseIDSender sends the SetAccessControlWithLeaseID request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetAccessControlWithLeaseIDSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetAccessControlWithLeaseIDResponder handles the response to the SetAccessControlWithLeaseID request. The method always +// closes the http.Response Body. +func (client Client) SetAccessControlWithLeaseIDResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/containers/set_metadata.go b/storage/2018-11-09/blob/containers/set_metadata.go new file mode 100644 index 0000000..fb9e07f --- /dev/null +++ b/storage/2018-11-09/blob/containers/set_metadata.go @@ -0,0 +1,105 @@ +package containers + +import ( + "context" + "fmt" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// SetMetaData sets the specified MetaData on the Container without a Lease ID +func (client Client) SetMetaData(ctx context.Context, accountName, containerName string, metaData map[string]string) (autorest.Response, error) { + return client.SetMetaDataWithLeaseID(ctx, accountName, containerName, "", metaData) +} + +// SetMetaDataWithLeaseID sets the specified MetaData on the Container using the specified Lease ID +func (client Client) SetMetaDataWithLeaseID(ctx context.Context, accountName, containerName, leaseID string, metaData map[string]string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("containers.Client", "SetMetaData", "`accountName` cannot be an empty string.") + } + if containerName == "" { + return result, validation.NewError("containers.Client", "SetMetaData", "`containerName` cannot be an empty string.") + } + if err := metadata.Validate(metaData); err != nil { + return result, validation.NewError("containers.Client", "SetMetaData", fmt.Sprintf("`metaData` is not valid: %s.", err)) + } + + req, err := client.SetMetaDataWithLeaseIDPreparer(ctx, accountName, containerName, leaseID, metaData) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "SetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.SetMetaDataWithLeaseIDSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "containers.Client", "SetMetaData", resp, "Failure sending request") + return + } + + result, err = client.SetMetaDataWithLeaseIDResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "containers.Client", "SetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// SetMetaDataWithLeaseIDPreparer prepares the SetMetaDataWithLeaseID request. +func (client Client) SetMetaDataWithLeaseIDPreparer(ctx context.Context, accountName, containerName, leaseID string, metaData map[string]string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "containerName": autorest.Encode("path", containerName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("path", "metadata"), + "restype": autorest.Encode("path", "container"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = metadata.SetIntoHeaders(headers, metaData) + + // If specified, Get Container Properties only succeeds if the container’s lease is active and matches this ID. + // If there is no active lease or the ID does not match, 412 (Precondition Failed) is returned. + if leaseID != "" { + headers["x-ms-lease-id"] = leaseID + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetBlobEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{containerName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetMetaDataWithLeaseIDSender sends the SetMetaDataWithLeaseID request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetMetaDataWithLeaseIDSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetMetaDataWithLeaseIDResponder handles the response to the SetMetaDataWithLeaseID request. The method always +// closes the http.Response Body. +func (client Client) SetMetaDataWithLeaseIDResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/blob/containers/version.go b/storage/2018-11-09/blob/containers/version.go new file mode 100644 index 0000000..7047f30 --- /dev/null +++ b/storage/2018-11-09/blob/containers/version.go @@ -0,0 +1,14 @@ +package containers + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2018-11-09" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2018-11-09/file/directories/README.md b/storage/2018-11-09/file/directories/README.md new file mode 100644 index 0000000..cd120aa --- /dev/null +++ b/storage/2018-11-09/file/directories/README.md @@ -0,0 +1,44 @@ +## File Storage Directories SDK for API version 2018-11-09 + +This package allows you to interact with the Directories File Storage API + +### Supported Authorizers + +* Azure Active Directory (for the Resource Endpoint `https://storage.azure.com`) +* SharedKeyLite (Blob, File & Queue) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/file/directories" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + shareName := "myshare" + directoryName := "myfiles" + + storageAuth := autorest.NewSharedKeyLiteAuthorizer(accountName, storageAccountKey) + directoriesClient := directories.New() + directoriesClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + metadata := map[string]string{ + "hello": "world", + } + if _, err := directoriesClient.Create(ctx, accountName, shareName, directoryName, metadata); err != nil { + return fmt.Errorf("Error creating Directory: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2018-11-09/file/directories/client.go b/storage/2018-11-09/file/directories/client.go new file mode 100644 index 0000000..bf2d315 --- /dev/null +++ b/storage/2018-11-09/file/directories/client.go @@ -0,0 +1,25 @@ +package directories + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for File Storage Shares. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithEnvironment creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2018-11-09/file/directories/create.go b/storage/2018-11-09/file/directories/create.go new file mode 100644 index 0000000..93f5c82 --- /dev/null +++ b/storage/2018-11-09/file/directories/create.go @@ -0,0 +1,101 @@ +package directories + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// Create creates a new directory under the specified share or parent directory. +func (client Client) Create(ctx context.Context, accountName, shareName, path string, metaData map[string]string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("directories.Client", "Create", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("directories.Client", "Create", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("directories.Client", "Create", "`shareName` must be a lower-cased string.") + } + if path == "" { + return result, validation.NewError("directories.Client", "Create", "`path` cannot be an empty string.") + } + if err := metadata.Validate(metaData); err != nil { + return result, validation.NewError("directories.Client", "Create", fmt.Sprintf("`metadata` is not valid: %s.", err)) + } + + req, err := client.CreatePreparer(ctx, accountName, shareName, path, metaData) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "Create", nil, "Failure preparing request") + return + } + + resp, err := client.CreateSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "directories.Client", "Create", resp, "Failure sending request") + return + } + + result, err = client.CreateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "Create", resp, "Failure responding to request") + return + } + + return +} + +// CreatePreparer prepares the Create request. +func (client Client) CreatePreparer(ctx context.Context, accountName, shareName, path string, metaData map[string]string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "directory"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = metadata.SetIntoHeaders(headers, metaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CreateSender sends the Create request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CreateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CreateResponder handles the response to the Create request. The method always +// closes the http.Response Body. +func (client Client) CreateResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/directories/delete.go b/storage/2018-11-09/file/directories/delete.go new file mode 100644 index 0000000..9443c25 --- /dev/null +++ b/storage/2018-11-09/file/directories/delete.go @@ -0,0 +1,95 @@ +package directories + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Delete removes the specified empty directory +// Note that the directory must be empty before it can be deleted. +func (client Client) Delete(ctx context.Context, accountName, shareName, path string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("directories.Client", "Delete", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("directories.Client", "Delete", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("directories.Client", "Delete", "`shareName` must be a lower-cased string.") + } + if path == "" { + return result, validation.NewError("directories.Client", "Delete", "`path` cannot be an empty string.") + } + + req, err := client.DeletePreparer(ctx, accountName, shareName, path) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "directories.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName, shareName, path string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "directory"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/directories/get.go b/storage/2018-11-09/file/directories/get.go new file mode 100644 index 0000000..817d680 --- /dev/null +++ b/storage/2018-11-09/file/directories/get.go @@ -0,0 +1,112 @@ +package directories + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetResult struct { + autorest.Response + + // A set of name-value pairs that contain metadata for the directory. + MetaData map[string]string + + // The value of this header is set to true if the directory metadata is completely + // encrypted using the specified algorithm. Otherwise, the value is set to false. + DirectoryMetaDataEncrypted bool +} + +// Get returns all system properties for the specified directory, +// and can also be used to check the existence of a directory. +func (client Client) Get(ctx context.Context, accountName, shareName, path string) (result GetResult, err error) { + if accountName == "" { + return result, validation.NewError("directories.Client", "Get", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("directories.Client", "Get", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("directories.Client", "Get", "`shareName` must be a lower-cased string.") + } + if path == "" { + return result, validation.NewError("directories.Client", "Get", "`path` cannot be an empty string.") + } + + req, err := client.GetPreparer(ctx, accountName, shareName, path) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "Get", nil, "Failure preparing request") + return + } + + resp, err := client.GetSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "directories.Client", "Get", resp, "Failure sending request") + return + } + + result, err = client.GetResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "Get", resp, "Failure responding to request") + return + } + + return +} + +// GetPreparer prepares the Get request. +func (client Client) GetPreparer(ctx context.Context, accountName, shareName, path string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "directory"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetSender sends the Get request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetResponder handles the response to the Get request. The method always +// closes the http.Response Body. +func (client Client) GetResponder(resp *http.Response) (result GetResult, err error) { + if resp != nil && resp.Header != nil { + result.MetaData = metadata.ParseFromHeaders(resp.Header) + result.DirectoryMetaDataEncrypted = strings.EqualFold(resp.Header.Get("x-ms-server-encrypted"), "true") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/directories/lifecycle_test.go b/storage/2018-11-09/file/directories/lifecycle_test.go new file mode 100644 index 0000000..fb1bc28 --- /dev/null +++ b/storage/2018-11-09/file/directories/lifecycle_test.go @@ -0,0 +1,107 @@ +package directories + +import ( + "context" + "fmt" + "log" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/file/shares" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestDirectoriesLifeCycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + shareName := fmt.Sprintf("share-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + sharesClient := shares.NewWithEnvironment(client.Environment) + sharesClient.Client = client.PrepareWithAuthorizer(sharesClient.Client, storageAuth) + + directoriesClient := NewWithEnvironment(client.Environment) + directoriesClient.Client = client.PrepareWithAuthorizer(directoriesClient.Client, storageAuth) + + input := shares.CreateInput{ + QuotaInGB: 1, + } + _, err = sharesClient.Create(ctx, accountName, shareName, input) + if err != nil { + t.Fatalf("Error creating fileshare: %s", err) + } + defer sharesClient.Delete(ctx, accountName, shareName, true) + + metaData := map[string]string{ + "hello": "world", + } + + log.Printf("[DEBUG] Creating Top Level..") + if _, err := directoriesClient.Create(ctx, accountName, shareName, "hello", metaData); err != nil { + t.Fatalf("Error creating Top Level Directory: %s", err) + } + + log.Printf("[DEBUG] Creating Inner..") + if _, err := directoriesClient.Create(ctx, accountName, shareName, "hello/there", metaData); err != nil { + t.Fatalf("Error creating Inner Directory: %s", err) + } + + log.Printf("[DEBUG] Retrieving share") + innerDir, err := directoriesClient.Get(ctx, accountName, shareName, "hello/there") + if err != nil { + t.Fatalf("Error retrieving Inner Directory: %s", err) + } + + if innerDir.DirectoryMetaDataEncrypted != true { + t.Fatalf("Expected MetaData to be encrypted but got: %t", innerDir.DirectoryMetaDataEncrypted) + } + + if len(innerDir.MetaData) != 1 { + t.Fatalf("Expected MetaData to contain 1 item but got %d", len(innerDir.MetaData)) + } + if innerDir.MetaData["hello"] != "world" { + t.Fatalf("Expected MetaData `hello` to be `world`: %s", innerDir.MetaData["hello"]) + } + + log.Printf("[DEBUG] Setting MetaData") + updatedMetaData := map[string]string{ + "panda": "pops", + } + if _, err := directoriesClient.SetMetaData(ctx, accountName, shareName, "hello/there", updatedMetaData); err != nil { + t.Fatalf("Error updating MetaData: %s", err) + } + + log.Printf("[DEBUG] Retrieving MetaData") + retrievedMetaData, err := directoriesClient.GetMetaData(ctx, accountName, shareName, "hello/there") + if err != nil { + t.Fatalf("Error retrieving the updated metadata: %s", err) + } + if len(retrievedMetaData.MetaData) != 1 { + t.Fatalf("Expected the updated metadata to have 1 item but got %d", len(retrievedMetaData.MetaData)) + } + if retrievedMetaData.MetaData["panda"] != "pops" { + t.Fatalf("Expected the metadata `panda` to be `pops` but got %q", retrievedMetaData.MetaData["panda"]) + } + + t.Logf("[DEBUG] Deleting Inner..") + if _, err := directoriesClient.Delete(ctx, accountName, shareName, "hello/there"); err != nil { + t.Fatalf("Error deleting Inner Directory: %s", err) + } + + t.Logf("[DEBUG] Deleting Top Level..") + if _, err := directoriesClient.Delete(ctx, accountName, shareName, "hello"); err != nil { + t.Fatalf("Error deleting Top Level Directory: %s", err) + } +} diff --git a/storage/2018-11-09/file/directories/metadata_get.go b/storage/2018-11-09/file/directories/metadata_get.go new file mode 100644 index 0000000..173716d --- /dev/null +++ b/storage/2018-11-09/file/directories/metadata_get.go @@ -0,0 +1,106 @@ +package directories + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetMetaDataResult struct { + autorest.Response + + MetaData map[string]string +} + +// GetMetaData returns all user-defined metadata for the specified directory +func (client Client) GetMetaData(ctx context.Context, accountName, shareName, path string) (result GetMetaDataResult, err error) { + if accountName == "" { + return result, validation.NewError("directories.Client", "GetMetaData", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("directories.Client", "GetMetaData", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("directories.Client", "GetMetaData", "`shareName` must be a lower-cased string.") + } + if path == "" { + return result, validation.NewError("directories.Client", "GetMetaData", "`path` cannot be an empty string.") + } + + req, err := client.GetMetaDataPreparer(ctx, accountName, shareName, path) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "GetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.GetMetaDataSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "directories.Client", "GetMetaData", resp, "Failure sending request") + return + } + + result, err = client.GetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "GetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// GetMetaDataPreparer prepares the GetMetaData request. +func (client Client) GetMetaDataPreparer(ctx context.Context, accountName, shareName, path string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "directory"), + "comp": autorest.Encode("query", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetMetaDataSender sends the GetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetMetaDataResponder handles the response to the GetMetaData request. The method always +// closes the http.Response Body. +func (client Client) GetMetaDataResponder(resp *http.Response) (result GetMetaDataResult, err error) { + if resp != nil && resp.Header != nil { + result.MetaData = metadata.ParseFromHeaders(resp.Header) + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/directories/metadata_set.go b/storage/2018-11-09/file/directories/metadata_set.go new file mode 100644 index 0000000..cb13312 --- /dev/null +++ b/storage/2018-11-09/file/directories/metadata_set.go @@ -0,0 +1,102 @@ +package directories + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// SetMetaData updates user defined metadata for the specified directory +func (client Client) SetMetaData(ctx context.Context, accountName, shareName, path string, metaData map[string]string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("directories.Client", "SetMetaData", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("directories.Client", "SetMetaData", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("directories.Client", "SetMetaData", "`shareName` must be a lower-cased string.") + } + if path == "" { + return result, validation.NewError("directories.Client", "SetMetaData", "`path` cannot be an empty string.") + } + if err := metadata.Validate(metaData); err != nil { + return result, validation.NewError("directories.Client", "SetMetaData", fmt.Sprintf("`metaData` is not valid: %s.", err)) + } + + req, err := client.SetMetaDataPreparer(ctx, accountName, shareName, path, metaData) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "SetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.SetMetaDataSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "directories.Client", "SetMetaData", resp, "Failure sending request") + return + } + + result, err = client.SetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "directories.Client", "SetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// SetMetaDataPreparer prepares the SetMetaData request. +func (client Client) SetMetaDataPreparer(ctx context.Context, accountName, shareName, path string, metaData map[string]string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "directory"), + "comp": autorest.Encode("query", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = metadata.SetIntoHeaders(headers, metaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetMetaDataSender sends the SetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetMetaDataResponder handles the response to the SetMetaData request. The method always +// closes the http.Response Body. +func (client Client) SetMetaDataResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/directories/resource_id.go b/storage/2018-11-09/file/directories/resource_id.go new file mode 100644 index 0000000..44607c4 --- /dev/null +++ b/storage/2018-11-09/file/directories/resource_id.go @@ -0,0 +1,56 @@ +package directories + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given Directory +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, shareName, directoryName string) string { + domain := endpoints.GetFileEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s/%s", domain, shareName, directoryName) +} + +type ResourceID struct { + AccountName string + DirectoryName string + ShareName string +} + +// ParseResourceID parses the Resource ID into an Object +// which can be used to interact with the Directory within the File Share +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://foo.file.core.windows.net/Bar/Folder + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + path := strings.TrimPrefix(uri.Path, "/") + segments := strings.Split(path, "/") + if len(segments) == 0 { + return nil, fmt.Errorf("Expected the path to contain segments but got none") + } + + shareName := segments[0] + directoryName := strings.TrimPrefix(path, shareName) + directoryName = strings.TrimPrefix(directoryName, "/") + return &ResourceID{ + AccountName: *accountName, + ShareName: shareName, + DirectoryName: directoryName, + }, nil +} diff --git a/storage/2018-11-09/file/directories/resource_id_test.go b/storage/2018-11-09/file/directories/resource_id_test.go new file mode 100644 index 0000000..0be800d --- /dev/null +++ b/storage/2018-11-09/file/directories/resource_id_test.go @@ -0,0 +1,81 @@ +package directories + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.file.core.chinacloudapi.cn/share1/directory1", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.file.core.cloudapi.de/share1/directory1", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.file.core.windows.net/share1/directory1", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.file.core.usgovcloudapi.net/share1/directory1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "share1", "directory1") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.file.core.chinacloudapi.cn/share1/directory1", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.file.core.cloudapi.de/share1/directory1", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.file.core.windows.net/share1/directory1", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.file.core.usgovcloudapi.net/share1/directory1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.ShareName != "share1" { + t.Fatalf("Expected Share Name to be `share1` but got %q", actual.ShareName) + } + if actual.DirectoryName != "directory1" { + t.Fatalf("Expected Directory Name to be `directory1` but got %q", actual.DirectoryName) + } + } +} diff --git a/storage/2018-11-09/file/directories/version.go b/storage/2018-11-09/file/directories/version.go new file mode 100644 index 0000000..6e8fb25 --- /dev/null +++ b/storage/2018-11-09/file/directories/version.go @@ -0,0 +1,14 @@ +package directories + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2018-11-09" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2018-11-09/file/files/README.md b/storage/2018-11-09/file/files/README.md new file mode 100644 index 0000000..19b7af7 --- /dev/null +++ b/storage/2018-11-09/file/files/README.md @@ -0,0 +1,43 @@ +## File Storage Files SDK for API version 2018-11-09 + +This package allows you to interact with the Files File Storage API + +### Supported Authorizers + +* Azure Active Directory (for the Resource Endpoint `https://storage.azure.com`) +* SharedKeyLite (Blob, File & Queue) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/file/files" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + shareName := "myshare" + directoryName := "myfiles" + fileName := "example.txt" + + storageAuth := autorest.NewSharedKeyLiteAuthorizer(accountName, storageAccountKey) + filesClient := files.New() + filesClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + input := files.CreateInput{} + if _, err := filesClient.Create(ctx, accountName, shareName, directoryName, fileName, input); err != nil { + return fmt.Errorf("Error creating File: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2018-11-09/file/files/client.go b/storage/2018-11-09/file/files/client.go new file mode 100644 index 0000000..ecca815 --- /dev/null +++ b/storage/2018-11-09/file/files/client.go @@ -0,0 +1,25 @@ +package files + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for File Storage Shares. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithEnvironment creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2018-11-09/file/files/copy.go b/storage/2018-11-09/file/files/copy.go new file mode 100644 index 0000000..31768b3 --- /dev/null +++ b/storage/2018-11-09/file/files/copy.go @@ -0,0 +1,132 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type CopyInput struct { + // Specifies the URL of the source file or blob, up to 2 KB in length. + // + // To copy a file to another file within the same storage account, you may use Shared Key to authenticate + // the source file. If you are copying a file from another storage account, or if you are copying a blob from + // the same storage account or another storage account, then you must authenticate the source file or blob using a + // shared access signature. If the source is a public blob, no authentication is required to perform the copy + // operation. A file in a share snapshot can also be specified as a copy source. + CopySource string + + MetaData map[string]string +} + +type CopyResult struct { + autorest.Response + + // The CopyID, which can be passed to AbortCopy to abort the copy. + CopyID string + + // Either `success` or `pending` + CopySuccess string +} + +// Copy copies a blob or file to a destination file within the storage account asynchronously. +func (client Client) Copy(ctx context.Context, accountName, shareName, path, fileName string, input CopyInput) (result CopyResult, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "Copy", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "Copy", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "Copy", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "Copy", "`fileName` cannot be an empty string.") + } + if input.CopySource == "" { + return result, validation.NewError("files.Client", "Copy", "`input.CopySource` cannot be an empty string.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("files.Client", "Copy", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.CopyPreparer(ctx, accountName, shareName, path, fileName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "Copy", nil, "Failure preparing request") + return + } + + resp, err := client.CopySender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "Copy", resp, "Failure sending request") + return + } + + result, err = client.CopyResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "Copy", resp, "Failure responding to request") + return + } + + return +} + +// CopyPreparer prepares the Copy request. +func (client Client) CopyPreparer(ctx context.Context, accountName, shareName, path, fileName string, input CopyInput) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-copy-source": input.CopySource, + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CopySender sends the Copy request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CopySender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CopyResponder handles the response to the Copy request. The method always +// closes the http.Response Body. +func (client Client) CopyResponder(resp *http.Response) (result CopyResult, err error) { + if resp != nil && resp.Header != nil { + result.CopyID = resp.Header.Get("x-ms-copy-id") + result.CopySuccess = resp.Header.Get("x-ms-copy-status") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/files/copy_abort.go b/storage/2018-11-09/file/files/copy_abort.go new file mode 100644 index 0000000..2f09131 --- /dev/null +++ b/storage/2018-11-09/file/files/copy_abort.go @@ -0,0 +1,104 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// AbortCopy aborts a pending Copy File operation, and leaves a destination file with zero length and full metadata +func (client Client) AbortCopy(ctx context.Context, accountName, shareName, path, fileName, copyID string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "AbortCopy", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "AbortCopy", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "AbortCopy", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "AbortCopy", "`fileName` cannot be an empty string.") + } + if copyID == "" { + return result, validation.NewError("files.Client", "AbortCopy", "`copyID` cannot be an empty string.") + } + + req, err := client.AbortCopyPreparer(ctx, accountName, shareName, path, fileName, copyID) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "AbortCopy", nil, "Failure preparing request") + return + } + + resp, err := client.AbortCopySender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "AbortCopy", resp, "Failure sending request") + return + } + + result, err = client.AbortCopyResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "AbortCopy", resp, "Failure responding to request") + return + } + + return +} + +// AbortCopyPreparer prepares the AbortCopy request. +func (client Client) AbortCopyPreparer(ctx context.Context, accountName, shareName, path, fileName, copyID string) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "copy"), + "copyid": autorest.Encode("query", copyID), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-copy-action": "abort", + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// AbortCopySender sends the AbortCopy request. The method will close the +// http.Response Body if it receives an error. +func (client Client) AbortCopySender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// AbortCopyResponder handles the response to the AbortCopy request. The method always +// closes the http.Response Body. +func (client Client) AbortCopyResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/files/copy_wait.go b/storage/2018-11-09/file/files/copy_wait.go new file mode 100644 index 0000000..e6a646b --- /dev/null +++ b/storage/2018-11-09/file/files/copy_wait.go @@ -0,0 +1,55 @@ +package files + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/Azure/go-autorest/autorest" +) + +type CopyAndWaitResult struct { + autorest.Response + + CopyID string +} + +const DefaultCopyPollDuration = 15 * time.Second + +// CopyAndWait is a convenience method which doesn't exist in the API, which copies the file and then waits for the copy to complete +func (client Client) CopyAndWait(ctx context.Context, accountName, shareName, path, fileName string, input CopyInput, pollDuration time.Duration) (result CopyResult, err error) { + copy, e := client.Copy(ctx, accountName, shareName, path, fileName, input) + if err != nil { + result.Response = copy.Response + err = fmt.Errorf("Error copying: %s", e) + return + } + + result.CopyID = copy.CopyID + + // since the API doesn't return a LRO, this is a hack which also polls every 10s, but should be sufficient + for true { + props, e := client.GetProperties(ctx, accountName, shareName, path, fileName) + if e != nil { + result.Response = copy.Response + err = fmt.Errorf("Error waiting for copy: %s", e) + return + } + + switch strings.ToLower(props.CopyStatus) { + case "pending": + time.Sleep(pollDuration) + continue + + case "success": + return + + default: + err = fmt.Errorf("Unexpected CopyState %q", e) + return + } + } + + return +} diff --git a/storage/2018-11-09/file/files/copy_wait_test.go b/storage/2018-11-09/file/files/copy_wait_test.go new file mode 100644 index 0000000..95f1ed0 --- /dev/null +++ b/storage/2018-11-09/file/files/copy_wait_test.go @@ -0,0 +1,129 @@ +package files + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/file/shares" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestFilesCopyAndWaitFromURL(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + shareName := fmt.Sprintf("share-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + sharesClient := shares.NewWithEnvironment(client.Environment) + sharesClient.Client = client.PrepareWithAuthorizer(sharesClient.Client, storageAuth) + + input := shares.CreateInput{ + QuotaInGB: 10, + } + _, err = sharesClient.Create(ctx, accountName, shareName, input) + if err != nil { + t.Fatalf("Error creating fileshare: %s", err) + } + defer sharesClient.Delete(ctx, accountName, shareName, false) + + filesClient := NewWithEnvironment(client.Environment) + filesClient.Client = client.PrepareWithAuthorizer(filesClient.Client, storageAuth) + + copiedFileName := "ubuntu.iso" + copyInput := CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + + t.Logf("[DEBUG] Copy And Waiting..") + if _, err := filesClient.CopyAndWait(ctx, accountName, shareName, "", copiedFileName, copyInput, DefaultCopyPollDuration); err != nil { + t.Fatalf("Error copy & waiting: %s", err) + } + + t.Logf("[DEBUG] Asserting that the file's ready..") + + props, err := filesClient.GetProperties(ctx, accountName, shareName, "", copiedFileName) + if err != nil { + t.Fatalf("Error retrieving file: %s", err) + } + + if !strings.EqualFold(props.CopyStatus, "success") { + t.Fatalf("Expected the Copy Status to be `Success` but got %q", props.CopyStatus) + } +} + +func TestFilesCopyAndWaitFromBlob(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + shareName := fmt.Sprintf("share-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + sharesClient := shares.NewWithEnvironment(client.Environment) + sharesClient.Client = client.PrepareWithAuthorizer(sharesClient.Client, storageAuth) + + input := shares.CreateInput{ + QuotaInGB: 10, + } + _, err = sharesClient.Create(ctx, accountName, shareName, input) + if err != nil { + t.Fatalf("Error creating fileshare: %s", err) + } + defer sharesClient.Delete(ctx, accountName, shareName, false) + + filesClient := NewWithEnvironment(client.Environment) + filesClient.Client = client.PrepareWithAuthorizer(filesClient.Client, storageAuth) + + originalFileName := "ubuntu.iso" + copiedFileName := "ubuntu-copied.iso" + copyInput := CopyInput{ + CopySource: "http://releases.ubuntu.com/18.04.2/ubuntu-18.04.2-desktop-amd64.iso", + } + t.Logf("[DEBUG] Copy And Waiting the original file..") + if _, err := filesClient.CopyAndWait(ctx, accountName, shareName, "", originalFileName, copyInput, DefaultCopyPollDuration); err != nil { + t.Fatalf("Error copy & waiting: %s", err) + } + + t.Logf("[DEBUG] Now copying that blob..") + duplicateInput := CopyInput{ + CopySource: fmt.Sprintf("%s/%s/%s", endpoints.GetFileEndpoint(filesClient.BaseURI, accountName), shareName, originalFileName), + } + if _, err := filesClient.CopyAndWait(ctx, accountName, shareName, "", copiedFileName, duplicateInput, DefaultCopyPollDuration); err != nil { + t.Fatalf("Error copying duplicate: %s", err) + } + + t.Logf("[DEBUG] Asserting that the file's ready..") + props, err := filesClient.GetProperties(ctx, accountName, shareName, "", copiedFileName) + if err != nil { + t.Fatalf("Error retrieving file: %s", err) + } + + if !strings.EqualFold(props.CopyStatus, "success") { + t.Fatalf("Expected the Copy Status to be `Success` but got %q", props.CopyStatus) + } +} diff --git a/storage/2018-11-09/file/files/create.go b/storage/2018-11-09/file/files/create.go new file mode 100644 index 0000000..85d4b0b --- /dev/null +++ b/storage/2018-11-09/file/files/create.go @@ -0,0 +1,146 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type CreateInput struct { + // This header specifies the maximum size for the file, up to 1 TiB. + ContentLength int64 + + // The MIME content type of the file + // If not specified, the default type is application/octet-stream. + ContentType *string + + // Specifies which content encodings have been applied to the file. + // This value is returned to the client when the Get File operation is performed + // on the file resource and can be used to decode file content. + ContentEncoding *string + + // Specifies the natural languages used by this resource. + ContentLanguage *string + + // The File service stores this value but does not use or modify it. + CacheControl *string + + // Sets the file's MD5 hash. + ContentMD5 *string + + // Sets the file’s Content-Disposition header. + ContentDisposition *string + + MetaData map[string]string +} + +// Create creates a new file or replaces a file. +func (client Client) Create(ctx context.Context, accountName, shareName, path, fileName string, input CreateInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "Create", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "Create", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "Create", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "Create", "`fileName` cannot be an empty string.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("files.Client", "Create", "`input.MetaData` cannot be an empty string.") + } + + req, err := client.CreatePreparer(ctx, accountName, shareName, path, fileName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "Create", nil, "Failure preparing request") + return + } + + resp, err := client.CreateSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "Create", resp, "Failure sending request") + return + } + + result, err = client.CreateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "Create", resp, "Failure responding to request") + return + } + + return +} + +// CreatePreparer prepares the Create request. +func (client Client) CreatePreparer(ctx context.Context, accountName, shareName, path, fileName string, input CreateInput) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-content-length": input.ContentLength, + "x-ms-type": "file", + } + + if input.ContentDisposition != nil { + headers["x-ms-content-disposition"] = *input.ContentDisposition + } + + if input.ContentEncoding != nil { + headers["x-ms-content-encoding"] = *input.ContentEncoding + } + + if input.ContentMD5 != nil { + headers["x-ms-content-md5"] = *input.ContentMD5 + } + + if input.ContentType != nil { + headers["x-ms-content-type"] = *input.ContentType + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CreateSender sends the Create request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CreateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CreateResponder handles the response to the Create request. The method always +// closes the http.Response Body. +func (client Client) CreateResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/files/delete.go b/storage/2018-11-09/file/files/delete.go new file mode 100644 index 0000000..5debd76 --- /dev/null +++ b/storage/2018-11-09/file/files/delete.go @@ -0,0 +1,94 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Delete immediately deletes the file from the File Share. +func (client Client) Delete(ctx context.Context, accountName, shareName, path, fileName string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "Delete", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "Delete", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "Delete", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "Delete", "`fileName` cannot be an empty string.") + } + + req, err := client.DeletePreparer(ctx, accountName, shareName, path, fileName) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName, shareName, path, fileName string) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/files/lifecycle_test.go b/storage/2018-11-09/file/files/lifecycle_test.go new file mode 100644 index 0000000..8b2578a --- /dev/null +++ b/storage/2018-11-09/file/files/lifecycle_test.go @@ -0,0 +1,144 @@ +package files + +import ( + "context" + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/file/shares" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestFilesLifeCycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + shareName := fmt.Sprintf("share-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + sharesClient := shares.NewWithEnvironment(client.Environment) + sharesClient.Client = client.PrepareWithAuthorizer(sharesClient.Client, storageAuth) + + input := shares.CreateInput{ + QuotaInGB: 1, + } + _, err = sharesClient.Create(ctx, accountName, shareName, input) + if err != nil { + t.Fatalf("Error creating fileshare: %s", err) + } + defer sharesClient.Delete(ctx, accountName, shareName, false) + + filesClient := NewWithEnvironment(client.Environment) + filesClient.Client = client.PrepareWithAuthorizer(filesClient.Client, storageAuth) + + fileName := "bled5.png" + contentEncoding := "application/vnd+panda" + + t.Logf("[DEBUG] Creating Top Level File..") + createInput := CreateInput{ + ContentLength: 1024, + ContentEncoding: &contentEncoding, + } + if _, err := filesClient.Create(ctx, accountName, shareName, "", fileName, createInput); err != nil { + t.Fatalf("Error creating Top-Level File: %s", err) + } + + t.Logf("[DEBUG] Retrieving Properties for the Top-Level File..") + file, err := filesClient.GetProperties(ctx, accountName, shareName, "", fileName) + if err != nil { + t.Fatalf("Error retrieving Top-Level File: %s", err) + } + + if *file.ContentLength != 1024 { + t.Fatalf("Expected the Content-Length to be 1024 but got %d", *file.ContentLength) + } + + if file.ContentEncoding != contentEncoding { + t.Fatalf("Expected the Content-Encoding to be %q but got %q", contentEncoding, file.ContentEncoding) + } + + updatedSize := int64(2048) + updatedEncoding := "application/vnd+pandas2" + updatedInput := SetPropertiesInput{ + ContentEncoding: &updatedEncoding, + ContentLength: &updatedSize, + } + if _, err := filesClient.SetProperties(ctx, accountName, shareName, "", fileName, updatedInput); err != nil { + t.Fatalf("Error setting properties: %s", err) + } + + t.Logf("[DEBUG] Re-retrieving Properties for the Top-Level File..") + file, err = filesClient.GetProperties(ctx, accountName, shareName, "", fileName) + if err != nil { + t.Fatalf("Error retrieving Top-Level File: %s", err) + } + + if *file.ContentLength != 2048 { + t.Fatalf("Expected the Content-Length to be 1024 but got %d", *file.ContentLength) + } + + if file.ContentEncoding != updatedEncoding { + t.Fatalf("Expected the Content-Encoding to be %q but got %q", updatedEncoding, file.ContentEncoding) + } + + t.Logf("[DEBUG] Setting MetaData..") + metaData := map[string]string{ + "hello": "there", + } + if _, err := filesClient.SetMetaData(ctx, accountName, shareName, "", fileName, metaData); err != nil { + t.Fatalf("Error setting MetaData: %s", err) + } + + t.Logf("[DEBUG] Retrieving MetaData..") + retrievedMetaData, err := filesClient.GetMetaData(ctx, accountName, shareName, "", fileName) + if err != nil { + t.Fatalf("Error retrieving MetaData: %s", err) + } + if len(retrievedMetaData.MetaData) != 1 { + t.Fatalf("Expected 1 item but got %d", len(retrievedMetaData.MetaData)) + } + if retrievedMetaData.MetaData["hello"] != "there" { + t.Fatalf("Expected `hello` to be `there` but got %q", retrievedMetaData.MetaData["hello"]) + } + + t.Logf("[DEBUG] Re-Setting MetaData..") + metaData = map[string]string{ + "hello": "there", + "second": "thing", + } + if _, err := filesClient.SetMetaData(ctx, accountName, shareName, "", fileName, metaData); err != nil { + t.Fatalf("Error setting MetaData: %s", err) + } + + t.Logf("[DEBUG] Re-Retrieving MetaData..") + retrievedMetaData, err = filesClient.GetMetaData(ctx, accountName, shareName, "", fileName) + if err != nil { + t.Fatalf("Error retrieving MetaData: %s", err) + } + if len(retrievedMetaData.MetaData) != 2 { + t.Fatalf("Expected 2 items but got %d", len(retrievedMetaData.MetaData)) + } + if retrievedMetaData.MetaData["hello"] != "there" { + t.Fatalf("Expected `hello` to be `there` but got %q", retrievedMetaData.MetaData["hello"]) + } + if retrievedMetaData.MetaData["second"] != "thing" { + t.Fatalf("Expected `second` to be `thing` but got %q", retrievedMetaData.MetaData["second"]) + } + + t.Logf("[DEBUG] Deleting Top Level File..") + if _, err := filesClient.Delete(ctx, accountName, shareName, "", fileName); err != nil { + t.Fatalf("Error deleting Top-Level File: %s", err) + } +} diff --git a/storage/2018-11-09/file/files/metadata_get.go b/storage/2018-11-09/file/files/metadata_get.go new file mode 100644 index 0000000..fd62f90 --- /dev/null +++ b/storage/2018-11-09/file/files/metadata_get.go @@ -0,0 +1,111 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetMetaDataResult struct { + autorest.Response + + MetaData map[string]string +} + +// GetMetaData returns the MetaData for the specified File. +func (client Client) GetMetaData(ctx context.Context, accountName, shareName, path, fileName string) (result GetMetaDataResult, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "GetMetaData", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "GetMetaData", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "GetMetaData", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "GetMetaData", "`fileName` cannot be an empty string.") + } + + req, err := client.GetMetaDataPreparer(ctx, accountName, shareName, path, fileName) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "GetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.GetMetaDataSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "GetMetaData", resp, "Failure sending request") + return + } + + result, err = client.GetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "GetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// GetMetaDataPreparer prepares the GetMetaData request. +func (client Client) GetMetaDataPreparer(ctx context.Context, accountName, shareName, path, fileName string) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetMetaDataSender sends the GetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetMetaDataResponder handles the response to the GetMetaData request. The method always +// closes the http.Response Body. +func (client Client) GetMetaDataResponder(resp *http.Response) (result GetMetaDataResult, err error) { + if resp != nil && resp.Header != nil { + result.MetaData = metadata.ParseFromHeaders(resp.Header) + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + //metadata.ByParsingFromHeaders(&result.MetaData), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/files/metadata_set.go b/storage/2018-11-09/file/files/metadata_set.go new file mode 100644 index 0000000..41e3ffc --- /dev/null +++ b/storage/2018-11-09/file/files/metadata_set.go @@ -0,0 +1,105 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// SetMetaData updates the specified File to have the specified MetaData. +func (client Client) SetMetaData(ctx context.Context, accountName, shareName, path, fileName string, metaData map[string]string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "SetMetaData", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "SetMetaData", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "SetMetaData", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "SetMetaData", "`fileName` cannot be an empty string.") + } + if err := metadata.Validate(metaData); err != nil { + return result, validation.NewError("files.Client", "SetMetaData", fmt.Sprintf("`metaData` is not valid: %s.", err)) + } + + req, err := client.SetMetaDataPreparer(ctx, accountName, shareName, path, fileName, metaData) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "SetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.SetMetaDataSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "SetMetaData", resp, "Failure sending request") + return + } + + result, err = client.SetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "SetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// SetMetaDataPreparer prepares the SetMetaData request. +func (client Client) SetMetaDataPreparer(ctx context.Context, accountName, shareName, path, fileName string, metaData map[string]string) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = metadata.SetIntoHeaders(headers, metaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetMetaDataSender sends the SetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetMetaDataResponder handles the response to the SetMetaData request. The method always +// closes the http.Response Body. +func (client Client) SetMetaDataResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/files/properties_get.go b/storage/2018-11-09/file/files/properties_get.go new file mode 100644 index 0000000..c6a0c39 --- /dev/null +++ b/storage/2018-11-09/file/files/properties_get.go @@ -0,0 +1,144 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetResult struct { + autorest.Response + + CacheControl string + ContentDisposition string + ContentEncoding string + ContentLanguage string + ContentLength *int64 + ContentMD5 string + ContentType string + CopyID string + CopyStatus string + CopySource string + CopyProgress string + CopyStatusDescription string + CopyCompletionTime string + Encrypted bool + + MetaData map[string]string +} + +// GetProperties returns the Properties for the specified file +func (client Client) GetProperties(ctx context.Context, accountName, shareName, path, fileName string) (result GetResult, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "GetProperties", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "GetProperties", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "GetProperties", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "GetProperties", "`fileName` cannot be an empty string.") + } + + req, err := client.GetPropertiesPreparer(ctx, accountName, shareName, path, fileName) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "GetProperties", nil, "Failure preparing request") + return + } + + resp, err := client.GetPropertiesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "GetProperties", resp, "Failure sending request") + return + } + + result, err = client.GetPropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "GetProperties", resp, "Failure responding to request") + return + } + + return +} + +// GetPropertiesPreparer prepares the GetProperties request. +func (client Client) GetPropertiesPreparer(ctx context.Context, accountName, shareName, path, fileName string) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsHead(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetPropertiesSender sends the GetProperties request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetPropertiesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetPropertiesResponder handles the response to the GetProperties request. The method always +// closes the http.Response Body. +func (client Client) GetPropertiesResponder(resp *http.Response) (result GetResult, err error) { + if resp != nil && resp.Header != nil { + result.CacheControl = resp.Header.Get("Cache-Control") + result.ContentDisposition = resp.Header.Get("Content-Disposition") + result.ContentEncoding = resp.Header.Get("Content-Encoding") + result.ContentLanguage = resp.Header.Get("Content-Language") + result.ContentMD5 = resp.Header.Get("x-ms-content-md5") + result.ContentType = resp.Header.Get("Content-Type") + result.CopyID = resp.Header.Get("x-ms-copy-id") + result.CopyProgress = resp.Header.Get("x-ms-copy-progress") + result.CopySource = resp.Header.Get("x-ms-copy-source") + result.CopyStatus = resp.Header.Get("x-ms-copy-status") + result.CopyStatusDescription = resp.Header.Get("x-ms-copy-status-description") + result.CopyCompletionTime = resp.Header.Get("x-ms-copy-completion-time") + result.Encrypted = strings.EqualFold(resp.Header.Get("x-ms-server-encrypted"), "true") + result.MetaData = metadata.ParseFromHeaders(resp.Header) + + contentLengthRaw := resp.Header.Get("Content-Length") + if contentLengthRaw != "" { + contentLength, err := strconv.Atoi(contentLengthRaw) + if err != nil { + return result, fmt.Errorf("Error parsing %q for Content-Length as an integer: %s", contentLengthRaw, err) + } + contentLengthI64 := int64(contentLength) + result.ContentLength = &contentLengthI64 + } + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/files/properties_set.go b/storage/2018-11-09/file/files/properties_set.go new file mode 100644 index 0000000..79fffc2 --- /dev/null +++ b/storage/2018-11-09/file/files/properties_set.go @@ -0,0 +1,160 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type SetPropertiesInput struct { + // Resizes a file to the specified size. + // If the specified byte value is less than the current size of the file, + // then all ranges above the specified byte value are cleared. + ContentLength *int64 + + // Modifies the cache control string for the file. + // If this property is not specified on the request, then the property will be cleared for the file. + // Subsequent calls to Get File Properties will not return this property, + // unless it is explicitly set on the file again. + ContentControl *string + + // Sets the file’s Content-Disposition header. + // If this property is not specified on the request, then the property will be cleared for the file. + // Subsequent calls to Get File Properties will not return this property, + // unless it is explicitly set on the file again. + ContentDisposition *string + + // Sets the file's content encoding. + // If this property is not specified on the request, then the property will be cleared for the file. + // Subsequent calls to Get File Properties will not return this property, + // unless it is explicitly set on the file again. + ContentEncoding *string + + // Sets the file's content language. + // If this property is not specified on the request, then the property will be cleared for the file. + // Subsequent calls to Get File Properties will not return this property, + // unless it is explicitly set on the file again. + ContentLanguage *string + + // Sets the file's MD5 hash. + // If this property is not specified on the request, then the property will be cleared for the file. + // Subsequent calls to Get File Properties will not return this property, + // unless it is explicitly set on the file again. + ContentMD5 *string + + // Sets the file's content type. + // If this property is not specified on the request, then the property will be cleared for the file. + // Subsequent calls to Get File Properties will not return this property, + // unless it is explicitly set on the file again. + ContentType *string +} + +// SetProperties sets the specified properties on the specified File +func (client Client) SetProperties(ctx context.Context, accountName, shareName, path, fileName string, input SetPropertiesInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "SetProperties", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "SetProperties", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "SetProperties", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "SetProperties", "`fileName` cannot be an empty string.") + } + + req, err := client.SetPropertiesPreparer(ctx, accountName, shareName, path, fileName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "SetProperties", nil, "Failure preparing request") + return + } + + resp, err := client.SetPropertiesSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "SetProperties", resp, "Failure sending request") + return + } + + result, err = client.SetPropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "SetProperties", resp, "Failure responding to request") + return + } + + return +} + +// SetPropertiesPreparer prepares the SetProperties request. +func (client Client) SetPropertiesPreparer(ctx context.Context, accountName, shareName, path, fileName string, input SetPropertiesInput) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-type": "file", + } + + if input.ContentControl != nil { + headers["x-ms-cache-control"] = *input.ContentControl + } + if input.ContentDisposition != nil { + headers["x-ms-content-disposition"] = *input.ContentDisposition + } + if input.ContentEncoding != nil { + headers["x-ms-content-encoding"] = *input.ContentEncoding + } + if input.ContentLanguage != nil { + headers["x-ms-content-language"] = *input.ContentLanguage + } + if input.ContentLength != nil { + headers["x-ms-content-length"] = *input.ContentLength + } + if input.ContentMD5 != nil { + headers["x-ms-content-md5"] = *input.ContentMD5 + } + if input.ContentType != nil { + headers["x-ms-content-type"] = *input.ContentType + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetPropertiesSender sends the SetProperties request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetPropertiesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetPropertiesResponder handles the response to the SetProperties request. The method always +// closes the http.Response Body. +func (client Client) SetPropertiesResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/files/range_clear.go b/storage/2018-11-09/file/files/range_clear.go new file mode 100644 index 0000000..5d8145f --- /dev/null +++ b/storage/2018-11-09/file/files/range_clear.go @@ -0,0 +1,112 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type ClearByteRangeInput struct { + StartBytes int64 + EndBytes int64 +} + +// ClearByteRange clears the specified Byte Range from within the specified File +func (client Client) ClearByteRange(ctx context.Context, accountName, shareName, path, fileName string, input ClearByteRangeInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "ClearByteRange", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "ClearByteRange", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "ClearByteRange", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "ClearByteRange", "`fileName` cannot be an empty string.") + } + if input.StartBytes < 0 { + return result, validation.NewError("files.Client", "ClearByteRange", "`input.StartBytes` must be greater or equal to 0.") + } + if input.EndBytes <= 0 { + return result, validation.NewError("files.Client", "ClearByteRange", "`input.EndBytes` must be greater than 0.") + } + + req, err := client.ClearByteRangePreparer(ctx, accountName, shareName, path, fileName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "ClearByteRange", nil, "Failure preparing request") + return + } + + resp, err := client.ClearByteRangeSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "ClearByteRange", resp, "Failure sending request") + return + } + + result, err = client.ClearByteRangeResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "ClearByteRange", resp, "Failure responding to request") + return + } + + return +} + +// ClearByteRangePreparer prepares the ClearByteRange request. +func (client Client) ClearByteRangePreparer(ctx context.Context, accountName, shareName, path, fileName string, input ClearByteRangeInput) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "range"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-write": "clear", + "x-ms-range": fmt.Sprintf("bytes=%d-%d", input.StartBytes, input.EndBytes), + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ClearByteRangeSender sends the ClearByteRange request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ClearByteRangeSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ClearByteRangeResponder handles the response to the ClearByteRange request. The method always +// closes the http.Response Body. +func (client Client) ClearByteRangeResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/files/range_get.go b/storage/2018-11-09/file/files/range_get.go new file mode 100644 index 0000000..733d3f5 --- /dev/null +++ b/storage/2018-11-09/file/files/range_get.go @@ -0,0 +1,121 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetByteRangeInput struct { + StartBytes int64 + EndBytes int64 +} + +type GetByteRangeResult struct { + autorest.Response + + Contents []byte +} + +// GetByteRange returns the specified Byte Range from the specified File. +func (client Client) GetByteRange(ctx context.Context, accountName, shareName, path, fileName string, input GetByteRangeInput) (result GetByteRangeResult, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "GetByteRange", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "GetByteRange", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "GetByteRange", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "GetByteRange", "`fileName` cannot be an empty string.") + } + if input.StartBytes < 0 { + return result, validation.NewError("files.Client", "GetByteRange", "`input.StartBytes` must be greater or equal to 0.") + } + if input.EndBytes <= 0 { + return result, validation.NewError("files.Client", "GetByteRange", "`input.EndBytes` must be greater than 0.") + } + expectedBytes := input.EndBytes - input.StartBytes + if expectedBytes < (4 * 1024) { + return result, validation.NewError("files.Client", "GetByteRange", "Requested Byte Range must be at least 4KB.") + } + if expectedBytes > (4 * 1024 * 1024) { + return result, validation.NewError("files.Client", "GetByteRange", "Requested Byte Range must be at most 4MB.") + } + + req, err := client.GetByteRangePreparer(ctx, accountName, shareName, path, fileName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "GetByteRange", nil, "Failure preparing request") + return + } + + resp, err := client.GetByteRangeSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "GetByteRange", resp, "Failure sending request") + return + } + + result, err = client.GetByteRangeResponder(resp, expectedBytes) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "GetByteRange", resp, "Failure responding to request") + return + } + + return +} + +// GetByteRangePreparer prepares the GetByteRange request. +func (client Client) GetByteRangePreparer(ctx context.Context, accountName, shareName, path, fileName string, input GetByteRangeInput) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-range": fmt.Sprintf("bytes=%d-%d", input.StartBytes, input.EndBytes-1), + } + + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetByteRangeSender sends the GetByteRange request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetByteRangeSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetByteRangeResponder handles the response to the GetByteRange request. The method always +// closes the http.Response Body. +func (client Client) GetByteRangeResponder(resp *http.Response, length int64) (result GetByteRangeResult, err error) { + result.Contents = make([]byte, length) + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusPartialContent), + autorest.ByUnmarshallingBytes(&result.Contents), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/files/range_get_file.go b/storage/2018-11-09/file/files/range_get_file.go new file mode 100644 index 0000000..9e5be17 --- /dev/null +++ b/storage/2018-11-09/file/files/range_get_file.go @@ -0,0 +1,128 @@ +package files + +import ( + "context" + "fmt" + "log" + "math" + "runtime" + "sync" + + "github.com/Azure/go-autorest/autorest" +) + +// GetFile is a helper method to download a file by chunking it automatically +func (client Client) GetFile(ctx context.Context, accountName, shareName, path, fileName string, parallelism int) (result autorest.Response, outputBytes []byte, err error) { + + // first look up the file and check out how many bytes it is + file, e := client.GetProperties(ctx, accountName, shareName, path, fileName) + if err != nil { + result = file.Response + err = e + return + } + + if file.ContentLength == nil { + err = fmt.Errorf("Content-Length was nil!") + return + } + + length := int64(*file.ContentLength) + chunkSize := int64(4 * 1024 * 1024) // 4MB + + if chunkSize > length { + chunkSize = length + } + + // then split that up into chunks and retrieve it retrieve it into the 'results' set + chunks := int(math.Ceil(float64(length) / float64(chunkSize))) + workerCount := parallelism * runtime.NumCPU() + if workerCount > chunks { + workerCount = chunks + } + + var waitGroup sync.WaitGroup + waitGroup.Add(workerCount) + + results := make([]*downloadFileChunkResult, chunks) + errors := make(chan error, chunkSize) + + for i := 0; i < chunks; i++ { + go func(i int) { + log.Printf("[DEBUG] Downloading Chunk %d of %d", i+1, chunks) + + dfci := downloadFileChunkInput{ + thisChunk: i, + chunkSize: chunkSize, + fileSize: length, + } + + result, err := client.downloadFileChunk(ctx, accountName, shareName, path, fileName, dfci) + if err != nil { + errors <- err + waitGroup.Done() + return + } + + // if there's no error, we should have bytes, so this is safe + results[i] = result + + waitGroup.Done() + }(i) + } + waitGroup.Wait() + + // TODO: we should switch to hashicorp/multi-error here + if len(errors) > 0 { + err = fmt.Errorf("Error downloading file: %s", <-errors) + return + } + + // then finally put it all together, in order and return it + output := make([]byte, length) + for _, v := range results { + copy(output[v.startBytes:v.endBytes], v.bytes) + } + + outputBytes = output + return +} + +type downloadFileChunkInput struct { + thisChunk int + chunkSize int64 + fileSize int64 +} + +type downloadFileChunkResult struct { + startBytes int64 + endBytes int64 + bytes []byte +} + +func (client Client) downloadFileChunk(ctx context.Context, accountName, shareName, path, fileName string, input downloadFileChunkInput) (*downloadFileChunkResult, error) { + startBytes := input.chunkSize * int64(input.thisChunk) + endBytes := startBytes + input.chunkSize + + // the last chunk may exceed the size of the file + remaining := input.fileSize - startBytes + if input.chunkSize > remaining { + endBytes = startBytes + remaining + } + + getInput := GetByteRangeInput{ + StartBytes: startBytes, + EndBytes: endBytes, + } + result, err := client.GetByteRange(ctx, accountName, shareName, path, fileName, getInput) + if err != nil { + return nil, fmt.Errorf("Error putting bytes: %s", err) + } + + output := downloadFileChunkResult{ + startBytes: startBytes, + endBytes: endBytes, + bytes: result.Contents, + } + return &output, nil +} diff --git a/storage/2018-11-09/file/files/range_get_file_test.go b/storage/2018-11-09/file/files/range_get_file_test.go new file mode 100644 index 0000000..7b2c569 --- /dev/null +++ b/storage/2018-11-09/file/files/range_get_file_test.go @@ -0,0 +1,108 @@ +package files + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/file/shares" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestGetSmallFile(t *testing.T) { + // the purpose of this test is to verify that the small, single-chunked file gets downloaded correctly + testGetFile(t, "small-file.png", "image/png") +} + +func TestGetLargeFile(t *testing.T) { + // the purpose of this test is to verify that the large, multi-chunked file gets downloaded correctly + testGetFile(t, "blank-large-file.dmg", "application/x-apple-diskimage") +} + +func testGetFile(t *testing.T, fileName string, contentType string) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + shareName := fmt.Sprintf("share-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + sharesClient := shares.NewWithEnvironment(client.Environment) + sharesClient.Client = client.PrepareWithAuthorizer(sharesClient.Client, storageAuth) + + input := shares.CreateInput{ + QuotaInGB: 10, + } + _, err = sharesClient.Create(ctx, accountName, shareName, input) + if err != nil { + t.Fatalf("Error creating fileshare: %s", err) + } + defer sharesClient.Delete(ctx, accountName, shareName, false) + + filesClient := NewWithEnvironment(client.Environment) + filesClient.Client = client.PrepareWithAuthorizer(filesClient.Client, storageAuth) + + // store files outside of this directory, since they're reused + file, err := os.Open("../../../testdata/" + fileName) + if err != nil { + t.Fatalf("Error opening: %s", err) + } + + info, err := file.Stat() + if err != nil { + t.Fatalf("Error 'stat'-ing: %s", err) + } + + t.Logf("[DEBUG] Creating Top Level File..") + createFileInput := CreateInput{ + ContentLength: info.Size(), + ContentType: &contentType, + } + if _, err := filesClient.Create(ctx, accountName, shareName, "", fileName, createFileInput); err != nil { + t.Fatalf("Error creating Top-Level File: %s", err) + } + + t.Logf("[DEBUG] Uploading File..") + if err := filesClient.PutFile(ctx, accountName, shareName, "", fileName, file, 4); err != nil { + t.Fatalf("Error uploading File: %s", err) + } + + t.Logf("[DEBUG] Downloading file..") + _, downloadedBytes, err := filesClient.GetFile(ctx, accountName, shareName, "", fileName, 4) + if err != nil { + t.Fatalf("Error downloading file: %s", err) + } + + t.Logf("[DEBUG] Asserting the files are the same size..") + expectedBytes := make([]byte, info.Size()) + file.Read(expectedBytes) + if len(expectedBytes) != len(downloadedBytes) { + t.Fatalf("Expected %d bytes but got %d", len(expectedBytes), len(downloadedBytes)) + } + + t.Logf("[DEBUG] Asserting the files are the same content-wise..") + // overkill, but it's this or shasum-ing + for i := int64(0); i < info.Size(); i++ { + if expectedBytes[i] != downloadedBytes[i] { + t.Fatalf("Expected byte %d to be %q but got %q", i, expectedBytes[i], downloadedBytes[i]) + } + } + + t.Logf("[DEBUG] Deleting Top Level File..") + if _, err := filesClient.Delete(ctx, accountName, shareName, "", fileName); err != nil { + t.Fatalf("Error deleting Top-Level File: %s", err) + } + +} diff --git a/storage/2018-11-09/file/files/range_put.go b/storage/2018-11-09/file/files/range_put.go new file mode 100644 index 0000000..77fe101 --- /dev/null +++ b/storage/2018-11-09/file/files/range_put.go @@ -0,0 +1,129 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type PutByteRangeInput struct { + StartBytes int64 + EndBytes int64 + + // Content is the File Contents for the specified range + // which can be at most 4MB + Content []byte +} + +// PutByteRange puts the specified Byte Range in the specified File. +func (client Client) PutByteRange(ctx context.Context, accountName, shareName, path, fileName string, input PutByteRangeInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "PutByteRange", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "PutByteRange", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "PutByteRange", "`shareName` must be a lower-cased string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "PutByteRange", "`fileName` cannot be an empty string.") + } + if input.StartBytes < 0 { + return result, validation.NewError("files.Client", "PutByteRange", "`input.StartBytes` must be greater or equal to 0.") + } + if input.EndBytes <= 0 { + return result, validation.NewError("files.Client", "PutByteRange", "`input.EndBytes` must be greater than 0.") + } + + expectedBytes := input.EndBytes - input.StartBytes + actualBytes := len(input.Content) + if expectedBytes != int64(actualBytes) { + return result, validation.NewError("files.Client", "PutByteRange", fmt.Sprintf("The specified byte-range (%d) didn't match the content size (%d).", expectedBytes, actualBytes)) + } + if expectedBytes < (4 * 1024) { + return result, validation.NewError("files.Client", "PutByteRange", "Specified Byte Range must be at least 4KB.") + } + + if expectedBytes > (4 * 1024 * 1024) { + return result, validation.NewError("files.Client", "PutByteRange", "Specified Byte Range must be at most 4MB.") + } + + req, err := client.PutByteRangePreparer(ctx, accountName, shareName, path, fileName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "PutByteRange", nil, "Failure preparing request") + return + } + + resp, err := client.PutByteRangeSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "PutByteRange", resp, "Failure sending request") + return + } + + result, err = client.PutByteRangeResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "PutByteRange", resp, "Failure responding to request") + return + } + + return +} + +// PutByteRangePreparer prepares the PutByteRange request. +func (client Client) PutByteRangePreparer(ctx context.Context, accountName, shareName, path, fileName string, input PutByteRangeInput) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "range"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-write": "update", + "x-ms-range": fmt.Sprintf("bytes=%d-%d", input.StartBytes, input.EndBytes-1), + } + + preparer := autorest.CreatePreparer( + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters), + autorest.WithBytes(&input.Content)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutByteRangeSender sends the PutByteRange request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutByteRangeSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutByteRangeResponder handles the response to the PutByteRange request. The method always +// closes the http.Response Body. +func (client Client) PutByteRangeResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/files/range_put_file.go b/storage/2018-11-09/file/files/range_put_file.go new file mode 100644 index 0000000..a39cd37 --- /dev/null +++ b/storage/2018-11-09/file/files/range_put_file.go @@ -0,0 +1,107 @@ +package files + +import ( + "context" + "fmt" + "io" + "log" + "math" + "os" + "runtime" + "sync" + + "github.com/Azure/go-autorest/autorest" +) + +// PutFile is a helper method which takes a file, and automatically chunks it up, rather than having to do this yourself +func (client Client) PutFile(ctx context.Context, accountName, shareName, path, fileName string, file *os.File, parallelism int) error { + fileInfo, err := file.Stat() + if err != nil { + return fmt.Errorf("Error loading file info: %s", err) + } + + fileSize := fileInfo.Size() + chunkSize := 4 * 1024 * 1024 // 4MB + if chunkSize > int(fileSize) { + chunkSize = int(fileSize) + } + chunks := int(math.Ceil(float64(fileSize) / float64(chunkSize*1.0))) + + workerCount := parallelism * runtime.NumCPU() + if workerCount > chunks { + workerCount = chunks + } + + var waitGroup sync.WaitGroup + waitGroup.Add(workerCount) + errors := make(chan error, chunkSize) + + for i := 0; i < chunks; i++ { + go func(i int) { + log.Printf("[DEBUG] Chunk %d of %d", i+1, chunks) + + uci := uploadChunkInput{ + thisChunk: i, + chunkSize: chunkSize, + fileSize: fileSize, + } + + _, err := client.uploadChunk(ctx, accountName, shareName, path, fileName, uci, file) + if err != nil { + errors <- err + waitGroup.Done() + return + } + + waitGroup.Done() + return + }(i) + } + waitGroup.Wait() + + // TODO: we should switch to hashicorp/multi-error here + if len(errors) > 0 { + return fmt.Errorf("Error uploading file: %s", <-errors) + } + + return nil +} + +type uploadChunkInput struct { + thisChunk int + chunkSize int + fileSize int64 +} + +func (client Client) uploadChunk(ctx context.Context, accountName, shareName, path, fileName string, input uploadChunkInput, file *os.File) (result autorest.Response, err error) { + startBytes := int64(input.chunkSize * input.thisChunk) + endBytes := startBytes + int64(input.chunkSize) + + // the last size may exceed the size of the file + remaining := input.fileSize - startBytes + if int64(input.chunkSize) > remaining { + endBytes = startBytes + remaining + } + + bytesToRead := int(endBytes) - int(startBytes) + bytes := make([]byte, bytesToRead) + + _, err = file.ReadAt(bytes, startBytes) + if err != nil { + if err != io.EOF { + return result, fmt.Errorf("Error reading bytes: %s", err) + } + } + + putBytesInput := PutByteRangeInput{ + StartBytes: startBytes, + EndBytes: endBytes, + Content: bytes, + } + result, err = client.PutByteRange(ctx, accountName, shareName, path, fileName, putBytesInput) + if err != nil { + return result, fmt.Errorf("Error putting bytes: %s", err) + } + + return +} diff --git a/storage/2018-11-09/file/files/range_put_file_test.go b/storage/2018-11-09/file/files/range_put_file_test.go new file mode 100644 index 0000000..841df78 --- /dev/null +++ b/storage/2018-11-09/file/files/range_put_file_test.go @@ -0,0 +1,86 @@ +package files + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/file/shares" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestPutSmallFile(t *testing.T) { + // the purpose of this test is to ensure that a small file (< 4MB) is a single chunk + testPutFile(t, "small-file.png", "image/png") +} + +func TestPutLargeFile(t *testing.T) { + // the purpose of this test is to ensure that large files (> 4MB) are chunked + testPutFile(t, "blank-large-file.dmg", "application/x-apple-diskimage") +} + +func testPutFile(t *testing.T, fileName string, contentType string) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + shareName := fmt.Sprintf("share-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + sharesClient := shares.NewWithEnvironment(client.Environment) + sharesClient.Client = client.PrepareWithAuthorizer(sharesClient.Client, storageAuth) + + input := shares.CreateInput{ + QuotaInGB: 10, + } + _, err = sharesClient.Create(ctx, accountName, shareName, input) + if err != nil { + t.Fatalf("Error creating fileshare: %s", err) + } + defer sharesClient.Delete(ctx, accountName, shareName, false) + + filesClient := NewWithEnvironment(client.Environment) + filesClient.Client = client.PrepareWithAuthorizer(filesClient.Client, storageAuth) + + // store files outside of this directory, since they're reused + file, err := os.Open("../../../testdata/" + fileName) + if err != nil { + t.Fatalf("Error opening: %s", err) + } + + info, err := file.Stat() + if err != nil { + t.Fatalf("Error 'stat'-ing: %s", err) + } + + t.Logf("[DEBUG] Creating Top Level File..") + createFileInput := CreateInput{ + ContentLength: info.Size(), + ContentType: &contentType, + } + if _, err := filesClient.Create(ctx, accountName, shareName, "", fileName, createFileInput); err != nil { + t.Fatalf("Error creating Top-Level File: %s", err) + } + + t.Logf("[DEBUG] Uploading File..") + if err := filesClient.PutFile(ctx, accountName, shareName, "", fileName, file, 4); err != nil { + t.Fatalf("Error uploading File: %s", err) + } + + t.Logf("[DEBUG] Deleting Top Level File..") + if _, err := filesClient.Delete(ctx, accountName, shareName, "", fileName); err != nil { + t.Fatalf("Error deleting Top-Level File: %s", err) + } +} diff --git a/storage/2018-11-09/file/files/ranges_list.go b/storage/2018-11-09/file/files/ranges_list.go new file mode 100644 index 0000000..ea309f9 --- /dev/null +++ b/storage/2018-11-09/file/files/ranges_list.go @@ -0,0 +1,114 @@ +package files + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type ListRangesResult struct { + autorest.Response + + Ranges []Range `xml:"Range"` +} + +type Range struct { + Start string `xml:"Start"` + End string `xml:"End"` +} + +// ListRanges returns the list of valid ranges for the specified File. +func (client Client) ListRanges(ctx context.Context, accountName, shareName, path, fileName string) (result ListRangesResult, err error) { + if accountName == "" { + return result, validation.NewError("files.Client", "ListRanges", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("files.Client", "ListRanges", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("files.Client", "ListRanges", "`shareName` must be a lower-cased string.") + } + if path == "" { + return result, validation.NewError("files.Client", "ListRanges", "`path` cannot be an empty string.") + } + if fileName == "" { + return result, validation.NewError("files.Client", "ListRanges", "`fileName` cannot be an empty string.") + } + + req, err := client.ListRangesPreparer(ctx, accountName, shareName, path, fileName) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "ListRanges", nil, "Failure preparing request") + return + } + + resp, err := client.ListRangesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "files.Client", "ListRanges", resp, "Failure sending request") + return + } + + result, err = client.ListRangesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "files.Client", "ListRanges", resp, "Failure responding to request") + return + } + + return +} + +// ListRangesPreparer prepares the ListRanges request. +func (client Client) ListRangesPreparer(ctx context.Context, accountName, shareName, path, fileName string) (*http.Request, error) { + if path != "" { + path = fmt.Sprintf("%s/", path) + } + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + "directory": autorest.Encode("path", path), + "fileName": autorest.Encode("path", fileName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "rangelist"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}/{directory}{fileName}", pathParameters), + autorest.WithHeaders(headers), + autorest.WithQueryParameters(queryParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ListRangesSender sends the ListRanges request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ListRangesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ListRangesResponder handles the response to the ListRanges request. The method always +// closes the http.Response Body. +func (client Client) ListRangesResponder(resp *http.Response) (result ListRangesResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/files/resource_id.go b/storage/2018-11-09/file/files/resource_id.go new file mode 100644 index 0000000..ed1208d --- /dev/null +++ b/storage/2018-11-09/file/files/resource_id.go @@ -0,0 +1,64 @@ +package files + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given File +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, shareName, directoryName, filePath string) string { + domain := endpoints.GetFileEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s/%s/%s", domain, shareName, directoryName, filePath) +} + +type ResourceID struct { + AccountName string + DirectoryName string + FileName string + ShareName string +} + +// ParseResourceID parses the specified Resource ID and returns an object +// which can be used to interact with Files within a Storage Share. +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://account1.file.core.chinacloudapi.cn/share1/directory1/file1.txt + // example: https://account1.file.core.chinacloudapi.cn/share1/directory1/directory2/file1.txt + + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + path := strings.TrimPrefix(uri.Path, "/") + segments := strings.Split(path, "/") + if len(segments) == 0 { + return nil, fmt.Errorf("Expected the path to contain segments but got none") + } + + shareName := segments[0] + fileName := segments[len(segments)-1] + + directoryName := strings.TrimPrefix(path, shareName) + directoryName = strings.TrimPrefix(directoryName, "/") + directoryName = strings.TrimSuffix(directoryName, fileName) + directoryName = strings.TrimSuffix(directoryName, "/") + return &ResourceID{ + AccountName: *accountName, + ShareName: shareName, + DirectoryName: directoryName, + FileName: fileName, + }, nil +} diff --git a/storage/2018-11-09/file/files/resource_id_test.go b/storage/2018-11-09/file/files/resource_id_test.go new file mode 100644 index 0000000..1b521ac --- /dev/null +++ b/storage/2018-11-09/file/files/resource_id_test.go @@ -0,0 +1,131 @@ +package files + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.file.core.chinacloudapi.cn/share1/directory1/file1.txt", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.file.core.cloudapi.de/share1/directory1/file1.txt", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.file.core.windows.net/share1/directory1/file1.txt", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.file.core.usgovcloudapi.net/share1/directory1/file1.txt", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "share1", "directory1", "file1.txt") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.file.core.chinacloudapi.cn/share1/directory1/file1.txt", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.file.core.cloudapi.de/share1/directory1/file1.txt", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.file.core.windows.net/share1/directory1/file1.txt", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.file.core.usgovcloudapi.net/share1/directory1/file1.txt", + }, + } + + t.Logf("[DEBUG] Top Level Files") + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.ShareName != "share1" { + t.Fatalf("Expected Share Name to be `share1` but got %q", actual.ShareName) + } + if actual.DirectoryName != "directory1" { + t.Fatalf("Expected Directory Name to be `directory1` but got %q", actual.DirectoryName) + } + if actual.FileName != "file1.txt" { + t.Fatalf("Expected File Name to be `file1.txt` but got %q", actual.FileName) + } + } + + testData = []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.file.core.chinacloudapi.cn/share1/directory1/directory2/file1.txt", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.file.core.cloudapi.de/share1/directory1/directory2/file1.txt", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.file.core.windows.net/share1/directory1/directory2/file1.txt", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.file.core.usgovcloudapi.net/share1/directory1/directory2/file1.txt", + }, + } + + t.Logf("[DEBUG] Nested Files") + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.ShareName != "share1" { + t.Fatalf("Expected Share Name to be `share1` but got %q", actual.ShareName) + } + if actual.DirectoryName != "directory1/directory2" { + t.Fatalf("Expected Directory Name to be `directory1/directory2` but got %q", actual.DirectoryName) + } + if actual.FileName != "file1.txt" { + t.Fatalf("Expected File Name to be `file1.txt` but got %q", actual.FileName) + } + } +} diff --git a/storage/2018-11-09/file/files/version.go b/storage/2018-11-09/file/files/version.go new file mode 100644 index 0000000..8d135a3 --- /dev/null +++ b/storage/2018-11-09/file/files/version.go @@ -0,0 +1,14 @@ +package files + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2018-11-09" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2018-11-09/file/shares/README.md b/storage/2018-11-09/file/shares/README.md new file mode 100644 index 0000000..94b3bdb --- /dev/null +++ b/storage/2018-11-09/file/shares/README.md @@ -0,0 +1,43 @@ +## File Storage Shares SDK for API version 2018-11-09 + +This package allows you to interact with the Shares File Storage API + +### Supported Authorizers + +* Azure Active Directory (for the Resource Endpoint `https://storage.azure.com`) +* SharedKeyLite (Blob, File & Queue) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/file/shares" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + shareName := "myshare" + + storageAuth := autorest.NewSharedKeyLiteAuthorizer(accountName, storageAccountKey) + sharesClient := shares.New() + sharesClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + input := shares.CreateInput{ + QuotaInGB: 2, + } + if _, err := sharesClient.Create(ctx, accountName, shareName, input); err != nil { + return fmt.Errorf("Error creating Share: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2018-11-09/file/shares/acl_get.go b/storage/2018-11-09/file/shares/acl_get.go new file mode 100644 index 0000000..ea6ff4c --- /dev/null +++ b/storage/2018-11-09/file/shares/acl_get.go @@ -0,0 +1,98 @@ +package shares + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetACLResult struct { + autorest.Response + + SignedIdentifiers []SignedIdentifier `xml:"SignedIdentifier"` +} + +// GetACL get the Access Control List for the specified Storage Share +func (client Client) GetACL(ctx context.Context, accountName, shareName string) (result GetACLResult, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "GetACL", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "GetACL", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "GetACL", "`shareName` must be a lower-cased string.") + } + + req, err := client.GetACLPreparer(ctx, accountName, shareName) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetACL", nil, "Failure preparing request") + return + } + + resp, err := client.GetACLSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "GetACL", resp, "Failure sending request") + return + } + + result, err = client.GetACLResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetACL", resp, "Failure responding to request") + return + } + + return +} + +// GetACLPreparer prepares the GetACL request. +func (client Client) GetACLPreparer(ctx context.Context, accountName, shareName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + "comp": autorest.Encode("query", "acl"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetACLSender sends the GetACL request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetACLSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetACLResponder handles the response to the GetACL request. The method always +// closes the http.Response Body. +func (client Client) GetACLResponder(resp *http.Response) (result GetACLResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/shares/acl_set.go b/storage/2018-11-09/file/shares/acl_set.go new file mode 100644 index 0000000..18d1788 --- /dev/null +++ b/storage/2018-11-09/file/shares/acl_set.go @@ -0,0 +1,103 @@ +package shares + +import ( + "context" + "encoding/xml" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type setAcl struct { + SignedIdentifiers []SignedIdentifier `xml:"SignedIdentifier"` + + XMLName xml.Name `xml:"SignedIdentifiers"` +} + +// SetACL sets the specified Access Control List on the specified Storage Share +func (client Client) SetACL(ctx context.Context, accountName, shareName string, acls []SignedIdentifier) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "SetACL", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "SetACL", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "SetACL", "`shareName` must be a lower-cased string.") + } + + req, err := client.SetACLPreparer(ctx, accountName, shareName, acls) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "SetACL", nil, "Failure preparing request") + return + } + + resp, err := client.SetACLSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "SetACL", resp, "Failure sending request") + return + } + + result, err = client.SetACLResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "SetACL", resp, "Failure responding to request") + return + } + + return +} + +// SetACLPreparer prepares the SetACL request. +func (client Client) SetACLPreparer(ctx context.Context, accountName, shareName string, acls []SignedIdentifier) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + "comp": autorest.Encode("query", "acl"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + input := setAcl{ + SignedIdentifiers: acls, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers), + autorest.WithXML(&input)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetACLSender sends the SetACL request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetACLSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetACLResponder handles the response to the SetACL request. The method always +// closes the http.Response Body. +func (client Client) SetACLResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/shares/client.go b/storage/2018-11-09/file/shares/client.go new file mode 100644 index 0000000..4f3a6f9 --- /dev/null +++ b/storage/2018-11-09/file/shares/client.go @@ -0,0 +1,25 @@ +package shares + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for File Storage Shares. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithEnvironment creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2018-11-09/file/shares/create.go b/storage/2018-11-09/file/shares/create.go new file mode 100644 index 0000000..84fd40d --- /dev/null +++ b/storage/2018-11-09/file/shares/create.go @@ -0,0 +1,109 @@ +package shares + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type CreateInput struct { + // Specifies the maximum size of the share, in gigabytes. + // Must be greater than 0, and less than or equal to 5TB (5120). + QuotaInGB int + + MetaData map[string]string +} + +// Create creates the specified Storage Share within the specified Storage Account +func (client Client) Create(ctx context.Context, accountName, shareName string, input CreateInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "Create", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "Create", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "Create", "`shareName` must be a lower-cased string.") + } + if input.QuotaInGB <= 0 || input.QuotaInGB > 5120 { + return result, validation.NewError("shares.Client", "Create", "`input.QuotaInGB` must be greater than 0, and less than/equal to 5TB (5120 GB)") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("shares.Client", "Create", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.CreatePreparer(ctx, accountName, shareName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "Create", nil, "Failure preparing request") + return + } + + resp, err := client.CreateSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "Create", resp, "Failure sending request") + return + } + + result, err = client.CreateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "Create", resp, "Failure responding to request") + return + } + + return +} + +// CreatePreparer prepares the Create request. +func (client Client) CreatePreparer(ctx context.Context, accountName, shareName string, input CreateInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "share"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-share-quota": input.QuotaInGB, + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CreateSender sends the Create request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CreateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CreateResponder handles the response to the Create request. The method always +// closes the http.Response Body. +func (client Client) CreateResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/shares/delete.go b/storage/2018-11-09/file/shares/delete.go new file mode 100644 index 0000000..70ef985 --- /dev/null +++ b/storage/2018-11-09/file/shares/delete.go @@ -0,0 +1,94 @@ +package shares + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Delete deletes the specified Storage Share from within a Storage Account +func (client Client) Delete(ctx context.Context, accountName, shareName string, deleteSnapshots bool) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "Delete", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "Delete", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "Delete", "`shareName` must be a lower-cased string.") + } + + req, err := client.DeletePreparer(ctx, accountName, shareName, deleteSnapshots) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName, shareName string, deleteSnapshots bool) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "share"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + if deleteSnapshots { + headers["x-ms-delete-snapshots"] = "include" + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/shares/lifecycle_test.go b/storage/2018-11-09/file/shares/lifecycle_test.go new file mode 100644 index 0000000..fbab96d --- /dev/null +++ b/storage/2018-11-09/file/shares/lifecycle_test.go @@ -0,0 +1,152 @@ +package shares + +import ( + "context" + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestSharesLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + shareName := fmt.Sprintf("share-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + sharesClient := NewWithEnvironment(client.Environment) + sharesClient.Client = client.PrepareWithAuthorizer(sharesClient.Client, storageAuth) + + input := CreateInput{ + QuotaInGB: 1, + } + _, err = sharesClient.Create(ctx, accountName, shareName, input) + if err != nil { + t.Fatalf("Error creating fileshare: %s", err) + } + + snapshot, err := sharesClient.CreateSnapshot(ctx, accountName, shareName, CreateSnapshotInput{}) + if err != nil { + t.Fatalf("Error taking snapshot: %s", err) + } + t.Logf("Snapshot Date Time: %s", snapshot.SnapshotDateTime) + + snapshotDetails, err := sharesClient.GetSnapshot(ctx, accountName, shareName, snapshot.SnapshotDateTime) + if err != nil { + t.Fatalf("Error retrieving snapshot: %s", err) + } + + t.Logf("MetaData: %s", snapshotDetails.MetaData) + + _, err = sharesClient.DeleteSnapshot(ctx, accountName, shareName, snapshot.SnapshotDateTime) + if err != nil { + t.Fatalf("Error deleting snapshot: %s", err) + } + + stats, err := sharesClient.GetStats(ctx, accountName, shareName) + if err != nil { + t.Fatalf("Error retrieving stats: %s", err) + } + + if stats.ShareUsageBytes != 0 { + t.Fatalf("Expected `stats.ShareUsageBytes` to be 0 but got: %d", stats.ShareUsageBytes) + } + + share, err := sharesClient.GetProperties(ctx, accountName, shareName) + if err != nil { + t.Fatalf("Error retrieving share: %s", err) + } + if share.ShareQuota != 1 { + t.Fatalf("Expected Quota to be 1 but got: %d", share.ShareQuota) + } + + _, err = sharesClient.SetProperties(ctx, accountName, shareName, 5) + if err != nil { + t.Fatalf("Error updating quota: %s", err) + } + + share, err = sharesClient.GetProperties(ctx, accountName, shareName) + if err != nil { + t.Fatalf("Error retrieving share: %s", err) + } + if share.ShareQuota != 5 { + t.Fatalf("Expected Quota to be 5 but got: %d", share.ShareQuota) + } + + updatedMetaData := map[string]string{ + "hello": "world", + } + _, err = sharesClient.SetMetaData(ctx, accountName, shareName, updatedMetaData) + if err != nil { + t.Fatalf("Erorr setting metadata: %s", err) + } + + result, err := sharesClient.GetMetaData(ctx, accountName, shareName) + if err != nil { + t.Fatalf("Error retrieving metadata: %s", err) + } + + if result.MetaData["hello"] != "world" { + t.Fatalf("Expected metadata `hello` to be `world` but got: %q", result.MetaData["hello"]) + } + if len(result.MetaData) != 1 { + t.Fatalf("Expected metadata to be 1 item but got: %s", result.MetaData) + } + + acls, err := sharesClient.GetACL(ctx, accountName, shareName) + if err != nil { + t.Fatalf("Error retrieving ACL's: %s", err) + } + if len(acls.SignedIdentifiers) != 0 { + t.Fatalf("Expected 0 identifiers but got %d", len(acls.SignedIdentifiers)) + } + + updatedAcls := []SignedIdentifier{ + { + Id: "abc123", + AccessPolicy: AccessPolicy{ + Start: "2020-07-01T08:49:37.0000000Z", + Expiry: "2020-07-01T09:49:37.0000000Z", + Permission: "rwd", + }, + }, + { + Id: "bcd234", + AccessPolicy: AccessPolicy{ + Start: "2020-07-01T08:49:37.0000000Z", + Expiry: "2020-07-01T09:49:37.0000000Z", + Permission: "rwd", + }, + }, + } + _, err = sharesClient.SetACL(ctx, accountName, shareName, updatedAcls) + if err != nil { + t.Fatalf("Error setting ACL's: %s", err) + } + + acls, err = sharesClient.GetACL(ctx, accountName, shareName) + if err != nil { + t.Fatalf("Error retrieving ACL's: %s", err) + } + if len(acls.SignedIdentifiers) != 2 { + t.Fatalf("Expected 2 identifiers but got %d", len(acls.SignedIdentifiers)) + } + + _, err = sharesClient.Delete(ctx, accountName, shareName, false) + if err != nil { + t.Fatalf("Error deleting Share: %s", err) + } +} diff --git a/storage/2018-11-09/file/shares/metadata_get.go b/storage/2018-11-09/file/shares/metadata_get.go new file mode 100644 index 0000000..9fa4d9f --- /dev/null +++ b/storage/2018-11-09/file/shares/metadata_get.go @@ -0,0 +1,102 @@ +package shares + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetMetaDataResult struct { + autorest.Response + + MetaData map[string]string +} + +// GetMetaData returns the MetaData associated with the specified Storage Share +func (client Client) GetMetaData(ctx context.Context, accountName, shareName string) (result GetMetaDataResult, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "GetMetaData", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "GetMetaData", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "GetMetaData", "`shareName` must be a lower-cased string.") + } + + req, err := client.GetMetaDataPreparer(ctx, accountName, shareName) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.GetMetaDataSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "GetMetaData", resp, "Failure sending request") + return + } + + result, err = client.GetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// GetMetaDataPreparer prepares the GetMetaData request. +func (client Client) GetMetaDataPreparer(ctx context.Context, accountName, shareName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + "comp": autorest.Encode("query", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetMetaDataSender sends the GetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetMetaDataResponder handles the response to the GetMetaData request. The method always +// closes the http.Response Body. +func (client Client) GetMetaDataResponder(resp *http.Response) (result GetMetaDataResult, err error) { + if resp.Header != nil { + result.MetaData = metadata.ParseFromHeaders(resp.Header) + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/shares/metadata_set.go b/storage/2018-11-09/file/shares/metadata_set.go new file mode 100644 index 0000000..7e64e60 --- /dev/null +++ b/storage/2018-11-09/file/shares/metadata_set.go @@ -0,0 +1,97 @@ +package shares + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// SetMetaData sets the MetaData on the specified Storage Share +func (client Client) SetMetaData(ctx context.Context, accountName, shareName string, metaData map[string]string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "SetMetaData", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "SetMetaData", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "SetMetaData", "`shareName` must be a lower-cased string.") + } + if err := metadata.Validate(metaData); err != nil { + return result, validation.NewError("shares.Client", "SetMetaData", fmt.Sprintf("`metadata` is not valid: %s.", err)) + } + + req, err := client.SetMetaDataPreparer(ctx, accountName, shareName, metaData) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "SetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.SetMetaDataSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "SetMetaData", resp, "Failure sending request") + return + } + + result, err = client.SetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "SetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// SetMetaDataPreparer prepares the SetMetaData request. +func (client Client) SetMetaDataPreparer(ctx context.Context, accountName, shareName string, metaData map[string]string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + "comp": autorest.Encode("query", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + headers = metadata.SetIntoHeaders(headers, metaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetPropertiesSetMetaDataSender sends the SetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetMetaDataResponder handles the response to the SetMetaData request. The method always +// closes the http.Response Body. +func (client Client) SetMetaDataResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/shares/models.go b/storage/2018-11-09/file/shares/models.go new file mode 100644 index 0000000..31ef7c2 --- /dev/null +++ b/storage/2018-11-09/file/shares/models.go @@ -0,0 +1,12 @@ +package shares + +type SignedIdentifier struct { + Id string `xml:"Id"` + AccessPolicy AccessPolicy `xml:"AccessPolicy"` +} + +type AccessPolicy struct { + Start string `xml:"Start"` + Expiry string `xml:"Expiry"` + Permission string `xml:"Permission"` +} diff --git a/storage/2018-11-09/file/shares/properties_get.go b/storage/2018-11-09/file/shares/properties_get.go new file mode 100644 index 0000000..80e26a4 --- /dev/null +++ b/storage/2018-11-09/file/shares/properties_get.go @@ -0,0 +1,111 @@ +package shares + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetPropertiesResult struct { + autorest.Response + + MetaData map[string]string + ShareQuota int +} + +// GetProperties returns the properties about the specified Storage Share +func (client Client) GetProperties(ctx context.Context, accountName, shareName string) (result GetPropertiesResult, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "GetProperties", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "GetProperties", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "GetProperties", "`shareName` must be a lower-cased string.") + } + + req, err := client.GetPropertiesPreparer(ctx, accountName, shareName) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetProperties", nil, "Failure preparing request") + return + } + + resp, err := client.GetPropertiesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "GetProperties", resp, "Failure sending request") + return + } + + result, err = client.GetPropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetProperties", resp, "Failure responding to request") + return + } + + return +} + +// GetPropertiesPreparer prepares the GetProperties request. +func (client Client) GetPropertiesPreparer(ctx context.Context, accountName, shareName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetPropertiesSender sends the GetProperties request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetPropertiesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetPropertiesResponder handles the response to the GetProperties request. The method always +// closes the http.Response Body. +func (client Client) GetPropertiesResponder(resp *http.Response) (result GetPropertiesResult, err error) { + if resp.Header != nil { + result.MetaData = metadata.ParseFromHeaders(resp.Header) + + quotaRaw := resp.Header.Get("x-ms-share-quota") + quota, e := strconv.Atoi(quotaRaw) + if e != nil { + return result, fmt.Errorf("Error converting %q to an integer: %s", quotaRaw, err) + } + result.ShareQuota = quota + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/shares/properties_set.go b/storage/2018-11-09/file/shares/properties_set.go new file mode 100644 index 0000000..4553e5e --- /dev/null +++ b/storage/2018-11-09/file/shares/properties_set.go @@ -0,0 +1,95 @@ +package shares + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// SetProperties lets you update the Quota for the specified Storage Share +func (client Client) SetProperties(ctx context.Context, accountName, shareName string, newQuotaGB int) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "SetProperties", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "SetProperties", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "SetProperties", "`shareName` must be a lower-cased string.") + } + if newQuotaGB <= 0 || newQuotaGB > 5120 { + return result, validation.NewError("shares.Client", "SetProperties", "`newQuotaGB` must be greater than 0, and less than/equal to 5TB (5120 GB)") + } + + req, err := client.SetPropertiesPreparer(ctx, accountName, shareName, newQuotaGB) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "SetProperties", nil, "Failure preparing request") + return + } + + resp, err := client.SetPropertiesSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "SetProperties", resp, "Failure sending request") + return + } + + result, err = client.SetPropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "SetProperties", resp, "Failure responding to request") + return + } + + return +} + +// SetPropertiesPreparer prepares the SetProperties request. +func (client Client) SetPropertiesPreparer(ctx context.Context, accountName, shareName string, quotaGB int) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + "comp": autorest.Encode("query", "properties"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "x-ms-share-quota": quotaGB, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetPropertiesSender sends the SetProperties request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetPropertiesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetPropertiesResponder handles the response to the SetProperties request. The method always +// closes the http.Response Body. +func (client Client) SetPropertiesResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/shares/resource_id.go b/storage/2018-11-09/file/shares/resource_id.go new file mode 100644 index 0000000..bfdcbfd --- /dev/null +++ b/storage/2018-11-09/file/shares/resource_id.go @@ -0,0 +1,46 @@ +package shares + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given File Share +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, shareName string) string { + domain := endpoints.GetFileEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s", domain, shareName) +} + +type ResourceID struct { + AccountName string + ShareName string +} + +// ParseResourceID parses the specified Resource ID and returns an object +// which can be used to interact with the Storage Shares SDK +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://foo.file.core.windows.net/Bar + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + shareName := strings.TrimPrefix(uri.Path, "/") + return &ResourceID{ + AccountName: *accountName, + ShareName: shareName, + }, nil +} diff --git a/storage/2018-11-09/file/shares/resource_id_test.go b/storage/2018-11-09/file/shares/resource_id_test.go new file mode 100644 index 0000000..1b7eea3 --- /dev/null +++ b/storage/2018-11-09/file/shares/resource_id_test.go @@ -0,0 +1,79 @@ +package shares + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.file.core.chinacloudapi.cn/share1", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.file.core.cloudapi.de/share1", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.file.core.windows.net/share1", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.file.core.usgovcloudapi.net/share1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "share1") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.file.core.chinacloudapi.cn/share1", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.file.core.cloudapi.de/share1", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.file.core.windows.net/share1", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.file.core.usgovcloudapi.net/share1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected the account name to be `account1` but got %q", actual.AccountName) + } + + if actual.ShareName != "share1" { + t.Fatalf("Expected the share name to be `share1` but got %q", actual.ShareName) + } + } +} diff --git a/storage/2018-11-09/file/shares/snapshot_create.go b/storage/2018-11-09/file/shares/snapshot_create.go new file mode 100644 index 0000000..0ded38b --- /dev/null +++ b/storage/2018-11-09/file/shares/snapshot_create.go @@ -0,0 +1,115 @@ +package shares + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type CreateSnapshotInput struct { + MetaData map[string]string +} + +type CreateSnapshotResult struct { + autorest.Response + + // This header is a DateTime value that uniquely identifies the share snapshot. + // The value of this header may be used in subsequent requests to access the share snapshot. + // This value is opaque. + SnapshotDateTime string +} + +// CreateSnapshot creates a read-only snapshot of the share +// A share can support creation of 200 share snapshots. Attempting to create more than 200 share snapshots fails with 409 (Conflict). +// Attempting to create a share snapshot while a previous Snapshot Share operation is in progress fails with 409 (Conflict). +func (client Client) CreateSnapshot(ctx context.Context, accountName, shareName string, input CreateSnapshotInput) (result CreateSnapshotResult, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "CreateSnapshot", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "CreateSnapshot", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "CreateSnapshot", "`shareName` must be a lower-cased string.") + } + if err := metadata.Validate(input.MetaData); err != nil { + return result, validation.NewError("shares.Client", "CreateSnapshot", fmt.Sprintf("`input.MetaData` is not valid: %s.", err)) + } + + req, err := client.CreateSnapshotPreparer(ctx, accountName, shareName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "CreateSnapshot", nil, "Failure preparing request") + return + } + + resp, err := client.CreateSnapshotSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "CreateSnapshot", resp, "Failure sending request") + return + } + + result, err = client.CreateSnapshotResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "CreateSnapshot", resp, "Failure responding to request") + return + } + + return +} + +// CreateSnapshotPreparer prepares the CreateSnapshot request. +func (client Client) CreateSnapshotPreparer(ctx context.Context, accountName, shareName string, input CreateSnapshotInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "snapshot"), + "restype": autorest.Encode("query", "share"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = metadata.SetIntoHeaders(headers, input.MetaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CreateSnapshotSender sends the CreateSnapshot request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CreateSnapshotSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CreateSnapshotResponder handles the response to the CreateSnapshot request. The method always +// closes the http.Response Body. +func (client Client) CreateSnapshotResponder(resp *http.Response) (result CreateSnapshotResult, err error) { + result.SnapshotDateTime = resp.Header.Get("x-ms-snapshot") + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/shares/snapshot_delete.go b/storage/2018-11-09/file/shares/snapshot_delete.go new file mode 100644 index 0000000..1f5d665 --- /dev/null +++ b/storage/2018-11-09/file/shares/snapshot_delete.go @@ -0,0 +1,94 @@ +package shares + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// DeleteSnapshot deletes the specified Snapshot of a Storage Share +func (client Client) DeleteSnapshot(ctx context.Context, accountName, shareName string, shareSnapshot string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "DeleteSnapshot", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "DeleteSnapshot", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "DeleteSnapshot", "`shareName` must be a lower-cased string.") + } + if shareSnapshot == "" { + return result, validation.NewError("shares.Client", "DeleteSnapshot", "`shareSnapshot` cannot be an empty string.") + } + + req, err := client.DeleteSnapshotPreparer(ctx, accountName, shareName, shareSnapshot) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "DeleteSnapshot", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSnapshotSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "DeleteSnapshot", resp, "Failure sending request") + return + } + + result, err = client.DeleteSnapshotResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "DeleteSnapshot", resp, "Failure responding to request") + return + } + + return +} + +// DeleteSnapshotPreparer prepares the DeleteSnapshot request. +func (client Client) DeleteSnapshotPreparer(ctx context.Context, accountName, shareName string, shareSnapshot string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("path", "share"), + "sharesnapshot": autorest.Encode("query", shareSnapshot), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSnapshotSender sends the DeleteSnapshot request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSnapshotSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteSnapshotResponder handles the response to the DeleteSnapshot request. The method always +// closes the http.Response Body. +func (client Client) DeleteSnapshotResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/shares/snapshot_get.go b/storage/2018-11-09/file/shares/snapshot_get.go new file mode 100644 index 0000000..2cf5f16 --- /dev/null +++ b/storage/2018-11-09/file/shares/snapshot_get.go @@ -0,0 +1,105 @@ +package shares + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetSnapshotPropertiesResult struct { + autorest.Response + + MetaData map[string]string +} + +// GetSnapshot gets information about the specified Snapshot of the specified Storage Share +func (client Client) GetSnapshot(ctx context.Context, accountName, shareName, snapshotShare string) (result GetSnapshotPropertiesResult, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "GetSnapshot", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "GetSnapshot", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "GetSnapshot", "`shareName` must be a lower-cased string.") + } + if snapshotShare == "" { + return result, validation.NewError("shares.Client", "GetSnapshot", "`snapshotShare` cannot be an empty string.") + } + + req, err := client.GetSnapshotPreparer(ctx, accountName, shareName, snapshotShare) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetSnapshot", nil, "Failure preparing request") + return + } + + resp, err := client.GetSnapshotSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "GetSnapshot", resp, "Failure sending request") + return + } + + result, err = client.GetSnapshotResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetSnapshot", resp, "Failure responding to request") + return + } + + return +} + +// GetSnapshotPreparer prepares the GetSnapshot request. +func (client Client) GetSnapshotPreparer(ctx context.Context, accountName, shareName, snapshotShare string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + "snapshot": autorest.Encode("query", snapshotShare), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetSnapshotSender sends the GetSnapshot request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetSnapshotSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetSnapshotResponder handles the response to the GetSnapshot request. The method always +// closes the http.Response Body. +func (client Client) GetSnapshotResponder(resp *http.Response) (result GetSnapshotPropertiesResult, err error) { + if resp.Header != nil { + result.MetaData = metadata.ParseFromHeaders(resp.Header) + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/shares/stats.go b/storage/2018-11-09/file/shares/stats.go new file mode 100644 index 0000000..3539ecc --- /dev/null +++ b/storage/2018-11-09/file/shares/stats.go @@ -0,0 +1,100 @@ +package shares + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetStatsResult struct { + autorest.Response + + // The approximate size of the data stored on the share. + // Note that this value may not include all recently created or recently resized files. + ShareUsageBytes int64 `xml:"ShareUsageBytes"` +} + +// GetStats returns information about the specified Storage Share +func (client Client) GetStats(ctx context.Context, accountName, shareName string) (result GetStatsResult, err error) { + if accountName == "" { + return result, validation.NewError("shares.Client", "GetStats", "`accountName` cannot be an empty string.") + } + if shareName == "" { + return result, validation.NewError("shares.Client", "GetStats", "`shareName` cannot be an empty string.") + } + if strings.ToLower(shareName) != shareName { + return result, validation.NewError("shares.Client", "GetStats", "`shareName` must be a lower-cased string.") + } + + req, err := client.GetStatsPreparer(ctx, accountName, shareName) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetStats", nil, "Failure preparing request") + return + } + + resp, err := client.GetStatsSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "shares.Client", "GetStats", resp, "Failure sending request") + return + } + + result, err = client.GetStatsResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "shares.Client", "GetStats", resp, "Failure responding to request") + return + } + + return +} + +// GetStatsPreparer prepares the GetStats request. +func (client Client) GetStatsPreparer(ctx context.Context, accountName, shareName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "shareName": autorest.Encode("path", shareName), + } + + queryParameters := map[string]interface{}{ + "restype": autorest.Encode("query", "share"), + "comp": autorest.Encode("query", "stats"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetFileEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{shareName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetStatsSender sends the GetStats request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetStatsSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetStatsResponder handles the response to the GetStats request. The method always +// closes the http.Response Body. +func (client Client) GetStatsResponder(resp *http.Response) (result GetStatsResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/file/shares/version.go b/storage/2018-11-09/file/shares/version.go new file mode 100644 index 0000000..9249149 --- /dev/null +++ b/storage/2018-11-09/file/shares/version.go @@ -0,0 +1,14 @@ +package shares + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2018-11-09" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2018-11-09/queue/messages/README.md b/storage/2018-11-09/queue/messages/README.md new file mode 100644 index 0000000..3c06965 --- /dev/null +++ b/storage/2018-11-09/queue/messages/README.md @@ -0,0 +1,43 @@ +## Queue Storage Messages SDK for API version 2018-11-09 + +This package allows you to interact with the Messages Queue Storage API + +### Supported Authorizers + +* Azure Active Directory (for the Resource Endpoint `https://storage.azure.com`) +* SharedKeyLite (Blob, File & Queue) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/queue/messages" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + queueName := "myqueue" + + storageAuth := autorest.NewSharedKeyLiteAuthorizer(accountName, storageAccountKey) + messagesClient := messages.New() + messagesClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + input := messages.PutInput{ + Message: "hello", + } + if _, err := messagesClient.Put(ctx, accountName, queueName, input); err != nil { + return fmt.Errorf("Error creating Message: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2018-11-09/queue/messages/client.go b/storage/2018-11-09/queue/messages/client.go new file mode 100644 index 0000000..08b1801 --- /dev/null +++ b/storage/2018-11-09/queue/messages/client.go @@ -0,0 +1,25 @@ +package messages + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for Messages. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithEnvironment creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2018-11-09/queue/messages/delete.go b/storage/2018-11-09/queue/messages/delete.go new file mode 100644 index 0000000..1ec0e1a --- /dev/null +++ b/storage/2018-11-09/queue/messages/delete.go @@ -0,0 +1,97 @@ +package messages + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Delete deletes a specific message +func (client Client) Delete(ctx context.Context, accountName, queueName, messageID, popReceipt string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("messages.Client", "Delete", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("messages.Client", "Delete", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("messages.Client", "Delete", "`queueName` must be a lower-cased string.") + } + if messageID == "" { + return result, validation.NewError("messages.Client", "Delete", "`messageID` cannot be an empty string.") + } + if popReceipt == "" { + return result, validation.NewError("messages.Client", "Delete", "`popReceipt` cannot be an empty string.") + } + + req, err := client.DeletePreparer(ctx, accountName, queueName, messageID, popReceipt) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "messages.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName, queueName, messageID, popReceipt string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + "messageID": autorest.Encode("path", messageID), + } + + queryParameters := map[string]interface{}{ + "popreceipt": autorest.Encode("query", popReceipt), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}/messages/{messageID}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/queue/messages/get.go b/storage/2018-11-09/queue/messages/get.go new file mode 100644 index 0000000..4edeb6d --- /dev/null +++ b/storage/2018-11-09/queue/messages/get.go @@ -0,0 +1,112 @@ +package messages + +import ( + "context" + "net/http" + "strings" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetInput struct { + // VisibilityTimeout specifies the new visibility timeout value, in seconds, relative to server time. + // The new value must be larger than or equal to 0, and cannot be larger than 7 days. + VisibilityTimeout *int +} + +// Get retrieves one or more messages from the front of the queue +func (client Client) Get(ctx context.Context, accountName, queueName string, numberOfMessages int, input GetInput) (result QueueMessagesListResult, err error) { + if accountName == "" { + return result, validation.NewError("messages.Client", "Get", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("messages.Client", "Get", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("messages.Client", "Get", "`queueName` must be a lower-cased string.") + } + if numberOfMessages < 1 || numberOfMessages > 32 { + return result, validation.NewError("messages.Client", "Get", "`numberOfMessages` must be between 1 and 32.") + } + if input.VisibilityTimeout != nil { + t := *input.VisibilityTimeout + maxTime := (time.Hour * 24 * 7).Seconds() + if t < 1 || t < int(maxTime) { + return result, validation.NewError("messages.Client", "Get", "`input.VisibilityTimeout` must be larger than or equal to 1 second, and cannot be larger than 7 days.") + } + } + + req, err := client.GetPreparer(ctx, accountName, queueName, numberOfMessages, input) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Get", nil, "Failure preparing request") + return + } + + resp, err := client.GetSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "messages.Client", "Get", resp, "Failure sending request") + return + } + + result, err = client.GetResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Get", resp, "Failure responding to request") + return + } + + return +} + +// GetPreparer prepares the Get request. +func (client Client) GetPreparer(ctx context.Context, accountName, queueName string, numberOfMessages int, input GetInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + } + + queryParameters := map[string]interface{}{ + "numofmessages": autorest.Encode("query", numberOfMessages), + } + + if input.VisibilityTimeout != nil { + queryParameters["visibilitytimeout"] = autorest.Encode("query", *input.VisibilityTimeout) + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}/messages", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetSender sends the Get request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetResponder handles the response to the Get request. The method always +// closes the http.Response Body. +func (client Client) GetResponder(resp *http.Response) (result QueueMessagesListResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + autorest.ByUnmarshallingXML(&result), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/queue/messages/lifecycle_test.go b/storage/2018-11-09/queue/messages/lifecycle_test.go new file mode 100644 index 0000000..95d8da5 --- /dev/null +++ b/storage/2018-11-09/queue/messages/lifecycle_test.go @@ -0,0 +1,95 @@ +package messages + +import ( + "context" + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/queue/queues" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestLifeCycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + queueName := fmt.Sprintf("queue-%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + queuesClient := queues.NewWithEnvironment(client.Environment) + queuesClient.Client = client.PrepareWithStorageResourceManagerAuth(queuesClient.Client) + + storageAuth := auth.NewSharedKeyLiteAuthorizer(accountName, testData.StorageAccountKey) + messagesClient := NewWithEnvironment(client.Environment) + messagesClient.Client = client.PrepareWithAuthorizer(messagesClient.Client, storageAuth) + + _, err = queuesClient.Create(ctx, accountName, queueName, map[string]string{}) + if err != nil { + t.Fatalf("Error creating queue: %s", err) + } + defer queuesClient.Delete(ctx, accountName, queueName) + + input := PutInput{ + Message: "ohhai", + } + putResp, err := messagesClient.Put(ctx, accountName, queueName, input) + if err != nil { + t.Fatalf("Error putting message in queue: %s", err) + } + + messageId := (*putResp.QueueMessages)[0].MessageId + popReceipt := (*putResp.QueueMessages)[0].PopReceipt + + _, err = messagesClient.Update(ctx, accountName, queueName, messageId, UpdateInput{ + PopReceipt: popReceipt, + Message: "Updated message", + VisibilityTimeout: 65, + }) + if err != nil { + t.Fatalf("Error updating: %s", err) + } + + for i := 0; i < 5; i++ { + input := PutInput{ + Message: fmt.Sprintf("Message %d", i), + } + _, err := messagesClient.Put(ctx, accountName, queueName, input) + if err != nil { + t.Fatalf("Error putting message %d in queue: %s", i, err) + } + } + + peakedMessages, err := messagesClient.Peek(ctx, accountName, queueName, 3) + if err != nil { + t.Fatalf("Error peaking messages: %s", err) + } + + for _, v := range *peakedMessages.QueueMessages { + t.Logf("Message: %q", v.MessageId) + } + + retrievedMessages, err := messagesClient.Get(ctx, accountName, queueName, 6, GetInput{}) + if err != nil { + t.Fatalf("Error retrieving messages: %s", err) + } + + for _, v := range *retrievedMessages.QueueMessages { + t.Logf("Message: %q", v.MessageId) + + _, err = messagesClient.Delete(ctx, accountName, queueName, v.MessageId, v.PopReceipt) + if err != nil { + t.Fatalf("Error deleting message from queue: %s", err) + } + } +} diff --git a/storage/2018-11-09/queue/messages/models.go b/storage/2018-11-09/queue/messages/models.go new file mode 100644 index 0000000..67815a8 --- /dev/null +++ b/storage/2018-11-09/queue/messages/models.go @@ -0,0 +1,21 @@ +package messages + +import "github.com/Azure/go-autorest/autorest" + +type QueueMessage struct { + MessageText string `xml:"MessageText"` +} + +type QueueMessagesListResult struct { + autorest.Response + + QueueMessages *[]QueueMessageResponse `xml:"QueueMessage"` +} + +type QueueMessageResponse struct { + MessageId string `xml:"MessageId"` + InsertionTime string `xml:"InsertionTime"` + ExpirationTime string `xml:"ExpirationTime"` + PopReceipt string `xml:"PopReceipt"` + TimeNextVisible string `xml:"TimeNextVisible"` +} diff --git a/storage/2018-11-09/queue/messages/peek.go b/storage/2018-11-09/queue/messages/peek.go new file mode 100644 index 0000000..7288bd5 --- /dev/null +++ b/storage/2018-11-09/queue/messages/peek.go @@ -0,0 +1,95 @@ +package messages + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Peek retrieves one or more messages from the front of the queue, but doesn't alter the visibility of the messages +func (client Client) Peek(ctx context.Context, accountName, queueName string, numberOfMessages int) (result QueueMessagesListResult, err error) { + if accountName == "" { + return result, validation.NewError("messages.Client", "Peek", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("messages.Client", "Peek", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("messages.Client", "Peek", "`queueName` must be a lower-cased string.") + } + if numberOfMessages < 1 || numberOfMessages > 32 { + return result, validation.NewError("messages.Client", "Peek", "`numberOfMessages` must be between 1 and 32.") + } + + req, err := client.PeekPreparer(ctx, accountName, queueName, numberOfMessages) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Peek", nil, "Failure preparing request") + return + } + + resp, err := client.PeekSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "messages.Client", "Peek", resp, "Failure sending request") + return + } + + result, err = client.PeekResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Peek", resp, "Failure responding to request") + return + } + + return +} + +// PeekPreparer prepares the Peek request. +func (client Client) PeekPreparer(ctx context.Context, accountName, queueName string, numberOfMessages int) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + } + + queryParameters := map[string]interface{}{ + "numofmessages": autorest.Encode("query", numberOfMessages), + "peekonly": autorest.Encode("query", true), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}/messages", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PeekSender sends the Peek request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PeekSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PeekResponder handles the response to the Peek request. The method always +// closes the http.Response Body. +func (client Client) PeekResponder(resp *http.Response) (result QueueMessagesListResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + autorest.ByUnmarshallingXML(&result), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/queue/messages/put.go b/storage/2018-11-09/queue/messages/put.go new file mode 100644 index 0000000..612b4a1 --- /dev/null +++ b/storage/2018-11-09/queue/messages/put.go @@ -0,0 +1,120 @@ +package messages + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type PutInput struct { + // A message must be in a format that can be included in an XML request with UTF-8 encoding. + // The encoded message can be up to 64 KB in size. + Message string + + // The maximum time-to-live can be any positive number, + // as well as -1 indicating that the message does not expire. + // If this parameter is omitted, the default time-to-live is 7 days. + MessageTtl *int + + // Specifies the new visibility timeout value, in seconds, relative to server time. + // The new value must be larger than or equal to 0, and cannot be larger than 7 days. + // The visibility timeout of a message cannot be set to a value later than the expiry time. + // visibilitytimeout should be set to a value smaller than the time-to-live value. + // If not specified, the default value is 0. + VisibilityTimeout *int +} + +// Put adds a new message to the back of the message queue +func (client Client) Put(ctx context.Context, accountName, queueName string, input PutInput) (result QueueMessagesListResult, err error) { + if accountName == "" { + return result, validation.NewError("messages.Client", "Put", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("messages.Client", "Put", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("messages.Client", "Put", "`queueName` must be a lower-cased string.") + } + + req, err := client.PutPreparer(ctx, accountName, queueName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Put", nil, "Failure preparing request") + return + } + + resp, err := client.PutSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "messages.Client", "Put", resp, "Failure sending request") + return + } + + result, err = client.PutResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Put", resp, "Failure responding to request") + return + } + + return +} + +// PutPreparer prepares the Put request. +func (client Client) PutPreparer(ctx context.Context, accountName, queueName string, input PutInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + } + + queryParameters := map[string]interface{}{} + + if input.MessageTtl != nil { + queryParameters["messagettl"] = autorest.Encode("path", *input.MessageTtl) + } + + if input.VisibilityTimeout != nil { + queryParameters["visibilitytimeout"] = autorest.Encode("path", *input.VisibilityTimeout) + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + body := QueueMessage{ + MessageText: input.Message, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPost(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}/messages", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithXML(body), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// PutSender sends the Put request. The method will close the +// http.Response Body if it receives an error. +func (client Client) PutSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// PutResponder handles the response to the Put request. The method always +// closes the http.Response Body. +func (client Client) PutResponder(resp *http.Response) (result QueueMessagesListResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + autorest.ByUnmarshallingXML(&result), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/queue/messages/resource_id.go b/storage/2018-11-09/queue/messages/resource_id.go new file mode 100644 index 0000000..7ece98a --- /dev/null +++ b/storage/2018-11-09/queue/messages/resource_id.go @@ -0,0 +1,56 @@ +package messages + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given Message within a Queue +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, queueName, messageID string) string { + domain := endpoints.GetQueueEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s/messages/%s", domain, queueName, messageID) +} + +type ResourceID struct { + AccountName string + QueueName string + MessageID string +} + +// ParseResourceID parses the specified Resource ID and returns an object +// which can be used to interact with the Message within a Queue +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://account1.queue.core.chinacloudapi.cn/queue1/messages/message1 + + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + path := strings.TrimPrefix(uri.Path, "/") + segments := strings.Split(path, "/") + if len(segments) != 3 { + return nil, fmt.Errorf("Expected the path to contain 3 segments but got %d", len(segments)) + } + + queueName := segments[0] + messageID := segments[2] + return &ResourceID{ + AccountName: *accountName, + MessageID: messageID, + QueueName: queueName, + }, nil +} diff --git a/storage/2018-11-09/queue/messages/resource_id_test.go b/storage/2018-11-09/queue/messages/resource_id_test.go new file mode 100644 index 0000000..5053279 --- /dev/null +++ b/storage/2018-11-09/queue/messages/resource_id_test.go @@ -0,0 +1,81 @@ +package messages + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.queue.core.chinacloudapi.cn/queue1/messages/message1", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.queue.core.cloudapi.de/queue1/messages/message1", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.queue.core.windows.net/queue1/messages/message1", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.queue.core.usgovcloudapi.net/queue1/messages/message1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "queue1", "message1") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.queue.core.chinacloudapi.cn/queue1/messages/message1", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.queue.core.cloudapi.de/queue1/messages/message1", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.queue.core.windows.net/queue1/messages/message1", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.queue.core.usgovcloudapi.net/queue1/messages/message1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.QueueName != "queue1" { + t.Fatalf("Expected Queue Name to be `queue1` but got %q", actual.QueueName) + } + if actual.MessageID != "message1" { + t.Fatalf("Expected Message ID to be `message1` but got %q", actual.MessageID) + } + } +} diff --git a/storage/2018-11-09/queue/messages/update.go b/storage/2018-11-09/queue/messages/update.go new file mode 100644 index 0000000..fb10fad --- /dev/null +++ b/storage/2018-11-09/queue/messages/update.go @@ -0,0 +1,115 @@ +package messages + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type UpdateInput struct { + // A message must be in a format that can be included in an XML request with UTF-8 encoding. + // The encoded message can be up to 64 KB in size. + Message string + + // Specifies the valid pop receipt value required to modify this message. + PopReceipt string + + // Specifies the new visibility timeout value, in seconds, relative to server time. + // The new value must be larger than or equal to 0, and cannot be larger than 7 days. + // The visibility timeout of a message cannot be set to a value later than the expiry time. + // A message can be updated until it has been deleted or has expired. + VisibilityTimeout int +} + +// Update updates an existing message based on it's Pop Receipt +func (client Client) Update(ctx context.Context, accountName, queueName string, messageID string, input UpdateInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("messages.Client", "Update", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("messages.Client", "Update", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("messages.Client", "Update", "`queueName` must be a lower-cased string.") + } + if input.PopReceipt == "" { + return result, validation.NewError("messages.Client", "Update", "`input.PopReceipt` cannot be an empty string.") + } + + req, err := client.UpdatePreparer(ctx, accountName, queueName, messageID, input) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Update", nil, "Failure preparing request") + return + } + + resp, err := client.UpdateSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "messages.Client", "Update", resp, "Failure sending request") + return + } + + result, err = client.UpdateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "messages.Client", "Update", resp, "Failure responding to request") + return + } + + return +} + +// UpdatePreparer prepares the Update request. +func (client Client) UpdatePreparer(ctx context.Context, accountName, queueName string, messageID string, input UpdateInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + "messageID": autorest.Encode("path", messageID), + } + + queryParameters := map[string]interface{}{ + "popreceipt": autorest.Encode("query", input.PopReceipt), + "visibilitytimeout": autorest.Encode("query", input.VisibilityTimeout), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + body := QueueMessage{ + MessageText: input.Message, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}/messages/{messageID}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithXML(body), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// UpdateSender sends the Update request. The method will close the +// http.Response Body if it receives an error. +func (client Client) UpdateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// UpdateResponder handles the response to the Update request. The method always +// closes the http.Response Body. +func (client Client) UpdateResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/queue/messages/version.go b/storage/2018-11-09/queue/messages/version.go new file mode 100644 index 0000000..87c2420 --- /dev/null +++ b/storage/2018-11-09/queue/messages/version.go @@ -0,0 +1,14 @@ +package messages + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2018-11-09" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2018-11-09/queue/queues/README.md b/storage/2018-11-09/queue/queues/README.md new file mode 100644 index 0000000..1769bfa --- /dev/null +++ b/storage/2018-11-09/queue/queues/README.md @@ -0,0 +1,43 @@ +## Queue Storage Queues SDK for API version 2018-11-09 + +This package allows you to interact with the Queues Queue Storage API + +### Supported Authorizers + +* Azure Active Directory (for the Resource Endpoint `https://storage.azure.com`) +* SharedKeyLite (Blob, File & Queue) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/queue/queues" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + queueName := "myqueue" + + storageAuth := autorest.NewSharedKeyLiteAuthorizer(accountName, storageAccountKey) + queuesClient := queues.New() + queuesClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + metadata := map[string]string{ + "hello": "world", + } + if _, err := queuesClient.Create(ctx, accountName, queueName, metadata); err != nil { + return fmt.Errorf("Error creating Queue: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2018-11-09/queue/queues/client.go b/storage/2018-11-09/queue/queues/client.go new file mode 100644 index 0000000..2f80085 --- /dev/null +++ b/storage/2018-11-09/queue/queues/client.go @@ -0,0 +1,25 @@ +package queues + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for Queue Storage Shares. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithEnvironment creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2018-11-09/queue/queues/create.go b/storage/2018-11-09/queue/queues/create.go new file mode 100644 index 0000000..f18910a --- /dev/null +++ b/storage/2018-11-09/queue/queues/create.go @@ -0,0 +1,92 @@ +package queues + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// Create creates the specified Queue within the specified Storage Account +func (client Client) Create(ctx context.Context, accountName, queueName string, metaData map[string]string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("queues.Client", "Create", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("queues.Client", "Create", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("queues.Client", "Create", "`queueName` must be a lower-cased string.") + } + if err := metadata.Validate(metaData); err != nil { + return result, validation.NewError("queues.Client", "Create", fmt.Sprintf("`metadata` is not valid: %s.", err)) + } + + req, err := client.CreatePreparer(ctx, accountName, queueName, metaData) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "Create", nil, "Failure preparing request") + return + } + + resp, err := client.CreateSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "queues.Client", "Create", resp, "Failure sending request") + return + } + + result, err = client.CreateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "Create", resp, "Failure responding to request") + return + } + + return +} + +// CreatePreparer prepares the Create request. +func (client Client) CreatePreparer(ctx context.Context, accountName string, queueName string, metaData map[string]string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = metadata.SetIntoHeaders(headers, metaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CreateSender sends the Create request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CreateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CreateResponder handles the response to the Create request. The method always +// closes the http.Response Body. +func (client Client) CreateResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusCreated), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/queue/queues/delete.go b/storage/2018-11-09/queue/queues/delete.go new file mode 100644 index 0000000..5f70595 --- /dev/null +++ b/storage/2018-11-09/queue/queues/delete.go @@ -0,0 +1,85 @@ +package queues + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Delete deletes the specified Queue within the specified Storage Account +func (client Client) Delete(ctx context.Context, accountName, queueName string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("queues.Client", "Delete", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("queues.Client", "Delete", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("queues.Client", "Delete", "`queueName` must be a lower-cased string.") + } + + req, err := client.DeletePreparer(ctx, accountName, queueName) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "queues.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName string, queueName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/queue/queues/lifecycle_test.go b/storage/2018-11-09/queue/queues/lifecycle_test.go new file mode 100644 index 0000000..ff720f6 --- /dev/null +++ b/storage/2018-11-09/queue/queues/lifecycle_test.go @@ -0,0 +1,155 @@ +package queues + +import ( + "context" + "fmt" + "log" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestQueuesLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + queueName := fmt.Sprintf("queue-%d", testhelpers.RandomInt()) + + _, err = client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + queuesClient := NewWithEnvironment(client.Environment) + queuesClient.Client = client.PrepareWithStorageResourceManagerAuth(queuesClient.Client) + + // first let's test an empty container + _, err = queuesClient.Create(ctx, accountName, queueName, map[string]string{}) + if err != nil { + t.Fatal(fmt.Errorf("Error creating: %s", err)) + } + + // then let's retrieve it to ensure there's no metadata.. + resp, err := queuesClient.GetMetaData(ctx, accountName, queueName) + if err != nil { + t.Fatalf("Error retrieving MetaData: %s", err) + } + if len(resp.MetaData) != 0 { + t.Fatalf("Expected no MetaData but got: %s", err) + } + + // then let's add some.. + updatedMetaData := map[string]string{ + "band": "panic", + "boots": "the-overpass", + } + _, err = queuesClient.SetMetaData(ctx, accountName, queueName, updatedMetaData) + if err != nil { + t.Fatalf("Error setting MetaData: %s", err) + } + + resp, err = queuesClient.GetMetaData(ctx, accountName, queueName) + if err != nil { + t.Fatalf("Error re-retrieving MetaData: %s", err) + } + + if len(resp.MetaData) != 2 { + t.Fatalf("Expected metadata to have 2 items but got: %s", resp.MetaData) + } + if resp.MetaData["band"] != "panic" { + t.Fatalf("Expected `band` to be `panic` but got: %s", resp.MetaData["band"]) + } + if resp.MetaData["boots"] != "the-overpass" { + t.Fatalf("Expected `boots` to be `the-overpass` but got: %s", resp.MetaData["boots"]) + } + + // and woo let's remove it again + _, err = queuesClient.SetMetaData(ctx, accountName, queueName, map[string]string{}) + if err != nil { + t.Fatalf("Error setting MetaData: %s", err) + } + + resp, err = queuesClient.GetMetaData(ctx, accountName, queueName) + if err != nil { + t.Fatalf("Error retrieving MetaData: %s", err) + } + if len(resp.MetaData) != 0 { + t.Fatalf("Expected no MetaData but got: %s", err) + } + + // set some properties + props := StorageServiceProperties{ + Logging: &LoggingConfig{ + Version: "1.0", + Delete: true, + Read: true, + Write: true, + RetentionPolicy: RetentionPolicy{ + Enabled: true, + Days: 7, + }, + }, + Cors: &Cors{ + CorsRule: CorsRule{ + AllowedMethods: "GET,PUT", + AllowedOrigins: "http://www.example.com", + ExposedHeaders: "x-tempo-*", + AllowedHeaders: "x-tempo-*", + MaxAgeInSeconds: 500, + }, + }, + HourMetrics: &MetricsConfig{ + Version: "1.0", + Enabled: false, + RetentionPolicy: RetentionPolicy{ + Enabled: true, + Days: 7, + }, + }, + MinuteMetrics: &MetricsConfig{ + Version: "1.0", + Enabled: false, + RetentionPolicy: RetentionPolicy{ + Enabled: true, + Days: 7, + }, + }, + } + _, err = queuesClient.SetServiceProperties(ctx, accountName, props) + if err != nil { + t.Fatalf("SetServiceProperties failed: %s", err) + } + + properties, err := queuesClient.GetServiceProperties(ctx, accountName) + if err != nil { + t.Fatalf("GetServiceProperties failed: %s", err) + } + + if properties.Cors.CorsRule.AllowedMethods != "GET,PUT" { + t.Fatalf("CORS Methods weren't set!") + } + + if properties.HourMetrics.Enabled { + t.Fatalf("HourMetrics were enabled when they shouldn't be!") + } + + if properties.MinuteMetrics.Enabled { + t.Fatalf("MinuteMetrics were enabled when they shouldn't be!") + } + + if !properties.Logging.Write { + t.Fatalf("Logging Write's was not enabled when they should be!") + } + + log.Printf("[DEBUG] Deleting..") + _, err = queuesClient.Delete(ctx, accountName, queueName) + if err != nil { + t.Fatal(fmt.Errorf("Error deleting: %s", err)) + } +} diff --git a/storage/2018-11-09/queue/queues/metadata_get.go b/storage/2018-11-09/queue/queues/metadata_get.go new file mode 100644 index 0000000..9c230b6 --- /dev/null +++ b/storage/2018-11-09/queue/queues/metadata_get.go @@ -0,0 +1,101 @@ +package queues + +import ( + "context" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +type GetMetaDataResult struct { + autorest.Response + + MetaData map[string]string +} + +// GetMetaData returns the metadata for this Queue +func (client Client) GetMetaData(ctx context.Context, accountName, queueName string) (result GetMetaDataResult, err error) { + if accountName == "" { + return result, validation.NewError("queues.Client", "GetMetaData", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("queues.Client", "GetMetaData", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("queues.Client", "GetMetaData", "`queueName` must be a lower-cased string.") + } + + req, err := client.GetMetaDataPreparer(ctx, accountName, queueName) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "GetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.GetMetaDataSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "queues.Client", "GetMetaData", resp, "Failure sending request") + return + } + + result, err = client.GetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "GetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// GetMetaDataPreparer prepares the GetMetaData request. +func (client Client) GetMetaDataPreparer(ctx context.Context, accountName, queueName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("path", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetMetaDataSender sends the GetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetMetaDataResponder handles the response to the GetMetaData request. The method always +// closes the http.Response Body. +func (client Client) GetMetaDataResponder(resp *http.Response) (result GetMetaDataResult, err error) { + if resp != nil { + result.MetaData = metadata.ParseFromHeaders(resp.Header) + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/queue/queues/metadata_set.go b/storage/2018-11-09/queue/queues/metadata_set.go new file mode 100644 index 0000000..51154a5 --- /dev/null +++ b/storage/2018-11-09/queue/queues/metadata_set.go @@ -0,0 +1,97 @@ +package queues + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" + "github.com/tombuildsstuff/giovanni/storage/internal/metadata" +) + +// SetMetaData returns the metadata for this Queue +func (client Client) SetMetaData(ctx context.Context, accountName, queueName string, metaData map[string]string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("queues.Client", "SetMetaData", "`accountName` cannot be an empty string.") + } + if queueName == "" { + return result, validation.NewError("queues.Client", "SetMetaData", "`queueName` cannot be an empty string.") + } + if strings.ToLower(queueName) != queueName { + return result, validation.NewError("queues.Client", "SetMetaData", "`queueName` must be a lower-cased string.") + } + if err := metadata.Validate(metaData); err != nil { + return result, validation.NewError("queues.Client", "SetMetaData", fmt.Sprintf("`metadata` is not valid: %s.", err)) + } + + req, err := client.SetMetaDataPreparer(ctx, accountName, queueName, metaData) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "SetMetaData", nil, "Failure preparing request") + return + } + + resp, err := client.SetMetaDataSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "queues.Client", "SetMetaData", resp, "Failure sending request") + return + } + + result, err = client.SetMetaDataResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "SetMetaData", resp, "Failure responding to request") + return + } + + return +} + +// SetMetaDataPreparer prepares the SetMetaData request. +func (client Client) SetMetaDataPreparer(ctx context.Context, accountName, queueName string, metaData map[string]string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "queueName": autorest.Encode("path", queueName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("path", "metadata"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + headers = metadata.SetIntoHeaders(headers, metaData) + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{queueName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetMetaDataSender sends the SetMetaData request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetMetaDataSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetMetaDataResponder handles the response to the SetMetaData request. The method always +// closes the http.Response Body. +func (client Client) SetMetaDataResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/queue/queues/models.go b/storage/2018-11-09/queue/queues/models.go new file mode 100644 index 0000000..89c2380 --- /dev/null +++ b/storage/2018-11-09/queue/queues/models.go @@ -0,0 +1,42 @@ +package queues + +type StorageServiceProperties struct { + Logging *LoggingConfig `xml:"Logging,omitempty"` + HourMetrics *MetricsConfig `xml:"HourMetrics,omitempty"` + MinuteMetrics *MetricsConfig `xml:"MinuteMetrics,omitempty"` + Cors *Cors `xml:"Cors,omitempty"` +} + +type LoggingConfig struct { + Version string `xml:"Version"` + Delete bool `xml:"Delete"` + Read bool `xml:"Read"` + Write bool `xml:"Write"` + RetentionPolicy RetentionPolicy `xml:"RetentionPolicy"` +} + +type MetricsConfig struct { + Version string `xml:"Version"` + Enabled bool `xml:"Enabled"` + RetentionPolicy RetentionPolicy `xml:"RetentionPolicy"` + + // Element IncludeAPIs is only expected when Metrics is enabled + IncludeAPIs *bool `xml:"IncludeAPIs,omitempty"` +} + +type RetentionPolicy struct { + Enabled bool `xml:"Enabled"` + Days int `xml:"Days"` +} + +type Cors struct { + CorsRule CorsRule `xml:"CorsRule"` +} + +type CorsRule struct { + AllowedOrigins string `xml:"AllowedOrigins"` + AllowedMethods string `xml:"AllowedMethods"` + AllowedHeaders string `xml:"AllowedHeaders` + ExposedHeaders string `xml:"ExposedHeaders"` + MaxAgeInSeconds int `xml:"MaxAgeInSeconds"` +} diff --git a/storage/2018-11-09/queue/queues/properties_get.go b/storage/2018-11-09/queue/queues/properties_get.go new file mode 100644 index 0000000..9d17fb2 --- /dev/null +++ b/storage/2018-11-09/queue/queues/properties_get.go @@ -0,0 +1,85 @@ +package queues + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type StorageServicePropertiesResponse struct { + StorageServiceProperties + autorest.Response +} + +// SetServiceProperties gets the properties for this queue +func (client Client) GetServiceProperties(ctx context.Context, accountName string) (result StorageServicePropertiesResponse, err error) { + if accountName == "" { + return result, validation.NewError("queues.Client", "SetServiceProperties", "`accountName` cannot be an empty string.") + } + + req, err := client.GetServicePropertiesPreparer(ctx, accountName) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "SetServiceProperties", nil, "Failure preparing request") + return + } + + resp, err := client.GetServicePropertiesSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "queues.Client", "SetServiceProperties", resp, "Failure sending request") + return + } + + result, err = client.GetServicePropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "SetServiceProperties", resp, "Failure responding to request") + return + } + + return +} + +// GetServicePropertiesPreparer prepares the GetServiceProperties request. +func (client Client) GetServicePropertiesPreparer(ctx context.Context, accountName string) (*http.Request, error) { + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("path", "properties"), + "restype": autorest.Encode("path", "service"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetServicePropertiesSender sends the GetServiceProperties request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetServicePropertiesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetServicePropertiesResponder handles the response to the GetServiceProperties request. The method always +// closes the http.Response Body. +func (client Client) GetServicePropertiesResponder(resp *http.Response) (result StorageServicePropertiesResponse, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/queue/queues/properties_set.go b/storage/2018-11-09/queue/queues/properties_set.go new file mode 100644 index 0000000..d6f6392 --- /dev/null +++ b/storage/2018-11-09/queue/queues/properties_set.go @@ -0,0 +1,80 @@ +package queues + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// SetServiceProperties sets the properties for this queue +func (client Client) SetServiceProperties(ctx context.Context, accountName string, properties StorageServiceProperties) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("queues.Client", "SetServiceProperties", "`accountName` cannot be an empty string.") + } + + req, err := client.SetServicePropertiesPreparer(ctx, accountName, properties) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "SetServiceProperties", nil, "Failure preparing request") + return + } + + resp, err := client.SetServicePropertiesSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "queues.Client", "SetServiceProperties", resp, "Failure sending request") + return + } + + result, err = client.SetServicePropertiesResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "queues.Client", "SetServiceProperties", resp, "Failure responding to request") + return + } + + return +} + +// SetServicePropertiesPreparer prepares the SetServiceProperties request. +func (client Client) SetServicePropertiesPreparer(ctx context.Context, accountName string, properties StorageServiceProperties) (*http.Request, error) { + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("path", "properties"), + "restype": autorest.Encode("path", "service"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetQueueEndpoint(client.BaseURI, accountName)), + autorest.WithQueryParameters(queryParameters), + autorest.WithXML(properties), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetServicePropertiesSender sends the SetServiceProperties request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetServicePropertiesSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetServicePropertiesResponder handles the response to the SetServiceProperties request. The method always +// closes the http.Response Body. +func (client Client) SetServicePropertiesResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusAccepted), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/queue/queues/resource_id.go b/storage/2018-11-09/queue/queues/resource_id.go new file mode 100644 index 0000000..ee28b8b --- /dev/null +++ b/storage/2018-11-09/queue/queues/resource_id.go @@ -0,0 +1,46 @@ +package queues + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given Queue +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, queueName string) string { + domain := endpoints.GetQueueEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s", domain, queueName) +} + +type ResourceID struct { + AccountName string + QueueName string +} + +// ParseResourceID parses the Resource ID and returns an Object which +// can be used to interact with a Queue within a Storage Account +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://foo.queue.core.windows.net/Bar + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + queueName := strings.TrimPrefix(uri.Path, "/") + return &ResourceID{ + AccountName: *accountName, + QueueName: queueName, + }, nil +} diff --git a/storage/2018-11-09/queue/queues/resource_id_test.go b/storage/2018-11-09/queue/queues/resource_id_test.go new file mode 100644 index 0000000..89323d7 --- /dev/null +++ b/storage/2018-11-09/queue/queues/resource_id_test.go @@ -0,0 +1,79 @@ +package queues + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.queue.core.chinacloudapi.cn/queue1", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.queue.core.cloudapi.de/queue1", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.queue.core.windows.net/queue1", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.queue.core.usgovcloudapi.net/queue1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "queue1") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.queue.core.chinacloudapi.cn/queue1", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.queue.core.cloudapi.de/queue1", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.queue.core.windows.net/queue1", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.queue.core.usgovcloudapi.net/queue1", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected the account name to be `account1` but got %q", actual.AccountName) + } + + if actual.QueueName != "queue1" { + t.Fatalf("Expected the queue name to be `queue1` but got %q", actual.QueueName) + } + } +} diff --git a/storage/2018-11-09/queue/queues/version.go b/storage/2018-11-09/queue/queues/version.go new file mode 100644 index 0000000..13c7d2f --- /dev/null +++ b/storage/2018-11-09/queue/queues/version.go @@ -0,0 +1,14 @@ +package queues + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2018-11-09" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2018-11-09/table/entities/README.md b/storage/2018-11-09/table/entities/README.md new file mode 100644 index 0000000..06c2cb8 --- /dev/null +++ b/storage/2018-11-09/table/entities/README.md @@ -0,0 +1,48 @@ +## Table Storage Entities SDK for API version 2018-11-09 + +This package allows you to interact with the Entities Table Storage API + +### Supported Authorizers + +* SharedKeyLite (Table) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/table/entities" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + tableName := "mytable" + + storageAuth := autorest.NewSharedKeyLiteTableAuthorizer(accountName, storageAccountKey) + entitiesClient := entities.New() + entitiesClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + input := entities.InsertEntityInput{ + PartitionKey: "abc", + RowKey: "123", + MetaDataLevel: entities.NoMetaData, + Entity: map[string]interface{}{ + "title": "Don't Kill My Vibe", + "artist": "Sigrid", + }, + } + if _, err := entitiesClient.Insert(ctx, accountName, tableName, input); err != nil { + return fmt.Errorf("Error creating Entity: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2018-11-09/table/entities/client.go b/storage/2018-11-09/table/entities/client.go new file mode 100644 index 0000000..17e9d75 --- /dev/null +++ b/storage/2018-11-09/table/entities/client.go @@ -0,0 +1,25 @@ +package entities + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for Table Storage Shares. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithEnvironment creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2018-11-09/table/entities/delete.go b/storage/2018-11-09/table/entities/delete.go new file mode 100644 index 0000000..83e9188 --- /dev/null +++ b/storage/2018-11-09/table/entities/delete.go @@ -0,0 +1,99 @@ +package entities + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type DeleteEntityInput struct { + // When inserting an entity into a table, you must specify values for the PartitionKey and RowKey system properties. + // Together, these properties form the primary key and must be unique within the table. + // Both the PartitionKey and RowKey values must be string values; each key value may be up to 64 KB in size. + // If you are using an integer value for the key value, you should convert the integer to a fixed-width string, + // because they are canonically sorted. For example, you should convert the value 1 to 0000001 to ensure proper sorting. + RowKey string + PartitionKey string +} + +// Delete deletes an existing entity in a table. +func (client Client) Delete(ctx context.Context, accountName, tableName string, input DeleteEntityInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("entities.Client", "Delete", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("entities.Client", "Delete", "`tableName` cannot be an empty string.") + } + if input.PartitionKey == "" { + return result, validation.NewError("entities.Client", "Delete", "`input.PartitionKey` cannot be an empty string.") + } + if input.RowKey == "" { + return result, validation.NewError("entities.Client", "Delete", "`input.RowKey` cannot be an empty string.") + } + + req, err := client.DeletePreparer(ctx, accountName, tableName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "entities.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName, tableName string, input DeleteEntityInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + "partitionKey": autorest.Encode("path", input.PartitionKey), + "rowKey": autorest.Encode("path", input.RowKey), + } + + headers := map[string]interface{}{ + // TODO: support for eTags + "If-Match": "*", + } + + preparer := autorest.CreatePreparer( + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}(PartitionKey='{partitionKey}', RowKey='{rowKey}')", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/table/entities/get.go b/storage/2018-11-09/table/entities/get.go new file mode 100644 index 0000000..bdb4018 --- /dev/null +++ b/storage/2018-11-09/table/entities/get.go @@ -0,0 +1,108 @@ +package entities + +import ( + "context" + "fmt" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetEntityInput struct { + PartitionKey string + RowKey string + + // The Level of MetaData which should be returned + MetaDataLevel MetaDataLevel +} + +type GetEntityResult struct { + autorest.Response + + Entity map[string]interface{} +} + +// Get queries entities in a table and includes the $filter and $select options. +func (client Client) Get(ctx context.Context, accountName, tableName string, input GetEntityInput) (result GetEntityResult, err error) { + if accountName == "" { + return result, validation.NewError("entities.Client", "Get", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("entities.Client", "Get", "`tableName` cannot be an empty string.") + } + if input.PartitionKey == "" { + return result, validation.NewError("entities.Client", "Get", "`input.PartitionKey` cannot be an empty string.") + } + if input.RowKey == "" { + return result, validation.NewError("entities.Client", "Get", "`input.RowKey` cannot be an empty string.") + } + + req, err := client.GetPreparer(ctx, accountName, tableName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Get", nil, "Failure preparing request") + return + } + + resp, err := client.GetSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "entities.Client", "Get", resp, "Failure sending request") + return + } + + result, err = client.GetResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Get", resp, "Failure responding to request") + return + } + + return +} + +// GetPreparer prepares the Get request. +func (client Client) GetPreparer(ctx context.Context, accountName, tableName string, input GetEntityInput) (*http.Request, error) { + + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + "partitionKey": autorest.Encode("path", input.PartitionKey), + "rowKey": autorest.Encode("path", input.RowKey), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "Accept": fmt.Sprintf("application/json;odata=%s", input.MetaDataLevel), + "DataServiceVersion": "3.0;NetFx", + "MaxDataServiceVersion": "3.0;NetFx", + } + + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}(PartitionKey='{partitionKey}',RowKey='{rowKey}')", pathParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetSender sends the Get request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetResponder handles the response to the Get request. The method always +// closes the http.Response Body. +func (client Client) GetResponder(resp *http.Response) (result GetEntityResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingJSON(&result.Entity), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/table/entities/insert.go b/storage/2018-11-09/table/entities/insert.go new file mode 100644 index 0000000..92b05ce --- /dev/null +++ b/storage/2018-11-09/table/entities/insert.go @@ -0,0 +1,112 @@ +package entities + +import ( + "context" + "fmt" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type InsertEntityInput struct { + // The level of MetaData provided for this Entity + MetaDataLevel MetaDataLevel + + // The Entity which should be inserted, by default all values are strings + // To explicitly type a property, specify the appropriate OData data type by setting + // the m:type attribute within the property definition + Entity map[string]interface{} + + // When inserting an entity into a table, you must specify values for the PartitionKey and RowKey system properties. + // Together, these properties form the primary key and must be unique within the table. + // Both the PartitionKey and RowKey values must be string values; each key value may be up to 64 KB in size. + // If you are using an integer value for the key value, you should convert the integer to a fixed-width string, + // because they are canonically sorted. For example, you should convert the value 1 to 0000001 to ensure proper sorting. + RowKey string + PartitionKey string +} + +// Insert inserts a new entity into a table. +func (client Client) Insert(ctx context.Context, accountName, tableName string, input InsertEntityInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("entities.Client", "Insert", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("entities.Client", "Insert", "`tableName` cannot be an empty string.") + } + if input.PartitionKey == "" { + return result, validation.NewError("entities.Client", "Insert", "`input.PartitionKey` cannot be an empty string.") + } + if input.RowKey == "" { + return result, validation.NewError("entities.Client", "Insert", "`input.RowKey` cannot be an empty string.") + } + + req, err := client.InsertPreparer(ctx, accountName, tableName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Insert", nil, "Failure preparing request") + return + } + + resp, err := client.InsertSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "entities.Client", "Insert", resp, "Failure sending request") + return + } + + result, err = client.InsertResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Insert", resp, "Failure responding to request") + return + } + + return +} + +// InsertPreparer prepares the Insert request. +func (client Client) InsertPreparer(ctx context.Context, accountName, tableName string, input InsertEntityInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "Accept": fmt.Sprintf("application/json;odata=%s", input.MetaDataLevel), + "Prefer": "return-no-content", + } + + input.Entity["PartitionKey"] = input.PartitionKey + input.Entity["RowKey"] = input.RowKey + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/json"), + autorest.AsPost(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}", pathParameters), + autorest.WithJSON(input.Entity), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// InsertSender sends the Insert request. The method will close the +// http.Response Body if it receives an error. +func (client Client) InsertSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// InsertResponder handles the response to the Insert request. The method always +// closes the http.Response Body. +func (client Client) InsertResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/table/entities/insert_or_merge.go b/storage/2018-11-09/table/entities/insert_or_merge.go new file mode 100644 index 0000000..1fb4ed3 --- /dev/null +++ b/storage/2018-11-09/table/entities/insert_or_merge.go @@ -0,0 +1,108 @@ +package entities + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type InsertOrMergeEntityInput struct { + // The Entity which should be inserted, by default all values are strings + // To explicitly type a property, specify the appropriate OData data type by setting + // the m:type attribute within the property definition + Entity map[string]interface{} + + // When inserting an entity into a table, you must specify values for the PartitionKey and RowKey system properties. + // Together, these properties form the primary key and must be unique within the table. + // Both the PartitionKey and RowKey values must be string values; each key value may be up to 64 KB in size. + // If you are using an integer value for the key value, you should convert the integer to a fixed-width string, + // because they are canonically sorted. For example, you should convert the value 1 to 0000001 to ensure proper sorting. + RowKey string + PartitionKey string +} + +// InsertOrMerge updates an existing entity or inserts a new entity if it does not exist in the table. +// Because this operation can insert or update an entity, it is also known as an upsert operation. +func (client Client) InsertOrMerge(ctx context.Context, accountName, tableName string, input InsertOrMergeEntityInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("entities.Client", "InsertOrMerge", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("entities.Client", "InsertOrMerge", "`tableName` cannot be an empty string.") + } + if input.PartitionKey == "" { + return result, validation.NewError("entities.Client", "InsertOrMerge", "`input.PartitionKey` cannot be an empty string.") + } + if input.RowKey == "" { + return result, validation.NewError("entities.Client", "InsertOrMerge", "`input.RowKey` cannot be an empty string.") + } + + req, err := client.InsertOrMergePreparer(ctx, accountName, tableName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "InsertOrMerge", nil, "Failure preparing request") + return + } + + resp, err := client.InsertOrMergeSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "entities.Client", "InsertOrMerge", resp, "Failure sending request") + return + } + + result, err = client.InsertOrMergeResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "InsertOrMerge", resp, "Failure responding to request") + return + } + + return +} + +// InsertOrMergePreparer prepares the InsertOrMerge request. +func (client Client) InsertOrMergePreparer(ctx context.Context, accountName, tableName string, input InsertOrMergeEntityInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + "partitionKey": autorest.Encode("path", input.PartitionKey), + "rowKey": autorest.Encode("path", input.RowKey), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "Accept": "application/json", + "Prefer": "return-no-content", + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/json"), + autorest.AsMerge(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}(PartitionKey='{partitionKey}', RowKey='{rowKey}')", pathParameters), + autorest.WithJSON(input.Entity), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// InsertOrMergeSender sends the InsertOrMerge request. The method will close the +// http.Response Body if it receives an error. +func (client Client) InsertOrMergeSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// InsertOrMergeResponder handles the response to the InsertOrMerge request. The method always +// closes the http.Response Body. +func (client Client) InsertOrMergeResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/table/entities/insert_or_replace.go b/storage/2018-11-09/table/entities/insert_or_replace.go new file mode 100644 index 0000000..036ba5d --- /dev/null +++ b/storage/2018-11-09/table/entities/insert_or_replace.go @@ -0,0 +1,108 @@ +package entities + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type InsertOrReplaceEntityInput struct { + // The Entity which should be inserted, by default all values are strings + // To explicitly type a property, specify the appropriate OData data type by setting + // the m:type attribute within the property definition + Entity map[string]interface{} + + // When inserting an entity into a table, you must specify values for the PartitionKey and RowKey system properties. + // Together, these properties form the primary key and must be unique within the table. + // Both the PartitionKey and RowKey values must be string values; each key value may be up to 64 KB in size. + // If you are using an integer value for the key value, you should convert the integer to a fixed-width string, + // because they are canonically sorted. For example, you should convert the value 1 to 0000001 to ensure proper sorting. + RowKey string + PartitionKey string +} + +// InsertOrReplace replaces an existing entity or inserts a new entity if it does not exist in the table. +// Because this operation can insert or update an entity, it is also known as an upsert operation. +func (client Client) InsertOrReplace(ctx context.Context, accountName, tableName string, input InsertOrReplaceEntityInput) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("entities.Client", "InsertOrReplace", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("entities.Client", "InsertOrReplace", "`tableName` cannot be an empty string.") + } + if input.PartitionKey == "" { + return result, validation.NewError("entities.Client", "InsertOrReplace", "`input.PartitionKey` cannot be an empty string.") + } + if input.RowKey == "" { + return result, validation.NewError("entities.Client", "InsertOrReplace", "`input.RowKey` cannot be an empty string.") + } + + req, err := client.InsertOrReplacePreparer(ctx, accountName, tableName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "InsertOrReplace", nil, "Failure preparing request") + return + } + + resp, err := client.InsertOrReplaceSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "entities.Client", "InsertOrReplace", resp, "Failure sending request") + return + } + + result, err = client.InsertOrReplaceResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "InsertOrReplace", resp, "Failure responding to request") + return + } + + return +} + +// InsertOrReplacePreparer prepares the InsertOrReplace request. +func (client Client) InsertOrReplacePreparer(ctx context.Context, accountName, tableName string, input InsertOrReplaceEntityInput) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + "partitionKey": autorest.Encode("path", input.PartitionKey), + "rowKey": autorest.Encode("path", input.RowKey), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "Accept": "application/json", + "Prefer": "return-no-content", + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/json"), + autorest.AsMerge(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}(PartitionKey='{partitionKey}', RowKey='{rowKey}')", pathParameters), + autorest.WithJSON(input.Entity), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// InsertOrReplaceSender sends the InsertOrReplace request. The method will close the +// http.Response Body if it receives an error. +func (client Client) InsertOrReplaceSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// InsertOrReplaceResponder handles the response to the InsertOrReplace request. The method always +// closes the http.Response Body. +func (client Client) InsertOrReplaceResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/table/entities/lifecycle_test.go b/storage/2018-11-09/table/entities/lifecycle_test.go new file mode 100644 index 0000000..237aa89 --- /dev/null +++ b/storage/2018-11-09/table/entities/lifecycle_test.go @@ -0,0 +1,135 @@ +package entities + +import ( + "context" + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/table/tables" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestEntitiesLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + tableName := fmt.Sprintf("table%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteTableAuthorizer(accountName, testData.StorageAccountKey) + tablesClient := tables.NewWithEnvironment(client.Environment) + tablesClient.Client = client.PrepareWithAuthorizer(tablesClient.Client, storageAuth) + + t.Logf("[DEBUG] Creating Table..") + if _, err := tablesClient.Create(ctx, accountName, tableName); err != nil { + t.Fatalf("Error creating Table %q: %s", tableName, err) + } + defer tablesClient.Delete(ctx, accountName, tableName) + + entitiesClient := NewWithEnvironment(client.Environment) + entitiesClient.Client = client.PrepareWithAuthorizer(entitiesClient.Client, storageAuth) + + partitionKey := "hello" + rowKey := "there" + + t.Logf("[DEBUG] Inserting..") + insertInput := InsertEntityInput{ + MetaDataLevel: NoMetaData, + PartitionKey: partitionKey, + RowKey: rowKey, + Entity: map[string]interface{}{ + "hello": "world", + }, + } + if _, err := entitiesClient.Insert(ctx, accountName, tableName, insertInput); err != nil { + t.Logf("Error retrieving: %s", err) + } + + t.Logf("[DEBUG] Insert or Merging..") + insertOrMergeInput := InsertOrMergeEntityInput{ + PartitionKey: partitionKey, + RowKey: rowKey, + Entity: map[string]interface{}{ + "hello": "ther88e", + }, + } + if _, err := entitiesClient.InsertOrMerge(ctx, accountName, tableName, insertOrMergeInput); err != nil { + t.Logf("Error insert/merging: %s", err) + } + + t.Logf("[DEBUG] Insert or Replacing..") + insertOrReplaceInput := InsertOrReplaceEntityInput{ + PartitionKey: partitionKey, + RowKey: rowKey, + Entity: map[string]interface{}{ + "hello": "pandas", + }, + } + if _, err := entitiesClient.InsertOrReplace(ctx, accountName, tableName, insertOrReplaceInput); err != nil { + t.Logf("Error inserting/replacing: %s", err) + } + + t.Logf("[DEBUG] Querying..") + queryInput := QueryEntitiesInput{ + MetaDataLevel: NoMetaData, + } + results, err := entitiesClient.Query(ctx, accountName, tableName, queryInput) + if err != nil { + t.Logf("Error querying: %s", err) + } + + if len(results.Entities) != 1 { + t.Fatalf("Expected 1 item but got %d", len(results.Entities)) + } + + for _, v := range results.Entities { + thisPartitionKey := v["PartitionKey"].(string) + thisRowKey := v["RowKey"].(string) + if partitionKey != thisPartitionKey { + t.Fatalf("Expected Partition Key to be %q but got %q", partitionKey, thisPartitionKey) + } + if rowKey != thisRowKey { + t.Fatalf("Expected Partition Key to be %q but got %q", rowKey, thisRowKey) + } + } + + t.Logf("[DEBUG] Retrieving..") + getInput := GetEntityInput{ + MetaDataLevel: MinimalMetaData, + PartitionKey: partitionKey, + RowKey: rowKey, + } + getResults, err := entitiesClient.Get(ctx, accountName, tableName, getInput) + if err != nil { + t.Logf("Error querying: %s", err) + } + + partitionKey2 := getResults.Entity["PartitionKey"].(string) + rowKey2 := getResults.Entity["RowKey"].(string) + if partitionKey2 != partitionKey { + t.Fatalf("Expected Partition Key to be %q but got %q", partitionKey, partitionKey2) + } + if rowKey2 != rowKey { + t.Fatalf("Expected Row Key to be %q but got %q", rowKey, rowKey2) + } + + t.Logf("[DEBUG] Deleting..") + deleteInput := DeleteEntityInput{ + PartitionKey: partitionKey, + RowKey: rowKey, + } + if _, err := entitiesClient.Delete(ctx, accountName, tableName, deleteInput); err != nil { + t.Logf("Error deleting: %s", err) + } +} diff --git a/storage/2018-11-09/table/entities/models.go b/storage/2018-11-09/table/entities/models.go new file mode 100644 index 0000000..e3c6ccc --- /dev/null +++ b/storage/2018-11-09/table/entities/models.go @@ -0,0 +1,9 @@ +package entities + +type MetaDataLevel string + +var ( + NoMetaData MetaDataLevel = "nometadata" + MinimalMetaData MetaDataLevel = "minimalmetadata" + FullMetaData MetaDataLevel = "fullmetadata" +) diff --git a/storage/2018-11-09/table/entities/query.go b/storage/2018-11-09/table/entities/query.go new file mode 100644 index 0000000..a768b83 --- /dev/null +++ b/storage/2018-11-09/table/entities/query.go @@ -0,0 +1,155 @@ +package entities + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type QueryEntitiesInput struct { + // An optional OData filter + Filter *string + + // An optional comma-separated + PropertyNamesToSelect *[]string + + PartitionKey string + RowKey string + + // The Level of MetaData which should be returned + MetaDataLevel MetaDataLevel + + // The Next Partition Key used to load data from a previous point + NextPartitionKey *string + + // The Next Row Key used to load data from a previous point + NextRowKey *string +} + +type QueryEntitiesResult struct { + autorest.Response + + NextPartitionKey string + NextRowKey string + + MetaData string `json:"odata.metadata,omitempty"` + Entities []map[string]interface{} `json:"value"` +} + +// Query queries entities in a table and includes the $filter and $select options. +func (client Client) Query(ctx context.Context, accountName, tableName string, input QueryEntitiesInput) (result QueryEntitiesResult, err error) { + if accountName == "" { + return result, validation.NewError("entities.Client", "Query", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("entities.Client", "Query", "`tableName` cannot be an empty string.") + } + + req, err := client.QueryPreparer(ctx, accountName, tableName, input) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Query", nil, "Failure preparing request") + return + } + + resp, err := client.QuerySender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "entities.Client", "Query", resp, "Failure sending request") + return + } + + result, err = client.QueryResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "entities.Client", "Query", resp, "Failure responding to request") + return + } + + return +} + +// QueryPreparer prepares the Query request. +func (client Client) QueryPreparer(ctx context.Context, accountName, tableName string, input QueryEntitiesInput) (*http.Request, error) { + + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + "additionalParameters": "", + } + + //PartitionKey='',RowKey='' + additionalParams := make([]string, 0) + if input.PartitionKey != "" { + additionalParams = append(additionalParams, fmt.Sprintf("PartitionKey='%s'", input.PartitionKey)) + } + if input.RowKey != "" { + additionalParams = append(additionalParams, fmt.Sprintf("RowKey='%s'", input.RowKey)) + } + if len(additionalParams) > 0 { + pathParameters["additionalParameters"] = autorest.Encode("path", strings.Join(additionalParams, ",")) + } + + queryParameters := map[string]interface{}{} + + if input.Filter != nil { + queryParameters["filter"] = autorest.Encode("query", input.Filter) + } + + if input.PropertyNamesToSelect != nil { + queryParameters["$select"] = autorest.Encode("query", strings.Join(*input.PropertyNamesToSelect, ",")) + } + + if input.NextPartitionKey != nil { + queryParameters["NextPartitionKey"] = *input.NextPartitionKey + } + + if input.NextRowKey != nil { + queryParameters["NextRowKey"] = *input.NextRowKey + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "Accept": fmt.Sprintf("application/json;odata=%s", input.MetaDataLevel), + "DataServiceVersion": "3.0;NetFx", + "MaxDataServiceVersion": "3.0;NetFx", + } + + // GET /myaccount/Customers()?$filter=(Rating%20ge%203)%20and%20(Rating%20le%206)&$select=PartitionKey,RowKey,Address,CustomerSince + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}({additionalParameters})", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// QuerySender sends the Query request. The method will close the +// http.Response Body if it receives an error. +func (client Client) QuerySender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// QueryResponder handles the response to the Query request. The method always +// closes the http.Response Body. +func (client Client) QueryResponder(resp *http.Response) (result QueryEntitiesResult, err error) { + if resp != nil && resp.Header != nil { + result.NextPartitionKey = resp.Header.Get("x-ms-continuation-NextPartitionKey") + result.NextRowKey = resp.Header.Get("x-ms-continuation-NextRowKey") + } + + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingJSON(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/table/entities/resource_id.go b/storage/2018-11-09/table/entities/resource_id.go new file mode 100644 index 0000000..59366a2 --- /dev/null +++ b/storage/2018-11-09/table/entities/resource_id.go @@ -0,0 +1,91 @@ +package entities + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given Entity +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, tableName, partitionKey, rowKey string) string { + domain := endpoints.GetTableEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/%s(PartitionKey='%s',RowKey='%s')", domain, tableName, partitionKey, rowKey) +} + +type ResourceID struct { + AccountName string + TableName string + PartitionKey string + RowKey string +} + +// ParseResourceID parses the specified Resource ID and returns an object which +// can be used to look up the specified Entity within the specified Table +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://account1.table.core.chinacloudapi.cn/table1(PartitionKey='partition1',RowKey='row1') + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + // assume there a `Table('')` + path := strings.TrimPrefix(uri.Path, "/") + if !strings.Contains(uri.Path, "(") || !strings.HasSuffix(uri.Path, ")") { + return nil, fmt.Errorf("Expected the Table Name to be in the format `tables(PartitionKey='',RowKey='')` but got %q", path) + } + + // NOTE: honestly this could probably be a RegEx, but this seemed like the simplest way to + // allow these two fields to be specified in either order + indexOfBracket := strings.IndexByte(path, '(') + tableName := path[0:indexOfBracket] + + // trim off the brackets + temp := strings.TrimPrefix(path, fmt.Sprintf("%s(", tableName)) + temp = strings.TrimSuffix(temp, ")") + + dictionary := strings.Split(temp, ",") + partitionKey := "" + rowKey := "" + for _, v := range dictionary { + split := strings.Split(v, "=") + if len(split) != 2 { + return nil, fmt.Errorf("Expected 2 segments but got %d for %q", len(split), v) + } + + key := split[0] + value := strings.TrimSuffix(strings.TrimPrefix(split[1], "'"), "'") + if strings.EqualFold(key, "PartitionKey") { + partitionKey = value + } else if strings.EqualFold(key, "RowKey") { + rowKey = value + } else { + return nil, fmt.Errorf("Unexpected Key %q", key) + } + } + + if partitionKey == "" { + return nil, fmt.Errorf("Expected a PartitionKey but didn't get one") + } + if rowKey == "" { + return nil, fmt.Errorf("Expected a RowKey but didn't get one") + } + + return &ResourceID{ + AccountName: *accountName, + TableName: tableName, + PartitionKey: partitionKey, + RowKey: rowKey, + }, nil +} diff --git a/storage/2018-11-09/table/entities/resource_id_test.go b/storage/2018-11-09/table/entities/resource_id_test.go new file mode 100644 index 0000000..e85af79 --- /dev/null +++ b/storage/2018-11-09/table/entities/resource_id_test.go @@ -0,0 +1,84 @@ +package entities + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.table.core.chinacloudapi.cn/table1(PartitionKey='partition1',RowKey='row1')", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.table.core.cloudapi.de/table1(PartitionKey='partition1',RowKey='row1')", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.table.core.windows.net/table1(PartitionKey='partition1',RowKey='row1')", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.table.core.usgovcloudapi.net/table1(PartitionKey='partition1',RowKey='row1')", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "table1", "partition1", "row1") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.table.core.chinacloudapi.cn/table1(PartitionKey='partition1',RowKey='row1')", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.table.core.cloudapi.de/table1(PartitionKey='partition1',RowKey='row1')", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.table.core.windows.net/table1(PartitionKey='partition1',RowKey='row1')", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.table.core.usgovcloudapi.net/table1(PartitionKey='partition1',RowKey='row1')", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.TableName != "table1" { + t.Fatalf("Expected Table Name to be `table1` but got %q", actual.TableName) + } + if actual.PartitionKey != "partition1" { + t.Fatalf("Expected Partition Key to be `partition1` but got %q", actual.PartitionKey) + } + if actual.RowKey != "row1" { + t.Fatalf("Expected Row Key to be `row1` but got %q", actual.RowKey) + } + } +} diff --git a/storage/2018-11-09/table/entities/version.go b/storage/2018-11-09/table/entities/version.go new file mode 100644 index 0000000..0914b24 --- /dev/null +++ b/storage/2018-11-09/table/entities/version.go @@ -0,0 +1,14 @@ +package entities + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2018-11-09" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/2018-11-09/table/tables/README.md b/storage/2018-11-09/table/tables/README.md new file mode 100644 index 0000000..ee05271 --- /dev/null +++ b/storage/2018-11-09/table/tables/README.md @@ -0,0 +1,39 @@ +## Table Storage Tables SDK for API version 2018-11-09 + +This package allows you to interact with the Tables Table Storage API + +### Supported Authorizers + +* SharedKeyLite (Table) + +### Example Usage + +```go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/go-autorest/autorest" + "github.com/tombuildsstuff/giovanni/storage/2018-11-09/table/tables" +) + +func Example() error { + accountName := "storageaccount1" + storageAccountKey := "ABC123...." + tableName := "mytable" + + storageAuth := autorest.NewSharedKeyLiteTableAuthorizer(accountName, storageAccountKey) + tablesClient := tables.New() + tablesClient.Client.Authorizer = storageAuth + + ctx := context.TODO() + if _, err := tablesClient.Insert(ctx, accountName, tableName); err != nil { + return fmt.Errorf("Error creating Table: %s", err) + } + + return nil +} +``` \ No newline at end of file diff --git a/storage/2018-11-09/table/tables/acl_get.go b/storage/2018-11-09/table/tables/acl_get.go new file mode 100644 index 0000000..0ef0000 --- /dev/null +++ b/storage/2018-11-09/table/tables/acl_get.go @@ -0,0 +1,93 @@ +package tables + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetACLResult struct { + autorest.Response + + SignedIdentifiers []SignedIdentifier `xml:"SignedIdentifier"` +} + +// GetACL returns the Access Control List for the specified Table +func (client Client) GetACL(ctx context.Context, accountName, tableName string) (result GetACLResult, err error) { + if accountName == "" { + return result, validation.NewError("tables.Client", "GetACL", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("tables.Client", "GetACL", "`tableName` cannot be an empty string.") + } + + req, err := client.GetACLPreparer(ctx, accountName, tableName) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "GetACL", nil, "Failure preparing request") + return + } + + resp, err := client.GetACLSender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "tables.Client", "GetACL", resp, "Failure sending request") + return + } + + result, err = client.GetACLResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "GetACL", resp, "Failure responding to request") + return + } + + return +} + +// GetACLPreparer prepares the GetACL request. +func (client Client) GetACLPreparer(ctx context.Context, accountName, tableName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "acl"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// GetACLSender sends the GetACL request. The method will close the +// http.Response Body if it receives an error. +func (client Client) GetACLSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// GetACLResponder handles the response to the GetACL request. The method always +// closes the http.Response Body. +func (client Client) GetACLResponder(resp *http.Response) (result GetACLResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingXML(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/table/tables/acl_set.go b/storage/2018-11-09/table/tables/acl_set.go new file mode 100644 index 0000000..c26bffc --- /dev/null +++ b/storage/2018-11-09/table/tables/acl_set.go @@ -0,0 +1,98 @@ +package tables + +import ( + "context" + "encoding/xml" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type setAcl struct { + SignedIdentifiers []SignedIdentifier `xml:"SignedIdentifier"` + + XMLName xml.Name `xml:"SignedIdentifiers"` +} + +// SetACL sets the specified Access Control List for the specified Table +func (client Client) SetACL(ctx context.Context, accountName, tableName string, acls []SignedIdentifier) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("tables.Client", "SetACL", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("tables.Client", "SetACL", "`tableName` cannot be an empty string.") + } + + req, err := client.SetACLPreparer(ctx, accountName, tableName, acls) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "SetACL", nil, "Failure preparing request") + return + } + + resp, err := client.SetACLSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "tables.Client", "SetACL", resp, "Failure sending request") + return + } + + result, err = client.SetACLResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "SetACL", resp, "Failure responding to request") + return + } + + return +} + +// SetACLPreparer prepares the SetACL request. +func (client Client) SetACLPreparer(ctx context.Context, accountName, tableName string, acls []SignedIdentifier) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + } + + queryParameters := map[string]interface{}{ + "comp": autorest.Encode("query", "acl"), + } + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + } + + input := setAcl{ + SignedIdentifiers: acls, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/xml; charset=utf-8"), + autorest.AsPut(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/{tableName}", pathParameters), + autorest.WithQueryParameters(queryParameters), + autorest.WithHeaders(headers), + autorest.WithXML(&input)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// SetACLSender sends the SetACL request. The method will close the +// http.Response Body if it receives an error. +func (client Client) SetACLSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// SetACLResponder handles the response to the SetACL request. The method always +// closes the http.Response Body. +func (client Client) SetACLResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/table/tables/client.go b/storage/2018-11-09/table/tables/client.go new file mode 100644 index 0000000..56724b9 --- /dev/null +++ b/storage/2018-11-09/table/tables/client.go @@ -0,0 +1,25 @@ +package tables + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// Client is the base client for Table Storage Shares. +type Client struct { + autorest.Client + BaseURI string +} + +// New creates an instance of the Client client. +func New() Client { + return NewWithEnvironment(azure.PublicCloud) +} + +// NewWithEnvironment creates an instance of the Client client. +func NewWithEnvironment(environment azure.Environment) Client { + return Client{ + Client: autorest.NewClientWithUserAgent(UserAgent()), + BaseURI: environment.StorageEndpointSuffix, + } +} diff --git a/storage/2018-11-09/table/tables/create.go b/storage/2018-11-09/table/tables/create.go new file mode 100644 index 0000000..561f574 --- /dev/null +++ b/storage/2018-11-09/table/tables/create.go @@ -0,0 +1,90 @@ +package tables + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type createTableRequest struct { + TableName string `json:"TableName"` +} + +// Create creates a new table in the storage account. +func (client Client) Create(ctx context.Context, accountName, tableName string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("tables.Client", "Create", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("tables.Client", "Create", "`tableName` cannot be an empty string.") + } + + req, err := client.CreatePreparer(ctx, accountName, tableName) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Create", nil, "Failure preparing request") + return + } + + resp, err := client.CreateSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "tables.Client", "Create", resp, "Failure sending request") + return + } + + result, err = client.CreateResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Create", resp, "Failure responding to request") + return + } + + return +} + +// CreatePreparer prepares the Create request. +func (client Client) CreatePreparer(ctx context.Context, accountName, tableName string) (*http.Request, error) { + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + // NOTE: we could support returning metadata here, but it doesn't appear to be directly useful + // vs making a request using the Get methods as-necessary? + "Accept": "application/json;odata=nometadata", + "Prefer": "return-no-content", + } + + body := createTableRequest{ + TableName: tableName, + } + + preparer := autorest.CreatePreparer( + autorest.AsContentType("application/json"), + autorest.AsPost(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPath("/Tables"), + autorest.WithJSON(body), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// CreateSender sends the Create request. The method will close the +// http.Response Body if it receives an error. +func (client Client) CreateSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// CreateResponder handles the response to the Create request. The method always +// closes the http.Response Body. +func (client Client) CreateResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/table/tables/delete.go b/storage/2018-11-09/table/tables/delete.go new file mode 100644 index 0000000..5b5ec86 --- /dev/null +++ b/storage/2018-11-09/table/tables/delete.go @@ -0,0 +1,79 @@ +package tables + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Delete deletes the specified table and any data it contains. +func (client Client) Delete(ctx context.Context, accountName, tableName string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("tables.Client", "Delete", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("tables.Client", "Delete", "`tableName` cannot be an empty string.") + } + + req, err := client.DeletePreparer(ctx, accountName, tableName) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Delete", nil, "Failure preparing request") + return + } + + resp, err := client.DeleteSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "tables.Client", "Delete", resp, "Failure sending request") + return + } + + result, err = client.DeleteResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Delete", resp, "Failure responding to request") + return + } + + return +} + +// DeletePreparer prepares the Delete request. +func (client Client) DeletePreparer(ctx context.Context, accountName, tableName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + } + + // NOTE: whilst the API documentation says that API Version is Optional + // apparently specifying it causes an "invalid content type" to always be returned + // as such we omit it here :shrug: + + preparer := autorest.CreatePreparer( + autorest.AsDelete(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/Tables('{tableName}')", pathParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// DeleteSender sends the Delete request. The method will close the +// http.Response Body if it receives an error. +func (client Client) DeleteSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// DeleteResponder handles the response to the Delete request. The method always +// closes the http.Response Body. +func (client Client) DeleteResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusNoContent), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/table/tables/exists.go b/storage/2018-11-09/table/tables/exists.go new file mode 100644 index 0000000..b3a2718 --- /dev/null +++ b/storage/2018-11-09/table/tables/exists.go @@ -0,0 +1,80 @@ +package tables + +import ( + "context" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// Exists checks that the specified table exists +func (client Client) Exists(ctx context.Context, accountName, tableName string) (result autorest.Response, err error) { + if accountName == "" { + return result, validation.NewError("tables.Client", "Exists", "`accountName` cannot be an empty string.") + } + if tableName == "" { + return result, validation.NewError("tables.Client", "Exists", "`tableName` cannot be an empty string.") + } + + req, err := client.ExistsPreparer(ctx, accountName, tableName) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Exists", nil, "Failure preparing request") + return + } + + resp, err := client.ExistsSender(req) + if err != nil { + result = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "tables.Client", "Exists", resp, "Failure sending request") + return + } + + result, err = client.ExistsResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Exists", resp, "Failure responding to request") + return + } + + return +} + +// ExistsPreparer prepares the Exists request. +func (client Client) ExistsPreparer(ctx context.Context, accountName, tableName string) (*http.Request, error) { + pathParameters := map[string]interface{}{ + "tableName": autorest.Encode("path", tableName), + } + + // NOTE: whilst the API documentation says that API Version is Optional + // apparently specifying it causes an "invalid content type" to always be returned + // as such we omit it here :shrug: + + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.AsContentType("application/xml"), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPathParameters("/Tables('{tableName}')", pathParameters)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// ExistsSender sends the Exists request. The method will close the +// http.Response Body if it receives an error. +func (client Client) ExistsSender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// ExistsResponder handles the response to the Exists request. The method always +// closes the http.Response Body. +func (client Client) ExistsResponder(resp *http.Response) (result autorest.Response, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByClosing()) + result = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/table/tables/lifecycle_test.go b/storage/2018-11-09/table/tables/lifecycle_test.go new file mode 100644 index 0000000..74ab0fe --- /dev/null +++ b/storage/2018-11-09/table/tables/lifecycle_test.go @@ -0,0 +1,112 @@ +package tables + +import ( + "context" + "fmt" + "log" + "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/tombuildsstuff/giovanni/storage/internal/auth" + "github.com/tombuildsstuff/giovanni/testhelpers" +) + +func TestTablesLifecycle(t *testing.T) { + client, err := testhelpers.Build() + if err != nil { + t.Fatal(err) + } + ctx := context.TODO() + resourceGroup := fmt.Sprintf("acctestrg-%d", testhelpers.RandomInt()) + accountName := fmt.Sprintf("acctestsa%s", testhelpers.RandomString()) + tableName := fmt.Sprintf("table%d", testhelpers.RandomInt()) + + testData, err := client.BuildTestResources(ctx, resourceGroup, accountName, storage.Storage) + if err != nil { + t.Fatal(err) + } + defer client.DestroyTestResources(ctx, resourceGroup, accountName) + + storageAuth := auth.NewSharedKeyLiteTableAuthorizer(accountName, testData.StorageAccountKey) + tablesClient := NewWithEnvironment(client.Environment) + tablesClient.Client = client.PrepareWithAuthorizer(tablesClient.Client, storageAuth) + + t.Logf("[DEBUG] Creating Table..") + if _, err := tablesClient.Create(ctx, accountName, tableName); err != nil { + t.Fatalf("Error creating Table %q: %s", tableName, err) + } + + // first look it up directly and confirm it's there + t.Logf("[DEBUG] Checking if Table exists..") + if _, err := tablesClient.Exists(ctx, accountName, tableName); err != nil { + t.Fatalf("Error checking if Table %q exists: %s", tableName, err) + } + + // then confirm it exists in the Query too + t.Logf("[DEBUG] Querying for Tables..") + result, err := tablesClient.Query(ctx, accountName, NoMetaData) + if err != nil { + t.Fatalf("Error retrieving Tables: %s", err) + } + found := false + for _, v := range result.Tables { + log.Printf("[DEBUG] Table: %q", v.TableName) + + if v.TableName == tableName { + found = true + } + } + if !found { + t.Fatalf("%q was not found in the Query response!", tableName) + } + + t.Logf("[DEBUG] Setting ACL's for Table %q..", tableName) + acls := []SignedIdentifier{ + { + Id: "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=", + AccessPolicy: AccessPolicy{ + Permission: "raud", + Start: "2020-11-26T08:49:37.0000000Z", + Expiry: "2020-11-27T08:49:37.0000000Z", + }, + }, + } + if _, err := tablesClient.SetACL(ctx, accountName, tableName, acls); err != nil { + t.Fatalf("Error setting ACLs: %s", err) + } + + t.Logf("[DEBUG] Retrieving ACL's for Table %q..", tableName) + retrievedACLs, err := tablesClient.GetACL(ctx, accountName, tableName) + if err != nil { + t.Fatalf("Error retrieving ACLs: %s", err) + } + + if len(retrievedACLs.SignedIdentifiers) != len(acls) { + t.Fatalf("Expected %d but got %q ACLs", len(retrievedACLs.SignedIdentifiers), len(acls)) + } + + for i, retrievedAcl := range retrievedACLs.SignedIdentifiers { + expectedAcl := acls[i] + + if retrievedAcl.Id != expectedAcl.Id { + t.Fatalf("Expected ID to be %q but got %q", retrievedAcl.Id, expectedAcl.Id) + } + + if retrievedAcl.AccessPolicy.Start != expectedAcl.AccessPolicy.Start { + t.Fatalf("Expected Start to be %q but got %q", retrievedAcl.AccessPolicy.Start, expectedAcl.AccessPolicy.Start) + } + + if retrievedAcl.AccessPolicy.Expiry != expectedAcl.AccessPolicy.Expiry { + t.Fatalf("Expected Expiry to be %q but got %q", retrievedAcl.AccessPolicy.Expiry, expectedAcl.AccessPolicy.Expiry) + } + + if retrievedAcl.AccessPolicy.Permission != expectedAcl.AccessPolicy.Permission { + t.Fatalf("Expected Permission to be %q but got %q", retrievedAcl.AccessPolicy.Permission, expectedAcl.AccessPolicy.Permission) + } + } + + t.Logf("[DEBUG] Deleting Table %q..", tableName) + if _, err := tablesClient.Delete(ctx, accountName, tableName); err != nil { + t.Fatalf("Error deleting %q: %s", tableName, err) + } +} diff --git a/storage/2018-11-09/table/tables/models.go b/storage/2018-11-09/table/tables/models.go new file mode 100644 index 0000000..d7c382a --- /dev/null +++ b/storage/2018-11-09/table/tables/models.go @@ -0,0 +1,29 @@ +package tables + +type MetaDataLevel string + +var ( + NoMetaData MetaDataLevel = "nometadata" + MinimalMetaData MetaDataLevel = "minimalmetadata" + FullMetaData MetaDataLevel = "fullmetadata" +) + +type GetResultItem struct { + TableName string `json:"TableName"` + + // Optional, depending on the MetaData Level + ODataType string `json:"odata.type,omitempty"` + ODataID string `json:"odata.id,omitEmpty"` + ODataEditLink string `json:"odata.editLink,omitEmpty"` +} + +type SignedIdentifier struct { + Id string `xml:"Id"` + AccessPolicy AccessPolicy `xml:"AccessPolicy"` +} + +type AccessPolicy struct { + Start string `xml:"Start"` + Expiry string `xml:"Expiry"` + Permission string `xml:"Permission"` +} diff --git a/storage/2018-11-09/table/tables/query.go b/storage/2018-11-09/table/tables/query.go new file mode 100644 index 0000000..475370f --- /dev/null +++ b/storage/2018-11-09/table/tables/query.go @@ -0,0 +1,87 @@ +package tables + +import ( + "context" + "fmt" + "net/http" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/validation" + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +type GetResult struct { + autorest.Response + + MetaData string `json:"odata.metadata,omitempty"` + Tables []GetResultItem `json:"value"` +} + +// Query returns a list of tables under the specified account. +func (client Client) Query(ctx context.Context, accountName string, metaDataLevel MetaDataLevel) (result GetResult, err error) { + if accountName == "" { + return result, validation.NewError("tables.Client", "Query", "`accountName` cannot be an empty string.") + } + + req, err := client.QueryPreparer(ctx, accountName, metaDataLevel) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Query", nil, "Failure preparing request") + return + } + + resp, err := client.QuerySender(req) + if err != nil { + result.Response = autorest.Response{Response: resp} + err = autorest.NewErrorWithError(err, "tables.Client", "Query", resp, "Failure sending request") + return + } + + result, err = client.QueryResponder(resp) + if err != nil { + err = autorest.NewErrorWithError(err, "tables.Client", "Query", resp, "Failure responding to request") + return + } + + return +} + +// QueryPreparer prepares the Query request. +func (client Client) QueryPreparer(ctx context.Context, accountName string, metaDataLevel MetaDataLevel) (*http.Request, error) { + // NOTE: whilst this supports ContinuationTokens and 'Top' + // it appears that 'Skip' returns a '501 Not Implemented' + // as such, we intentionally don't support those right now + + headers := map[string]interface{}{ + "x-ms-version": APIVersion, + "Accept": fmt.Sprintf("application/json;odata=%s", metaDataLevel), + } + + preparer := autorest.CreatePreparer( + autorest.AsGet(), + autorest.WithBaseURL(endpoints.GetTableEndpoint(client.BaseURI, accountName)), + autorest.WithPath("/Tables"), + autorest.WithHeaders(headers)) + return preparer.Prepare((&http.Request{}).WithContext(ctx)) +} + +// QuerySender sends the Query request. The method will close the +// http.Response Body if it receives an error. +func (client Client) QuerySender(req *http.Request) (*http.Response, error) { + return autorest.SendWithSender(client, req, + azure.DoRetryWithRegistration(client.Client)) +} + +// QueryResponder handles the response to the Query request. The method always +// closes the http.Response Body. +func (client Client) QueryResponder(resp *http.Response) (result GetResult, err error) { + err = autorest.Respond( + resp, + client.ByInspecting(), + azure.WithErrorUnlessStatusCode(http.StatusOK), + autorest.ByUnmarshallingJSON(&result), + autorest.ByClosing()) + result.Response = autorest.Response{Response: resp} + + return +} diff --git a/storage/2018-11-09/table/tables/resource_id.go b/storage/2018-11-09/table/tables/resource_id.go new file mode 100644 index 0000000..1052317 --- /dev/null +++ b/storage/2018-11-09/table/tables/resource_id.go @@ -0,0 +1,54 @@ +package tables + +import ( + "fmt" + "net/url" + "strings" + + "github.com/tombuildsstuff/giovanni/storage/internal/endpoints" +) + +// GetResourceID returns the Resource ID for the given Table +// This can be useful when, for example, you're using this as a unique identifier +func (client Client) GetResourceID(accountName, tableName string) string { + domain := endpoints.GetTableEndpoint(client.BaseURI, accountName) + return fmt.Sprintf("%s/Tables('%s')", domain, tableName) +} + +type ResourceID struct { + AccountName string + TableName string +} + +// ParseResourceID parses the Resource ID and returns an object which +// can be used to interact with the Table within the specified Storage Account +func (client Client) ParseResourceID(id string) (*ResourceID, error) { + // example: https://foo.table.core.windows.net/Table('foo') + if id == "" { + return nil, fmt.Errorf("`id` was empty") + } + + uri, err := url.Parse(id) + if err != nil { + return nil, fmt.Errorf("Error parsing ID as a URL: %s", err) + } + + accountName, err := endpoints.GetAccountNameFromEndpoint(uri.Host) + if err != nil { + return nil, fmt.Errorf("Error parsing Account Name: %s", err) + } + + // assume there a `Table('')` + path := strings.TrimPrefix(uri.Path, "/") + if !strings.HasPrefix(path, "Tables('") || !strings.HasSuffix(path, "')") { + return nil, fmt.Errorf("Expected the Table Name to be in the format `Tables('name')` but got %q", path) + } + + // strip off the `Table('')` + tableName := strings.TrimPrefix(uri.Path, "/Tables('") + tableName = strings.TrimSuffix(tableName, "')") + return &ResourceID{ + AccountName: *accountName, + TableName: tableName, + }, nil +} diff --git a/storage/2018-11-09/table/tables/resource_id_test.go b/storage/2018-11-09/table/tables/resource_id_test.go new file mode 100644 index 0000000..5557f81 --- /dev/null +++ b/storage/2018-11-09/table/tables/resource_id_test.go @@ -0,0 +1,78 @@ +package tables + +import ( + "testing" + + "github.com/Azure/go-autorest/autorest/azure" +) + +func TestGetResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Expected string + }{ + { + Environment: azure.ChinaCloud, + Expected: "https://account1.table.core.chinacloudapi.cn/Tables('table1')", + }, + { + Environment: azure.GermanCloud, + Expected: "https://account1.table.core.cloudapi.de/Tables('table1')", + }, + { + Environment: azure.PublicCloud, + Expected: "https://account1.table.core.windows.net/Tables('table1')", + }, + { + Environment: azure.USGovernmentCloud, + Expected: "https://account1.table.core.usgovcloudapi.net/Tables('table1')", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual := c.GetResourceID("account1", "table1") + if actual != v.Expected { + t.Fatalf("Expected the Resource ID to be %q but got %q", v.Expected, actual) + } + } +} + +func TestParseResourceID(t *testing.T) { + testData := []struct { + Environment azure.Environment + Input string + }{ + { + Environment: azure.ChinaCloud, + Input: "https://account1.table.core.chinacloudapi.cn/Tables('table1')", + }, + { + Environment: azure.GermanCloud, + Input: "https://account1.table.core.cloudapi.de/Tables('table1')", + }, + { + Environment: azure.PublicCloud, + Input: "https://account1.table.core.windows.net/Tables('table1')", + }, + { + Environment: azure.USGovernmentCloud, + Input: "https://account1.table.core.usgovcloudapi.net/Tables('table1')", + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing Environment %q", v.Environment.Name) + c := NewWithEnvironment(v.Environment) + actual, err := c.ParseResourceID(v.Input) + if err != nil { + t.Fatal(err) + } + + if actual.AccountName != "account1" { + t.Fatalf("Expected Account Name to be `account1` but got %q", actual.AccountName) + } + if actual.TableName != "table1" { + t.Fatalf("Expected Table Name to be `table1` but got %q", actual.TableName) + } + } +} diff --git a/storage/2018-11-09/table/tables/version.go b/storage/2018-11-09/table/tables/version.go new file mode 100644 index 0000000..c682db5 --- /dev/null +++ b/storage/2018-11-09/table/tables/version.go @@ -0,0 +1,14 @@ +package tables + +import ( + "fmt" + + "github.com/tombuildsstuff/giovanni/version" +) + +// APIVersion is the version of the API used for all Storage API Operations +const APIVersion = "2018-11-09" + +func UserAgent() string { + return fmt.Sprintf("tombuildsstuff/giovanni/%s storage/%s", version.Number, APIVersion) +} diff --git a/storage/internal/auth/TODO.md b/storage/internal/auth/TODO.md new file mode 100644 index 0000000..e514d2d --- /dev/null +++ b/storage/internal/auth/TODO.md @@ -0,0 +1 @@ +TODO: this can be removed once https://github.com/Azure/go-autorest/pull/416 has been merged \ No newline at end of file diff --git a/storage/internal/auth/authorizer_shared_key_lite.go b/storage/internal/auth/authorizer_shared_key_lite.go new file mode 100644 index 0000000..012441e --- /dev/null +++ b/storage/internal/auth/authorizer_shared_key_lite.go @@ -0,0 +1,87 @@ +package auth + +import ( + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" +) + +// SharedKeyLiteAuthorizer implements an authorization for Shared Key Lite +// this can be used for interaction with Blob, File and Queue Storage Endpoints +type SharedKeyLiteAuthorizer struct { + storageAccountName string + storageAccountKey string +} + +// NewSharedKeyLiteAuthorizer crates a SharedKeyLiteAuthorizer using the given credentials +func NewSharedKeyLiteAuthorizer(accountName, accountKey string) *SharedKeyLiteAuthorizer { + return &SharedKeyLiteAuthorizer{ + storageAccountName: accountName, + storageAccountKey: accountKey, + } +} + +// WithAuthorization returns a PrepareDecorator that adds an HTTP Authorization header whose +// value is "SharedKeyLite " followed by the computed key. +// This can be used for the Blob, Queue, and File Services +// +// from: https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key +// You may use Shared Key Lite authorization to authorize a request made against the +// 2009-09-19 version and later of the Blob and Queue services, +// and version 2014-02-14 and later of the File services. +func (skl *SharedKeyLiteAuthorizer) WithAuthorization() autorest.PrepareDecorator { + return func(p autorest.Preparer) autorest.Preparer { + return autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) { + r, err := p.Prepare(r) + if err != nil { + return r, err + } + + key, err := buildSharedKeyLite(skl.storageAccountName, skl.storageAccountKey, r) + if err != nil { + return r, err + } + + sharedKeyHeader := formatSharedKeyLiteAuthorizationHeader(skl.storageAccountName, *key) + return autorest.Prepare(r, autorest.WithHeader(HeaderAuthorization, sharedKeyHeader)) + }) + } +} +func buildSharedKeyLite(accountName, storageAccountKey string, r *http.Request) (*string, error) { + // first ensure the relevant headers are configured + prepareHeadersForRequest(r) + + sharedKey, err := computeSharedKeyLite(r.Method, r.URL.String(), accountName, r.Header) + if err != nil { + return nil, err + } + + // we then need to HMAC that value + hmacdValue := hmacValue(storageAccountKey, *sharedKey) + return &hmacdValue, nil +} + +// computeSharedKeyLite computes the Shared Key Lite required for Storage Authentication +// NOTE: this function assumes that the `x-ms-date` field is set +func computeSharedKeyLite(verb, url string, accountName string, headers http.Header) (*string, error) { + canonicalizedResource, err := buildCanonicalizedResource(url, accountName) + if err != nil { + return nil, err + } + + canonicalizedHeaders := buildCanonicalizedHeader(headers) + canonicalizedString := buildCanonicalizedStringForSharedKeyLite(verb, headers, canonicalizedHeaders, *canonicalizedResource) + return &canonicalizedString, nil +} + +func buildCanonicalizedStringForSharedKeyLite(verb string, headers http.Header, canonicalizedHeaders, canonicalizedResource string) string { + return strings.Join([]string{ + verb, + headers.Get(HeaderContentMD5), // TODO: this appears to always be empty? + headers.Get(HeaderContentType), + "", // date should be nil, apparently :shrug: + canonicalizedHeaders, + canonicalizedResource, + }, "\n") +} diff --git a/storage/internal/auth/authorizer_shared_key_lite_table.go b/storage/internal/auth/authorizer_shared_key_lite_table.go new file mode 100644 index 0000000..67296d0 --- /dev/null +++ b/storage/internal/auth/authorizer_shared_key_lite_table.go @@ -0,0 +1,84 @@ +package auth + +import ( + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" +) + +// SharedKeyLiteTableAuthorizer implements an authorization for Shared Key Lite +// this can be used for interaction with Table Storage Endpoints +type SharedKeyLiteTableAuthorizer struct { + storageAccountName string + storageAccountKey string +} + +// NewSharedKeyLiteAuthorizer crates a SharedKeyLiteAuthorizer using the given credentials +func NewSharedKeyLiteTableAuthorizer(accountName, accountKey string) *SharedKeyLiteTableAuthorizer { + return &SharedKeyLiteTableAuthorizer{ + storageAccountName: accountName, + storageAccountKey: accountKey, + } +} + +// WithAuthorization returns a PrepareDecorator that adds an HTTP Authorization header whose +// value is "SharedKeyLite " followed by the computed key. +// This can be used for the Blob, Queue, and File Services +// +// from: https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key +// You may use Shared Key Lite authorization to authorize a request made against the +// 2009-09-19 version and later of the Blob and Queue services, +// and version 2014-02-14 and later of the File services. +func (skl *SharedKeyLiteTableAuthorizer) WithAuthorization() autorest.PrepareDecorator { + return func(p autorest.Preparer) autorest.Preparer { + return autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) { + r, err := p.Prepare(r) + if err != nil { + return r, err + } + + key, err := buildSharedKeyLiteTable(skl.storageAccountName, skl.storageAccountKey, r) + if err != nil { + return r, err + } + + sharedKeyHeader := formatSharedKeyLiteAuthorizationHeader(skl.storageAccountName, *key) + return autorest.Prepare(r, autorest.WithHeader(HeaderAuthorization, sharedKeyHeader)) + }) + } +} + +func buildSharedKeyLiteTable(accountName, storageAccountKey string, r *http.Request) (*string, error) { + // first ensure the relevant headers are configured + prepareHeadersForRequest(r) + + sharedKey, err := computeSharedKeyLiteTable(r.URL.String(), accountName, r.Header) + if err != nil { + return nil, err + } + + // we then need to HMAC that value + hmacdValue := hmacValue(storageAccountKey, *sharedKey) + return &hmacdValue, nil +} + +// computeSharedKeyLite computes the Shared Key Lite required for Storage Authentication +// NOTE: this function assumes that the `x-ms-date` field is set +func computeSharedKeyLiteTable(url string, accountName string, headers http.Header) (*string, error) { + dateHeader := headers.Get("x-ms-date") + canonicalizedResource, err := buildCanonicalizedResource(url, accountName) + if err != nil { + return nil, err + } + + canonicalizedString := buildCanonicalizedStringForSharedKeyLiteTable(*canonicalizedResource, dateHeader) + return &canonicalizedString, nil +} + +func buildCanonicalizedStringForSharedKeyLiteTable(canonicalizedResource, dateHeader string) string { + return strings.Join([]string{ + dateHeader, + canonicalizedResource, + }, "\n") +} diff --git a/storage/internal/auth/authorizer_shared_key_lite_test.go b/storage/internal/auth/authorizer_shared_key_lite_test.go new file mode 100644 index 0000000..54e6a88 --- /dev/null +++ b/storage/internal/auth/authorizer_shared_key_lite_test.go @@ -0,0 +1,36 @@ +package auth + +import ( + "testing" +) + +func TestBuildCanonicalizedStringForSharedKeyLite(t *testing.T) { + testData := []struct { + name string + headers map[string][]string + canonicalizedHeaders string + canonicalizedResource string + verb string + expected string + }{ + { + name: "completed", + verb: "NOM", + headers: map[string][]string{ + "Content-MD5": {"abc123"}, + "Content-Type": {"vnd/panda-pops+v1"}, + }, + canonicalizedHeaders: "all-the-headers", + canonicalizedResource: "all-the-resources", + expected: "NOM\n\nvnd/panda-pops+v1\n\nall-the-headers\nall-the-resources", + }, + } + + for _, test := range testData { + t.Logf("Test: %q", test.name) + actual := buildCanonicalizedStringForSharedKeyLite(test.verb, test.headers, test.canonicalizedHeaders, test.canonicalizedResource) + if actual != test.expected { + t.Fatalf("Expected %q but got %q", test.expected, actual) + } + } +} diff --git a/storage/internal/auth/consts.go b/storage/internal/auth/consts.go new file mode 100644 index 0000000..612be83 --- /dev/null +++ b/storage/internal/auth/consts.go @@ -0,0 +1,19 @@ +package auth + +var ( + HeaderAuthorization = "Authorization" + HeaderContentLength = "Content-Length" + HeaderContentEncoding = "Content-Encoding" + HeaderContentLanguage = "Content-Language" + HeaderContentType = "Content-Type" + HeaderContentMD5 = "Content-MD5" + HeaderIfModifiedSince = "If-Modified-Since" + HeaderIfMatch = "If-Match" + HeaderIfNoneMatch = "If-None-Match" + HeaderIfUnmodifiedSince = "If-Unmodified-Since" + HeaderMSDate = "X-Ms-Date" + HeaderRange = "Range" + + StorageEmulatorAccountName = "devstoreaccount1" + StorageEmulatorAccountKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" +) diff --git a/storage/internal/auth/helpers.go b/storage/internal/auth/helpers.go new file mode 100644 index 0000000..19e4c05 --- /dev/null +++ b/storage/internal/auth/helpers.go @@ -0,0 +1,122 @@ +package auth + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "fmt" + "net/http" + "net/url" + "sort" + "strings" + "time" +) + +// NOTE: this all came from the Azure SDK for Go, but has been refactored to aid creating a Preparer. + +// buildCanonicalizedHeader builds the Canonicalized Header required to sign Storage Requests +func buildCanonicalizedHeader(headers http.Header) string { + cm := make(map[string]string) + + for k, v := range headers { + headerName := strings.TrimSpace(strings.ToLower(k)) + if strings.HasPrefix(headerName, "x-ms-") { + cm[headerName] = v[0] + } + } + + if len(cm) == 0 { + return "" + } + + var keys []string + for key := range cm { + keys = append(keys, key) + } + + sort.Strings(keys) + + ch := bytes.NewBufferString("") + + for _, key := range keys { + ch.WriteString(key) + ch.WriteRune(':') + ch.WriteString(cm[key]) + ch.WriteRune('\n') + } + + return strings.TrimSuffix(string(ch.Bytes()), "\n") +} + +// buildCanonicalizedResource builds the Canonical Resource required for to sign Storage Account requests +func buildCanonicalizedResource(uri, accountName string) (*string, error) { + u, err := url.Parse(uri) + if err != nil { + return nil, err + } + + cr := bytes.NewBufferString("") + if accountName != StorageEmulatorAccountName { + cr.WriteString("/") + cr.WriteString(primaryStorageAccountName(accountName)) + } + + if len(u.Path) > 0 { + // Any portion of the CanonicalizedResource string that is derived from + // the resource's URI should be encoded exactly as it is in the URI. + // -- https://msdn.microsoft.com/en-gb/library/azure/dd179428.aspx + cr.WriteString(u.EscapedPath()) + } + + // TODO: replace this with less of a hack + if comp := u.Query().Get("comp"); comp != "" { + cr.WriteString(fmt.Sprintf("?comp=%s", comp)) + } + + out := string(cr.Bytes()) + return &out, nil +} + +func formatSharedKeyLiteAuthorizationHeader(accountName, key string) string { + canonicalizedAccountName := primaryStorageAccountName(accountName) + return fmt.Sprintf("SharedKeyLite %s:%s", canonicalizedAccountName, key) +} + +// hmacValue base-64 decodes the storageAccountKey, then signs the string with it +// as outlined here: https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key +func hmacValue(storageAccountKey, canonicalizedString string) string { + key, err := base64.StdEncoding.DecodeString(storageAccountKey) + if err != nil { + return "" + } + + encr := hmac.New(sha256.New, []byte(key)) + encr.Write([]byte(canonicalizedString)) + return base64.StdEncoding.EncodeToString(encr.Sum(nil)) +} + +// prepareHeadersForRequest prepares a request so that it can be signed +// by ensuring the `date` and `x-ms-date` headers are set +func prepareHeadersForRequest(r *http.Request) { + if r.Header == nil { + r.Header = http.Header{} + } + + date := time.Now().UTC().Format(http.TimeFormat) + + // a date must be set, X-Ms-Date should be used when both are set; but let's set both for completeness + r.Header.Set("date", date) + r.Header.Set("x-ms-date", date) +} + +// primaryStorageAccountName returns the name of the primary for a given Storage Account +func primaryStorageAccountName(input string) string { + // from https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key + // If you are accessing the secondary location in a storage account for which + // read-access geo-replication (RA-GRS) is enabled, do not include the + // -secondary designation in the authorization header. + // For authorization purposes, the account name is always the name of the primary location, + // even for secondary access. + return strings.TrimSuffix(input, "-secondary") +} diff --git a/storage/internal/auth/helpers_test.go b/storage/internal/auth/helpers_test.go new file mode 100644 index 0000000..4354f26 --- /dev/null +++ b/storage/internal/auth/helpers_test.go @@ -0,0 +1,271 @@ +package auth + +import ( + "encoding/base64" + "net/http" + "testing" +) + +func TestBuildCanonicalizedHeader(t *testing.T) { + testData := []struct { + Input http.Header + Expected string + }{ + { + // no headers + Expected: "", + Input: map[string][]string{ + "": {""}, + }, + }, + { + // no x-ms headers + Expected: "", + Input: map[string][]string{ + "panda": {"pops"}, + }, + }, + { + // only a single x-ms header + Expected: "x-ms-panda:nom", + Input: map[string][]string{ + "x-ms-panda": {"nom"}, + }, + }, + { + // multiple x-ms headers + Expected: "x-ms-panda:nom\nx-ms-tiger:rawr", + Input: map[string][]string{ + "x-ms-panda": {"nom"}, + "x-ms-tiger": {"rawr"}, + }, + }, + { + // multiple x-ms headers, out of order + Expected: "x-ms-panda:nom\nx-ms-tiger:rawr", + Input: map[string][]string{ + "x-ms-tiger": {"rawr"}, + "x-ms-panda": {"nom"}, + }, + }, + { + // mixed headers (some ms, some non-ms) + Expected: "x-ms-panda:nom\nx-ms-tiger:rawr", + Input: map[string][]string{ + "x-ms-tiger": {"rawr"}, + "panda": {"pops"}, + "x-ms-panda": {"nom"}, + }, + }, + { + // casing + Expected: "x-ms-panda:nom\nx-ms-tiger:rawr", + Input: map[string][]string{ + "X-Ms-Tiger": {"rawr"}, + "X-Ms-Panda": {"nom"}, + }, + }, + } + + for _, v := range testData { + actual := buildCanonicalizedHeader(v.Input) + if actual != v.Expected { + t.Fatalf("Expected %q but got %q", v.Expected, actual) + } + } +} + +func TestBuildCanonicalizedResource(t *testing.T) { + testData := []struct { + name string + accountName string + uri string + expected string + expectError bool + }{ + { + name: "invalid uri", + accountName: "example", + uri: "://example.com", + expected: "", + expectError: true, + }, + { + name: "storage emulator doesn't get prefix", + accountName: StorageEmulatorAccountName, + uri: "http://www.example.com/foo", + expected: "/foo", + }, + { + name: "non storage emulator gets prefix", + accountName: StorageEmulatorAccountName + "test", + uri: "http://www.example.com/foo", + expected: "/" + StorageEmulatorAccountName + "test/foo", + }, + { + name: "uri encoding", + accountName: "example", + uri: "", + expected: "/example%3Chello%3E", + }, + { + name: "comp-arg", + accountName: "example", + uri: "/endpoint?first=true&comp=bar&second=false&third=panda", + expected: "/example/endpoint?comp=bar", + }, + { + name: "arguments", + accountName: "example", + uri: "/endpoint?first=true&second=false&third=panda", + expected: "/example/endpoint", + }, + } + + for _, test := range testData { + t.Logf("Test %q", test.name) + actual, err := buildCanonicalizedResource(test.uri, test.accountName) + if err != nil { + if test.expectError { + continue + } + + t.Fatalf("Error: %s", err) + } + + if *actual != test.expected { + t.Fatalf("Expected %q but got %q", test.expected, *actual) + } + } +} + +func TestFormatSharedKeyLiteAuthorizationHeader(t *testing.T) { + testData := []struct { + name string + accountName string + accountKey string + expected string + }{ + { + name: "primary", + accountName: "account1", + accountKey: "examplekey", + expected: "SharedKeyLite account1:examplekey", + }, + { + name: "secondary", + accountName: "account1-secondary", + accountKey: "examplekey", + expected: "SharedKeyLite account1:examplekey", + }, + } + + for _, test := range testData { + t.Logf("Test: %q", test.name) + actual := formatSharedKeyLiteAuthorizationHeader(test.accountName, test.accountKey) + + if actual != test.expected { + t.Fatalf("Expected %q but got %q", test.expected, actual) + } + } +} + +func TestHMAC(t *testing.T) { + testData := []struct { + Expected string + StorageAccountKey string + CanonicalizedString string + }{ + { + // When Storage Key isn't base-64 encoded + Expected: "", + StorageAccountKey: "bar", + CanonicalizedString: "foobarzoo", + }, + { + // Valid + Expected: "h5U0ATVX6SpbFX1H6GNuxIMeXXCILLoIvhflPtuQZ30=", + StorageAccountKey: base64.StdEncoding.EncodeToString([]byte("bar")), + CanonicalizedString: "foobarzoo", + }, + } + + for _, v := range testData { + actual := hmacValue(v.StorageAccountKey, v.CanonicalizedString) + if actual != v.Expected { + t.Fatalf("Expected %q but got %q", v.Expected, actual) + } + } +} + +func TestTestPrepareHeadersForRequest(t *testing.T) { + request, err := http.NewRequest("GET", "http://example.com", nil) + if err != nil { + t.Fatal(err) + } + + headers := []string{ + "Date", + "X-Ms-Date", + } + + for _, header := range headers { + existingVal := request.Header.Get(header) + if existingVal != "" { + t.Fatalf("%q had a value prior to being set: %q", header, existingVal) + } + } + + prepareHeadersForRequest(request) + + for _, header := range headers { + updatedVal := request.Header.Get(header) + if updatedVal == "" { + t.Fatalf("%q didn't have a value after being set: %q", header, updatedVal) + } + } +} + +func TestPrepareHeadersForRequestWithNoneConfigured(t *testing.T) { + request, err := http.NewRequest("GET", "http://example.com", nil) + if err != nil { + t.Fatal(err) + } + + request.Header = nil + prepareHeadersForRequest(request) + + if request.Header == nil { + t.Fatalf("Expected `request.Header` to not be nil, but it was!") + } +} + +func TestPrimaryStorageAccountName(t *testing.T) { + testData := []struct { + Expected string + Input string + }{ + { + // Empty + Expected: "", + Input: "", + }, + { + // Primary + Expected: "bar", + Input: "bar", + }, + { + // Secondary + Expected: "bar", + Input: "bar-secondary", + }, + } + + for _, v := range testData { + actual := primaryStorageAccountName(v.Input) + if actual != v.Expected { + t.Fatalf("Expected %q but got %q", v.Expected, actual) + } + } +} diff --git a/storage/internal/endpoints/endpoints.go b/storage/internal/endpoints/endpoints.go new file mode 100644 index 0000000..e9a18c8 --- /dev/null +++ b/storage/internal/endpoints/endpoints.go @@ -0,0 +1,34 @@ +package endpoints + +import ( + "fmt" + "strings" +) + +func GetAccountNameFromEndpoint(endpoint string) (*string, error) { + segments := strings.Split(endpoint, ".") + if len(segments) == 0 { + return nil, fmt.Errorf("The Endpoint contained no segments") + } + return &segments[0], nil +} + +// GetBlobEndpoint returns the endpoint for Blob API Operations on this storage account +func GetBlobEndpoint(baseUri string, accountName string) string { + return fmt.Sprintf("https://%s.blob.%s", accountName, baseUri) +} + +// GetFileEndpoint returns the endpoint for File Share API Operations on this storage account +func GetFileEndpoint(baseUri string, accountName string) string { + return fmt.Sprintf("https://%s.file.%s", accountName, baseUri) +} + +// GetQueueEndpoint returns the endpoint for Queue API Operations on this storage account +func GetQueueEndpoint(baseUri string, accountName string) string { + return fmt.Sprintf("https://%s.queue.%s", accountName, baseUri) +} + +// GetTableEndpoint returns the endpoint for Table API Operations on this storage account +func GetTableEndpoint(baseUri string, accountName string) string { + return fmt.Sprintf("https://%s.table.%s", accountName, baseUri) +} diff --git a/storage/internal/helpers/errors.go b/storage/internal/helpers/errors.go new file mode 100644 index 0000000..9090bf6 --- /dev/null +++ b/storage/internal/helpers/errors.go @@ -0,0 +1,107 @@ +package helpers + +import ( + "encoding/json" + "encoding/xml" + "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/azure" +) + +// TODO: trial switching over to this, then upstream it + +// WithErrorUnlessStatusCode returns a RespondDecorator that emits an +// azure.RequestError by reading the response body unless the response HTTP status code +// is among the set passed. +// +// If there is a chance service may return responses other than the Azure error +// format and the response cannot be parsed into an error, a decoding error will +// be returned containing the response body. In any case, the Responder will +// return an error if the status code is not satisfied. +// +// If this Responder returns an error, the response body will be replaced with +// an in-memory reader, which needs no further closing. +func WithErrorUnlessStatusCode(codes ...int) autorest.RespondDecorator { + return func(r autorest.Responder) autorest.Responder { + return autorest.ResponderFunc(func(resp *http.Response) error { + err := r.Respond(resp) + if err == nil && !autorest.ResponseHasStatusCode(resp, codes...) { + var e azure.RequestError + defer resp.Body.Close() + + contentType := autorest.EncodedAsJSON + if resp != nil { + contentTypeStr := resp.Header.Get("Content-Type") + if strings.EqualFold(contentTypeStr, "application/xml") { + contentType = autorest.EncodedAsXML + } + } + + // Copy and replace the Body in case it does not contain an error object. + // This will leave the Body available to the caller. + b, decodeErr := autorest.CopyAndDecode(contentType, resp.Body, &e) + resp.Body = ioutil.NopCloser(&b) + if decodeErr != nil { + return fmt.Errorf("autorest/azure: error response cannot be parsed: %q error: %v", b.String(), decodeErr) + } + if e.ServiceError == nil { + switch contentType { + case autorest.EncodedAsJSON: + // Check if error is unwrapped ServiceError + if err := json.Unmarshal(b.Bytes(), &e.ServiceError); err != nil { + return err + } + break + case autorest.EncodedAsXML: + // Check if error is unwrapped ServiceError + if err := xml.Unmarshal(b.Bytes(), &e.ServiceError); err != nil { + return err + } + break + } + } + if e.ServiceError.Message == "" { + + rawBody := map[string]interface{}{} + + switch contentType { + case autorest.EncodedAsJSON: + // if we're here it means the returned error wasn't OData v4 compliant. + // try to unmarshal the body as raw JSON in hopes of getting something. + if err := json.Unmarshal(b.Bytes(), &rawBody); err != nil { + return err + } + break + + case autorest.EncodedAsXML: + // if we're here it means the returned error wasn't OData v4 compliant. + // try to unmarshal the body as raw XML in hopes of getting something. + if err := xml.Unmarshal(b.Bytes(), &rawBody); err != nil { + return err + } + break + } + + e.ServiceError = &azure.ServiceError{ + Code: "Unknown", + Message: "Unknown service error", + } + if len(rawBody) > 0 { + e.ServiceError.Details = []map[string]interface{}{rawBody} + } + } + e.Response = resp + e.RequestID = azure.ExtractRequestID(resp) + if e.StatusCode == nil { + e.StatusCode = resp.StatusCode + } + err = &e + } + return err + }) + } +} diff --git a/storage/internal/metadata/parse.go b/storage/internal/metadata/parse.go new file mode 100644 index 0000000..1880d40 --- /dev/null +++ b/storage/internal/metadata/parse.go @@ -0,0 +1,22 @@ +package metadata + +import ( + "net/http" + "strings" +) + +// ParseFromHeaders parses the meta data from the headers +func ParseFromHeaders(headers http.Header) map[string]string { + metaData := make(map[string]string, 0) + for k, v := range headers { + key := strings.ToLower(k) + prefix := "x-ms-meta-" + if !strings.HasPrefix(key, prefix) { + continue + } + + key = strings.TrimPrefix(key, prefix) + metaData[key] = v[0] + } + return metaData +} diff --git a/storage/internal/metadata/set.go b/storage/internal/metadata/set.go new file mode 100644 index 0000000..d88fbd7 --- /dev/null +++ b/storage/internal/metadata/set.go @@ -0,0 +1,13 @@ +package metadata + +import "fmt" + +// SetIntoHeaders sets the provided MetaData into the headers +func SetIntoHeaders(headers map[string]interface{}, metaData map[string]string) map[string]interface{} { + for k, v := range metaData { + key := fmt.Sprintf("x-ms-meta-%s", k) + headers[key] = v + } + + return headers +} diff --git a/storage/internal/metadata/validation.go b/storage/internal/metadata/validation.go new file mode 100644 index 0000000..1fa1f9a --- /dev/null +++ b/storage/internal/metadata/validation.go @@ -0,0 +1,105 @@ +package metadata + +import ( + "fmt" + "regexp" + "strings" +) + +var cSharpKeywords = map[string]*struct{}{ + "abstract": {}, + "as": {}, + "base": {}, + "bool": {}, + "break": {}, + "byte": {}, + "case": {}, + "catch": {}, + "char": {}, + "checked": {}, + "class": {}, + "const": {}, + "continue": {}, + "decimal": {}, + "default": {}, + "delegate": {}, + "do": {}, + "double": {}, + "else": {}, + "enum": {}, + "event": {}, + "explicit": {}, + "extern": {}, + "false": {}, + "finally": {}, + "fixed": {}, + "float": {}, + "for": {}, + "foreach": {}, + "goto": {}, + "if": {}, + "implicit": {}, + "in": {}, + "int": {}, + "interface": {}, + "internal": {}, + "is": {}, + "lock": {}, + "long": {}, + "namespace": {}, + "new": {}, + "null": {}, + "object": {}, + "operator": {}, + "out": {}, + "override": {}, + "params": {}, + "private": {}, + "protected": {}, + "public": {}, + "readonly": {}, + "ref": {}, + "return": {}, + "sbyte": {}, + "sealed": {}, + "short": {}, + "sizeof": {}, + "stackalloc": {}, + "static": {}, + "string": {}, + "struct": {}, + "switch": {}, + "this": {}, + "throw": {}, + "true": {}, + "try": {}, + "typeof": {}, + "uint": {}, + "ulong": {}, + "unchecked": {}, + "unsafe": {}, + "ushort": {}, + "using": {}, + "void": {}, + "volatile": {}, + "while": {}, +} + +func Validate(input map[string]string) error { + + for k := range input { + isCSharpKeyword := cSharpKeywords[strings.ToLower(k)] != nil + if isCSharpKeyword { + return fmt.Errorf("%q is not a valid key (C# keyword)", k) + } + + // must begin with a letter, underscore + // the rest: letters, digits and underscores + r, _ := regexp.Compile(`^([A-Za-z_]{1}[A-Za-z0-9_]{1,})$`) + if !r.MatchString(k) { + return fmt.Errorf("MetaData must start with letters or an underscores. Got %q.", k) + } + } + + return nil +} diff --git a/storage/internal/metadata/validation_test.go b/storage/internal/metadata/validation_test.go new file mode 100644 index 0000000..bd39192 --- /dev/null +++ b/storage/internal/metadata/validation_test.go @@ -0,0 +1,68 @@ +package metadata + +import "testing" + +func TestValidationCSharpKeywords(t *testing.T) { + for key := range cSharpKeywords { + t.Logf("[DEBUG] Testing %q", key) + + err := Validate(map[string]string{ + key: "value", + }) + if err == nil { + t.Fatalf("Expected an error but didn't get one for %q", key) + } + } +} + +func TestValidation(t *testing.T) { + testData := []struct { + Input string + ShouldBeValid bool + }{ + { + Input: "", + ShouldBeValid: false, + }, + { + Input: "abc123", + ShouldBeValid: true, + }, + { + Input: "_abc123", + ShouldBeValid: true, + }, + { + Input: "123abc", + ShouldBeValid: false, + }, + { + Input: "a_123abc", + ShouldBeValid: true, + }, + { + Input: "abc_123", + ShouldBeValid: true, + }, + { + Input: "abc123_", + ShouldBeValid: true, + }, + { + Input: "ABC123", + ShouldBeValid: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Input) + + err := Validate(map[string]string{ + v.Input: "value", + }) + actual := err == nil + if v.ShouldBeValid != actual { + t.Fatalf("Expected %t but got %t for %q", v.ShouldBeValid, actual, v.Input) + } + } +} diff --git a/storage/testdata/blank-large-file.dmg b/storage/testdata/blank-large-file.dmg new file mode 100644 index 0000000..90be405 Binary files /dev/null and b/storage/testdata/blank-large-file.dmg differ diff --git a/storage/testdata/small-file.png b/storage/testdata/small-file.png new file mode 100644 index 0000000..0776b1b Binary files /dev/null and b/storage/testdata/small-file.png differ diff --git a/testhelpers/client.go b/testhelpers/client.go new file mode 100644 index 0000000..71ef33d --- /dev/null +++ b/testhelpers/client.go @@ -0,0 +1,194 @@ +package testhelpers + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources" + "github.com/Azure/azure-sdk-for-go/profiles/latest/storage/mgmt/storage" + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/adal" + "github.com/Azure/go-autorest/autorest/azure" + "github.com/hashicorp/go-azure-helpers/authentication" +) + +type Client struct { + ResourceGroupsClient resources.GroupsClient + StorageClient storage.AccountsClient + + auth *autorest.BearerAuthorizer + Environment azure.Environment +} + +func toPointeredString(input string) *string { + return &input +} + +type TestResources struct { + ResourceGroup string + StorageAccountName string + StorageAccountKey string +} + +func (client Client) BuildTestResources(ctx context.Context, resourceGroup, name string, kind storage.Kind) (*TestResources, error) { + location := toPointeredString(os.Getenv("ARM_TEST_LOCATION")) + _, err := client.ResourceGroupsClient.CreateOrUpdate(ctx, resourceGroup, resources.Group{ + Location: location, + }) + if err != nil { + return nil, fmt.Errorf("Error creating Resource Group %q: %s", resourceGroup, err) + } + + props := storage.AccountPropertiesCreateParameters{} + if kind == storage.BlobStorage { + props.AccessTier = storage.Hot + } + future, err := client.StorageClient.Create(ctx, resourceGroup, name, storage.AccountCreateParameters{ + Location: location, + Sku: &storage.Sku{ + Name: storage.StandardLRS, + }, + Kind: kind, + AccountPropertiesCreateParameters: &props, + }) + + if err != nil { + return nil, fmt.Errorf("Error creating Account %q (Resource Group %q): %s", name, resourceGroup, err) + } + + err = future.WaitForCompletionRef(ctx, client.StorageClient.Client) + if err != nil { + return nil, fmt.Errorf("Error waiting for the creation of Account %q (Resource Group %q): %s", name, resourceGroup, err) + } + + keys, err := client.StorageClient.ListKeys(ctx, resourceGroup, name) + if err != nil { + return nil, fmt.Errorf("Error listing keys for Storage Account %q (Resource Group %q): %s", name, resourceGroup, err) + } + + // sure we could poll to get around the inconsistency, but where's the fun in that + time.Sleep(5 * time.Second) + + accountKeys := *keys.Keys + return &TestResources{ + ResourceGroup: resourceGroup, + StorageAccountName: name, + StorageAccountKey: *(accountKeys[0]).Value, + }, nil +} + +func (client Client) DestroyTestResources(ctx context.Context, resourceGroup, name string) error { + _, err := client.StorageClient.Delete(ctx, resourceGroup, name) + if err != nil { + return fmt.Errorf("Error deleting Account %q (Resource Group %q): %s", name, resourceGroup, err) + } + + future, err := client.ResourceGroupsClient.Delete(ctx, resourceGroup) + if err != nil { + return fmt.Errorf("Error deleting Resource Group %q: %s", resourceGroup, err) + } + + err = future.WaitForCompletionRef(ctx, client.ResourceGroupsClient.Client) + if err != nil { + return fmt.Errorf("Error waiting for deletion of Resource Group %q: %s", resourceGroup, err) + } + + return nil +} + +func Build() (*Client, error) { + authClient, env, err := buildAuthClient() + if err != nil { + return nil, fmt.Errorf("Error building Auth Client: %s", err) + } + + if env == nil { + return nil, fmt.Errorf("Environment was nil: %s", err) + } + + apiClient, err := buildAPIClient(authClient, *env) + if err != nil { + return nil, fmt.Errorf("Error building API Client: %s", err) + } + + return apiClient, nil +} + +func buildAPIClient(config *authentication.Config, env azure.Environment) (*Client, error) { + oauthConfig, err := adal.NewOAuthConfig(env.ActiveDirectoryEndpoint, config.TenantID) + if err != nil { + return nil, err + } + + // OAuthConfigForTenant returns a pointer, which can be nil. + if oauthConfig == nil { + return nil, fmt.Errorf("Unable to configure OAuthConfig for tenant %s", config.TenantID) + } + + sender := buildSender() + + armAuth, err := config.GetAuthorizationToken(sender, oauthConfig, env.ResourceManagerEndpoint) + if err != nil { + return nil, err + } + + storageAuth, err := config.GetAuthorizationToken(sender, oauthConfig, "https://storage.azure.com/") + if err != nil { + return nil, err + } + + client := Client{ + Environment: env, + auth: storageAuth, + } + + resourceGroupsClient := resources.NewGroupsClientWithBaseURI(env.ResourceManagerEndpoint, config.SubscriptionID) + resourceGroupsClient.Client = client.PrepareWithStorageResourceManagerAuth(resourceGroupsClient.Client) + resourceGroupsClient.Authorizer = armAuth + client.ResourceGroupsClient = resourceGroupsClient + + storageClient := storage.NewAccountsClientWithBaseURI(env.ResourceManagerEndpoint, config.SubscriptionID) + storageClient.Client = client.PrepareWithStorageResourceManagerAuth(storageClient.Client) + storageClient.Authorizer = armAuth + client.StorageClient = storageClient + + return &client, nil +} + +func (client Client) PrepareWithStorageResourceManagerAuth(input autorest.Client) autorest.Client { + return client.PrepareWithAuthorizer(input, client.auth) +} + +func (client Client) PrepareWithAuthorizer(input autorest.Client, authorizer autorest.Authorizer) autorest.Client { + input.Authorizer = authorizer + input.Sender = buildSender() + input.SkipResourceProviderRegistration = true + return input +} + +func buildAuthClient() (*authentication.Config, *azure.Environment, error) { + builder := &authentication.Builder{ + SubscriptionID: os.Getenv("ARM_SUBSCRIPTION_ID"), + ClientID: os.Getenv("ARM_CLIENT_ID"), + ClientSecret: os.Getenv("ARM_CLIENT_SECRET"), + TenantID: os.Getenv("ARM_TENANT_ID"), + Environment: os.Getenv("ARM_ENVIRONMENT"), + + // Feature Toggles + SupportsClientSecretAuth: true, + } + + c, err := builder.Build() + if err != nil { + return nil, nil, fmt.Errorf("Error building AzureRM Client: %s", err) + } + + env, err := authentication.DetermineEnvironment(c.Environment) + if err != nil { + return nil, nil, err + } + + return c, env, nil +} diff --git a/testhelpers/random.go b/testhelpers/random.go new file mode 100644 index 0000000..23f1dde --- /dev/null +++ b/testhelpers/random.go @@ -0,0 +1,27 @@ +package testhelpers + +import ( + "math/rand" + "time" +) + +func RandomInt() int { + reseed() + return rand.New(rand.NewSource(time.Now().UnixNano())).Int() +} + +func RandomString() string { + size := 5 + charSet := "abcdefghijklmnopqrstuvwxyz0123456789" + + reseed() + result := make([]byte, size) + for i := 0; i < size; i++ { + result[i] = charSet[rand.Intn(len(charSet))] + } + return string(result) +} + +func reseed() { + rand.Seed(time.Now().UTC().UnixNano()) +} diff --git a/testhelpers/sender.go b/testhelpers/sender.go new file mode 100644 index 0000000..b566259 --- /dev/null +++ b/testhelpers/sender.go @@ -0,0 +1,65 @@ +package testhelpers + +import ( + "log" + "net/http" + "net/http/httputil" + "os" + + "github.com/Azure/go-autorest/autorest" +) + +func buildSender() autorest.Sender { + return autorest.DecorateSender(&http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + }, + }, withRequestLogging()) +} + +func withRequestLogging() autorest.SendDecorator { + return func(s autorest.Sender) autorest.Sender { + return autorest.SenderFunc(func(r *http.Request) (*http.Response, error) { + shouldLog := os.Getenv("TEST_LOG") != "" + + if shouldLog { + // strip the authorization header prior to printing + authHeaderName := "Authorization" + auth := r.Header.Get(authHeaderName) + if auth != "" { + r.Header.Del(authHeaderName) + } + + // dump request to wire format + if dump, err := httputil.DumpRequestOut(r, true); err == nil { + log.Printf("[DEBUG] AzureRM Request: \n%s\n", dump) + } else { + // fallback to basic message + log.Printf("[DEBUG] AzureRM Request: %s to %s\n", r.Method, r.URL) + } + + // add the auth header back + if auth != "" { + r.Header.Add(authHeaderName, auth) + } + } + + resp, err := s.Do(r) + + if shouldLog { + if resp != nil { + // dump response to wire format + if dump, err2 := httputil.DumpResponse(resp, true); err2 == nil { + log.Printf("[DEBUG] AzureRM Response for %s: \n%s\n", r.URL, dump) + } else { + // fallback to basic message + log.Printf("[DEBUG] AzureRM Response: %s for %s\n", resp.Status, r.URL) + } + } else { + log.Printf("[DEBUG] Request to %s completed with no response", r.URL) + } + } + return resp, err + }) + } +} diff --git a/version/version.go b/version/version.go new file mode 100644 index 0000000..9307eba --- /dev/null +++ b/version/version.go @@ -0,0 +1,3 @@ +package version + +const Number = "v0.0.1"