provider/terraform: reorganize for merge into core
The "terraform" provider was previously split out into its own repository, but that turned out to be a mistake due to how tightly it depends on aspects of Terraform Core. Here we prepare to bring it back into the core repository by reorganizing the directory layout to conform with what's expected there.
This commit is contained in:
parent
b599dc6471
commit
a3ced1a367
|
@ -1,5 +0,0 @@
|
||||||
# Code of Conduct
|
|
||||||
|
|
||||||
HashiCorp Community Guidelines apply to you when interacting with the community here on GitHub and contributing code.
|
|
||||||
|
|
||||||
Please read the full text at https://www.hashicorp.com/community-guidelines
|
|
|
@ -1,43 +0,0 @@
|
||||||
Hi there,
|
|
||||||
|
|
||||||
Thank you for opening an issue. Please note that we try to keep the Terraform issue tracker reserved for bug reports and feature requests. For general usage questions, please see: https://www.terraform.io/community.html.
|
|
||||||
|
|
||||||
### Terraform Version
|
|
||||||
Run `terraform -v` to show the version. If you are not running the latest version of Terraform, please upgrade because your issue may have already been fixed.
|
|
||||||
|
|
||||||
### Affected Resource(s)
|
|
||||||
Please list the resources as a list, for example:
|
|
||||||
- opc_instance
|
|
||||||
- opc_storage_volume
|
|
||||||
|
|
||||||
If this issue appears to affect multiple resources, it may be an issue with Terraform's core, so please mention this.
|
|
||||||
|
|
||||||
### Terraform Configuration Files
|
|
||||||
```hcl
|
|
||||||
# Copy-paste your Terraform configurations here - for large Terraform configs,
|
|
||||||
# please use a service like Dropbox and share a link to the ZIP file. For
|
|
||||||
# security, you can also encrypt the files using our GPG public key.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debug Output
|
|
||||||
Please provider a link to a GitHub Gist containing the complete debug output: https://www.terraform.io/docs/internals/debugging.html. Please do NOT paste the debug output in the issue; just paste a link to the Gist.
|
|
||||||
|
|
||||||
### Panic Output
|
|
||||||
If Terraform produced a panic, please provide a link to a GitHub Gist containing the output of the `crash.log`.
|
|
||||||
|
|
||||||
### Expected Behavior
|
|
||||||
What should have happened?
|
|
||||||
|
|
||||||
### Actual Behavior
|
|
||||||
What actually happened?
|
|
||||||
|
|
||||||
### Steps to Reproduce
|
|
||||||
Please list the steps required to reproduce the issue, for example:
|
|
||||||
1. `terraform apply`
|
|
||||||
|
|
||||||
### Important Factoids
|
|
||||||
Are there anything atypical about your accounts that we should know? For example: Running in EC2 Classic? Custom version of OpenStack? Tight ACLs?
|
|
||||||
|
|
||||||
### References
|
|
||||||
Are there any other GitHub issues (open or closed) or Pull Requests that should be linked here? For example:
|
|
||||||
- GH-1234
|
|
|
@ -1,5 +0,0 @@
|
||||||
# Support
|
|
||||||
|
|
||||||
Terraform is a mature project with a growing community. There are active, dedicated people willing to help you through various mediums.
|
|
||||||
|
|
||||||
Take a look at those mediums listed at https://www.terraform.io/community.html
|
|
|
@ -1,31 +0,0 @@
|
||||||
*.dll
|
|
||||||
*.exe
|
|
||||||
.DS_Store
|
|
||||||
example.tf
|
|
||||||
terraform.tfplan
|
|
||||||
terraform.tfstate
|
|
||||||
bin/
|
|
||||||
modules-dev/
|
|
||||||
/pkg/
|
|
||||||
website/.vagrant
|
|
||||||
website/.bundle
|
|
||||||
website/build
|
|
||||||
website/node_modules
|
|
||||||
.vagrant/
|
|
||||||
*.backup
|
|
||||||
./*.tfstate
|
|
||||||
.terraform/
|
|
||||||
*.log
|
|
||||||
*.bak
|
|
||||||
*~
|
|
||||||
.*.swp
|
|
||||||
.idea
|
|
||||||
*.iml
|
|
||||||
*.test
|
|
||||||
*.iml
|
|
||||||
|
|
||||||
website/vendor
|
|
||||||
|
|
||||||
# Test exclusions
|
|
||||||
!command/test-fixtures/**/*.tfstate
|
|
||||||
!command/test-fixtures/**/.terraform/
|
|
26
.travis.yml
26
.travis.yml
|
@ -1,26 +0,0 @@
|
||||||
dist: trusty
|
|
||||||
sudo: false
|
|
||||||
language: go
|
|
||||||
go:
|
|
||||||
- 1.8.1
|
|
||||||
|
|
||||||
install:
|
|
||||||
# This script is used by the Travis build to install a cookie for
|
|
||||||
# go.googlesource.com so rate limits are higher when using `go get` to fetch
|
|
||||||
# packages that live there.
|
|
||||||
# See: https://github.com/golang/go/issues/12933
|
|
||||||
- bash scripts/gogetcookie.sh
|
|
||||||
- go get github.com/kardianos/govendor
|
|
||||||
|
|
||||||
script:
|
|
||||||
- make test
|
|
||||||
- make vendor-status
|
|
||||||
- make vet
|
|
||||||
|
|
||||||
branches:
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
matrix:
|
|
||||||
fast_finish: true
|
|
||||||
allow_failures:
|
|
||||||
- go: tip
|
|
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -1,17 +0,0 @@
|
||||||
## 1.0.2 (Unreleased)
|
|
||||||
## 1.0.1 (October 25, 2017)
|
|
||||||
|
|
||||||
* Updated the libraries that we share with Terraform core so that this provider can now use all the same backend features as Terraform Core v0.10.8.
|
|
||||||
|
|
||||||
## 1.0.0 (September 14, 2017)
|
|
||||||
|
|
||||||
ENHANCEMENTS:
|
|
||||||
|
|
||||||
* `terraform_remote_state` now accepts backend configuration arguments that were introduced to the backends in Terraform 0.10, including the `s3` backend's `workspace_dir_prefix` argument. ([#6](https://github.com/terraform-providers/terraform-provider-terraform/issues/6))
|
|
||||||
* New argument `defaults` on `terraform_remote_state` allows setting default values for outputs that are not set in the remote state. ([#11](https://github.com/terraform-providers/terraform-provider-terraform/issues/11))
|
|
||||||
|
|
||||||
## 0.1.0 (June 21, 2017)
|
|
||||||
|
|
||||||
NOTES:
|
|
||||||
|
|
||||||
* Same functionality as that of Terraform 0.9.8. Repacked as part of [Provider Splitout](https://www.hashicorp.com/blog/upcoming-provider-changes-in-terraform-0-10/)
|
|
47
GNUmakefile
47
GNUmakefile
|
@ -1,47 +0,0 @@
|
||||||
TEST?=$$(go list ./... |grep -v 'vendor')
|
|
||||||
GOFMT_FILES?=$$(find . -name '*.go' |grep -v vendor)
|
|
||||||
|
|
||||||
default: build
|
|
||||||
|
|
||||||
build: fmtcheck
|
|
||||||
go install
|
|
||||||
|
|
||||||
test: fmtcheck
|
|
||||||
go test -i $(TEST) || exit 1
|
|
||||||
echo $(TEST) | \
|
|
||||||
xargs -t -n4 go test $(TESTARGS) -timeout=30s -parallel=4
|
|
||||||
|
|
||||||
testacc: fmtcheck
|
|
||||||
TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout 120m
|
|
||||||
|
|
||||||
vet:
|
|
||||||
@echo "go vet ."
|
|
||||||
@go vet $$(go list ./... | grep -v vendor/) ; if [ $$? -eq 1 ]; then \
|
|
||||||
echo ""; \
|
|
||||||
echo "Vet found suspicious constructs. Please check the reported constructs"; \
|
|
||||||
echo "and fix them if necessary before submitting the code for review."; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
fmt:
|
|
||||||
gofmt -w $(GOFMT_FILES)
|
|
||||||
|
|
||||||
fmtcheck:
|
|
||||||
@sh -c "'$(CURDIR)/scripts/gofmtcheck.sh'"
|
|
||||||
|
|
||||||
errcheck:
|
|
||||||
@sh -c "'$(CURDIR)/scripts/errcheck.sh'"
|
|
||||||
|
|
||||||
vendor-status:
|
|
||||||
@govendor status
|
|
||||||
|
|
||||||
test-compile:
|
|
||||||
@if [ "$(TEST)" = "./..." ]; then \
|
|
||||||
echo "ERROR: Set TEST to a specific package. For example,"; \
|
|
||||||
echo " make test-compile TEST=./aws"; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
go test -c $(TEST) $(TESTARGS)
|
|
||||||
|
|
||||||
.PHONY: build test testacc vet fmt fmtcheck errcheck vendor-status test-compile
|
|
||||||
|
|
373
LICENSE
373
LICENSE
|
@ -1,373 +0,0 @@
|
||||||
Mozilla Public License Version 2.0
|
|
||||||
==================================
|
|
||||||
|
|
||||||
1. Definitions
|
|
||||||
--------------
|
|
||||||
|
|
||||||
1.1. "Contributor"
|
|
||||||
means each individual or legal entity that creates, contributes to
|
|
||||||
the creation of, or owns Covered Software.
|
|
||||||
|
|
||||||
1.2. "Contributor Version"
|
|
||||||
means the combination of the Contributions of others (if any) used
|
|
||||||
by a Contributor and that particular Contributor's Contribution.
|
|
||||||
|
|
||||||
1.3. "Contribution"
|
|
||||||
means Covered Software of a particular Contributor.
|
|
||||||
|
|
||||||
1.4. "Covered Software"
|
|
||||||
means Source Code Form to which the initial Contributor has attached
|
|
||||||
the notice in Exhibit A, the Executable Form of such Source Code
|
|
||||||
Form, and Modifications of such Source Code Form, in each case
|
|
||||||
including portions thereof.
|
|
||||||
|
|
||||||
1.5. "Incompatible With Secondary Licenses"
|
|
||||||
means
|
|
||||||
|
|
||||||
(a) that the initial Contributor has attached the notice described
|
|
||||||
in Exhibit B to the Covered Software; or
|
|
||||||
|
|
||||||
(b) that the Covered Software was made available under the terms of
|
|
||||||
version 1.1 or earlier of the License, but not also under the
|
|
||||||
terms of a Secondary License.
|
|
||||||
|
|
||||||
1.6. "Executable Form"
|
|
||||||
means any form of the work other than Source Code Form.
|
|
||||||
|
|
||||||
1.7. "Larger Work"
|
|
||||||
means a work that combines Covered Software with other material, in
|
|
||||||
a separate file or files, that is not Covered Software.
|
|
||||||
|
|
||||||
1.8. "License"
|
|
||||||
means this document.
|
|
||||||
|
|
||||||
1.9. "Licensable"
|
|
||||||
means having the right to grant, to the maximum extent possible,
|
|
||||||
whether at the time of the initial grant or subsequently, any and
|
|
||||||
all of the rights conveyed by this License.
|
|
||||||
|
|
||||||
1.10. "Modifications"
|
|
||||||
means any of the following:
|
|
||||||
|
|
||||||
(a) any file in Source Code Form that results from an addition to,
|
|
||||||
deletion from, or modification of the contents of Covered
|
|
||||||
Software; or
|
|
||||||
|
|
||||||
(b) any new file in Source Code Form that contains any Covered
|
|
||||||
Software.
|
|
||||||
|
|
||||||
1.11. "Patent Claims" of a Contributor
|
|
||||||
means any patent claim(s), including without limitation, method,
|
|
||||||
process, and apparatus claims, in any patent Licensable by such
|
|
||||||
Contributor that would be infringed, but for the grant of the
|
|
||||||
License, by the making, using, selling, offering for sale, having
|
|
||||||
made, import, or transfer of either its Contributions or its
|
|
||||||
Contributor Version.
|
|
||||||
|
|
||||||
1.12. "Secondary License"
|
|
||||||
means either the GNU General Public License, Version 2.0, the GNU
|
|
||||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
|
||||||
Public License, Version 3.0, or any later versions of those
|
|
||||||
licenses.
|
|
||||||
|
|
||||||
1.13. "Source Code Form"
|
|
||||||
means the form of the work preferred for making modifications.
|
|
||||||
|
|
||||||
1.14. "You" (or "Your")
|
|
||||||
means an individual or a legal entity exercising rights under this
|
|
||||||
License. For legal entities, "You" includes any entity that
|
|
||||||
controls, is controlled by, or is under common control with You. For
|
|
||||||
purposes of this definition, "control" means (a) the power, direct
|
|
||||||
or indirect, to cause the direction or management of such entity,
|
|
||||||
whether by contract or otherwise, or (b) ownership of more than
|
|
||||||
fifty percent (50%) of the outstanding shares or beneficial
|
|
||||||
ownership of such entity.
|
|
||||||
|
|
||||||
2. License Grants and Conditions
|
|
||||||
--------------------------------
|
|
||||||
|
|
||||||
2.1. Grants
|
|
||||||
|
|
||||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
|
||||||
non-exclusive license:
|
|
||||||
|
|
||||||
(a) under intellectual property rights (other than patent or trademark)
|
|
||||||
Licensable by such Contributor to use, reproduce, make available,
|
|
||||||
modify, display, perform, distribute, and otherwise exploit its
|
|
||||||
Contributions, either on an unmodified basis, with Modifications, or
|
|
||||||
as part of a Larger Work; and
|
|
||||||
|
|
||||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
|
||||||
for sale, have made, import, and otherwise transfer either its
|
|
||||||
Contributions or its Contributor Version.
|
|
||||||
|
|
||||||
2.2. Effective Date
|
|
||||||
|
|
||||||
The licenses granted in Section 2.1 with respect to any Contribution
|
|
||||||
become effective for each Contribution on the date the Contributor first
|
|
||||||
distributes such Contribution.
|
|
||||||
|
|
||||||
2.3. Limitations on Grant Scope
|
|
||||||
|
|
||||||
The licenses granted in this Section 2 are the only rights granted under
|
|
||||||
this License. No additional rights or licenses will be implied from the
|
|
||||||
distribution or licensing of Covered Software under this License.
|
|
||||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
|
||||||
Contributor:
|
|
||||||
|
|
||||||
(a) for any code that a Contributor has removed from Covered Software;
|
|
||||||
or
|
|
||||||
|
|
||||||
(b) for infringements caused by: (i) Your and any other third party's
|
|
||||||
modifications of Covered Software, or (ii) the combination of its
|
|
||||||
Contributions with other software (except as part of its Contributor
|
|
||||||
Version); or
|
|
||||||
|
|
||||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
|
||||||
its Contributions.
|
|
||||||
|
|
||||||
This License does not grant any rights in the trademarks, service marks,
|
|
||||||
or logos of any Contributor (except as may be necessary to comply with
|
|
||||||
the notice requirements in Section 3.4).
|
|
||||||
|
|
||||||
2.4. Subsequent Licenses
|
|
||||||
|
|
||||||
No Contributor makes additional grants as a result of Your choice to
|
|
||||||
distribute the Covered Software under a subsequent version of this
|
|
||||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
|
||||||
permitted under the terms of Section 3.3).
|
|
||||||
|
|
||||||
2.5. Representation
|
|
||||||
|
|
||||||
Each Contributor represents that the Contributor believes its
|
|
||||||
Contributions are its original creation(s) or it has sufficient rights
|
|
||||||
to grant the rights to its Contributions conveyed by this License.
|
|
||||||
|
|
||||||
2.6. Fair Use
|
|
||||||
|
|
||||||
This License is not intended to limit any rights You have under
|
|
||||||
applicable copyright doctrines of fair use, fair dealing, or other
|
|
||||||
equivalents.
|
|
||||||
|
|
||||||
2.7. Conditions
|
|
||||||
|
|
||||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
|
||||||
in Section 2.1.
|
|
||||||
|
|
||||||
3. Responsibilities
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
3.1. Distribution of Source Form
|
|
||||||
|
|
||||||
All distribution of Covered Software in Source Code Form, including any
|
|
||||||
Modifications that You create or to which You contribute, must be under
|
|
||||||
the terms of this License. You must inform recipients that the Source
|
|
||||||
Code Form of the Covered Software is governed by the terms of this
|
|
||||||
License, and how they can obtain a copy of this License. You may not
|
|
||||||
attempt to alter or restrict the recipients' rights in the Source Code
|
|
||||||
Form.
|
|
||||||
|
|
||||||
3.2. Distribution of Executable Form
|
|
||||||
|
|
||||||
If You distribute Covered Software in Executable Form then:
|
|
||||||
|
|
||||||
(a) such Covered Software must also be made available in Source Code
|
|
||||||
Form, as described in Section 3.1, and You must inform recipients of
|
|
||||||
the Executable Form how they can obtain a copy of such Source Code
|
|
||||||
Form by reasonable means in a timely manner, at a charge no more
|
|
||||||
than the cost of distribution to the recipient; and
|
|
||||||
|
|
||||||
(b) You may distribute such Executable Form under the terms of this
|
|
||||||
License, or sublicense it under different terms, provided that the
|
|
||||||
license for the Executable Form does not attempt to limit or alter
|
|
||||||
the recipients' rights in the Source Code Form under this License.
|
|
||||||
|
|
||||||
3.3. Distribution of a Larger Work
|
|
||||||
|
|
||||||
You may create and distribute a Larger Work under terms of Your choice,
|
|
||||||
provided that You also comply with the requirements of this License for
|
|
||||||
the Covered Software. If the Larger Work is a combination of Covered
|
|
||||||
Software with a work governed by one or more Secondary Licenses, and the
|
|
||||||
Covered Software is not Incompatible With Secondary Licenses, this
|
|
||||||
License permits You to additionally distribute such Covered Software
|
|
||||||
under the terms of such Secondary License(s), so that the recipient of
|
|
||||||
the Larger Work may, at their option, further distribute the Covered
|
|
||||||
Software under the terms of either this License or such Secondary
|
|
||||||
License(s).
|
|
||||||
|
|
||||||
3.4. Notices
|
|
||||||
|
|
||||||
You may not remove or alter the substance of any license notices
|
|
||||||
(including copyright notices, patent notices, disclaimers of warranty,
|
|
||||||
or limitations of liability) contained within the Source Code Form of
|
|
||||||
the Covered Software, except that You may alter any license notices to
|
|
||||||
the extent required to remedy known factual inaccuracies.
|
|
||||||
|
|
||||||
3.5. Application of Additional Terms
|
|
||||||
|
|
||||||
You may choose to offer, and to charge a fee for, warranty, support,
|
|
||||||
indemnity or liability obligations to one or more recipients of Covered
|
|
||||||
Software. However, You may do so only on Your own behalf, and not on
|
|
||||||
behalf of any Contributor. You must make it absolutely clear that any
|
|
||||||
such warranty, support, indemnity, or liability obligation is offered by
|
|
||||||
You alone, and You hereby agree to indemnify every Contributor for any
|
|
||||||
liability incurred by such Contributor as a result of warranty, support,
|
|
||||||
indemnity or liability terms You offer. You may include additional
|
|
||||||
disclaimers of warranty and limitations of liability specific to any
|
|
||||||
jurisdiction.
|
|
||||||
|
|
||||||
4. Inability to Comply Due to Statute or Regulation
|
|
||||||
---------------------------------------------------
|
|
||||||
|
|
||||||
If it is impossible for You to comply with any of the terms of this
|
|
||||||
License with respect to some or all of the Covered Software due to
|
|
||||||
statute, judicial order, or regulation then You must: (a) comply with
|
|
||||||
the terms of this License to the maximum extent possible; and (b)
|
|
||||||
describe the limitations and the code they affect. Such description must
|
|
||||||
be placed in a text file included with all distributions of the Covered
|
|
||||||
Software under this License. Except to the extent prohibited by statute
|
|
||||||
or regulation, such description must be sufficiently detailed for a
|
|
||||||
recipient of ordinary skill to be able to understand it.
|
|
||||||
|
|
||||||
5. Termination
|
|
||||||
--------------
|
|
||||||
|
|
||||||
5.1. The rights granted under this License will terminate automatically
|
|
||||||
if You fail to comply with any of its terms. However, if You become
|
|
||||||
compliant, then the rights granted under this License from a particular
|
|
||||||
Contributor are reinstated (a) provisionally, unless and until such
|
|
||||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
|
||||||
ongoing basis, if such Contributor fails to notify You of the
|
|
||||||
non-compliance by some reasonable means prior to 60 days after You have
|
|
||||||
come back into compliance. Moreover, Your grants from a particular
|
|
||||||
Contributor are reinstated on an ongoing basis if such Contributor
|
|
||||||
notifies You of the non-compliance by some reasonable means, this is the
|
|
||||||
first time You have received notice of non-compliance with this License
|
|
||||||
from such Contributor, and You become compliant prior to 30 days after
|
|
||||||
Your receipt of the notice.
|
|
||||||
|
|
||||||
5.2. If You initiate litigation against any entity by asserting a patent
|
|
||||||
infringement claim (excluding declaratory judgment actions,
|
|
||||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
|
||||||
directly or indirectly infringes any patent, then the rights granted to
|
|
||||||
You by any and all Contributors for the Covered Software under Section
|
|
||||||
2.1 of this License shall terminate.
|
|
||||||
|
|
||||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
|
||||||
end user license agreements (excluding distributors and resellers) which
|
|
||||||
have been validly granted by You or Your distributors under this License
|
|
||||||
prior to termination shall survive termination.
|
|
||||||
|
|
||||||
************************************************************************
|
|
||||||
* *
|
|
||||||
* 6. Disclaimer of Warranty *
|
|
||||||
* ------------------------- *
|
|
||||||
* *
|
|
||||||
* Covered Software is provided under this License on an "as is" *
|
|
||||||
* basis, without warranty of any kind, either expressed, implied, or *
|
|
||||||
* statutory, including, without limitation, warranties that the *
|
|
||||||
* Covered Software is free of defects, merchantable, fit for a *
|
|
||||||
* particular purpose or non-infringing. The entire risk as to the *
|
|
||||||
* quality and performance of the Covered Software is with You. *
|
|
||||||
* Should any Covered Software prove defective in any respect, You *
|
|
||||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
|
||||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
|
||||||
* essential part of this License. No use of any Covered Software is *
|
|
||||||
* authorized under this License except under this disclaimer. *
|
|
||||||
* *
|
|
||||||
************************************************************************
|
|
||||||
|
|
||||||
************************************************************************
|
|
||||||
* *
|
|
||||||
* 7. Limitation of Liability *
|
|
||||||
* -------------------------- *
|
|
||||||
* *
|
|
||||||
* Under no circumstances and under no legal theory, whether tort *
|
|
||||||
* (including negligence), contract, or otherwise, shall any *
|
|
||||||
* Contributor, or anyone who distributes Covered Software as *
|
|
||||||
* permitted above, be liable to You for any direct, indirect, *
|
|
||||||
* special, incidental, or consequential damages of any character *
|
|
||||||
* including, without limitation, damages for lost profits, loss of *
|
|
||||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
|
||||||
* and all other commercial damages or losses, even if such party *
|
|
||||||
* shall have been informed of the possibility of such damages. This *
|
|
||||||
* limitation of liability shall not apply to liability for death or *
|
|
||||||
* personal injury resulting from such party's negligence to the *
|
|
||||||
* extent applicable law prohibits such limitation. Some *
|
|
||||||
* jurisdictions do not allow the exclusion or limitation of *
|
|
||||||
* incidental or consequential damages, so this exclusion and *
|
|
||||||
* limitation may not apply to You. *
|
|
||||||
* *
|
|
||||||
************************************************************************
|
|
||||||
|
|
||||||
8. Litigation
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Any litigation relating to this License may be brought only in the
|
|
||||||
courts of a jurisdiction where the defendant maintains its principal
|
|
||||||
place of business and such litigation shall be governed by laws of that
|
|
||||||
jurisdiction, without reference to its conflict-of-law provisions.
|
|
||||||
Nothing in this Section shall prevent a party's ability to bring
|
|
||||||
cross-claims or counter-claims.
|
|
||||||
|
|
||||||
9. Miscellaneous
|
|
||||||
----------------
|
|
||||||
|
|
||||||
This License represents the complete agreement concerning the subject
|
|
||||||
matter hereof. If any provision of this License is held to be
|
|
||||||
unenforceable, such provision shall be reformed only to the extent
|
|
||||||
necessary to make it enforceable. Any law or regulation which provides
|
|
||||||
that the language of a contract shall be construed against the drafter
|
|
||||||
shall not be used to construe this License against a Contributor.
|
|
||||||
|
|
||||||
10. Versions of the License
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
10.1. New Versions
|
|
||||||
|
|
||||||
Mozilla Foundation is the license steward. Except as provided in Section
|
|
||||||
10.3, no one other than the license steward has the right to modify or
|
|
||||||
publish new versions of this License. Each version will be given a
|
|
||||||
distinguishing version number.
|
|
||||||
|
|
||||||
10.2. Effect of New Versions
|
|
||||||
|
|
||||||
You may distribute the Covered Software under the terms of the version
|
|
||||||
of the License under which You originally received the Covered Software,
|
|
||||||
or under the terms of any subsequent version published by the license
|
|
||||||
steward.
|
|
||||||
|
|
||||||
10.3. Modified Versions
|
|
||||||
|
|
||||||
If you create software not governed by this License, and you want to
|
|
||||||
create a new license for such software, you may create and use a
|
|
||||||
modified version of this License if you rename the license and remove
|
|
||||||
any references to the name of the license steward (except to note that
|
|
||||||
such modified license differs from this License).
|
|
||||||
|
|
||||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
|
||||||
Licenses
|
|
||||||
|
|
||||||
If You choose to distribute Source Code Form that is Incompatible With
|
|
||||||
Secondary Licenses under the terms of this version of the License, the
|
|
||||||
notice described in Exhibit B of this License must be attached.
|
|
||||||
|
|
||||||
Exhibit A - Source Code Form License Notice
|
|
||||||
-------------------------------------------
|
|
||||||
|
|
||||||
This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
If it is not possible or desirable to put the notice in a particular
|
|
||||||
file, then You may include the notice in a location (such as a LICENSE
|
|
||||||
file in a relevant directory) where a recipient would be likely to look
|
|
||||||
for such a notice.
|
|
||||||
|
|
||||||
You may add additional accurate notices of copyright ownership.
|
|
||||||
|
|
||||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
|
||||||
---------------------------------------------------------
|
|
||||||
|
|
||||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
|
||||||
defined by the Mozilla Public License, v. 2.0.
|
|
63
README.md
63
README.md
|
@ -1,63 +0,0 @@
|
||||||
Terraform Provider
|
|
||||||
==================
|
|
||||||
|
|
||||||
- Website: https://www.terraform.io
|
|
||||||
- [![Gitter chat](https://badges.gitter.im/hashicorp-terraform/Lobby.png)](https://gitter.im/hashicorp-terraform/Lobby)
|
|
||||||
- Mailing list: [Google Groups](http://groups.google.com/group/terraform-tool)
|
|
||||||
|
|
||||||
<img src="https://cdn.rawgit.com/hashicorp/terraform-website/master/content/source/assets/images/logo-hashicorp.svg" width="600px">
|
|
||||||
|
|
||||||
Requirements
|
|
||||||
------------
|
|
||||||
|
|
||||||
- [Terraform](https://www.terraform.io/downloads.html) 0.10.x
|
|
||||||
- [Go](https://golang.org/doc/install) 1.8 (to build the provider plugin)
|
|
||||||
|
|
||||||
Building The Provider
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
Clone repository to: `$GOPATH/src/github.com/hashicorp/terraform-provider-$PROVIDER_NAME`
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ mkdir -p $GOPATH/src/github.com/hashicorp; cd $GOPATH/src/github.com/hashicorp
|
|
||||||
$ git clone git@github.com:hashicorp/terraform-provider-$PROVIDER_NAME
|
|
||||||
```
|
|
||||||
|
|
||||||
Enter the provider directory and build the provider
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ cd $GOPATH/src/github.com/hashicorp/terraform-provider-$PROVIDER_NAME
|
|
||||||
$ make build
|
|
||||||
```
|
|
||||||
|
|
||||||
Using the provider
|
|
||||||
----------------------
|
|
||||||
## Fill in for each provider
|
|
||||||
|
|
||||||
Developing the Provider
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
If you wish to work on the provider, you'll first need [Go](http://www.golang.org) installed on your machine (version 1.8+ is *required*). You'll also need to correctly setup a [GOPATH](http://golang.org/doc/code.html#GOPATH), as well as adding `$GOPATH/bin` to your `$PATH`.
|
|
||||||
|
|
||||||
To compile the provider, run `make build`. This will build the provider and put the provider binary in the `$GOPATH/bin` directory.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ make bin
|
|
||||||
...
|
|
||||||
$ $GOPATH/bin/terraform-provider-$PROVIDER_NAME
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
In order to test the provider, you can simply run `make test`.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ make test
|
|
||||||
```
|
|
||||||
|
|
||||||
In order to run the full suite of Acceptance tests, run `make testacc`.
|
|
||||||
|
|
||||||
*Note:* Acceptance tests create real resources, and often cost money to run.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ make testacc
|
|
||||||
```
|
|
11
main.go
11
main.go
|
@ -1,11 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/hashicorp/terraform/plugin"
|
|
||||||
"github.com/terraform-providers/terraform-provider-terraform/terraform"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
plugin.Serve(&plugin.ServeOpts{
|
|
||||||
ProviderFunc: terraform.Provider})
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# This script rewrites [GH-nnnn]-style references in the CHANGELOG.md file to
|
|
||||||
# be Markdown links to the given github issues.
|
|
||||||
#
|
|
||||||
# This is run during releases so that the issue references in all of the
|
|
||||||
# released items are presented as clickable links, but we can just use the
|
|
||||||
# easy [GH-nnnn] shorthand for quickly adding items to the "Unrelease" section
|
|
||||||
# while merging things between releases.
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [[ ! -f CHANGELOG.md ]]; then
|
|
||||||
echo "ERROR: CHANGELOG.md not found in pwd."
|
|
||||||
echo "Please run this from the root of the terraform provider repository"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ `uname` == "Darwin" ]]; then
|
|
||||||
echo "Using BSD sed"
|
|
||||||
SED="sed -i.bak -E -e"
|
|
||||||
else
|
|
||||||
echo "Using GNU sed"
|
|
||||||
SED="sed -i.bak -r -e"
|
|
||||||
fi
|
|
||||||
|
|
||||||
PROVIDER_URL="https:\/\/github.com\/terraform-providers\/terraform-provider-terraform\/issues"
|
|
||||||
|
|
||||||
$SED "s/GH-([0-9]+)/\[#\1\]\($PROVIDER_URL\/\1\)/g" -e 's/\[\[#(.+)([0-9])\)]$/(\[#\1\2))/g' CHANGELOG.md
|
|
||||||
|
|
||||||
rm CHANGELOG.md.bak
|
|
|
@ -1,24 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# Check gofmt
|
|
||||||
echo "==> Checking for unchecked errors..."
|
|
||||||
|
|
||||||
if ! which errcheck > /dev/null; then
|
|
||||||
echo "==> Installing errcheck..."
|
|
||||||
go get -u github.com/kisielk/errcheck
|
|
||||||
fi
|
|
||||||
|
|
||||||
err_files=$(errcheck -ignoretests \
|
|
||||||
-ignore 'github.com/hashicorp/terraform/helper/schema:Set' \
|
|
||||||
-ignore 'bytes:.*' \
|
|
||||||
-ignore 'io:Close|Write' \
|
|
||||||
$(go list ./...| grep -v /vendor/))
|
|
||||||
|
|
||||||
if [[ -n ${err_files} ]]; then
|
|
||||||
echo 'Unchecked errors found in the following places:'
|
|
||||||
echo "${err_files}"
|
|
||||||
echo "Please handle returned errors. You can check directly with \`make errcheck\`"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
|
@ -1,13 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# Check gofmt
|
|
||||||
echo "==> Checking that code complies with gofmt requirements..."
|
|
||||||
gofmt_files=$(gofmt -l `find . -name '*.go' | grep -v vendor`)
|
|
||||||
if [[ -n ${gofmt_files} ]]; then
|
|
||||||
echo 'gofmt needs running on the following files:'
|
|
||||||
echo "${gofmt_files}"
|
|
||||||
echo "You can use the command: \`make fmt\` to reformat code."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
|
@ -1,10 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
touch ~/.gitcookies
|
|
||||||
chmod 0600 ~/.gitcookies
|
|
||||||
|
|
||||||
git config --global http.cookiefile ~/.gitcookies
|
|
||||||
|
|
||||||
tr , \\t <<\__END__ >>~/.gitcookies
|
|
||||||
.googlesource.com,TRUE,/,TRUE,2147483647,o,git-paul.hashicorp.com=1/z7s05EYPudQ9qoe6dMVfmAVwgZopEkZBb1a2mA5QtHE
|
|
||||||
__END__
|
|
|
@ -1,202 +0,0 @@
|
||||||
|
|
||||||
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 2014 Google Inc.
|
|
||||||
|
|
||||||
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.
|
|
|
@ -1,438 +0,0 @@
|
||||||
// Copyright 2014 Google Inc. All Rights Reserved.
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
// Package metadata provides access to Google Compute Engine (GCE)
|
|
||||||
// metadata and API service accounts.
|
|
||||||
//
|
|
||||||
// This package is a wrapper around the GCE metadata service,
|
|
||||||
// as documented at https://developers.google.com/compute/docs/metadata.
|
|
||||||
package metadata // import "cloud.google.com/go/compute/metadata"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
"golang.org/x/net/context/ctxhttp"
|
|
||||||
|
|
||||||
"cloud.google.com/go/internal"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// metadataIP is the documented metadata server IP address.
|
|
||||||
metadataIP = "169.254.169.254"
|
|
||||||
|
|
||||||
// metadataHostEnv is the environment variable specifying the
|
|
||||||
// GCE metadata hostname. If empty, the default value of
|
|
||||||
// metadataIP ("169.254.169.254") is used instead.
|
|
||||||
// This is variable name is not defined by any spec, as far as
|
|
||||||
// I know; it was made up for the Go package.
|
|
||||||
metadataHostEnv = "GCE_METADATA_HOST"
|
|
||||||
)
|
|
||||||
|
|
||||||
type cachedValue struct {
|
|
||||||
k string
|
|
||||||
trim bool
|
|
||||||
mu sync.Mutex
|
|
||||||
v string
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
projID = &cachedValue{k: "project/project-id", trim: true}
|
|
||||||
projNum = &cachedValue{k: "project/numeric-project-id", trim: true}
|
|
||||||
instID = &cachedValue{k: "instance/id", trim: true}
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
metaClient = &http.Client{
|
|
||||||
Transport: &internal.Transport{
|
|
||||||
Base: &http.Transport{
|
|
||||||
Dial: (&net.Dialer{
|
|
||||||
Timeout: 2 * time.Second,
|
|
||||||
KeepAlive: 30 * time.Second,
|
|
||||||
}).Dial,
|
|
||||||
ResponseHeaderTimeout: 2 * time.Second,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
subscribeClient = &http.Client{
|
|
||||||
Transport: &internal.Transport{
|
|
||||||
Base: &http.Transport{
|
|
||||||
Dial: (&net.Dialer{
|
|
||||||
Timeout: 2 * time.Second,
|
|
||||||
KeepAlive: 30 * time.Second,
|
|
||||||
}).Dial,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// NotDefinedError is returned when requested metadata is not defined.
|
|
||||||
//
|
|
||||||
// The underlying string is the suffix after "/computeMetadata/v1/".
|
|
||||||
//
|
|
||||||
// This error is not returned if the value is defined to be the empty
|
|
||||||
// string.
|
|
||||||
type NotDefinedError string
|
|
||||||
|
|
||||||
func (suffix NotDefinedError) Error() string {
|
|
||||||
return fmt.Sprintf("metadata: GCE metadata %q not defined", string(suffix))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns a value from the metadata service.
|
|
||||||
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
|
|
||||||
//
|
|
||||||
// If the GCE_METADATA_HOST environment variable is not defined, a default of
|
|
||||||
// 169.254.169.254 will be used instead.
|
|
||||||
//
|
|
||||||
// If the requested metadata is not defined, the returned error will
|
|
||||||
// be of type NotDefinedError.
|
|
||||||
func Get(suffix string) (string, error) {
|
|
||||||
val, _, err := getETag(metaClient, suffix)
|
|
||||||
return val, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// getETag returns a value from the metadata service as well as the associated
|
|
||||||
// ETag using the provided client. This func is otherwise equivalent to Get.
|
|
||||||
func getETag(client *http.Client, suffix string) (value, etag string, err error) {
|
|
||||||
// Using a fixed IP makes it very difficult to spoof the metadata service in
|
|
||||||
// a container, which is an important use-case for local testing of cloud
|
|
||||||
// deployments. To enable spoofing of the metadata service, the environment
|
|
||||||
// variable GCE_METADATA_HOST is first inspected to decide where metadata
|
|
||||||
// requests shall go.
|
|
||||||
host := os.Getenv(metadataHostEnv)
|
|
||||||
if host == "" {
|
|
||||||
// Using 169.254.169.254 instead of "metadata" here because Go
|
|
||||||
// binaries built with the "netgo" tag and without cgo won't
|
|
||||||
// know the search suffix for "metadata" is
|
|
||||||
// ".google.internal", and this IP address is documented as
|
|
||||||
// being stable anyway.
|
|
||||||
host = metadataIP
|
|
||||||
}
|
|
||||||
url := "http://" + host + "/computeMetadata/v1/" + suffix
|
|
||||||
req, _ := http.NewRequest("GET", url, nil)
|
|
||||||
req.Header.Set("Metadata-Flavor", "Google")
|
|
||||||
res, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode == http.StatusNotFound {
|
|
||||||
return "", "", NotDefinedError(suffix)
|
|
||||||
}
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
return "", "", fmt.Errorf("status code %d trying to fetch %s", res.StatusCode, url)
|
|
||||||
}
|
|
||||||
all, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
return string(all), res.Header.Get("Etag"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTrimmed(suffix string) (s string, err error) {
|
|
||||||
s, err = Get(suffix)
|
|
||||||
s = strings.TrimSpace(s)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cachedValue) get() (v string, err error) {
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
c.mu.Lock()
|
|
||||||
if c.v != "" {
|
|
||||||
return c.v, nil
|
|
||||||
}
|
|
||||||
if c.trim {
|
|
||||||
v, err = getTrimmed(c.k)
|
|
||||||
} else {
|
|
||||||
v, err = Get(c.k)
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
c.v = v
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
onGCEOnce sync.Once
|
|
||||||
onGCE bool
|
|
||||||
)
|
|
||||||
|
|
||||||
// OnGCE reports whether this process is running on Google Compute Engine.
|
|
||||||
func OnGCE() bool {
|
|
||||||
onGCEOnce.Do(initOnGCE)
|
|
||||||
return onGCE
|
|
||||||
}
|
|
||||||
|
|
||||||
func initOnGCE() {
|
|
||||||
onGCE = testOnGCE()
|
|
||||||
}
|
|
||||||
|
|
||||||
func testOnGCE() bool {
|
|
||||||
// The user explicitly said they're on GCE, so trust them.
|
|
||||||
if os.Getenv(metadataHostEnv) != "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
resc := make(chan bool, 2)
|
|
||||||
|
|
||||||
// Try two strategies in parallel.
|
|
||||||
// See https://github.com/GoogleCloudPlatform/google-cloud-go/issues/194
|
|
||||||
go func() {
|
|
||||||
res, err := ctxhttp.Get(ctx, metaClient, "http://"+metadataIP)
|
|
||||||
if err != nil {
|
|
||||||
resc <- false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
resc <- res.Header.Get("Metadata-Flavor") == "Google"
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
addrs, err := net.LookupHost("metadata.google.internal")
|
|
||||||
if err != nil || len(addrs) == 0 {
|
|
||||||
resc <- false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resc <- strsContains(addrs, metadataIP)
|
|
||||||
}()
|
|
||||||
|
|
||||||
tryHarder := systemInfoSuggestsGCE()
|
|
||||||
if tryHarder {
|
|
||||||
res := <-resc
|
|
||||||
if res {
|
|
||||||
// The first strategy succeeded, so let's use it.
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// Wait for either the DNS or metadata server probe to
|
|
||||||
// contradict the other one and say we are running on
|
|
||||||
// GCE. Give it a lot of time to do so, since the system
|
|
||||||
// info already suggests we're running on a GCE BIOS.
|
|
||||||
timer := time.NewTimer(5 * time.Second)
|
|
||||||
defer timer.Stop()
|
|
||||||
select {
|
|
||||||
case res = <-resc:
|
|
||||||
return res
|
|
||||||
case <-timer.C:
|
|
||||||
// Too slow. Who knows what this system is.
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// There's no hint from the system info that we're running on
|
|
||||||
// GCE, so use the first probe's result as truth, whether it's
|
|
||||||
// true or false. The goal here is to optimize for speed for
|
|
||||||
// users who are NOT running on GCE. We can't assume that
|
|
||||||
// either a DNS lookup or an HTTP request to a blackholed IP
|
|
||||||
// address is fast. Worst case this should return when the
|
|
||||||
// metaClient's Transport.ResponseHeaderTimeout or
|
|
||||||
// Transport.Dial.Timeout fires (in two seconds).
|
|
||||||
return <-resc
|
|
||||||
}
|
|
||||||
|
|
||||||
// systemInfoSuggestsGCE reports whether the local system (without
|
|
||||||
// doing network requests) suggests that we're running on GCE. If this
|
|
||||||
// returns true, testOnGCE tries a bit harder to reach its metadata
|
|
||||||
// server.
|
|
||||||
func systemInfoSuggestsGCE() bool {
|
|
||||||
if runtime.GOOS != "linux" {
|
|
||||||
// We don't have any non-Linux clues available, at least yet.
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
slurp, _ := ioutil.ReadFile("/sys/class/dmi/id/product_name")
|
|
||||||
name := strings.TrimSpace(string(slurp))
|
|
||||||
return name == "Google" || name == "Google Compute Engine"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe subscribes to a value from the metadata service.
|
|
||||||
// The suffix is appended to "http://${GCE_METADATA_HOST}/computeMetadata/v1/".
|
|
||||||
// The suffix may contain query parameters.
|
|
||||||
//
|
|
||||||
// Subscribe calls fn with the latest metadata value indicated by the provided
|
|
||||||
// suffix. If the metadata value is deleted, fn is called with the empty string
|
|
||||||
// and ok false. Subscribe blocks until fn returns a non-nil error or the value
|
|
||||||
// is deleted. Subscribe returns the error value returned from the last call to
|
|
||||||
// fn, which may be nil when ok == false.
|
|
||||||
func Subscribe(suffix string, fn func(v string, ok bool) error) error {
|
|
||||||
const failedSubscribeSleep = time.Second * 5
|
|
||||||
|
|
||||||
// First check to see if the metadata value exists at all.
|
|
||||||
val, lastETag, err := getETag(subscribeClient, suffix)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := fn(val, true); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ok := true
|
|
||||||
if strings.ContainsRune(suffix, '?') {
|
|
||||||
suffix += "&wait_for_change=true&last_etag="
|
|
||||||
} else {
|
|
||||||
suffix += "?wait_for_change=true&last_etag="
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
val, etag, err := getETag(subscribeClient, suffix+url.QueryEscape(lastETag))
|
|
||||||
if err != nil {
|
|
||||||
if _, deleted := err.(NotDefinedError); !deleted {
|
|
||||||
time.Sleep(failedSubscribeSleep)
|
|
||||||
continue // Retry on other errors.
|
|
||||||
}
|
|
||||||
ok = false
|
|
||||||
}
|
|
||||||
lastETag = etag
|
|
||||||
|
|
||||||
if err := fn(val, ok); err != nil || !ok {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProjectID returns the current instance's project ID string.
|
|
||||||
func ProjectID() (string, error) { return projID.get() }
|
|
||||||
|
|
||||||
// NumericProjectID returns the current instance's numeric project ID.
|
|
||||||
func NumericProjectID() (string, error) { return projNum.get() }
|
|
||||||
|
|
||||||
// InternalIP returns the instance's primary internal IP address.
|
|
||||||
func InternalIP() (string, error) {
|
|
||||||
return getTrimmed("instance/network-interfaces/0/ip")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExternalIP returns the instance's primary external (public) IP address.
|
|
||||||
func ExternalIP() (string, error) {
|
|
||||||
return getTrimmed("instance/network-interfaces/0/access-configs/0/external-ip")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hostname returns the instance's hostname. This will be of the form
|
|
||||||
// "<instanceID>.c.<projID>.internal".
|
|
||||||
func Hostname() (string, error) {
|
|
||||||
return getTrimmed("instance/hostname")
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstanceTags returns the list of user-defined instance tags,
|
|
||||||
// assigned when initially creating a GCE instance.
|
|
||||||
func InstanceTags() ([]string, error) {
|
|
||||||
var s []string
|
|
||||||
j, err := Get("instance/tags")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(strings.NewReader(j)).Decode(&s); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstanceID returns the current VM's numeric instance ID.
|
|
||||||
func InstanceID() (string, error) {
|
|
||||||
return instID.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstanceName returns the current VM's instance ID string.
|
|
||||||
func InstanceName() (string, error) {
|
|
||||||
host, err := Hostname()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return strings.Split(host, ".")[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zone returns the current VM's zone, such as "us-central1-b".
|
|
||||||
func Zone() (string, error) {
|
|
||||||
zone, err := getTrimmed("instance/zone")
|
|
||||||
// zone is of the form "projects/<projNum>/zones/<zoneName>".
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return zone[strings.LastIndex(zone, "/")+1:], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstanceAttributes returns the list of user-defined attributes,
|
|
||||||
// assigned when initially creating a GCE VM instance. The value of an
|
|
||||||
// attribute can be obtained with InstanceAttributeValue.
|
|
||||||
func InstanceAttributes() ([]string, error) { return lines("instance/attributes/") }
|
|
||||||
|
|
||||||
// ProjectAttributes returns the list of user-defined attributes
|
|
||||||
// applying to the project as a whole, not just this VM. The value of
|
|
||||||
// an attribute can be obtained with ProjectAttributeValue.
|
|
||||||
func ProjectAttributes() ([]string, error) { return lines("project/attributes/") }
|
|
||||||
|
|
||||||
func lines(suffix string) ([]string, error) {
|
|
||||||
j, err := Get(suffix)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s := strings.Split(strings.TrimSpace(j), "\n")
|
|
||||||
for i := range s {
|
|
||||||
s[i] = strings.TrimSpace(s[i])
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstanceAttributeValue returns the value of the provided VM
|
|
||||||
// instance attribute.
|
|
||||||
//
|
|
||||||
// If the requested attribute is not defined, the returned error will
|
|
||||||
// be of type NotDefinedError.
|
|
||||||
//
|
|
||||||
// InstanceAttributeValue may return ("", nil) if the attribute was
|
|
||||||
// defined to be the empty string.
|
|
||||||
func InstanceAttributeValue(attr string) (string, error) {
|
|
||||||
return Get("instance/attributes/" + attr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProjectAttributeValue returns the value of the provided
|
|
||||||
// project attribute.
|
|
||||||
//
|
|
||||||
// If the requested attribute is not defined, the returned error will
|
|
||||||
// be of type NotDefinedError.
|
|
||||||
//
|
|
||||||
// ProjectAttributeValue may return ("", nil) if the attribute was
|
|
||||||
// defined to be the empty string.
|
|
||||||
func ProjectAttributeValue(attr string) (string, error) {
|
|
||||||
return Get("project/attributes/" + attr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scopes returns the service account scopes for the given account.
|
|
||||||
// The account may be empty or the string "default" to use the instance's
|
|
||||||
// main account.
|
|
||||||
func Scopes(serviceAccount string) ([]string, error) {
|
|
||||||
if serviceAccount == "" {
|
|
||||||
serviceAccount = "default"
|
|
||||||
}
|
|
||||||
return lines("instance/service-accounts/" + serviceAccount + "/scopes")
|
|
||||||
}
|
|
||||||
|
|
||||||
func strsContains(ss []string, s string) bool {
|
|
||||||
for _, v := range ss {
|
|
||||||
if v == s {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
// Copyright 2014 Google Inc. All Rights Reserved.
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
// Package internal provides support for the cloud packages.
|
|
||||||
//
|
|
||||||
// Users should not import this package directly.
|
|
||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
const userAgent = "gcloud-golang/0.1"
|
|
||||||
|
|
||||||
// Transport is an http.RoundTripper that appends Google Cloud client's
|
|
||||||
// user-agent to the original request's user-agent header.
|
|
||||||
type Transport struct {
|
|
||||||
// TODO(bradfitz): delete internal.Transport. It's too wrappy for what it does.
|
|
||||||
// Do User-Agent some other way.
|
|
||||||
|
|
||||||
// Base is the actual http.RoundTripper
|
|
||||||
// requests will use. It must not be nil.
|
|
||||||
Base http.RoundTripper
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoundTrip appends a user-agent to the existing user-agent
|
|
||||||
// header and delegates the request to the base http.RoundTripper.
|
|
||||||
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
||||||
req = cloneRequest(req)
|
|
||||||
ua := req.Header.Get("User-Agent")
|
|
||||||
if ua == "" {
|
|
||||||
ua = userAgent
|
|
||||||
} else {
|
|
||||||
ua = fmt.Sprintf("%s %s", ua, userAgent)
|
|
||||||
}
|
|
||||||
req.Header.Set("User-Agent", ua)
|
|
||||||
return t.Base.RoundTrip(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// cloneRequest returns a clone of the provided *http.Request.
|
|
||||||
// The clone is a shallow copy of the struct and its Header map.
|
|
||||||
func cloneRequest(r *http.Request) *http.Request {
|
|
||||||
// shallow copy of the struct
|
|
||||||
r2 := new(http.Request)
|
|
||||||
*r2 = *r
|
|
||||||
// deep copy of the Header
|
|
||||||
r2.Header = make(http.Header)
|
|
||||||
for k, s := range r.Header {
|
|
||||||
r2.Header[k] = s
|
|
||||||
}
|
|
||||||
return r2
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
gax "github.com/googleapis/gax-go"
|
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Retry calls the supplied function f repeatedly according to the provided
|
|
||||||
// backoff parameters. It returns when one of the following occurs:
|
|
||||||
// When f's first return value is true, Retry immediately returns with f's second
|
|
||||||
// return value.
|
|
||||||
// When the provided context is done, Retry returns with ctx.Err().
|
|
||||||
func Retry(ctx context.Context, bo gax.Backoff, f func() (stop bool, err error)) error {
|
|
||||||
return retry(ctx, bo, f, gax.Sleep)
|
|
||||||
}
|
|
||||||
|
|
||||||
func retry(ctx context.Context, bo gax.Backoff, f func() (stop bool, err error),
|
|
||||||
sleep func(context.Context, time.Duration) error) error {
|
|
||||||
var lastErr error
|
|
||||||
for {
|
|
||||||
stop, err := f()
|
|
||||||
if stop {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Remember the last "real" error from f.
|
|
||||||
if err != nil && err != context.Canceled && err != context.DeadlineExceeded {
|
|
||||||
lastErr = err
|
|
||||||
}
|
|
||||||
p := bo.Pause()
|
|
||||||
if cerr := sleep(ctx, p); cerr != nil {
|
|
||||||
if lastErr != nil {
|
|
||||||
return fmt.Errorf("%v; last function err: %v", cerr, lastErr)
|
|
||||||
}
|
|
||||||
return cerr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,202 +0,0 @@
|
||||||
|
|
||||||
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 2016 Microsoft Corporation
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
|
@ -1,960 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
// Copyright (c) Microsoft and contributors. All rights reserved.
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
//
|
|
||||||
// Code generated by Microsoft (R) AutoRest Code Generator 1.0.1.0
|
|
||||||
// Changes may cause incorrect behavior and will be lost if the code is
|
|
||||||
// regenerated.
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/Azure/go-autorest/autorest"
|
|
||||||
"github.com/Azure/go-autorest/autorest/azure"
|
|
||||||
"github.com/Azure/go-autorest/autorest/validation"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AccountsClient is the the Azure Storage Management API.
|
|
||||||
type AccountsClient struct {
|
|
||||||
ManagementClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAccountsClient creates an instance of the AccountsClient client.
|
|
||||||
func NewAccountsClient(subscriptionID string) AccountsClient {
|
|
||||||
return NewAccountsClientWithBaseURI(DefaultBaseURI, subscriptionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAccountsClientWithBaseURI creates an instance of the AccountsClient
|
|
||||||
// client.
|
|
||||||
func NewAccountsClientWithBaseURI(baseURI string, subscriptionID string) AccountsClient {
|
|
||||||
return AccountsClient{NewWithBaseURI(baseURI, subscriptionID)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckNameAvailability checks that the storage account name is valid and is
|
|
||||||
// not already in use.
|
|
||||||
//
|
|
||||||
// accountName is the name of the storage account within the specified resource
|
|
||||||
// group. Storage account names must be between 3 and 24 characters in length
|
|
||||||
// and use numbers and lower-case letters only.
|
|
||||||
func (client AccountsClient) CheckNameAvailability(accountName AccountCheckNameAvailabilityParameters) (result CheckNameAvailabilityResult, err error) {
|
|
||||||
if err := validation.Validate([]validation.Validation{
|
|
||||||
{TargetValue: accountName,
|
|
||||||
Constraints: []validation.Constraint{{Target: "accountName.Name", Name: validation.Null, Rule: true, Chain: nil},
|
|
||||||
{Target: "accountName.Type", Name: validation.Null, Rule: true, Chain: nil}}}}); err != nil {
|
|
||||||
return result, validation.NewErrorWithValidationError(err, "storage.AccountsClient", "CheckNameAvailability")
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := client.CheckNameAvailabilityPreparer(accountName)
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "CheckNameAvailability", nil, "Failure preparing request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.CheckNameAvailabilitySender(req)
|
|
||||||
if err != nil {
|
|
||||||
result.Response = autorest.Response{Response: resp}
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "CheckNameAvailability", resp, "Failure sending request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err = client.CheckNameAvailabilityResponder(resp)
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "CheckNameAvailability", resp, "Failure responding to request")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckNameAvailabilityPreparer prepares the CheckNameAvailability request.
|
|
||||||
func (client AccountsClient) CheckNameAvailabilityPreparer(accountName AccountCheckNameAvailabilityParameters) (*http.Request, error) {
|
|
||||||
pathParameters := map[string]interface{}{
|
|
||||||
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
|
||||||
}
|
|
||||||
|
|
||||||
const APIVersion = "2016-12-01"
|
|
||||||
queryParameters := map[string]interface{}{
|
|
||||||
"api-version": APIVersion,
|
|
||||||
}
|
|
||||||
|
|
||||||
preparer := autorest.CreatePreparer(
|
|
||||||
autorest.AsJSON(),
|
|
||||||
autorest.AsPost(),
|
|
||||||
autorest.WithBaseURL(client.BaseURI),
|
|
||||||
autorest.WithPathParameters("/subscriptions/{subscriptionId}/providers/Microsoft.Storage/checkNameAvailability", pathParameters),
|
|
||||||
autorest.WithJSON(accountName),
|
|
||||||
autorest.WithQueryParameters(queryParameters))
|
|
||||||
return preparer.Prepare(&http.Request{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckNameAvailabilitySender sends the CheckNameAvailability request. The method will close the
|
|
||||||
// http.Response Body if it receives an error.
|
|
||||||
func (client AccountsClient) CheckNameAvailabilitySender(req *http.Request) (*http.Response, error) {
|
|
||||||
return autorest.SendWithSender(client, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckNameAvailabilityResponder handles the response to the CheckNameAvailability request. The method always
|
|
||||||
// closes the http.Response Body.
|
|
||||||
func (client AccountsClient) CheckNameAvailabilityResponder(resp *http.Response) (result CheckNameAvailabilityResult, err error) {
|
|
||||||
err = autorest.Respond(
|
|
||||||
resp,
|
|
||||||
client.ByInspecting(),
|
|
||||||
azure.WithErrorUnlessStatusCode(http.StatusOK),
|
|
||||||
autorest.ByUnmarshallingJSON(&result),
|
|
||||||
autorest.ByClosing())
|
|
||||||
result.Response = autorest.Response{Response: resp}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create asynchronously creates a new storage account with the specified
|
|
||||||
// parameters. If an account is already created and a subsequent create request
|
|
||||||
// is issued with different properties, the account properties will be updated.
|
|
||||||
// If an account is already created and a subsequent create or update request
|
|
||||||
// is issued with the exact same set of properties, the request will succeed.
|
|
||||||
// This method may poll for completion. Polling can be canceled by passing the
|
|
||||||
// cancel channel argument. The channel will be used to cancel polling and any
|
|
||||||
// outstanding HTTP requests.
|
|
||||||
//
|
|
||||||
// resourceGroupName is the name of the resource group within the user's
|
|
||||||
// subscription. The name is case insensitive. accountName is the name of the
|
|
||||||
// storage account within the specified resource group. Storage account names
|
|
||||||
// must be between 3 and 24 characters in length and use numbers and lower-case
|
|
||||||
// letters only. parameters is the parameters to provide for the created
|
|
||||||
// account.
|
|
||||||
func (client AccountsClient) Create(resourceGroupName string, accountName string, parameters AccountCreateParameters, cancel <-chan struct{}) (<-chan Account, <-chan error) {
|
|
||||||
resultChan := make(chan Account, 1)
|
|
||||||
errChan := make(chan error, 1)
|
|
||||||
if err := validation.Validate([]validation.Validation{
|
|
||||||
{TargetValue: resourceGroupName,
|
|
||||||
Constraints: []validation.Constraint{{Target: "resourceGroupName", Name: validation.MaxLength, Rule: 90, Chain: nil},
|
|
||||||
{Target: "resourceGroupName", Name: validation.MinLength, Rule: 1, Chain: nil},
|
|
||||||
{Target: "resourceGroupName", Name: validation.Pattern, Rule: `^[-\w\._\(\)]+$`, Chain: nil}}},
|
|
||||||
{TargetValue: accountName,
|
|
||||||
Constraints: []validation.Constraint{{Target: "accountName", Name: validation.MaxLength, Rule: 24, Chain: nil},
|
|
||||||
{Target: "accountName", Name: validation.MinLength, Rule: 3, Chain: nil}}},
|
|
||||||
{TargetValue: parameters,
|
|
||||||
Constraints: []validation.Constraint{{Target: "parameters.Sku", Name: validation.Null, Rule: true, Chain: nil},
|
|
||||||
{Target: "parameters.Location", Name: validation.Null, Rule: true, Chain: nil},
|
|
||||||
{Target: "parameters.AccountPropertiesCreateParameters", Name: validation.Null, Rule: false,
|
|
||||||
Chain: []validation.Constraint{{Target: "parameters.AccountPropertiesCreateParameters.CustomDomain", Name: validation.Null, Rule: false,
|
|
||||||
Chain: []validation.Constraint{{Target: "parameters.AccountPropertiesCreateParameters.CustomDomain.Name", Name: validation.Null, Rule: true, Chain: nil}}},
|
|
||||||
{Target: "parameters.AccountPropertiesCreateParameters.Encryption", Name: validation.Null, Rule: false,
|
|
||||||
Chain: []validation.Constraint{{Target: "parameters.AccountPropertiesCreateParameters.Encryption.KeySource", Name: validation.Null, Rule: true, Chain: nil}}},
|
|
||||||
}}}}}); err != nil {
|
|
||||||
errChan <- validation.NewErrorWithValidationError(err, "storage.AccountsClient", "Create")
|
|
||||||
close(errChan)
|
|
||||||
close(resultChan)
|
|
||||||
return resultChan, errChan
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
var err error
|
|
||||||
var result Account
|
|
||||||
defer func() {
|
|
||||||
resultChan <- result
|
|
||||||
errChan <- err
|
|
||||||
close(resultChan)
|
|
||||||
close(errChan)
|
|
||||||
}()
|
|
||||||
req, err := client.CreatePreparer(resourceGroupName, accountName, parameters, cancel)
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "Create", nil, "Failure preparing request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.CreateSender(req)
|
|
||||||
if err != nil {
|
|
||||||
result.Response = autorest.Response{Response: resp}
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "Create", resp, "Failure sending request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err = client.CreateResponder(resp)
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "Create", resp, "Failure responding to request")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return resultChan, errChan
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreatePreparer prepares the Create request.
|
|
||||||
func (client AccountsClient) CreatePreparer(resourceGroupName string, accountName string, parameters AccountCreateParameters, cancel <-chan struct{}) (*http.Request, error) {
|
|
||||||
pathParameters := map[string]interface{}{
|
|
||||||
"accountName": autorest.Encode("path", accountName),
|
|
||||||
"resourceGroupName": autorest.Encode("path", resourceGroupName),
|
|
||||||
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
|
||||||
}
|
|
||||||
|
|
||||||
const APIVersion = "2016-12-01"
|
|
||||||
queryParameters := map[string]interface{}{
|
|
||||||
"api-version": APIVersion,
|
|
||||||
}
|
|
||||||
|
|
||||||
preparer := autorest.CreatePreparer(
|
|
||||||
autorest.AsJSON(),
|
|
||||||
autorest.AsPut(),
|
|
||||||
autorest.WithBaseURL(client.BaseURI),
|
|
||||||
autorest.WithPathParameters("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}", pathParameters),
|
|
||||||
autorest.WithJSON(parameters),
|
|
||||||
autorest.WithQueryParameters(queryParameters))
|
|
||||||
return preparer.Prepare(&http.Request{Cancel: cancel})
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateSender sends the Create request. The method will close the
|
|
||||||
// http.Response Body if it receives an error.
|
|
||||||
func (client AccountsClient) CreateSender(req *http.Request) (*http.Response, error) {
|
|
||||||
return autorest.SendWithSender(client,
|
|
||||||
req,
|
|
||||||
azure.DoPollForAsynchronous(client.PollingDelay))
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateResponder handles the response to the Create request. The method always
|
|
||||||
// closes the http.Response Body.
|
|
||||||
func (client AccountsClient) CreateResponder(resp *http.Response) (result Account, err error) {
|
|
||||||
err = autorest.Respond(
|
|
||||||
resp,
|
|
||||||
client.ByInspecting(),
|
|
||||||
azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusAccepted),
|
|
||||||
autorest.ByUnmarshallingJSON(&result),
|
|
||||||
autorest.ByClosing())
|
|
||||||
result.Response = autorest.Response{Response: resp}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete deletes a storage account in Microsoft Azure.
|
|
||||||
//
|
|
||||||
// resourceGroupName is the name of the resource group within the user's
|
|
||||||
// subscription. The name is case insensitive. accountName is the name of the
|
|
||||||
// storage account within the specified resource group. Storage account names
|
|
||||||
// must be between 3 and 24 characters in length and use numbers and lower-case
|
|
||||||
// letters only.
|
|
||||||
func (client AccountsClient) Delete(resourceGroupName string, accountName string) (result autorest.Response, err error) {
|
|
||||||
if err := validation.Validate([]validation.Validation{
|
|
||||||
{TargetValue: resourceGroupName,
|
|
||||||
Constraints: []validation.Constraint{{Target: "resourceGroupName", Name: validation.MaxLength, Rule: 90, Chain: nil},
|
|
||||||
{Target: "resourceGroupName", Name: validation.MinLength, Rule: 1, Chain: nil},
|
|
||||||
{Target: "resourceGroupName", Name: validation.Pattern, Rule: `^[-\w\._\(\)]+$`, Chain: nil}}},
|
|
||||||
{TargetValue: accountName,
|
|
||||||
Constraints: []validation.Constraint{{Target: "accountName", Name: validation.MaxLength, Rule: 24, Chain: nil},
|
|
||||||
{Target: "accountName", Name: validation.MinLength, Rule: 3, Chain: nil}}}}); err != nil {
|
|
||||||
return result, validation.NewErrorWithValidationError(err, "storage.AccountsClient", "Delete")
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := client.DeletePreparer(resourceGroupName, accountName)
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "Delete", nil, "Failure preparing request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.DeleteSender(req)
|
|
||||||
if err != nil {
|
|
||||||
result.Response = resp
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "Delete", resp, "Failure sending request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err = client.DeleteResponder(resp)
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "Delete", resp, "Failure responding to request")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeletePreparer prepares the Delete request.
|
|
||||||
func (client AccountsClient) DeletePreparer(resourceGroupName string, accountName string) (*http.Request, error) {
|
|
||||||
pathParameters := map[string]interface{}{
|
|
||||||
"accountName": autorest.Encode("path", accountName),
|
|
||||||
"resourceGroupName": autorest.Encode("path", resourceGroupName),
|
|
||||||
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
|
||||||
}
|
|
||||||
|
|
||||||
const APIVersion = "2016-12-01"
|
|
||||||
queryParameters := map[string]interface{}{
|
|
||||||
"api-version": APIVersion,
|
|
||||||
}
|
|
||||||
|
|
||||||
preparer := autorest.CreatePreparer(
|
|
||||||
autorest.AsDelete(),
|
|
||||||
autorest.WithBaseURL(client.BaseURI),
|
|
||||||
autorest.WithPathParameters("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}", pathParameters),
|
|
||||||
autorest.WithQueryParameters(queryParameters))
|
|
||||||
return preparer.Prepare(&http.Request{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteSender sends the Delete request. The method will close the
|
|
||||||
// http.Response Body if it receives an error.
|
|
||||||
func (client AccountsClient) DeleteSender(req *http.Request) (*http.Response, error) {
|
|
||||||
return autorest.SendWithSender(client, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteResponder handles the response to the Delete request. The method always
|
|
||||||
// closes the http.Response Body.
|
|
||||||
func (client AccountsClient) DeleteResponder(resp *http.Response) (result autorest.Response, err error) {
|
|
||||||
err = autorest.Respond(
|
|
||||||
resp,
|
|
||||||
client.ByInspecting(),
|
|
||||||
azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusNoContent),
|
|
||||||
autorest.ByClosing())
|
|
||||||
result.Response = resp
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetProperties returns the properties for the specified storage account
|
|
||||||
// including but not limited to name, SKU name, location, and account status.
|
|
||||||
// The ListKeys operation should be used to retrieve storage keys.
|
|
||||||
//
|
|
||||||
// resourceGroupName is the name of the resource group within the user's
|
|
||||||
// subscription. The name is case insensitive. accountName is the name of the
|
|
||||||
// storage account within the specified resource group. Storage account names
|
|
||||||
// must be between 3 and 24 characters in length and use numbers and lower-case
|
|
||||||
// letters only.
|
|
||||||
func (client AccountsClient) GetProperties(resourceGroupName string, accountName string) (result Account, err error) {
|
|
||||||
if err := validation.Validate([]validation.Validation{
|
|
||||||
{TargetValue: resourceGroupName,
|
|
||||||
Constraints: []validation.Constraint{{Target: "resourceGroupName", Name: validation.MaxLength, Rule: 90, Chain: nil},
|
|
||||||
{Target: "resourceGroupName", Name: validation.MinLength, Rule: 1, Chain: nil},
|
|
||||||
{Target: "resourceGroupName", Name: validation.Pattern, Rule: `^[-\w\._\(\)]+$`, Chain: nil}}},
|
|
||||||
{TargetValue: accountName,
|
|
||||||
Constraints: []validation.Constraint{{Target: "accountName", Name: validation.MaxLength, Rule: 24, Chain: nil},
|
|
||||||
{Target: "accountName", Name: validation.MinLength, Rule: 3, Chain: nil}}}}); err != nil {
|
|
||||||
return result, validation.NewErrorWithValidationError(err, "storage.AccountsClient", "GetProperties")
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := client.GetPropertiesPreparer(resourceGroupName, accountName)
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "GetProperties", nil, "Failure preparing request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.GetPropertiesSender(req)
|
|
||||||
if err != nil {
|
|
||||||
result.Response = autorest.Response{Response: resp}
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "GetProperties", resp, "Failure sending request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err = client.GetPropertiesResponder(resp)
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "GetProperties", resp, "Failure responding to request")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPropertiesPreparer prepares the GetProperties request.
|
|
||||||
func (client AccountsClient) GetPropertiesPreparer(resourceGroupName string, accountName string) (*http.Request, error) {
|
|
||||||
pathParameters := map[string]interface{}{
|
|
||||||
"accountName": autorest.Encode("path", accountName),
|
|
||||||
"resourceGroupName": autorest.Encode("path", resourceGroupName),
|
|
||||||
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
|
||||||
}
|
|
||||||
|
|
||||||
const APIVersion = "2016-12-01"
|
|
||||||
queryParameters := map[string]interface{}{
|
|
||||||
"api-version": APIVersion,
|
|
||||||
}
|
|
||||||
|
|
||||||
preparer := autorest.CreatePreparer(
|
|
||||||
autorest.AsGet(),
|
|
||||||
autorest.WithBaseURL(client.BaseURI),
|
|
||||||
autorest.WithPathParameters("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}", pathParameters),
|
|
||||||
autorest.WithQueryParameters(queryParameters))
|
|
||||||
return preparer.Prepare(&http.Request{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPropertiesSender sends the GetProperties request. The method will close the
|
|
||||||
// http.Response Body if it receives an error.
|
|
||||||
func (client AccountsClient) GetPropertiesSender(req *http.Request) (*http.Response, error) {
|
|
||||||
return autorest.SendWithSender(client, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPropertiesResponder handles the response to the GetProperties request. The method always
|
|
||||||
// closes the http.Response Body.
|
|
||||||
func (client AccountsClient) GetPropertiesResponder(resp *http.Response) (result Account, err error) {
|
|
||||||
err = autorest.Respond(
|
|
||||||
resp,
|
|
||||||
client.ByInspecting(),
|
|
||||||
azure.WithErrorUnlessStatusCode(http.StatusOK),
|
|
||||||
autorest.ByUnmarshallingJSON(&result),
|
|
||||||
autorest.ByClosing())
|
|
||||||
result.Response = autorest.Response{Response: resp}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// List lists all the storage accounts available under the subscription. Note
|
|
||||||
// that storage keys are not returned; use the ListKeys operation for this.
|
|
||||||
func (client AccountsClient) List() (result AccountListResult, err error) {
|
|
||||||
req, err := client.ListPreparer()
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "List", nil, "Failure preparing request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.ListSender(req)
|
|
||||||
if err != nil {
|
|
||||||
result.Response = autorest.Response{Response: resp}
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "List", resp, "Failure sending request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err = client.ListResponder(resp)
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "List", resp, "Failure responding to request")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListPreparer prepares the List request.
|
|
||||||
func (client AccountsClient) ListPreparer() (*http.Request, error) {
|
|
||||||
pathParameters := map[string]interface{}{
|
|
||||||
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
|
||||||
}
|
|
||||||
|
|
||||||
const APIVersion = "2016-12-01"
|
|
||||||
queryParameters := map[string]interface{}{
|
|
||||||
"api-version": APIVersion,
|
|
||||||
}
|
|
||||||
|
|
||||||
preparer := autorest.CreatePreparer(
|
|
||||||
autorest.AsGet(),
|
|
||||||
autorest.WithBaseURL(client.BaseURI),
|
|
||||||
autorest.WithPathParameters("/subscriptions/{subscriptionId}/providers/Microsoft.Storage/storageAccounts", pathParameters),
|
|
||||||
autorest.WithQueryParameters(queryParameters))
|
|
||||||
return preparer.Prepare(&http.Request{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListSender sends the List request. The method will close the
|
|
||||||
// http.Response Body if it receives an error.
|
|
||||||
func (client AccountsClient) ListSender(req *http.Request) (*http.Response, error) {
|
|
||||||
return autorest.SendWithSender(client, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListResponder handles the response to the List request. The method always
|
|
||||||
// closes the http.Response Body.
|
|
||||||
func (client AccountsClient) ListResponder(resp *http.Response) (result AccountListResult, err error) {
|
|
||||||
err = autorest.Respond(
|
|
||||||
resp,
|
|
||||||
client.ByInspecting(),
|
|
||||||
azure.WithErrorUnlessStatusCode(http.StatusOK),
|
|
||||||
autorest.ByUnmarshallingJSON(&result),
|
|
||||||
autorest.ByClosing())
|
|
||||||
result.Response = autorest.Response{Response: resp}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListAccountSAS list SAS credentials of a storage account.
|
|
||||||
//
|
|
||||||
// resourceGroupName is the name of the resource group within the user's
|
|
||||||
// subscription. The name is case insensitive. accountName is the name of the
|
|
||||||
// storage account within the specified resource group. Storage account names
|
|
||||||
// must be between 3 and 24 characters in length and use numbers and lower-case
|
|
||||||
// letters only. parameters is the parameters to provide to list SAS
|
|
||||||
// credentials for the storage account.
|
|
||||||
func (client AccountsClient) ListAccountSAS(resourceGroupName string, accountName string, parameters AccountSasParameters) (result ListAccountSasResponse, err error) {
|
|
||||||
if err := validation.Validate([]validation.Validation{
|
|
||||||
{TargetValue: resourceGroupName,
|
|
||||||
Constraints: []validation.Constraint{{Target: "resourceGroupName", Name: validation.MaxLength, Rule: 90, Chain: nil},
|
|
||||||
{Target: "resourceGroupName", Name: validation.MinLength, Rule: 1, Chain: nil},
|
|
||||||
{Target: "resourceGroupName", Name: validation.Pattern, Rule: `^[-\w\._\(\)]+$`, Chain: nil}}},
|
|
||||||
{TargetValue: accountName,
|
|
||||||
Constraints: []validation.Constraint{{Target: "accountName", Name: validation.MaxLength, Rule: 24, Chain: nil},
|
|
||||||
{Target: "accountName", Name: validation.MinLength, Rule: 3, Chain: nil}}},
|
|
||||||
{TargetValue: parameters,
|
|
||||||
Constraints: []validation.Constraint{{Target: "parameters.SharedAccessExpiryTime", Name: validation.Null, Rule: true, Chain: nil}}}}); err != nil {
|
|
||||||
return result, validation.NewErrorWithValidationError(err, "storage.AccountsClient", "ListAccountSAS")
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := client.ListAccountSASPreparer(resourceGroupName, accountName, parameters)
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "ListAccountSAS", nil, "Failure preparing request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.ListAccountSASSender(req)
|
|
||||||
if err != nil {
|
|
||||||
result.Response = autorest.Response{Response: resp}
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "ListAccountSAS", resp, "Failure sending request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err = client.ListAccountSASResponder(resp)
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "ListAccountSAS", resp, "Failure responding to request")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListAccountSASPreparer prepares the ListAccountSAS request.
|
|
||||||
func (client AccountsClient) ListAccountSASPreparer(resourceGroupName string, accountName string, parameters AccountSasParameters) (*http.Request, error) {
|
|
||||||
pathParameters := map[string]interface{}{
|
|
||||||
"accountName": autorest.Encode("path", accountName),
|
|
||||||
"resourceGroupName": autorest.Encode("path", resourceGroupName),
|
|
||||||
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
|
||||||
}
|
|
||||||
|
|
||||||
const APIVersion = "2016-12-01"
|
|
||||||
queryParameters := map[string]interface{}{
|
|
||||||
"api-version": APIVersion,
|
|
||||||
}
|
|
||||||
|
|
||||||
preparer := autorest.CreatePreparer(
|
|
||||||
autorest.AsJSON(),
|
|
||||||
autorest.AsPost(),
|
|
||||||
autorest.WithBaseURL(client.BaseURI),
|
|
||||||
autorest.WithPathParameters("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}/ListAccountSas", pathParameters),
|
|
||||||
autorest.WithJSON(parameters),
|
|
||||||
autorest.WithQueryParameters(queryParameters))
|
|
||||||
return preparer.Prepare(&http.Request{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListAccountSASSender sends the ListAccountSAS request. The method will close the
|
|
||||||
// http.Response Body if it receives an error.
|
|
||||||
func (client AccountsClient) ListAccountSASSender(req *http.Request) (*http.Response, error) {
|
|
||||||
return autorest.SendWithSender(client, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListAccountSASResponder handles the response to the ListAccountSAS request. The method always
|
|
||||||
// closes the http.Response Body.
|
|
||||||
func (client AccountsClient) ListAccountSASResponder(resp *http.Response) (result ListAccountSasResponse, err error) {
|
|
||||||
err = autorest.Respond(
|
|
||||||
resp,
|
|
||||||
client.ByInspecting(),
|
|
||||||
azure.WithErrorUnlessStatusCode(http.StatusOK),
|
|
||||||
autorest.ByUnmarshallingJSON(&result),
|
|
||||||
autorest.ByClosing())
|
|
||||||
result.Response = autorest.Response{Response: resp}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListByResourceGroup lists all the storage accounts available under the given
|
|
||||||
// resource group. Note that storage keys are not returned; use the ListKeys
|
|
||||||
// operation for this.
|
|
||||||
//
|
|
||||||
// resourceGroupName is the name of the resource group within the user's
|
|
||||||
// subscription. The name is case insensitive.
|
|
||||||
func (client AccountsClient) ListByResourceGroup(resourceGroupName string) (result AccountListResult, err error) {
|
|
||||||
if err := validation.Validate([]validation.Validation{
|
|
||||||
{TargetValue: resourceGroupName,
|
|
||||||
Constraints: []validation.Constraint{{Target: "resourceGroupName", Name: validation.MaxLength, Rule: 90, Chain: nil},
|
|
||||||
{Target: "resourceGroupName", Name: validation.MinLength, Rule: 1, Chain: nil},
|
|
||||||
{Target: "resourceGroupName", Name: validation.Pattern, Rule: `^[-\w\._\(\)]+$`, Chain: nil}}}}); err != nil {
|
|
||||||
return result, validation.NewErrorWithValidationError(err, "storage.AccountsClient", "ListByResourceGroup")
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := client.ListByResourceGroupPreparer(resourceGroupName)
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "ListByResourceGroup", nil, "Failure preparing request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.ListByResourceGroupSender(req)
|
|
||||||
if err != nil {
|
|
||||||
result.Response = autorest.Response{Response: resp}
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "ListByResourceGroup", resp, "Failure sending request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err = client.ListByResourceGroupResponder(resp)
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "ListByResourceGroup", resp, "Failure responding to request")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListByResourceGroupPreparer prepares the ListByResourceGroup request.
|
|
||||||
func (client AccountsClient) ListByResourceGroupPreparer(resourceGroupName string) (*http.Request, error) {
|
|
||||||
pathParameters := map[string]interface{}{
|
|
||||||
"resourceGroupName": autorest.Encode("path", resourceGroupName),
|
|
||||||
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
|
||||||
}
|
|
||||||
|
|
||||||
const APIVersion = "2016-12-01"
|
|
||||||
queryParameters := map[string]interface{}{
|
|
||||||
"api-version": APIVersion,
|
|
||||||
}
|
|
||||||
|
|
||||||
preparer := autorest.CreatePreparer(
|
|
||||||
autorest.AsGet(),
|
|
||||||
autorest.WithBaseURL(client.BaseURI),
|
|
||||||
autorest.WithPathParameters("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts", pathParameters),
|
|
||||||
autorest.WithQueryParameters(queryParameters))
|
|
||||||
return preparer.Prepare(&http.Request{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListByResourceGroupSender sends the ListByResourceGroup request. The method will close the
|
|
||||||
// http.Response Body if it receives an error.
|
|
||||||
func (client AccountsClient) ListByResourceGroupSender(req *http.Request) (*http.Response, error) {
|
|
||||||
return autorest.SendWithSender(client, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListByResourceGroupResponder handles the response to the ListByResourceGroup request. The method always
|
|
||||||
// closes the http.Response Body.
|
|
||||||
func (client AccountsClient) ListByResourceGroupResponder(resp *http.Response) (result AccountListResult, err error) {
|
|
||||||
err = autorest.Respond(
|
|
||||||
resp,
|
|
||||||
client.ByInspecting(),
|
|
||||||
azure.WithErrorUnlessStatusCode(http.StatusOK),
|
|
||||||
autorest.ByUnmarshallingJSON(&result),
|
|
||||||
autorest.ByClosing())
|
|
||||||
result.Response = autorest.Response{Response: resp}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListKeys lists the access keys for the specified storage account.
|
|
||||||
//
|
|
||||||
// resourceGroupName is the name of the resource group within the user's
|
|
||||||
// subscription. The name is case insensitive. accountName is the name of the
|
|
||||||
// storage account within the specified resource group. Storage account names
|
|
||||||
// must be between 3 and 24 characters in length and use numbers and lower-case
|
|
||||||
// letters only.
|
|
||||||
func (client AccountsClient) ListKeys(resourceGroupName string, accountName string) (result AccountListKeysResult, err error) {
|
|
||||||
if err := validation.Validate([]validation.Validation{
|
|
||||||
{TargetValue: resourceGroupName,
|
|
||||||
Constraints: []validation.Constraint{{Target: "resourceGroupName", Name: validation.MaxLength, Rule: 90, Chain: nil},
|
|
||||||
{Target: "resourceGroupName", Name: validation.MinLength, Rule: 1, Chain: nil},
|
|
||||||
{Target: "resourceGroupName", Name: validation.Pattern, Rule: `^[-\w\._\(\)]+$`, Chain: nil}}},
|
|
||||||
{TargetValue: accountName,
|
|
||||||
Constraints: []validation.Constraint{{Target: "accountName", Name: validation.MaxLength, Rule: 24, Chain: nil},
|
|
||||||
{Target: "accountName", Name: validation.MinLength, Rule: 3, Chain: nil}}}}); err != nil {
|
|
||||||
return result, validation.NewErrorWithValidationError(err, "storage.AccountsClient", "ListKeys")
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := client.ListKeysPreparer(resourceGroupName, accountName)
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "ListKeys", nil, "Failure preparing request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.ListKeysSender(req)
|
|
||||||
if err != nil {
|
|
||||||
result.Response = autorest.Response{Response: resp}
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "ListKeys", resp, "Failure sending request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err = client.ListKeysResponder(resp)
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "ListKeys", resp, "Failure responding to request")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListKeysPreparer prepares the ListKeys request.
|
|
||||||
func (client AccountsClient) ListKeysPreparer(resourceGroupName string, accountName string) (*http.Request, error) {
|
|
||||||
pathParameters := map[string]interface{}{
|
|
||||||
"accountName": autorest.Encode("path", accountName),
|
|
||||||
"resourceGroupName": autorest.Encode("path", resourceGroupName),
|
|
||||||
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
|
||||||
}
|
|
||||||
|
|
||||||
const APIVersion = "2016-12-01"
|
|
||||||
queryParameters := map[string]interface{}{
|
|
||||||
"api-version": APIVersion,
|
|
||||||
}
|
|
||||||
|
|
||||||
preparer := autorest.CreatePreparer(
|
|
||||||
autorest.AsPost(),
|
|
||||||
autorest.WithBaseURL(client.BaseURI),
|
|
||||||
autorest.WithPathParameters("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}/listKeys", pathParameters),
|
|
||||||
autorest.WithQueryParameters(queryParameters))
|
|
||||||
return preparer.Prepare(&http.Request{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListKeysSender sends the ListKeys request. The method will close the
|
|
||||||
// http.Response Body if it receives an error.
|
|
||||||
func (client AccountsClient) ListKeysSender(req *http.Request) (*http.Response, error) {
|
|
||||||
return autorest.SendWithSender(client, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListKeysResponder handles the response to the ListKeys request. The method always
|
|
||||||
// closes the http.Response Body.
|
|
||||||
func (client AccountsClient) ListKeysResponder(resp *http.Response) (result AccountListKeysResult, err error) {
|
|
||||||
err = autorest.Respond(
|
|
||||||
resp,
|
|
||||||
client.ByInspecting(),
|
|
||||||
azure.WithErrorUnlessStatusCode(http.StatusOK),
|
|
||||||
autorest.ByUnmarshallingJSON(&result),
|
|
||||||
autorest.ByClosing())
|
|
||||||
result.Response = autorest.Response{Response: resp}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListServiceSAS list service SAS credentials of a specific resource.
|
|
||||||
//
|
|
||||||
// resourceGroupName is the name of the resource group within the user's
|
|
||||||
// subscription. The name is case insensitive. accountName is the name of the
|
|
||||||
// storage account within the specified resource group. Storage account names
|
|
||||||
// must be between 3 and 24 characters in length and use numbers and lower-case
|
|
||||||
// letters only. parameters is the parameters to provide to list service SAS
|
|
||||||
// credentials.
|
|
||||||
func (client AccountsClient) ListServiceSAS(resourceGroupName string, accountName string, parameters ServiceSasParameters) (result ListServiceSasResponse, err error) {
|
|
||||||
if err := validation.Validate([]validation.Validation{
|
|
||||||
{TargetValue: resourceGroupName,
|
|
||||||
Constraints: []validation.Constraint{{Target: "resourceGroupName", Name: validation.MaxLength, Rule: 90, Chain: nil},
|
|
||||||
{Target: "resourceGroupName", Name: validation.MinLength, Rule: 1, Chain: nil},
|
|
||||||
{Target: "resourceGroupName", Name: validation.Pattern, Rule: `^[-\w\._\(\)]+$`, Chain: nil}}},
|
|
||||||
{TargetValue: accountName,
|
|
||||||
Constraints: []validation.Constraint{{Target: "accountName", Name: validation.MaxLength, Rule: 24, Chain: nil},
|
|
||||||
{Target: "accountName", Name: validation.MinLength, Rule: 3, Chain: nil}}},
|
|
||||||
{TargetValue: parameters,
|
|
||||||
Constraints: []validation.Constraint{{Target: "parameters.CanonicalizedResource", Name: validation.Null, Rule: true, Chain: nil},
|
|
||||||
{Target: "parameters.Identifier", Name: validation.Null, Rule: false,
|
|
||||||
Chain: []validation.Constraint{{Target: "parameters.Identifier", Name: validation.MaxLength, Rule: 64, Chain: nil}}}}}}); err != nil {
|
|
||||||
return result, validation.NewErrorWithValidationError(err, "storage.AccountsClient", "ListServiceSAS")
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := client.ListServiceSASPreparer(resourceGroupName, accountName, parameters)
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "ListServiceSAS", nil, "Failure preparing request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.ListServiceSASSender(req)
|
|
||||||
if err != nil {
|
|
||||||
result.Response = autorest.Response{Response: resp}
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "ListServiceSAS", resp, "Failure sending request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err = client.ListServiceSASResponder(resp)
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "ListServiceSAS", resp, "Failure responding to request")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListServiceSASPreparer prepares the ListServiceSAS request.
|
|
||||||
func (client AccountsClient) ListServiceSASPreparer(resourceGroupName string, accountName string, parameters ServiceSasParameters) (*http.Request, error) {
|
|
||||||
pathParameters := map[string]interface{}{
|
|
||||||
"accountName": autorest.Encode("path", accountName),
|
|
||||||
"resourceGroupName": autorest.Encode("path", resourceGroupName),
|
|
||||||
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
|
||||||
}
|
|
||||||
|
|
||||||
const APIVersion = "2016-12-01"
|
|
||||||
queryParameters := map[string]interface{}{
|
|
||||||
"api-version": APIVersion,
|
|
||||||
}
|
|
||||||
|
|
||||||
preparer := autorest.CreatePreparer(
|
|
||||||
autorest.AsJSON(),
|
|
||||||
autorest.AsPost(),
|
|
||||||
autorest.WithBaseURL(client.BaseURI),
|
|
||||||
autorest.WithPathParameters("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}/ListServiceSas", pathParameters),
|
|
||||||
autorest.WithJSON(parameters),
|
|
||||||
autorest.WithQueryParameters(queryParameters))
|
|
||||||
return preparer.Prepare(&http.Request{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListServiceSASSender sends the ListServiceSAS request. The method will close the
|
|
||||||
// http.Response Body if it receives an error.
|
|
||||||
func (client AccountsClient) ListServiceSASSender(req *http.Request) (*http.Response, error) {
|
|
||||||
return autorest.SendWithSender(client, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListServiceSASResponder handles the response to the ListServiceSAS request. The method always
|
|
||||||
// closes the http.Response Body.
|
|
||||||
func (client AccountsClient) ListServiceSASResponder(resp *http.Response) (result ListServiceSasResponse, err error) {
|
|
||||||
err = autorest.Respond(
|
|
||||||
resp,
|
|
||||||
client.ByInspecting(),
|
|
||||||
azure.WithErrorUnlessStatusCode(http.StatusOK),
|
|
||||||
autorest.ByUnmarshallingJSON(&result),
|
|
||||||
autorest.ByClosing())
|
|
||||||
result.Response = autorest.Response{Response: resp}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegenerateKey regenerates one of the access keys for the specified storage
|
|
||||||
// account.
|
|
||||||
//
|
|
||||||
// resourceGroupName is the name of the resource group within the user's
|
|
||||||
// subscription. The name is case insensitive. accountName is the name of the
|
|
||||||
// storage account within the specified resource group. Storage account names
|
|
||||||
// must be between 3 and 24 characters in length and use numbers and lower-case
|
|
||||||
// letters only. regenerateKey is specifies name of the key which should be
|
|
||||||
// regenerated -- key1 or key2.
|
|
||||||
func (client AccountsClient) RegenerateKey(resourceGroupName string, accountName string, regenerateKey AccountRegenerateKeyParameters) (result AccountListKeysResult, err error) {
|
|
||||||
if err := validation.Validate([]validation.Validation{
|
|
||||||
{TargetValue: resourceGroupName,
|
|
||||||
Constraints: []validation.Constraint{{Target: "resourceGroupName", Name: validation.MaxLength, Rule: 90, Chain: nil},
|
|
||||||
{Target: "resourceGroupName", Name: validation.MinLength, Rule: 1, Chain: nil},
|
|
||||||
{Target: "resourceGroupName", Name: validation.Pattern, Rule: `^[-\w\._\(\)]+$`, Chain: nil}}},
|
|
||||||
{TargetValue: accountName,
|
|
||||||
Constraints: []validation.Constraint{{Target: "accountName", Name: validation.MaxLength, Rule: 24, Chain: nil},
|
|
||||||
{Target: "accountName", Name: validation.MinLength, Rule: 3, Chain: nil}}},
|
|
||||||
{TargetValue: regenerateKey,
|
|
||||||
Constraints: []validation.Constraint{{Target: "regenerateKey.KeyName", Name: validation.Null, Rule: true, Chain: nil}}}}); err != nil {
|
|
||||||
return result, validation.NewErrorWithValidationError(err, "storage.AccountsClient", "RegenerateKey")
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := client.RegenerateKeyPreparer(resourceGroupName, accountName, regenerateKey)
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "RegenerateKey", nil, "Failure preparing request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.RegenerateKeySender(req)
|
|
||||||
if err != nil {
|
|
||||||
result.Response = autorest.Response{Response: resp}
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "RegenerateKey", resp, "Failure sending request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err = client.RegenerateKeyResponder(resp)
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "RegenerateKey", resp, "Failure responding to request")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegenerateKeyPreparer prepares the RegenerateKey request.
|
|
||||||
func (client AccountsClient) RegenerateKeyPreparer(resourceGroupName string, accountName string, regenerateKey AccountRegenerateKeyParameters) (*http.Request, error) {
|
|
||||||
pathParameters := map[string]interface{}{
|
|
||||||
"accountName": autorest.Encode("path", accountName),
|
|
||||||
"resourceGroupName": autorest.Encode("path", resourceGroupName),
|
|
||||||
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
|
||||||
}
|
|
||||||
|
|
||||||
const APIVersion = "2016-12-01"
|
|
||||||
queryParameters := map[string]interface{}{
|
|
||||||
"api-version": APIVersion,
|
|
||||||
}
|
|
||||||
|
|
||||||
preparer := autorest.CreatePreparer(
|
|
||||||
autorest.AsJSON(),
|
|
||||||
autorest.AsPost(),
|
|
||||||
autorest.WithBaseURL(client.BaseURI),
|
|
||||||
autorest.WithPathParameters("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}/regenerateKey", pathParameters),
|
|
||||||
autorest.WithJSON(regenerateKey),
|
|
||||||
autorest.WithQueryParameters(queryParameters))
|
|
||||||
return preparer.Prepare(&http.Request{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegenerateKeySender sends the RegenerateKey request. The method will close the
|
|
||||||
// http.Response Body if it receives an error.
|
|
||||||
func (client AccountsClient) RegenerateKeySender(req *http.Request) (*http.Response, error) {
|
|
||||||
return autorest.SendWithSender(client, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegenerateKeyResponder handles the response to the RegenerateKey request. The method always
|
|
||||||
// closes the http.Response Body.
|
|
||||||
func (client AccountsClient) RegenerateKeyResponder(resp *http.Response) (result AccountListKeysResult, err error) {
|
|
||||||
err = autorest.Respond(
|
|
||||||
resp,
|
|
||||||
client.ByInspecting(),
|
|
||||||
azure.WithErrorUnlessStatusCode(http.StatusOK),
|
|
||||||
autorest.ByUnmarshallingJSON(&result),
|
|
||||||
autorest.ByClosing())
|
|
||||||
result.Response = autorest.Response{Response: resp}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the update operation can be used to update the SKU, encryption,
|
|
||||||
// access tier, or tags for a storage account. It can also be used to map the
|
|
||||||
// account to a custom domain. Only one custom domain is supported per storage
|
|
||||||
// account; the replacement/change of custom domain is not supported. In order
|
|
||||||
// to replace an old custom domain, the old value must be cleared/unregistered
|
|
||||||
// before a new value can be set. The update of multiple properties is
|
|
||||||
// supported. This call does not change the storage keys for the account. If
|
|
||||||
// you want to change the storage account keys, use the regenerate keys
|
|
||||||
// operation. The location and name of the storage account cannot be changed
|
|
||||||
// after creation.
|
|
||||||
//
|
|
||||||
// resourceGroupName is the name of the resource group within the user's
|
|
||||||
// subscription. The name is case insensitive. accountName is the name of the
|
|
||||||
// storage account within the specified resource group. Storage account names
|
|
||||||
// must be between 3 and 24 characters in length and use numbers and lower-case
|
|
||||||
// letters only. parameters is the parameters to provide for the updated
|
|
||||||
// account.
|
|
||||||
func (client AccountsClient) Update(resourceGroupName string, accountName string, parameters AccountUpdateParameters) (result Account, err error) {
|
|
||||||
if err := validation.Validate([]validation.Validation{
|
|
||||||
{TargetValue: resourceGroupName,
|
|
||||||
Constraints: []validation.Constraint{{Target: "resourceGroupName", Name: validation.MaxLength, Rule: 90, Chain: nil},
|
|
||||||
{Target: "resourceGroupName", Name: validation.MinLength, Rule: 1, Chain: nil},
|
|
||||||
{Target: "resourceGroupName", Name: validation.Pattern, Rule: `^[-\w\._\(\)]+$`, Chain: nil}}},
|
|
||||||
{TargetValue: accountName,
|
|
||||||
Constraints: []validation.Constraint{{Target: "accountName", Name: validation.MaxLength, Rule: 24, Chain: nil},
|
|
||||||
{Target: "accountName", Name: validation.MinLength, Rule: 3, Chain: nil}}}}); err != nil {
|
|
||||||
return result, validation.NewErrorWithValidationError(err, "storage.AccountsClient", "Update")
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := client.UpdatePreparer(resourceGroupName, accountName, parameters)
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "Update", nil, "Failure preparing request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.UpdateSender(req)
|
|
||||||
if err != nil {
|
|
||||||
result.Response = autorest.Response{Response: resp}
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "Update", resp, "Failure sending request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err = client.UpdateResponder(resp)
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.AccountsClient", "Update", resp, "Failure responding to request")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdatePreparer prepares the Update request.
|
|
||||||
func (client AccountsClient) UpdatePreparer(resourceGroupName string, accountName string, parameters AccountUpdateParameters) (*http.Request, error) {
|
|
||||||
pathParameters := map[string]interface{}{
|
|
||||||
"accountName": autorest.Encode("path", accountName),
|
|
||||||
"resourceGroupName": autorest.Encode("path", resourceGroupName),
|
|
||||||
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
|
||||||
}
|
|
||||||
|
|
||||||
const APIVersion = "2016-12-01"
|
|
||||||
queryParameters := map[string]interface{}{
|
|
||||||
"api-version": APIVersion,
|
|
||||||
}
|
|
||||||
|
|
||||||
preparer := autorest.CreatePreparer(
|
|
||||||
autorest.AsJSON(),
|
|
||||||
autorest.AsPatch(),
|
|
||||||
autorest.WithBaseURL(client.BaseURI),
|
|
||||||
autorest.WithPathParameters("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Storage/storageAccounts/{accountName}", pathParameters),
|
|
||||||
autorest.WithJSON(parameters),
|
|
||||||
autorest.WithQueryParameters(queryParameters))
|
|
||||||
return preparer.Prepare(&http.Request{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateSender sends the Update request. The method will close the
|
|
||||||
// http.Response Body if it receives an error.
|
|
||||||
func (client AccountsClient) UpdateSender(req *http.Request) (*http.Response, error) {
|
|
||||||
return autorest.SendWithSender(client, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateResponder handles the response to the Update request. The method always
|
|
||||||
// closes the http.Response Body.
|
|
||||||
func (client AccountsClient) UpdateResponder(resp *http.Response) (result Account, err error) {
|
|
||||||
err = autorest.Respond(
|
|
||||||
resp,
|
|
||||||
client.ByInspecting(),
|
|
||||||
azure.WithErrorUnlessStatusCode(http.StatusOK),
|
|
||||||
autorest.ByUnmarshallingJSON(&result),
|
|
||||||
autorest.ByClosing())
|
|
||||||
result.Response = autorest.Response{Response: resp}
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
// Package storage implements the Azure ARM Storage service API version
|
|
||||||
// 2016-12-01.
|
|
||||||
//
|
|
||||||
// The Azure Storage Management API.
|
|
||||||
package storage
|
|
||||||
|
|
||||||
// Copyright (c) Microsoft and contributors. All rights reserved.
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
//
|
|
||||||
// Code generated by Microsoft (R) AutoRest Code Generator 1.0.1.0
|
|
||||||
// Changes may cause incorrect behavior and will be lost if the code is
|
|
||||||
// regenerated.
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/Azure/go-autorest/autorest"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DefaultBaseURI is the default URI used for the service Storage
|
|
||||||
DefaultBaseURI = "https://management.azure.com"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ManagementClient is the base client for Storage.
|
|
||||||
type ManagementClient struct {
|
|
||||||
autorest.Client
|
|
||||||
BaseURI string
|
|
||||||
SubscriptionID string
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates an instance of the ManagementClient client.
|
|
||||||
func New(subscriptionID string) ManagementClient {
|
|
||||||
return NewWithBaseURI(DefaultBaseURI, subscriptionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewWithBaseURI creates an instance of the ManagementClient client.
|
|
||||||
func NewWithBaseURI(baseURI string, subscriptionID string) ManagementClient {
|
|
||||||
return ManagementClient{
|
|
||||||
Client: autorest.NewClientWithUserAgent(UserAgent()),
|
|
||||||
BaseURI: baseURI,
|
|
||||||
SubscriptionID: subscriptionID,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,452 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
// Copyright (c) Microsoft and contributors. All rights reserved.
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
//
|
|
||||||
// Code generated by Microsoft (R) AutoRest Code Generator 1.0.1.0
|
|
||||||
// Changes may cause incorrect behavior and will be lost if the code is
|
|
||||||
// regenerated.
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/Azure/go-autorest/autorest"
|
|
||||||
"github.com/Azure/go-autorest/autorest/date"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AccessTier enumerates the values for access tier.
|
|
||||||
type AccessTier string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Cool specifies the cool state for access tier.
|
|
||||||
Cool AccessTier = "Cool"
|
|
||||||
// Hot specifies the hot state for access tier.
|
|
||||||
Hot AccessTier = "Hot"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AccountStatus enumerates the values for account status.
|
|
||||||
type AccountStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Available specifies the available state for account status.
|
|
||||||
Available AccountStatus = "available"
|
|
||||||
// Unavailable specifies the unavailable state for account status.
|
|
||||||
Unavailable AccountStatus = "unavailable"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HTTPProtocol enumerates the values for http protocol.
|
|
||||||
type HTTPProtocol string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// HTTPS specifies the https state for http protocol.
|
|
||||||
HTTPS HTTPProtocol = "https"
|
|
||||||
// Httpshttp specifies the httpshttp state for http protocol.
|
|
||||||
Httpshttp HTTPProtocol = "https,http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// KeyPermission enumerates the values for key permission.
|
|
||||||
type KeyPermission string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Full specifies the full state for key permission.
|
|
||||||
Full KeyPermission = "Full"
|
|
||||||
// Read specifies the read state for key permission.
|
|
||||||
Read KeyPermission = "Read"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Kind enumerates the values for kind.
|
|
||||||
type Kind string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// BlobStorage specifies the blob storage state for kind.
|
|
||||||
BlobStorage Kind = "BlobStorage"
|
|
||||||
// Storage specifies the storage state for kind.
|
|
||||||
Storage Kind = "Storage"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Permissions enumerates the values for permissions.
|
|
||||||
type Permissions string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// A specifies the a state for permissions.
|
|
||||||
A Permissions = "a"
|
|
||||||
// C specifies the c state for permissions.
|
|
||||||
C Permissions = "c"
|
|
||||||
// D specifies the d state for permissions.
|
|
||||||
D Permissions = "d"
|
|
||||||
// L specifies the l state for permissions.
|
|
||||||
L Permissions = "l"
|
|
||||||
// P specifies the p state for permissions.
|
|
||||||
P Permissions = "p"
|
|
||||||
// R specifies the r state for permissions.
|
|
||||||
R Permissions = "r"
|
|
||||||
// U specifies the u state for permissions.
|
|
||||||
U Permissions = "u"
|
|
||||||
// W specifies the w state for permissions.
|
|
||||||
W Permissions = "w"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Permissions1 enumerates the values for permissions 1.
|
|
||||||
type Permissions1 string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Permissions1A specifies the permissions 1a state for permissions 1.
|
|
||||||
Permissions1A Permissions1 = "a"
|
|
||||||
// Permissions1C specifies the permissions 1c state for permissions 1.
|
|
||||||
Permissions1C Permissions1 = "c"
|
|
||||||
// Permissions1D specifies the permissions 1d state for permissions 1.
|
|
||||||
Permissions1D Permissions1 = "d"
|
|
||||||
// Permissions1L specifies the permissions 1l state for permissions 1.
|
|
||||||
Permissions1L Permissions1 = "l"
|
|
||||||
// Permissions1P specifies the permissions 1p state for permissions 1.
|
|
||||||
Permissions1P Permissions1 = "p"
|
|
||||||
// Permissions1R specifies the permissions 1r state for permissions 1.
|
|
||||||
Permissions1R Permissions1 = "r"
|
|
||||||
// Permissions1U specifies the permissions 1u state for permissions 1.
|
|
||||||
Permissions1U Permissions1 = "u"
|
|
||||||
// Permissions1W specifies the permissions 1w state for permissions 1.
|
|
||||||
Permissions1W Permissions1 = "w"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ProvisioningState enumerates the values for provisioning state.
|
|
||||||
type ProvisioningState string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Creating specifies the creating state for provisioning state.
|
|
||||||
Creating ProvisioningState = "Creating"
|
|
||||||
// ResolvingDNS specifies the resolving dns state for provisioning state.
|
|
||||||
ResolvingDNS ProvisioningState = "ResolvingDNS"
|
|
||||||
// Succeeded specifies the succeeded state for provisioning state.
|
|
||||||
Succeeded ProvisioningState = "Succeeded"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Reason enumerates the values for reason.
|
|
||||||
type Reason string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// AccountNameInvalid specifies the account name invalid state for reason.
|
|
||||||
AccountNameInvalid Reason = "AccountNameInvalid"
|
|
||||||
// AlreadyExists specifies the already exists state for reason.
|
|
||||||
AlreadyExists Reason = "AlreadyExists"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ResourceEnum enumerates the values for resource enum.
|
|
||||||
type ResourceEnum string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ResourceEnumB specifies the resource enum b state for resource enum.
|
|
||||||
ResourceEnumB ResourceEnum = "b"
|
|
||||||
// ResourceEnumC specifies the resource enum c state for resource enum.
|
|
||||||
ResourceEnumC ResourceEnum = "c"
|
|
||||||
// ResourceEnumF specifies the resource enum f state for resource enum.
|
|
||||||
ResourceEnumF ResourceEnum = "f"
|
|
||||||
// ResourceEnumS specifies the resource enum s state for resource enum.
|
|
||||||
ResourceEnumS ResourceEnum = "s"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ResourceTypes enumerates the values for resource types.
|
|
||||||
type ResourceTypes string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ResourceTypesC specifies the resource types c state for resource types.
|
|
||||||
ResourceTypesC ResourceTypes = "c"
|
|
||||||
// ResourceTypesO specifies the resource types o state for resource types.
|
|
||||||
ResourceTypesO ResourceTypes = "o"
|
|
||||||
// ResourceTypesS specifies the resource types s state for resource types.
|
|
||||||
ResourceTypesS ResourceTypes = "s"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Services enumerates the values for services.
|
|
||||||
type Services string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// B specifies the b state for services.
|
|
||||||
B Services = "b"
|
|
||||||
// F specifies the f state for services.
|
|
||||||
F Services = "f"
|
|
||||||
// Q specifies the q state for services.
|
|
||||||
Q Services = "q"
|
|
||||||
// T specifies the t state for services.
|
|
||||||
T Services = "t"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SkuName enumerates the values for sku name.
|
|
||||||
type SkuName string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// PremiumLRS specifies the premium lrs state for sku name.
|
|
||||||
PremiumLRS SkuName = "Premium_LRS"
|
|
||||||
// StandardGRS specifies the standard grs state for sku name.
|
|
||||||
StandardGRS SkuName = "Standard_GRS"
|
|
||||||
// StandardLRS specifies the standard lrs state for sku name.
|
|
||||||
StandardLRS SkuName = "Standard_LRS"
|
|
||||||
// StandardRAGRS specifies the standard ragrs state for sku name.
|
|
||||||
StandardRAGRS SkuName = "Standard_RAGRS"
|
|
||||||
// StandardZRS specifies the standard zrs state for sku name.
|
|
||||||
StandardZRS SkuName = "Standard_ZRS"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SkuTier enumerates the values for sku tier.
|
|
||||||
type SkuTier string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Premium specifies the premium state for sku tier.
|
|
||||||
Premium SkuTier = "Premium"
|
|
||||||
// Standard specifies the standard state for sku tier.
|
|
||||||
Standard SkuTier = "Standard"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UsageUnit enumerates the values for usage unit.
|
|
||||||
type UsageUnit string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Bytes specifies the bytes state for usage unit.
|
|
||||||
Bytes UsageUnit = "Bytes"
|
|
||||||
// BytesPerSecond specifies the bytes per second state for usage unit.
|
|
||||||
BytesPerSecond UsageUnit = "BytesPerSecond"
|
|
||||||
// Count specifies the count state for usage unit.
|
|
||||||
Count UsageUnit = "Count"
|
|
||||||
// CountsPerSecond specifies the counts per second state for usage unit.
|
|
||||||
CountsPerSecond UsageUnit = "CountsPerSecond"
|
|
||||||
// Percent specifies the percent state for usage unit.
|
|
||||||
Percent UsageUnit = "Percent"
|
|
||||||
// Seconds specifies the seconds state for usage unit.
|
|
||||||
Seconds UsageUnit = "Seconds"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Account is the storage account.
|
|
||||||
type Account struct {
|
|
||||||
autorest.Response `json:"-"`
|
|
||||||
ID *string `json:"id,omitempty"`
|
|
||||||
Name *string `json:"name,omitempty"`
|
|
||||||
Type *string `json:"type,omitempty"`
|
|
||||||
Location *string `json:"location,omitempty"`
|
|
||||||
Tags *map[string]*string `json:"tags,omitempty"`
|
|
||||||
Sku *Sku `json:"sku,omitempty"`
|
|
||||||
Kind Kind `json:"kind,omitempty"`
|
|
||||||
*AccountProperties `json:"properties,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccountCheckNameAvailabilityParameters is the parameters used to check the
|
|
||||||
// availabity of the storage account name.
|
|
||||||
type AccountCheckNameAvailabilityParameters struct {
|
|
||||||
Name *string `json:"name,omitempty"`
|
|
||||||
Type *string `json:"type,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccountCreateParameters is the parameters used when creating a storage
|
|
||||||
// account.
|
|
||||||
type AccountCreateParameters struct {
|
|
||||||
Sku *Sku `json:"sku,omitempty"`
|
|
||||||
Kind Kind `json:"kind,omitempty"`
|
|
||||||
Location *string `json:"location,omitempty"`
|
|
||||||
Tags *map[string]*string `json:"tags,omitempty"`
|
|
||||||
*AccountPropertiesCreateParameters `json:"properties,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccountKey is an access key for the storage account.
|
|
||||||
type AccountKey struct {
|
|
||||||
KeyName *string `json:"keyName,omitempty"`
|
|
||||||
Value *string `json:"value,omitempty"`
|
|
||||||
Permissions KeyPermission `json:"permissions,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccountListKeysResult is the response from the ListKeys operation.
|
|
||||||
type AccountListKeysResult struct {
|
|
||||||
autorest.Response `json:"-"`
|
|
||||||
Keys *[]AccountKey `json:"keys,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccountListResult is the response from the List Storage Accounts operation.
|
|
||||||
type AccountListResult struct {
|
|
||||||
autorest.Response `json:"-"`
|
|
||||||
Value *[]Account `json:"value,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccountProperties is properties of the storage account.
|
|
||||||
type AccountProperties struct {
|
|
||||||
ProvisioningState ProvisioningState `json:"provisioningState,omitempty"`
|
|
||||||
PrimaryEndpoints *Endpoints `json:"primaryEndpoints,omitempty"`
|
|
||||||
PrimaryLocation *string `json:"primaryLocation,omitempty"`
|
|
||||||
StatusOfPrimary AccountStatus `json:"statusOfPrimary,omitempty"`
|
|
||||||
LastGeoFailoverTime *date.Time `json:"lastGeoFailoverTime,omitempty"`
|
|
||||||
SecondaryLocation *string `json:"secondaryLocation,omitempty"`
|
|
||||||
StatusOfSecondary AccountStatus `json:"statusOfSecondary,omitempty"`
|
|
||||||
CreationTime *date.Time `json:"creationTime,omitempty"`
|
|
||||||
CustomDomain *CustomDomain `json:"customDomain,omitempty"`
|
|
||||||
SecondaryEndpoints *Endpoints `json:"secondaryEndpoints,omitempty"`
|
|
||||||
Encryption *Encryption `json:"encryption,omitempty"`
|
|
||||||
AccessTier AccessTier `json:"accessTier,omitempty"`
|
|
||||||
EnableHTTPSTrafficOnly *bool `json:"supportsHttpsTrafficOnly,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccountPropertiesCreateParameters is the parameters used to create the
|
|
||||||
// storage account.
|
|
||||||
type AccountPropertiesCreateParameters struct {
|
|
||||||
CustomDomain *CustomDomain `json:"customDomain,omitempty"`
|
|
||||||
Encryption *Encryption `json:"encryption,omitempty"`
|
|
||||||
AccessTier AccessTier `json:"accessTier,omitempty"`
|
|
||||||
EnableHTTPSTrafficOnly *bool `json:"supportsHttpsTrafficOnly,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccountPropertiesUpdateParameters is the parameters used when updating a
|
|
||||||
// storage account.
|
|
||||||
type AccountPropertiesUpdateParameters struct {
|
|
||||||
CustomDomain *CustomDomain `json:"customDomain,omitempty"`
|
|
||||||
Encryption *Encryption `json:"encryption,omitempty"`
|
|
||||||
AccessTier AccessTier `json:"accessTier,omitempty"`
|
|
||||||
EnableHTTPSTrafficOnly *bool `json:"supportsHttpsTrafficOnly,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccountRegenerateKeyParameters is the parameters used to regenerate the
|
|
||||||
// storage account key.
|
|
||||||
type AccountRegenerateKeyParameters struct {
|
|
||||||
KeyName *string `json:"keyName,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccountSasParameters is the parameters to list SAS credentials of a storage
|
|
||||||
// account.
|
|
||||||
type AccountSasParameters struct {
|
|
||||||
Services Services `json:"signedServices,omitempty"`
|
|
||||||
ResourceTypes ResourceTypes `json:"signedResourceTypes,omitempty"`
|
|
||||||
Permissions Permissions `json:"signedPermission,omitempty"`
|
|
||||||
IPAddressOrRange *string `json:"signedIp,omitempty"`
|
|
||||||
Protocols HTTPProtocol `json:"signedProtocol,omitempty"`
|
|
||||||
SharedAccessStartTime *date.Time `json:"signedStart,omitempty"`
|
|
||||||
SharedAccessExpiryTime *date.Time `json:"signedExpiry,omitempty"`
|
|
||||||
KeyToSign *string `json:"keyToSign,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccountUpdateParameters is the parameters that can be provided when updating
|
|
||||||
// the storage account properties.
|
|
||||||
type AccountUpdateParameters struct {
|
|
||||||
Sku *Sku `json:"sku,omitempty"`
|
|
||||||
Tags *map[string]*string `json:"tags,omitempty"`
|
|
||||||
*AccountPropertiesUpdateParameters `json:"properties,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckNameAvailabilityResult is the CheckNameAvailability operation response.
|
|
||||||
type CheckNameAvailabilityResult struct {
|
|
||||||
autorest.Response `json:"-"`
|
|
||||||
NameAvailable *bool `json:"nameAvailable,omitempty"`
|
|
||||||
Reason Reason `json:"reason,omitempty"`
|
|
||||||
Message *string `json:"message,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CustomDomain is the custom domain assigned to this storage account. This can
|
|
||||||
// be set via Update.
|
|
||||||
type CustomDomain struct {
|
|
||||||
Name *string `json:"name,omitempty"`
|
|
||||||
UseSubDomain *bool `json:"useSubDomain,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encryption is the encryption settings on the storage account.
|
|
||||||
type Encryption struct {
|
|
||||||
Services *EncryptionServices `json:"services,omitempty"`
|
|
||||||
KeySource *string `json:"keySource,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// EncryptionService is a service that allows server-side encryption to be
|
|
||||||
// used.
|
|
||||||
type EncryptionService struct {
|
|
||||||
Enabled *bool `json:"enabled,omitempty"`
|
|
||||||
LastEnabledTime *date.Time `json:"lastEnabledTime,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// EncryptionServices is a list of services that support encryption.
|
|
||||||
type EncryptionServices struct {
|
|
||||||
Blob *EncryptionService `json:"blob,omitempty"`
|
|
||||||
File *EncryptionService `json:"file,omitempty"`
|
|
||||||
Table *EncryptionService `json:"table,omitempty"`
|
|
||||||
Queue *EncryptionService `json:"queue,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Endpoints is the URIs that are used to perform a retrieval of a public blob,
|
|
||||||
// queue, or table object.
|
|
||||||
type Endpoints struct {
|
|
||||||
Blob *string `json:"blob,omitempty"`
|
|
||||||
Queue *string `json:"queue,omitempty"`
|
|
||||||
Table *string `json:"table,omitempty"`
|
|
||||||
File *string `json:"file,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListAccountSasResponse is the List SAS credentials operation response.
|
|
||||||
type ListAccountSasResponse struct {
|
|
||||||
autorest.Response `json:"-"`
|
|
||||||
AccountSasToken *string `json:"accountSasToken,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListServiceSasResponse is the List service SAS credentials operation
|
|
||||||
// response.
|
|
||||||
type ListServiceSasResponse struct {
|
|
||||||
autorest.Response `json:"-"`
|
|
||||||
ServiceSasToken *string `json:"serviceSasToken,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resource is describes a storage resource.
|
|
||||||
type Resource struct {
|
|
||||||
ID *string `json:"id,omitempty"`
|
|
||||||
Name *string `json:"name,omitempty"`
|
|
||||||
Type *string `json:"type,omitempty"`
|
|
||||||
Location *string `json:"location,omitempty"`
|
|
||||||
Tags *map[string]*string `json:"tags,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceSasParameters is the parameters to list service SAS credentials of a
|
|
||||||
// speicific resource.
|
|
||||||
type ServiceSasParameters struct {
|
|
||||||
CanonicalizedResource *string `json:"canonicalizedResource,omitempty"`
|
|
||||||
Resource Resource `json:"signedResource,omitempty"`
|
|
||||||
Permissions Permissions `json:"signedPermission,omitempty"`
|
|
||||||
IPAddressOrRange *string `json:"signedIp,omitempty"`
|
|
||||||
Protocols HTTPProtocol `json:"signedProtocol,omitempty"`
|
|
||||||
SharedAccessStartTime *date.Time `json:"signedStart,omitempty"`
|
|
||||||
SharedAccessExpiryTime *date.Time `json:"signedExpiry,omitempty"`
|
|
||||||
Identifier *string `json:"signedIdentifier,omitempty"`
|
|
||||||
PartitionKeyStart *string `json:"startPk,omitempty"`
|
|
||||||
PartitionKeyEnd *string `json:"endPk,omitempty"`
|
|
||||||
RowKeyStart *string `json:"startRk,omitempty"`
|
|
||||||
RowKeyEnd *string `json:"endRk,omitempty"`
|
|
||||||
KeyToSign *string `json:"keyToSign,omitempty"`
|
|
||||||
CacheControl *string `json:"rscc,omitempty"`
|
|
||||||
ContentDisposition *string `json:"rscd,omitempty"`
|
|
||||||
ContentEncoding *string `json:"rsce,omitempty"`
|
|
||||||
ContentLanguage *string `json:"rscl,omitempty"`
|
|
||||||
ContentType *string `json:"rsct,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sku is the SKU of the storage account.
|
|
||||||
type Sku struct {
|
|
||||||
Name SkuName `json:"name,omitempty"`
|
|
||||||
Tier SkuTier `json:"tier,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage is describes Storage Resource Usage.
|
|
||||||
type Usage struct {
|
|
||||||
Unit UsageUnit `json:"unit,omitempty"`
|
|
||||||
CurrentValue *int32 `json:"currentValue,omitempty"`
|
|
||||||
Limit *int32 `json:"limit,omitempty"`
|
|
||||||
Name *UsageName `json:"name,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UsageListResult is the response from the List Usages operation.
|
|
||||||
type UsageListResult struct {
|
|
||||||
autorest.Response `json:"-"`
|
|
||||||
Value *[]Usage `json:"value,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UsageName is the usage names that can be used; currently limited to
|
|
||||||
// StorageAccount.
|
|
||||||
type UsageName struct {
|
|
||||||
Value *string `json:"value,omitempty"`
|
|
||||||
LocalizedValue *string `json:"localizedValue,omitempty"`
|
|
||||||
}
|
|
|
@ -1,102 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
// Copyright (c) Microsoft and contributors. All rights reserved.
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
//
|
|
||||||
// Code generated by Microsoft (R) AutoRest Code Generator 1.0.1.0
|
|
||||||
// Changes may cause incorrect behavior and will be lost if the code is
|
|
||||||
// regenerated.
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/Azure/go-autorest/autorest"
|
|
||||||
"github.com/Azure/go-autorest/autorest/azure"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UsageClient is the the Azure Storage Management API.
|
|
||||||
type UsageClient struct {
|
|
||||||
ManagementClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewUsageClient creates an instance of the UsageClient client.
|
|
||||||
func NewUsageClient(subscriptionID string) UsageClient {
|
|
||||||
return NewUsageClientWithBaseURI(DefaultBaseURI, subscriptionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewUsageClientWithBaseURI creates an instance of the UsageClient client.
|
|
||||||
func NewUsageClientWithBaseURI(baseURI string, subscriptionID string) UsageClient {
|
|
||||||
return UsageClient{NewWithBaseURI(baseURI, subscriptionID)}
|
|
||||||
}
|
|
||||||
|
|
||||||
// List gets the current usage count and the limit for the resources under the
|
|
||||||
// subscription.
|
|
||||||
func (client UsageClient) List() (result UsageListResult, err error) {
|
|
||||||
req, err := client.ListPreparer()
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.UsageClient", "List", nil, "Failure preparing request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.ListSender(req)
|
|
||||||
if err != nil {
|
|
||||||
result.Response = autorest.Response{Response: resp}
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.UsageClient", "List", resp, "Failure sending request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err = client.ListResponder(resp)
|
|
||||||
if err != nil {
|
|
||||||
err = autorest.NewErrorWithError(err, "storage.UsageClient", "List", resp, "Failure responding to request")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListPreparer prepares the List request.
|
|
||||||
func (client UsageClient) ListPreparer() (*http.Request, error) {
|
|
||||||
pathParameters := map[string]interface{}{
|
|
||||||
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
|
||||||
}
|
|
||||||
|
|
||||||
const APIVersion = "2016-12-01"
|
|
||||||
queryParameters := map[string]interface{}{
|
|
||||||
"api-version": APIVersion,
|
|
||||||
}
|
|
||||||
|
|
||||||
preparer := autorest.CreatePreparer(
|
|
||||||
autorest.AsGet(),
|
|
||||||
autorest.WithBaseURL(client.BaseURI),
|
|
||||||
autorest.WithPathParameters("/subscriptions/{subscriptionId}/providers/Microsoft.Storage/usages", pathParameters),
|
|
||||||
autorest.WithQueryParameters(queryParameters))
|
|
||||||
return preparer.Prepare(&http.Request{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListSender sends the List request. The method will close the
|
|
||||||
// http.Response Body if it receives an error.
|
|
||||||
func (client UsageClient) ListSender(req *http.Request) (*http.Response, error) {
|
|
||||||
return autorest.SendWithSender(client, req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListResponder handles the response to the List request. The method always
|
|
||||||
// closes the http.Response Body.
|
|
||||||
func (client UsageClient) ListResponder(resp *http.Response) (result UsageListResult, err error) {
|
|
||||||
err = autorest.Respond(
|
|
||||||
resp,
|
|
||||||
client.ByInspecting(),
|
|
||||||
azure.WithErrorUnlessStatusCode(http.StatusOK),
|
|
||||||
autorest.ByUnmarshallingJSON(&result),
|
|
||||||
autorest.ByClosing())
|
|
||||||
result.Response = autorest.Response{Response: resp}
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
// Copyright (c) Microsoft and contributors. All rights reserved.
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
//
|
|
||||||
// Code generated by Microsoft (R) AutoRest Code Generator 1.0.1.0
|
|
||||||
// Changes may cause incorrect behavior and will be lost if the code is
|
|
||||||
// regenerated.
|
|
||||||
|
|
||||||
// UserAgent returns the UserAgent string to use when sending http.Requests.
|
|
||||||
func UserAgent() string {
|
|
||||||
return "Azure-SDK-For-Go/v10.0.2-beta arm-storage/2016-12-01"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Version returns the semantic version (see http://semver.org) of the client.
|
|
||||||
func Version() string {
|
|
||||||
return "v10.0.2-beta"
|
|
||||||
}
|
|
|
@ -1,70 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PutAppendBlob initializes an empty append blob with specified name. An
|
|
||||||
// append blob must be created using this method before appending blocks.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Put-Blob
|
|
||||||
func (b *Blob) PutAppendBlob(options *PutBlobOptions) error {
|
|
||||||
params := url.Values{}
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
headers["x-ms-blob-type"] = string(BlobTypeAppend)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(b.Properties))
|
|
||||||
headers = b.Container.bsc.client.addMetadataToHeaders(headers, b.Metadata)
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
|
|
||||||
|
|
||||||
resp, err := b.Container.bsc.client.exec(http.MethodPut, uri, headers, nil, b.Container.bsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
readAndCloseBody(resp.body)
|
|
||||||
return checkRespCode(resp.statusCode, []int{http.StatusCreated})
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppendBlockOptions includes the options for an append block operation
|
|
||||||
type AppendBlockOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
LeaseID string `header:"x-ms-lease-id"`
|
|
||||||
MaxSize *uint `header:"x-ms-blob-condition-maxsize"`
|
|
||||||
AppendPosition *uint `header:"x-ms-blob-condition-appendpos"`
|
|
||||||
IfModifiedSince *time.Time `header:"If-Modified-Since"`
|
|
||||||
IfUnmodifiedSince *time.Time `header:"If-Unmodified-Since"`
|
|
||||||
IfMatch string `header:"If-Match"`
|
|
||||||
IfNoneMatch string `header:"If-None-Match"`
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppendBlock appends a block to an append blob.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Append-Block
|
|
||||||
func (b *Blob) AppendBlock(chunk []byte, options *AppendBlockOptions) error {
|
|
||||||
params := url.Values{"comp": {"appendblock"}}
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
headers["x-ms-blob-type"] = string(BlobTypeAppend)
|
|
||||||
headers["Content-Length"] = fmt.Sprintf("%v", len(chunk))
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
|
|
||||||
|
|
||||||
resp, err := b.Container.bsc.client.exec(http.MethodPut, uri, headers, bytes.NewReader(chunk), b.Container.bsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
readAndCloseBody(resp.body)
|
|
||||||
return checkRespCode(resp.statusCode, []int{http.StatusCreated})
|
|
||||||
}
|
|
|
@ -1,227 +0,0 @@
|
||||||
// Package storage provides clients for Microsoft Azure Storage Services.
|
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// See: https://docs.microsoft.com/rest/api/storageservices/fileservices/authentication-for-the-azure-storage-services
|
|
||||||
|
|
||||||
type authentication string
|
|
||||||
|
|
||||||
const (
|
|
||||||
sharedKey authentication = "sharedKey"
|
|
||||||
sharedKeyForTable authentication = "sharedKeyTable"
|
|
||||||
sharedKeyLite authentication = "sharedKeyLite"
|
|
||||||
sharedKeyLiteForTable authentication = "sharedKeyLiteTable"
|
|
||||||
|
|
||||||
// headers
|
|
||||||
headerAcceptCharset = "Accept-Charset"
|
|
||||||
headerAuthorization = "Authorization"
|
|
||||||
headerContentLength = "Content-Length"
|
|
||||||
headerDate = "Date"
|
|
||||||
headerXmsDate = "x-ms-date"
|
|
||||||
headerXmsVersion = "x-ms-version"
|
|
||||||
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"
|
|
||||||
headerRange = "Range"
|
|
||||||
headerDataServiceVersion = "DataServiceVersion"
|
|
||||||
headerMaxDataServiceVersion = "MaxDataServiceVersion"
|
|
||||||
headerContentTransferEncoding = "Content-Transfer-Encoding"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *Client) addAuthorizationHeader(verb, url string, headers map[string]string, auth authentication) (map[string]string, error) {
|
|
||||||
authHeader, err := c.getSharedKey(verb, url, headers, auth)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
headers[headerAuthorization] = authHeader
|
|
||||||
return headers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) getSharedKey(verb, url string, headers map[string]string, auth authentication) (string, error) {
|
|
||||||
canRes, err := c.buildCanonicalizedResource(url, auth)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
canString, err := buildCanonicalizedString(verb, headers, canRes, auth)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return c.createAuthorizationHeader(canString, auth), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) buildCanonicalizedResource(uri string, auth authentication) (string, error) {
|
|
||||||
errMsg := "buildCanonicalizedResource error: %s"
|
|
||||||
u, err := url.Parse(uri)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf(errMsg, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
cr := bytes.NewBufferString("/")
|
|
||||||
cr.WriteString(c.getCanonicalizedAccountName())
|
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
|
|
||||||
params, err := url.ParseQuery(u.RawQuery)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf(errMsg, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// See https://github.com/Azure/azure-storage-net/blob/master/Lib/Common/Core/Util/AuthenticationUtility.cs#L277
|
|
||||||
if auth == sharedKey {
|
|
||||||
if len(params) > 0 {
|
|
||||||
cr.WriteString("\n")
|
|
||||||
|
|
||||||
keys := []string{}
|
|
||||||
for key := range params {
|
|
||||||
keys = append(keys, key)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
completeParams := []string{}
|
|
||||||
for _, key := range keys {
|
|
||||||
if len(params[key]) > 1 {
|
|
||||||
sort.Strings(params[key])
|
|
||||||
}
|
|
||||||
|
|
||||||
completeParams = append(completeParams, fmt.Sprintf("%s:%s", key, strings.Join(params[key], ",")))
|
|
||||||
}
|
|
||||||
cr.WriteString(strings.Join(completeParams, "\n"))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// search for "comp" parameter, if exists then add it to canonicalizedresource
|
|
||||||
if v, ok := params["comp"]; ok {
|
|
||||||
cr.WriteString("?comp=" + v[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(cr.Bytes()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) getCanonicalizedAccountName() string {
|
|
||||||
// since we may be trying to access a secondary storage account, we need to
|
|
||||||
// remove the -secondary part of the storage name
|
|
||||||
return strings.TrimSuffix(c.accountName, "-secondary")
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildCanonicalizedString(verb string, headers map[string]string, canonicalizedResource string, auth authentication) (string, error) {
|
|
||||||
contentLength := headers[headerContentLength]
|
|
||||||
if contentLength == "0" {
|
|
||||||
contentLength = ""
|
|
||||||
}
|
|
||||||
date := headers[headerDate]
|
|
||||||
if v, ok := headers[headerXmsDate]; ok {
|
|
||||||
if auth == sharedKey || auth == sharedKeyLite {
|
|
||||||
date = ""
|
|
||||||
} else {
|
|
||||||
date = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var canString string
|
|
||||||
switch auth {
|
|
||||||
case sharedKey:
|
|
||||||
canString = strings.Join([]string{
|
|
||||||
verb,
|
|
||||||
headers[headerContentEncoding],
|
|
||||||
headers[headerContentLanguage],
|
|
||||||
contentLength,
|
|
||||||
headers[headerContentMD5],
|
|
||||||
headers[headerContentType],
|
|
||||||
date,
|
|
||||||
headers[headerIfModifiedSince],
|
|
||||||
headers[headerIfMatch],
|
|
||||||
headers[headerIfNoneMatch],
|
|
||||||
headers[headerIfUnmodifiedSince],
|
|
||||||
headers[headerRange],
|
|
||||||
buildCanonicalizedHeader(headers),
|
|
||||||
canonicalizedResource,
|
|
||||||
}, "\n")
|
|
||||||
case sharedKeyForTable:
|
|
||||||
canString = strings.Join([]string{
|
|
||||||
verb,
|
|
||||||
headers[headerContentMD5],
|
|
||||||
headers[headerContentType],
|
|
||||||
date,
|
|
||||||
canonicalizedResource,
|
|
||||||
}, "\n")
|
|
||||||
case sharedKeyLite:
|
|
||||||
canString = strings.Join([]string{
|
|
||||||
verb,
|
|
||||||
headers[headerContentMD5],
|
|
||||||
headers[headerContentType],
|
|
||||||
date,
|
|
||||||
buildCanonicalizedHeader(headers),
|
|
||||||
canonicalizedResource,
|
|
||||||
}, "\n")
|
|
||||||
case sharedKeyLiteForTable:
|
|
||||||
canString = strings.Join([]string{
|
|
||||||
date,
|
|
||||||
canonicalizedResource,
|
|
||||||
}, "\n")
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("%s authentication is not supported yet", auth)
|
|
||||||
}
|
|
||||||
return canString, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildCanonicalizedHeader(headers map[string]string) 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cm) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) createAuthorizationHeader(canonicalizedString string, auth authentication) string {
|
|
||||||
signature := c.computeHmac256(canonicalizedString)
|
|
||||||
var key string
|
|
||||||
switch auth {
|
|
||||||
case sharedKey, sharedKeyForTable:
|
|
||||||
key = "SharedKey"
|
|
||||||
case sharedKeyLite, sharedKeyLiteForTable:
|
|
||||||
key = "SharedKeyLite"
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s %s:%s", key, c.getCanonicalizedAccountName(), signature)
|
|
||||||
}
|
|
|
@ -1,617 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A Blob is an entry in BlobListResponse.
|
|
||||||
type Blob struct {
|
|
||||||
Container *Container
|
|
||||||
Name string `xml:"Name"`
|
|
||||||
Snapshot time.Time `xml:"Snapshot"`
|
|
||||||
Properties BlobProperties `xml:"Properties"`
|
|
||||||
Metadata BlobMetadata `xml:"Metadata"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutBlobOptions includes the options any put blob operation
|
|
||||||
// (page, block, append)
|
|
||||||
type PutBlobOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
LeaseID string `header:"x-ms-lease-id"`
|
|
||||||
Origin string `header:"Origin"`
|
|
||||||
IfModifiedSince *time.Time `header:"If-Modified-Since"`
|
|
||||||
IfUnmodifiedSince *time.Time `header:"If-Unmodified-Since"`
|
|
||||||
IfMatch string `header:"If-Match"`
|
|
||||||
IfNoneMatch string `header:"If-None-Match"`
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// BlobMetadata is a set of custom name/value pairs.
|
|
||||||
//
|
|
||||||
// See https://msdn.microsoft.com/en-us/library/azure/dd179404.aspx
|
|
||||||
type BlobMetadata map[string]string
|
|
||||||
|
|
||||||
type blobMetadataEntries struct {
|
|
||||||
Entries []blobMetadataEntry `xml:",any"`
|
|
||||||
}
|
|
||||||
type blobMetadataEntry struct {
|
|
||||||
XMLName xml.Name
|
|
||||||
Value string `xml:",chardata"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalXML converts the xml:Metadata into Metadata map
|
|
||||||
func (bm *BlobMetadata) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
||||||
var entries blobMetadataEntries
|
|
||||||
if err := d.DecodeElement(&entries, &start); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, entry := range entries.Entries {
|
|
||||||
if *bm == nil {
|
|
||||||
*bm = make(BlobMetadata)
|
|
||||||
}
|
|
||||||
(*bm)[strings.ToLower(entry.XMLName.Local)] = entry.Value
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalXML implements the xml.Marshaler interface. It encodes
|
|
||||||
// metadata name/value pairs as they would appear in an Azure
|
|
||||||
// ListBlobs response.
|
|
||||||
func (bm BlobMetadata) MarshalXML(enc *xml.Encoder, start xml.StartElement) error {
|
|
||||||
entries := make([]blobMetadataEntry, 0, len(bm))
|
|
||||||
for k, v := range bm {
|
|
||||||
entries = append(entries, blobMetadataEntry{
|
|
||||||
XMLName: xml.Name{Local: http.CanonicalHeaderKey(k)},
|
|
||||||
Value: v,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return enc.EncodeElement(blobMetadataEntries{
|
|
||||||
Entries: entries,
|
|
||||||
}, start)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BlobProperties contains various properties of a blob
|
|
||||||
// returned in various endpoints like ListBlobs or GetBlobProperties.
|
|
||||||
type BlobProperties struct {
|
|
||||||
LastModified TimeRFC1123 `xml:"Last-Modified"`
|
|
||||||
Etag string `xml:"Etag"`
|
|
||||||
ContentMD5 string `xml:"Content-MD5" header:"x-ms-blob-content-md5"`
|
|
||||||
ContentLength int64 `xml:"Content-Length"`
|
|
||||||
ContentType string `xml:"Content-Type" header:"x-ms-blob-content-type"`
|
|
||||||
ContentEncoding string `xml:"Content-Encoding" header:"x-ms-blob-content-encoding"`
|
|
||||||
CacheControl string `xml:"Cache-Control" header:"x-ms-blob-cache-control"`
|
|
||||||
ContentLanguage string `xml:"Cache-Language" header:"x-ms-blob-content-language"`
|
|
||||||
ContentDisposition string `xml:"Content-Disposition" header:"x-ms-blob-content-disposition"`
|
|
||||||
BlobType BlobType `xml:"x-ms-blob-blob-type"`
|
|
||||||
SequenceNumber int64 `xml:"x-ms-blob-sequence-number"`
|
|
||||||
CopyID string `xml:"CopyId"`
|
|
||||||
CopyStatus string `xml:"CopyStatus"`
|
|
||||||
CopySource string `xml:"CopySource"`
|
|
||||||
CopyProgress string `xml:"CopyProgress"`
|
|
||||||
CopyCompletionTime TimeRFC1123 `xml:"CopyCompletionTime"`
|
|
||||||
CopyStatusDescription string `xml:"CopyStatusDescription"`
|
|
||||||
LeaseStatus string `xml:"LeaseStatus"`
|
|
||||||
LeaseState string `xml:"LeaseState"`
|
|
||||||
LeaseDuration string `xml:"LeaseDuration"`
|
|
||||||
ServerEncrypted bool `xml:"ServerEncrypted"`
|
|
||||||
IncrementalCopy bool `xml:"IncrementalCopy"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// BlobType defines the type of the Azure Blob.
|
|
||||||
type BlobType string
|
|
||||||
|
|
||||||
// Types of page blobs
|
|
||||||
const (
|
|
||||||
BlobTypeBlock BlobType = "BlockBlob"
|
|
||||||
BlobTypePage BlobType = "PageBlob"
|
|
||||||
BlobTypeAppend BlobType = "AppendBlob"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (b *Blob) buildPath() string {
|
|
||||||
return b.Container.buildPath() + "/" + b.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exists returns true if a blob with given name exists on the specified
|
|
||||||
// container of the storage account.
|
|
||||||
func (b *Blob) Exists() (bool, error) {
|
|
||||||
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), nil)
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
resp, err := b.Container.bsc.client.exec(http.MethodHead, uri, headers, nil, b.Container.bsc.auth)
|
|
||||||
if resp != nil {
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
if resp.statusCode == http.StatusOK || resp.statusCode == http.StatusNotFound {
|
|
||||||
return resp.statusCode == http.StatusOK, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetURL gets the canonical URL to the blob with the specified name in the
|
|
||||||
// specified container. If name is not specified, the canonical URL for the entire
|
|
||||||
// container is obtained.
|
|
||||||
// This method does not create a publicly accessible URL if the blob or container
|
|
||||||
// is private and this method does not check if the blob exists.
|
|
||||||
func (b *Blob) GetURL() string {
|
|
||||||
container := b.Container.Name
|
|
||||||
if container == "" {
|
|
||||||
container = "$root"
|
|
||||||
}
|
|
||||||
return b.Container.bsc.client.getEndpoint(blobServiceName, pathForResource(container, b.Name), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBlobRangeOptions includes the options for a get blob range operation
|
|
||||||
type GetBlobRangeOptions struct {
|
|
||||||
Range *BlobRange
|
|
||||||
GetRangeContentMD5 bool
|
|
||||||
*GetBlobOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBlobOptions includes the options for a get blob operation
|
|
||||||
type GetBlobOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
Snapshot *time.Time
|
|
||||||
LeaseID string `header:"x-ms-lease-id"`
|
|
||||||
Origin string `header:"Origin"`
|
|
||||||
IfModifiedSince *time.Time `header:"If-Modified-Since"`
|
|
||||||
IfUnmodifiedSince *time.Time `header:"If-Unmodified-Since"`
|
|
||||||
IfMatch string `header:"If-Match"`
|
|
||||||
IfNoneMatch string `header:"If-None-Match"`
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// BlobRange represents the bytes range to be get
|
|
||||||
type BlobRange struct {
|
|
||||||
Start uint64
|
|
||||||
End uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (br BlobRange) String() string {
|
|
||||||
return fmt.Sprintf("bytes=%d-%d", br.Start, br.End)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns a stream to read the blob. Caller must call both Read and Close()
|
|
||||||
// to correctly close the underlying connection.
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Get-Blob
|
|
||||||
func (b *Blob) Get(options *GetBlobOptions) (io.ReadCloser, error) {
|
|
||||||
rangeOptions := GetBlobRangeOptions{
|
|
||||||
GetBlobOptions: options,
|
|
||||||
}
|
|
||||||
resp, err := b.getRange(&rangeOptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := b.writePropoerties(resp.headers); err != nil {
|
|
||||||
return resp.body, err
|
|
||||||
}
|
|
||||||
return resp.body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRange reads the specified range of a blob to a stream. The bytesRange
|
|
||||||
// string must be in a format like "0-", "10-100" as defined in HTTP 1.1 spec.
|
|
||||||
// Caller must call both Read and Close()// to correctly close the underlying
|
|
||||||
// connection.
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Get-Blob
|
|
||||||
func (b *Blob) GetRange(options *GetBlobRangeOptions) (io.ReadCloser, error) {
|
|
||||||
resp, err := b.getRange(options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := checkRespCode(resp.statusCode, []int{http.StatusPartialContent}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := b.writePropoerties(resp.headers); err != nil {
|
|
||||||
return resp.body, err
|
|
||||||
}
|
|
||||||
return resp.body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Blob) getRange(options *GetBlobRangeOptions) (*storageResponse, error) {
|
|
||||||
params := url.Values{}
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
if options.Range != nil {
|
|
||||||
headers["Range"] = options.Range.String()
|
|
||||||
headers["x-ms-range-get-content-md5"] = fmt.Sprintf("%v", options.GetRangeContentMD5)
|
|
||||||
}
|
|
||||||
if options.GetBlobOptions != nil {
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options.GetBlobOptions))
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
params = addSnapshot(params, options.Snapshot)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
|
|
||||||
|
|
||||||
resp, err := b.Container.bsc.client.exec(http.MethodGet, uri, headers, nil, b.Container.bsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SnapshotOptions includes the options for a snapshot blob operation
|
|
||||||
type SnapshotOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
LeaseID string `header:"x-ms-lease-id"`
|
|
||||||
IfModifiedSince *time.Time `header:"If-Modified-Since"`
|
|
||||||
IfUnmodifiedSince *time.Time `header:"If-Unmodified-Since"`
|
|
||||||
IfMatch string `header:"If-Match"`
|
|
||||||
IfNoneMatch string `header:"If-None-Match"`
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateSnapshot creates a snapshot for a blob
|
|
||||||
// See https://msdn.microsoft.com/en-us/library/azure/ee691971.aspx
|
|
||||||
func (b *Blob) CreateSnapshot(options *SnapshotOptions) (snapshotTimestamp *time.Time, err error) {
|
|
||||||
params := url.Values{"comp": {"snapshot"}}
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
headers = b.Container.bsc.client.addMetadataToHeaders(headers, b.Metadata)
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
|
|
||||||
|
|
||||||
resp, err := b.Container.bsc.client.exec(http.MethodPut, uri, headers, nil, b.Container.bsc.auth)
|
|
||||||
if err != nil || resp == nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
if err := checkRespCode(resp.statusCode, []int{http.StatusCreated}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
snapshotResponse := resp.headers.Get(http.CanonicalHeaderKey("x-ms-snapshot"))
|
|
||||||
if snapshotResponse != "" {
|
|
||||||
snapshotTimestamp, err := time.Parse(time.RFC3339, snapshotResponse)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &snapshotTimestamp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errors.New("Snapshot not created")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBlobPropertiesOptions includes the options for a get blob properties operation
|
|
||||||
type GetBlobPropertiesOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
Snapshot *time.Time
|
|
||||||
LeaseID string `header:"x-ms-lease-id"`
|
|
||||||
IfModifiedSince *time.Time `header:"If-Modified-Since"`
|
|
||||||
IfUnmodifiedSince *time.Time `header:"If-Unmodified-Since"`
|
|
||||||
IfMatch string `header:"If-Match"`
|
|
||||||
IfNoneMatch string `header:"If-None-Match"`
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetProperties provides various information about the specified blob.
|
|
||||||
// See https://msdn.microsoft.com/en-us/library/azure/dd179394.aspx
|
|
||||||
func (b *Blob) GetProperties(options *GetBlobPropertiesOptions) error {
|
|
||||||
params := url.Values{}
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
params = addSnapshot(params, options.Snapshot)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
|
|
||||||
|
|
||||||
resp, err := b.Container.bsc.client.exec(http.MethodHead, uri, headers, nil, b.Container.bsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
if err = checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return b.writePropoerties(resp.headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Blob) writePropoerties(h http.Header) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
var contentLength int64
|
|
||||||
contentLengthStr := h.Get("Content-Length")
|
|
||||||
if contentLengthStr != "" {
|
|
||||||
contentLength, err = strconv.ParseInt(contentLengthStr, 0, 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var sequenceNum int64
|
|
||||||
sequenceNumStr := h.Get("x-ms-blob-sequence-number")
|
|
||||||
if sequenceNumStr != "" {
|
|
||||||
sequenceNum, err = strconv.ParseInt(sequenceNumStr, 0, 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastModified, err := getTimeFromHeaders(h, "Last-Modified")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
copyCompletionTime, err := getTimeFromHeaders(h, "x-ms-copy-completion-time")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
b.Properties = BlobProperties{
|
|
||||||
LastModified: TimeRFC1123(*lastModified),
|
|
||||||
Etag: h.Get("Etag"),
|
|
||||||
ContentMD5: h.Get("Content-MD5"),
|
|
||||||
ContentLength: contentLength,
|
|
||||||
ContentEncoding: h.Get("Content-Encoding"),
|
|
||||||
ContentType: h.Get("Content-Type"),
|
|
||||||
ContentDisposition: h.Get("Content-Disposition"),
|
|
||||||
CacheControl: h.Get("Cache-Control"),
|
|
||||||
ContentLanguage: h.Get("Content-Language"),
|
|
||||||
SequenceNumber: sequenceNum,
|
|
||||||
CopyCompletionTime: TimeRFC1123(*copyCompletionTime),
|
|
||||||
CopyStatusDescription: h.Get("x-ms-copy-status-description"),
|
|
||||||
CopyID: h.Get("x-ms-copy-id"),
|
|
||||||
CopyProgress: h.Get("x-ms-copy-progress"),
|
|
||||||
CopySource: h.Get("x-ms-copy-source"),
|
|
||||||
CopyStatus: h.Get("x-ms-copy-status"),
|
|
||||||
BlobType: BlobType(h.Get("x-ms-blob-type")),
|
|
||||||
LeaseStatus: h.Get("x-ms-lease-status"),
|
|
||||||
LeaseState: h.Get("x-ms-lease-state"),
|
|
||||||
}
|
|
||||||
b.writeMetadata(h)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetBlobPropertiesOptions contains various properties of a blob and is an entry
|
|
||||||
// in SetProperties
|
|
||||||
type SetBlobPropertiesOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
LeaseID string `header:"x-ms-lease-id"`
|
|
||||||
Origin string `header:"Origin"`
|
|
||||||
IfModifiedSince *time.Time `header:"If-Modified-Since"`
|
|
||||||
IfUnmodifiedSince *time.Time `header:"If-Unmodified-Since"`
|
|
||||||
IfMatch string `header:"If-Match"`
|
|
||||||
IfNoneMatch string `header:"If-None-Match"`
|
|
||||||
SequenceNumberAction *SequenceNumberAction
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SequenceNumberAction defines how the blob's sequence number should be modified
|
|
||||||
type SequenceNumberAction string
|
|
||||||
|
|
||||||
// Options for sequence number action
|
|
||||||
const (
|
|
||||||
SequenceNumberActionMax SequenceNumberAction = "max"
|
|
||||||
SequenceNumberActionUpdate SequenceNumberAction = "update"
|
|
||||||
SequenceNumberActionIncrement SequenceNumberAction = "increment"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SetProperties replaces the BlobHeaders for the specified blob.
|
|
||||||
//
|
|
||||||
// Some keys may be converted to Camel-Case before sending. All keys
|
|
||||||
// are returned in lower case by GetBlobProperties. HTTP header names
|
|
||||||
// are case-insensitive so case munging should not matter to other
|
|
||||||
// applications either.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Set-Blob-Properties
|
|
||||||
func (b *Blob) SetProperties(options *SetBlobPropertiesOptions) error {
|
|
||||||
params := url.Values{"comp": {"properties"}}
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(b.Properties))
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
|
|
||||||
|
|
||||||
if b.Properties.BlobType == BlobTypePage {
|
|
||||||
headers = addToHeaders(headers, "x-ms-blob-content-length", fmt.Sprintf("byte %v", b.Properties.ContentLength))
|
|
||||||
if options != nil || options.SequenceNumberAction != nil {
|
|
||||||
headers = addToHeaders(headers, "x-ms-sequence-number-action", string(*options.SequenceNumberAction))
|
|
||||||
if *options.SequenceNumberAction != SequenceNumberActionIncrement {
|
|
||||||
headers = addToHeaders(headers, "x-ms-blob-sequence-number", fmt.Sprintf("%v", b.Properties.SequenceNumber))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := b.Container.bsc.client.exec(http.MethodPut, uri, headers, nil, b.Container.bsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
readAndCloseBody(resp.body)
|
|
||||||
return checkRespCode(resp.statusCode, []int{http.StatusOK})
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetBlobMetadataOptions includes the options for a set blob metadata operation
|
|
||||||
type SetBlobMetadataOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
LeaseID string `header:"x-ms-lease-id"`
|
|
||||||
IfModifiedSince *time.Time `header:"If-Modified-Since"`
|
|
||||||
IfUnmodifiedSince *time.Time `header:"If-Unmodified-Since"`
|
|
||||||
IfMatch string `header:"If-Match"`
|
|
||||||
IfNoneMatch string `header:"If-None-Match"`
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetMetadata replaces the metadata for the specified blob.
|
|
||||||
//
|
|
||||||
// Some keys may be converted to Camel-Case before sending. All keys
|
|
||||||
// are returned in lower case by GetBlobMetadata. HTTP header names
|
|
||||||
// are case-insensitive so case munging should not matter to other
|
|
||||||
// applications either.
|
|
||||||
//
|
|
||||||
// See https://msdn.microsoft.com/en-us/library/azure/dd179414.aspx
|
|
||||||
func (b *Blob) SetMetadata(options *SetBlobMetadataOptions) error {
|
|
||||||
params := url.Values{"comp": {"metadata"}}
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
headers = b.Container.bsc.client.addMetadataToHeaders(headers, b.Metadata)
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
|
|
||||||
|
|
||||||
resp, err := b.Container.bsc.client.exec(http.MethodPut, uri, headers, nil, b.Container.bsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
readAndCloseBody(resp.body)
|
|
||||||
return checkRespCode(resp.statusCode, []int{http.StatusOK})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBlobMetadataOptions includes the options for a get blob metadata operation
|
|
||||||
type GetBlobMetadataOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
Snapshot *time.Time
|
|
||||||
LeaseID string `header:"x-ms-lease-id"`
|
|
||||||
IfModifiedSince *time.Time `header:"If-Modified-Since"`
|
|
||||||
IfUnmodifiedSince *time.Time `header:"If-Unmodified-Since"`
|
|
||||||
IfMatch string `header:"If-Match"`
|
|
||||||
IfNoneMatch string `header:"If-None-Match"`
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMetadata returns all user-defined metadata for the specified blob.
|
|
||||||
//
|
|
||||||
// All metadata keys will be returned in lower case. (HTTP header
|
|
||||||
// names are case-insensitive.)
|
|
||||||
//
|
|
||||||
// See https://msdn.microsoft.com/en-us/library/azure/dd179414.aspx
|
|
||||||
func (b *Blob) GetMetadata(options *GetBlobMetadataOptions) error {
|
|
||||||
params := url.Values{"comp": {"metadata"}}
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
params = addSnapshot(params, options.Snapshot)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
|
|
||||||
|
|
||||||
resp, err := b.Container.bsc.client.exec(http.MethodGet, uri, headers, nil, b.Container.bsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
b.writeMetadata(resp.headers)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Blob) writeMetadata(h http.Header) {
|
|
||||||
metadata := make(map[string]string)
|
|
||||||
for k, v := range h {
|
|
||||||
// Can't trust CanonicalHeaderKey() to munge case
|
|
||||||
// reliably. "_" is allowed in identifiers:
|
|
||||||
// https://msdn.microsoft.com/en-us/library/azure/dd179414.aspx
|
|
||||||
// https://msdn.microsoft.com/library/aa664670(VS.71).aspx
|
|
||||||
// http://tools.ietf.org/html/rfc7230#section-3.2
|
|
||||||
// ...but "_" is considered invalid by
|
|
||||||
// CanonicalMIMEHeaderKey in
|
|
||||||
// https://golang.org/src/net/textproto/reader.go?s=14615:14659#L542
|
|
||||||
// so k can be "X-Ms-Meta-Lol" or "x-ms-meta-lol_rofl".
|
|
||||||
k = strings.ToLower(k)
|
|
||||||
if len(v) == 0 || !strings.HasPrefix(k, strings.ToLower(userDefinedMetadataHeaderPrefix)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// metadata["lol"] = content of the last X-Ms-Meta-Lol header
|
|
||||||
k = k[len(userDefinedMetadataHeaderPrefix):]
|
|
||||||
metadata[k] = v[len(v)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
b.Metadata = BlobMetadata(metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteBlobOptions includes the options for a delete blob operation
|
|
||||||
type DeleteBlobOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
Snapshot *time.Time
|
|
||||||
LeaseID string `header:"x-ms-lease-id"`
|
|
||||||
DeleteSnapshots *bool
|
|
||||||
IfModifiedSince *time.Time `header:"If-Modified-Since"`
|
|
||||||
IfUnmodifiedSince *time.Time `header:"If-Unmodified-Since"`
|
|
||||||
IfMatch string `header:"If-Match"`
|
|
||||||
IfNoneMatch string `header:"If-None-Match"`
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete deletes the given blob from the specified container.
|
|
||||||
// If the blob does not exists at the time of the Delete Blob operation, it
|
|
||||||
// returns error.
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Delete-Blob
|
|
||||||
func (b *Blob) Delete(options *DeleteBlobOptions) error {
|
|
||||||
resp, err := b.delete(options)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
readAndCloseBody(resp.body)
|
|
||||||
return checkRespCode(resp.statusCode, []int{http.StatusAccepted})
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteIfExists deletes the given blob from the specified container If the
|
|
||||||
// blob is deleted with this call, returns true. Otherwise returns false.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Delete-Blob
|
|
||||||
func (b *Blob) DeleteIfExists(options *DeleteBlobOptions) (bool, error) {
|
|
||||||
resp, err := b.delete(options)
|
|
||||||
if resp != nil {
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
if resp.statusCode == http.StatusAccepted || resp.statusCode == http.StatusNotFound {
|
|
||||||
return resp.statusCode == http.StatusAccepted, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Blob) delete(options *DeleteBlobOptions) (*storageResponse, error) {
|
|
||||||
params := url.Values{}
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
params = addSnapshot(params, options.Snapshot)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
if options.DeleteSnapshots != nil {
|
|
||||||
if *options.DeleteSnapshots {
|
|
||||||
headers["x-ms-delete-snapshots"] = "include"
|
|
||||||
} else {
|
|
||||||
headers["x-ms-delete-snapshots"] = "only"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
|
|
||||||
return b.Container.bsc.client.exec(http.MethodDelete, uri, headers, nil, b.Container.bsc.auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper method to construct the path to either a blob or container
|
|
||||||
func pathForResource(container, name string) string {
|
|
||||||
if name != "" {
|
|
||||||
return fmt.Sprintf("/%s/%s", container, name)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("/%s", container)
|
|
||||||
}
|
|
|
@ -1,106 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetSASURIWithSignedIPAndProtocol creates an URL to the specified blob which contains the Shared
|
|
||||||
// Access Signature with specified permissions and expiration time. Also includes signedIPRange and allowed protocols.
|
|
||||||
// If old API version is used but no signedIP is passed (ie empty string) then this should still work.
|
|
||||||
// We only populate the signedIP when it non-empty.
|
|
||||||
//
|
|
||||||
// See https://msdn.microsoft.com/en-us/library/azure/ee395415.aspx
|
|
||||||
func (b *Blob) GetSASURIWithSignedIPAndProtocol(expiry time.Time, permissions string, signedIPRange string, HTTPSOnly bool) (string, error) {
|
|
||||||
var (
|
|
||||||
signedPermissions = permissions
|
|
||||||
blobURL = b.GetURL()
|
|
||||||
)
|
|
||||||
canonicalizedResource, err := b.Container.bsc.client.buildCanonicalizedResource(blobURL, b.Container.bsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// "The canonicalizedresouce portion of the string is a canonical path to the signed resource.
|
|
||||||
// It must include the service name (blob, table, queue or file) for version 2015-02-21 or
|
|
||||||
// later, the storage account name, and the resource name, and must be URL-decoded.
|
|
||||||
// -- https://msdn.microsoft.com/en-us/library/azure/dn140255.aspx
|
|
||||||
|
|
||||||
// We need to replace + with %2b first to avoid being treated as a space (which is correct for query strings, but not the path component).
|
|
||||||
canonicalizedResource = strings.Replace(canonicalizedResource, "+", "%2b", -1)
|
|
||||||
canonicalizedResource, err = url.QueryUnescape(canonicalizedResource)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
signedExpiry := expiry.UTC().Format(time.RFC3339)
|
|
||||||
|
|
||||||
//If blob name is missing, resource is a container
|
|
||||||
signedResource := "c"
|
|
||||||
if len(b.Name) > 0 {
|
|
||||||
signedResource = "b"
|
|
||||||
}
|
|
||||||
|
|
||||||
protocols := "https,http"
|
|
||||||
if HTTPSOnly {
|
|
||||||
protocols = "https"
|
|
||||||
}
|
|
||||||
stringToSign, err := blobSASStringToSign(b.Container.bsc.client.apiVersion, canonicalizedResource, signedExpiry, signedPermissions, signedIPRange, protocols)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
sig := b.Container.bsc.client.computeHmac256(stringToSign)
|
|
||||||
sasParams := url.Values{
|
|
||||||
"sv": {b.Container.bsc.client.apiVersion},
|
|
||||||
"se": {signedExpiry},
|
|
||||||
"sr": {signedResource},
|
|
||||||
"sp": {signedPermissions},
|
|
||||||
"sig": {sig},
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.Container.bsc.client.apiVersion >= "2015-04-05" {
|
|
||||||
sasParams.Add("spr", protocols)
|
|
||||||
if signedIPRange != "" {
|
|
||||||
sasParams.Add("sip", signedIPRange)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sasURL, err := url.Parse(blobURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
sasURL.RawQuery = sasParams.Encode()
|
|
||||||
return sasURL.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSASURI creates an URL to the specified blob which contains the Shared
|
|
||||||
// Access Signature with specified permissions and expiration time.
|
|
||||||
//
|
|
||||||
// See https://msdn.microsoft.com/en-us/library/azure/ee395415.aspx
|
|
||||||
func (b *Blob) GetSASURI(expiry time.Time, permissions string) (string, error) {
|
|
||||||
return b.GetSASURIWithSignedIPAndProtocol(expiry, permissions, "", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func blobSASStringToSign(signedVersion, canonicalizedResource, signedExpiry, signedPermissions string, signedIP string, protocols string) (string, error) {
|
|
||||||
var signedStart, signedIdentifier, rscc, rscd, rsce, rscl, rsct string
|
|
||||||
|
|
||||||
if signedVersion >= "2015-02-21" {
|
|
||||||
canonicalizedResource = "/blob" + canonicalizedResource
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://msdn.microsoft.com/en-us/library/azure/dn140255.aspx#Anchor_12
|
|
||||||
if signedVersion >= "2015-04-05" {
|
|
||||||
return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s", signedPermissions, signedStart, signedExpiry, canonicalizedResource, signedIdentifier, signedIP, protocols, signedVersion, rscc, rscd, rsce, rscl, rsct), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// reference: http://msdn.microsoft.com/en-us/library/azure/dn140255.aspx
|
|
||||||
if signedVersion >= "2013-08-15" {
|
|
||||||
return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s", signedPermissions, signedStart, signedExpiry, canonicalizedResource, signedIdentifier, signedVersion, rscc, rscd, rsce, rscl, rsct), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", errors.New("storage: not implemented SAS for versions earlier than 2013-08-15")
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BlobStorageClient contains operations for Microsoft Azure Blob Storage
|
|
||||||
// Service.
|
|
||||||
type BlobStorageClient struct {
|
|
||||||
client Client
|
|
||||||
auth authentication
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetServiceProperties gets the properties of your storage account's blob service.
|
|
||||||
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/get-blob-service-properties
|
|
||||||
func (b *BlobStorageClient) GetServiceProperties() (*ServiceProperties, error) {
|
|
||||||
return b.client.getServiceProperties(blobServiceName, b.auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetServiceProperties sets the properties of your storage account's blob service.
|
|
||||||
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/set-blob-service-properties
|
|
||||||
func (b *BlobStorageClient) SetServiceProperties(props ServiceProperties) error {
|
|
||||||
return b.client.setServiceProperties(props, blobServiceName, b.auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListContainersParameters defines the set of customizable parameters to make a
|
|
||||||
// List Containers call.
|
|
||||||
//
|
|
||||||
// See https://msdn.microsoft.com/en-us/library/azure/dd179352.aspx
|
|
||||||
type ListContainersParameters struct {
|
|
||||||
Prefix string
|
|
||||||
Marker string
|
|
||||||
Include string
|
|
||||||
MaxResults uint
|
|
||||||
Timeout uint
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetContainerReference returns a Container object for the specified container name.
|
|
||||||
func (b *BlobStorageClient) GetContainerReference(name string) *Container {
|
|
||||||
return &Container{
|
|
||||||
bsc: b,
|
|
||||||
Name: name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListContainers returns the list of containers in a storage account along with
|
|
||||||
// pagination token and other response details.
|
|
||||||
//
|
|
||||||
// See https://msdn.microsoft.com/en-us/library/azure/dd179352.aspx
|
|
||||||
func (b BlobStorageClient) ListContainers(params ListContainersParameters) (*ContainerListResponse, error) {
|
|
||||||
q := mergeParams(params.getParameters(), url.Values{"comp": {"list"}})
|
|
||||||
uri := b.client.getEndpoint(blobServiceName, "", q)
|
|
||||||
headers := b.client.getStandardHeaders()
|
|
||||||
|
|
||||||
var out ContainerListResponse
|
|
||||||
resp, err := b.client.exec(http.MethodGet, uri, headers, nil, b.auth)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.body.Close()
|
|
||||||
err = xmlUnmarshal(resp.body, &out)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// assign our client to the newly created Container objects
|
|
||||||
for i := range out.Containers {
|
|
||||||
out.Containers[i].bsc = &b
|
|
||||||
}
|
|
||||||
return &out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p ListContainersParameters) getParameters() url.Values {
|
|
||||||
out := url.Values{}
|
|
||||||
|
|
||||||
if p.Prefix != "" {
|
|
||||||
out.Set("prefix", p.Prefix)
|
|
||||||
}
|
|
||||||
if p.Marker != "" {
|
|
||||||
out.Set("marker", p.Marker)
|
|
||||||
}
|
|
||||||
if p.Include != "" {
|
|
||||||
out.Set("include", p.Include)
|
|
||||||
}
|
|
||||||
if p.MaxResults != 0 {
|
|
||||||
out.Set("maxresults", strconv.FormatUint(uint64(p.MaxResults), 10))
|
|
||||||
}
|
|
||||||
if p.Timeout != 0 {
|
|
||||||
out.Set("timeout", strconv.FormatUint(uint64(p.Timeout), 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
|
@ -1,225 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BlockListType is used to filter out types of blocks in a Get Blocks List call
|
|
||||||
// for a block blob.
|
|
||||||
//
|
|
||||||
// See https://msdn.microsoft.com/en-us/library/azure/dd179400.aspx for all
|
|
||||||
// block types.
|
|
||||||
type BlockListType string
|
|
||||||
|
|
||||||
// Filters for listing blocks in block blobs
|
|
||||||
const (
|
|
||||||
BlockListTypeAll BlockListType = "all"
|
|
||||||
BlockListTypeCommitted BlockListType = "committed"
|
|
||||||
BlockListTypeUncommitted BlockListType = "uncommitted"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Maximum sizes (per REST API) for various concepts
|
|
||||||
const (
|
|
||||||
MaxBlobBlockSize = 100 * 1024 * 1024
|
|
||||||
MaxBlobPageSize = 4 * 1024 * 1024
|
|
||||||
)
|
|
||||||
|
|
||||||
// BlockStatus defines states a block for a block blob can
|
|
||||||
// be in.
|
|
||||||
type BlockStatus string
|
|
||||||
|
|
||||||
// List of statuses that can be used to refer to a block in a block list
|
|
||||||
const (
|
|
||||||
BlockStatusUncommitted BlockStatus = "Uncommitted"
|
|
||||||
BlockStatusCommitted BlockStatus = "Committed"
|
|
||||||
BlockStatusLatest BlockStatus = "Latest"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Block is used to create Block entities for Put Block List
|
|
||||||
// call.
|
|
||||||
type Block struct {
|
|
||||||
ID string
|
|
||||||
Status BlockStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
// BlockListResponse contains the response fields from Get Block List call.
|
|
||||||
//
|
|
||||||
// See https://msdn.microsoft.com/en-us/library/azure/dd179400.aspx
|
|
||||||
type BlockListResponse struct {
|
|
||||||
XMLName xml.Name `xml:"BlockList"`
|
|
||||||
CommittedBlocks []BlockResponse `xml:"CommittedBlocks>Block"`
|
|
||||||
UncommittedBlocks []BlockResponse `xml:"UncommittedBlocks>Block"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// BlockResponse contains the block information returned
|
|
||||||
// in the GetBlockListCall.
|
|
||||||
type BlockResponse struct {
|
|
||||||
Name string `xml:"Name"`
|
|
||||||
Size int64 `xml:"Size"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateBlockBlob initializes an empty block blob with no blocks.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Put-Blob
|
|
||||||
func (b *Blob) CreateBlockBlob(options *PutBlobOptions) error {
|
|
||||||
return b.CreateBlockBlobFromReader(nil, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateBlockBlobFromReader initializes a block blob using data from
|
|
||||||
// reader. Size must be the number of bytes read from reader. To
|
|
||||||
// create an empty blob, use size==0 and reader==nil.
|
|
||||||
//
|
|
||||||
// The API rejects requests with size > 256 MiB (but this limit is not
|
|
||||||
// checked by the SDK). To write a larger blob, use CreateBlockBlob,
|
|
||||||
// PutBlock, and PutBlockList.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Put-Blob
|
|
||||||
func (b *Blob) CreateBlockBlobFromReader(blob io.Reader, options *PutBlobOptions) error {
|
|
||||||
params := url.Values{}
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
headers["x-ms-blob-type"] = string(BlobTypeBlock)
|
|
||||||
headers["Content-Length"] = fmt.Sprintf("%d", b.Properties.ContentLength)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(b.Properties))
|
|
||||||
headers = b.Container.bsc.client.addMetadataToHeaders(headers, b.Metadata)
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
|
|
||||||
|
|
||||||
resp, err := b.Container.bsc.client.exec(http.MethodPut, uri, headers, blob, b.Container.bsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
readAndCloseBody(resp.body)
|
|
||||||
return checkRespCode(resp.statusCode, []int{http.StatusCreated})
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutBlockOptions includes the options for a put block operation
|
|
||||||
type PutBlockOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
LeaseID string `header:"x-ms-lease-id"`
|
|
||||||
ContentMD5 string `header:"Content-MD5"`
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutBlock saves the given data chunk to the specified block blob with
|
|
||||||
// given ID.
|
|
||||||
//
|
|
||||||
// The API rejects chunks larger than 100 MiB (but this limit is not
|
|
||||||
// checked by the SDK).
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Put-Block
|
|
||||||
func (b *Blob) PutBlock(blockID string, chunk []byte, options *PutBlockOptions) error {
|
|
||||||
return b.PutBlockWithLength(blockID, uint64(len(chunk)), bytes.NewReader(chunk), options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutBlockWithLength saves the given data stream of exactly specified size to
|
|
||||||
// the block blob with given ID. It is an alternative to PutBlocks where data
|
|
||||||
// comes as stream but the length is known in advance.
|
|
||||||
//
|
|
||||||
// The API rejects requests with size > 100 MiB (but this limit is not
|
|
||||||
// checked by the SDK).
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Put-Block
|
|
||||||
func (b *Blob) PutBlockWithLength(blockID string, size uint64, blob io.Reader, options *PutBlockOptions) error {
|
|
||||||
query := url.Values{
|
|
||||||
"comp": {"block"},
|
|
||||||
"blockid": {blockID},
|
|
||||||
}
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
headers["Content-Length"] = fmt.Sprintf("%v", size)
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
query = addTimeout(query, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), query)
|
|
||||||
|
|
||||||
resp, err := b.Container.bsc.client.exec(http.MethodPut, uri, headers, blob, b.Container.bsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
readAndCloseBody(resp.body)
|
|
||||||
return checkRespCode(resp.statusCode, []int{http.StatusCreated})
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutBlockListOptions includes the options for a put block list operation
|
|
||||||
type PutBlockListOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
LeaseID string `header:"x-ms-lease-id"`
|
|
||||||
IfModifiedSince *time.Time `header:"If-Modified-Since"`
|
|
||||||
IfUnmodifiedSince *time.Time `header:"If-Unmodified-Since"`
|
|
||||||
IfMatch string `header:"If-Match"`
|
|
||||||
IfNoneMatch string `header:"If-None-Match"`
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutBlockList saves list of blocks to the specified block blob.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Put-Block-List
|
|
||||||
func (b *Blob) PutBlockList(blocks []Block, options *PutBlockListOptions) error {
|
|
||||||
params := url.Values{"comp": {"blocklist"}}
|
|
||||||
blockListXML := prepareBlockListRequest(blocks)
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
headers["Content-Length"] = fmt.Sprintf("%v", len(blockListXML))
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(b.Properties))
|
|
||||||
headers = b.Container.bsc.client.addMetadataToHeaders(headers, b.Metadata)
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
|
|
||||||
|
|
||||||
resp, err := b.Container.bsc.client.exec(http.MethodPut, uri, headers, strings.NewReader(blockListXML), b.Container.bsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
readAndCloseBody(resp.body)
|
|
||||||
return checkRespCode(resp.statusCode, []int{http.StatusCreated})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBlockListOptions includes the options for a get block list operation
|
|
||||||
type GetBlockListOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
Snapshot *time.Time
|
|
||||||
LeaseID string `header:"x-ms-lease-id"`
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBlockList retrieves list of blocks in the specified block blob.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Get-Block-List
|
|
||||||
func (b *Blob) GetBlockList(blockType BlockListType, options *GetBlockListOptions) (BlockListResponse, error) {
|
|
||||||
params := url.Values{
|
|
||||||
"comp": {"blocklist"},
|
|
||||||
"blocklisttype": {string(blockType)},
|
|
||||||
}
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
params = addSnapshot(params, options.Snapshot)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
|
|
||||||
|
|
||||||
var out BlockListResponse
|
|
||||||
resp, err := b.Container.bsc.client.exec(http.MethodGet, uri, headers, nil, b.Container.bsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
defer resp.body.Close()
|
|
||||||
|
|
||||||
err = xmlUnmarshal(resp.body, &out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
|
@ -1,652 +0,0 @@
|
||||||
// Package storage provides clients for Microsoft Azure Storage Services.
|
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"mime"
|
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"regexp"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Azure/go-autorest/autorest"
|
|
||||||
"github.com/Azure/go-autorest/autorest/azure"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DefaultBaseURL is the domain name used for storage requests in the
|
|
||||||
// public cloud when a default client is created.
|
|
||||||
DefaultBaseURL = "core.windows.net"
|
|
||||||
|
|
||||||
// DefaultAPIVersion is the Azure Storage API version string used when a
|
|
||||||
// basic client is created.
|
|
||||||
DefaultAPIVersion = "2016-05-31"
|
|
||||||
|
|
||||||
defaultUseHTTPS = true
|
|
||||||
|
|
||||||
// StorageEmulatorAccountName is the fixed storage account used by Azure Storage Emulator
|
|
||||||
StorageEmulatorAccountName = "devstoreaccount1"
|
|
||||||
|
|
||||||
// StorageEmulatorAccountKey is the the fixed storage account used by Azure Storage Emulator
|
|
||||||
StorageEmulatorAccountKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
|
|
||||||
|
|
||||||
blobServiceName = "blob"
|
|
||||||
tableServiceName = "table"
|
|
||||||
queueServiceName = "queue"
|
|
||||||
fileServiceName = "file"
|
|
||||||
|
|
||||||
storageEmulatorBlob = "127.0.0.1:10000"
|
|
||||||
storageEmulatorTable = "127.0.0.1:10002"
|
|
||||||
storageEmulatorQueue = "127.0.0.1:10001"
|
|
||||||
|
|
||||||
userAgentHeader = "User-Agent"
|
|
||||||
|
|
||||||
userDefinedMetadataHeaderPrefix = "x-ms-meta-"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
validStorageAccount = regexp.MustCompile("^[0-9a-z]{3,24}$")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Sender sends a request
|
|
||||||
type Sender interface {
|
|
||||||
Send(*Client, *http.Request) (*http.Response, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultSender is the default sender for the client. It implements
|
|
||||||
// an automatic retry strategy.
|
|
||||||
type DefaultSender struct {
|
|
||||||
RetryAttempts int
|
|
||||||
RetryDuration time.Duration
|
|
||||||
ValidStatusCodes []int
|
|
||||||
attempts int // used for testing
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send is the default retry strategy in the client
|
|
||||||
func (ds *DefaultSender) Send(c *Client, req *http.Request) (resp *http.Response, err error) {
|
|
||||||
b := []byte{}
|
|
||||||
if req.Body != nil {
|
|
||||||
b, err = ioutil.ReadAll(req.Body)
|
|
||||||
if err != nil {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for attempts := 0; attempts < ds.RetryAttempts; attempts++ {
|
|
||||||
if len(b) > 0 {
|
|
||||||
req.Body = ioutil.NopCloser(bytes.NewBuffer(b))
|
|
||||||
}
|
|
||||||
resp, err = c.HTTPClient.Do(req)
|
|
||||||
if err != nil || !autorest.ResponseHasStatusCode(resp, ds.ValidStatusCodes...) {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
autorest.DelayForBackoff(ds.RetryDuration, attempts, req.Cancel)
|
|
||||||
ds.attempts = attempts
|
|
||||||
}
|
|
||||||
ds.attempts++
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client is the object that needs to be constructed to perform
|
|
||||||
// operations on the storage account.
|
|
||||||
type Client struct {
|
|
||||||
// HTTPClient is the http.Client used to initiate API
|
|
||||||
// requests. http.DefaultClient is used when creating a
|
|
||||||
// client.
|
|
||||||
HTTPClient *http.Client
|
|
||||||
|
|
||||||
// Sender is an interface that sends the request. Clients are
|
|
||||||
// created with a DefaultSender. The DefaultSender has an
|
|
||||||
// automatic retry strategy built in. The Sender can be customized.
|
|
||||||
Sender Sender
|
|
||||||
|
|
||||||
accountName string
|
|
||||||
accountKey []byte
|
|
||||||
useHTTPS bool
|
|
||||||
UseSharedKeyLite bool
|
|
||||||
baseURL string
|
|
||||||
apiVersion string
|
|
||||||
userAgent string
|
|
||||||
}
|
|
||||||
|
|
||||||
type storageResponse struct {
|
|
||||||
statusCode int
|
|
||||||
headers http.Header
|
|
||||||
body io.ReadCloser
|
|
||||||
}
|
|
||||||
|
|
||||||
type odataResponse struct {
|
|
||||||
storageResponse
|
|
||||||
odata odataErrorMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
// AzureStorageServiceError contains fields of the error response from
|
|
||||||
// Azure Storage Service REST API. See https://msdn.microsoft.com/en-us/library/azure/dd179382.aspx
|
|
||||||
// Some fields might be specific to certain calls.
|
|
||||||
type AzureStorageServiceError struct {
|
|
||||||
Code string `xml:"Code"`
|
|
||||||
Message string `xml:"Message"`
|
|
||||||
AuthenticationErrorDetail string `xml:"AuthenticationErrorDetail"`
|
|
||||||
QueryParameterName string `xml:"QueryParameterName"`
|
|
||||||
QueryParameterValue string `xml:"QueryParameterValue"`
|
|
||||||
Reason string `xml:"Reason"`
|
|
||||||
StatusCode int
|
|
||||||
RequestID string
|
|
||||||
Date string
|
|
||||||
APIVersion string
|
|
||||||
}
|
|
||||||
|
|
||||||
type odataErrorMessageMessage struct {
|
|
||||||
Lang string `json:"lang"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type odataErrorMessageInternal struct {
|
|
||||||
Code string `json:"code"`
|
|
||||||
Message odataErrorMessageMessage `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type odataErrorMessage struct {
|
|
||||||
Err odataErrorMessageInternal `json:"odata.error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnexpectedStatusCodeError is returned when a storage service responds with neither an error
|
|
||||||
// nor with an HTTP status code indicating success.
|
|
||||||
type UnexpectedStatusCodeError struct {
|
|
||||||
allowed []int
|
|
||||||
got int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e UnexpectedStatusCodeError) Error() string {
|
|
||||||
s := func(i int) string { return fmt.Sprintf("%d %s", i, http.StatusText(i)) }
|
|
||||||
|
|
||||||
got := s(e.got)
|
|
||||||
expected := []string{}
|
|
||||||
for _, v := range e.allowed {
|
|
||||||
expected = append(expected, s(v))
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("storage: status code from service response is %s; was expecting %s", got, strings.Join(expected, " or "))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Got is the actual status code returned by Azure.
|
|
||||||
func (e UnexpectedStatusCodeError) Got() int {
|
|
||||||
return e.got
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBasicClient constructs a Client with given storage service name and
|
|
||||||
// key.
|
|
||||||
func NewBasicClient(accountName, accountKey string) (Client, error) {
|
|
||||||
if accountName == StorageEmulatorAccountName {
|
|
||||||
return NewEmulatorClient()
|
|
||||||
}
|
|
||||||
return NewClient(accountName, accountKey, DefaultBaseURL, DefaultAPIVersion, defaultUseHTTPS)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBasicClientOnSovereignCloud constructs a Client with given storage service name and
|
|
||||||
// key in the referenced cloud.
|
|
||||||
func NewBasicClientOnSovereignCloud(accountName, accountKey string, env azure.Environment) (Client, error) {
|
|
||||||
if accountName == StorageEmulatorAccountName {
|
|
||||||
return NewEmulatorClient()
|
|
||||||
}
|
|
||||||
return NewClient(accountName, accountKey, env.StorageEndpointSuffix, DefaultAPIVersion, defaultUseHTTPS)
|
|
||||||
}
|
|
||||||
|
|
||||||
//NewEmulatorClient contructs a Client intended to only work with Azure
|
|
||||||
//Storage Emulator
|
|
||||||
func NewEmulatorClient() (Client, error) {
|
|
||||||
return NewClient(StorageEmulatorAccountName, StorageEmulatorAccountKey, DefaultBaseURL, DefaultAPIVersion, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClient constructs a Client. This should be used if the caller wants
|
|
||||||
// to specify whether to use HTTPS, a specific REST API version or a custom
|
|
||||||
// storage endpoint than Azure Public Cloud.
|
|
||||||
func NewClient(accountName, accountKey, blobServiceBaseURL, apiVersion string, useHTTPS bool) (Client, error) {
|
|
||||||
var c Client
|
|
||||||
if !IsValidStorageAccount(accountName) {
|
|
||||||
return c, fmt.Errorf("azure: account name is not valid: it must be between 3 and 24 characters, and only may contain numbers and lowercase letters: %v", accountName)
|
|
||||||
} else if accountKey == "" {
|
|
||||||
return c, fmt.Errorf("azure: account key required")
|
|
||||||
} else if blobServiceBaseURL == "" {
|
|
||||||
return c, fmt.Errorf("azure: base storage service url required")
|
|
||||||
}
|
|
||||||
|
|
||||||
key, err := base64.StdEncoding.DecodeString(accountKey)
|
|
||||||
if err != nil {
|
|
||||||
return c, fmt.Errorf("azure: malformed storage account key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c = Client{
|
|
||||||
HTTPClient: http.DefaultClient,
|
|
||||||
accountName: accountName,
|
|
||||||
accountKey: key,
|
|
||||||
useHTTPS: useHTTPS,
|
|
||||||
baseURL: blobServiceBaseURL,
|
|
||||||
apiVersion: apiVersion,
|
|
||||||
UseSharedKeyLite: false,
|
|
||||||
Sender: &DefaultSender{
|
|
||||||
RetryAttempts: 5,
|
|
||||||
ValidStatusCodes: []int{
|
|
||||||
http.StatusRequestTimeout, // 408
|
|
||||||
http.StatusInternalServerError, // 500
|
|
||||||
http.StatusBadGateway, // 502
|
|
||||||
http.StatusServiceUnavailable, // 503
|
|
||||||
http.StatusGatewayTimeout, // 504
|
|
||||||
},
|
|
||||||
RetryDuration: time.Second * 5,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
c.userAgent = c.getDefaultUserAgent()
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsValidStorageAccount checks if the storage account name is valid.
|
|
||||||
// See https://docs.microsoft.com/en-us/azure/storage/storage-create-storage-account
|
|
||||||
func IsValidStorageAccount(account string) bool {
|
|
||||||
return validStorageAccount.MatchString(account)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Client) getDefaultUserAgent() string {
|
|
||||||
return fmt.Sprintf("Go/%s (%s-%s) azure-storage-go/%s api-version/%s",
|
|
||||||
runtime.Version(),
|
|
||||||
runtime.GOARCH,
|
|
||||||
runtime.GOOS,
|
|
||||||
sdkVersion,
|
|
||||||
c.apiVersion,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddToUserAgent adds an extension to the current user agent
|
|
||||||
func (c *Client) AddToUserAgent(extension string) error {
|
|
||||||
if extension != "" {
|
|
||||||
c.userAgent = fmt.Sprintf("%s %s", c.userAgent, extension)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("Extension was empty, User Agent stayed as %s", c.userAgent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// protectUserAgent is used in funcs that include extraheaders as a parameter.
|
|
||||||
// It prevents the User-Agent header to be overwritten, instead if it happens to
|
|
||||||
// be present, it gets added to the current User-Agent. Use it before getStandardHeaders
|
|
||||||
func (c *Client) protectUserAgent(extraheaders map[string]string) map[string]string {
|
|
||||||
if v, ok := extraheaders[userAgentHeader]; ok {
|
|
||||||
c.AddToUserAgent(v)
|
|
||||||
delete(extraheaders, userAgentHeader)
|
|
||||||
}
|
|
||||||
return extraheaders
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Client) getBaseURL(service string) *url.URL {
|
|
||||||
scheme := "http"
|
|
||||||
if c.useHTTPS {
|
|
||||||
scheme = "https"
|
|
||||||
}
|
|
||||||
host := ""
|
|
||||||
if c.accountName == StorageEmulatorAccountName {
|
|
||||||
switch service {
|
|
||||||
case blobServiceName:
|
|
||||||
host = storageEmulatorBlob
|
|
||||||
case tableServiceName:
|
|
||||||
host = storageEmulatorTable
|
|
||||||
case queueServiceName:
|
|
||||||
host = storageEmulatorQueue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
host = fmt.Sprintf("%s.%s.%s", c.accountName, service, c.baseURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &url.URL{
|
|
||||||
Scheme: scheme,
|
|
||||||
Host: host,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Client) getEndpoint(service, path string, params url.Values) string {
|
|
||||||
u := c.getBaseURL(service)
|
|
||||||
|
|
||||||
// API doesn't accept path segments not starting with '/'
|
|
||||||
if !strings.HasPrefix(path, "/") {
|
|
||||||
path = fmt.Sprintf("/%v", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.accountName == StorageEmulatorAccountName {
|
|
||||||
path = fmt.Sprintf("/%v%v", StorageEmulatorAccountName, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
u.Path = path
|
|
||||||
u.RawQuery = params.Encode()
|
|
||||||
return u.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBlobService returns a BlobStorageClient which can operate on the blob
|
|
||||||
// service of the storage account.
|
|
||||||
func (c Client) GetBlobService() BlobStorageClient {
|
|
||||||
b := BlobStorageClient{
|
|
||||||
client: c,
|
|
||||||
}
|
|
||||||
b.client.AddToUserAgent(blobServiceName)
|
|
||||||
b.auth = sharedKey
|
|
||||||
if c.UseSharedKeyLite {
|
|
||||||
b.auth = sharedKeyLite
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetQueueService returns a QueueServiceClient which can operate on the queue
|
|
||||||
// service of the storage account.
|
|
||||||
func (c Client) GetQueueService() QueueServiceClient {
|
|
||||||
q := QueueServiceClient{
|
|
||||||
client: c,
|
|
||||||
}
|
|
||||||
q.client.AddToUserAgent(queueServiceName)
|
|
||||||
q.auth = sharedKey
|
|
||||||
if c.UseSharedKeyLite {
|
|
||||||
q.auth = sharedKeyLite
|
|
||||||
}
|
|
||||||
return q
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTableService returns a TableServiceClient which can operate on the table
|
|
||||||
// service of the storage account.
|
|
||||||
func (c Client) GetTableService() TableServiceClient {
|
|
||||||
t := TableServiceClient{
|
|
||||||
client: c,
|
|
||||||
}
|
|
||||||
t.client.AddToUserAgent(tableServiceName)
|
|
||||||
t.auth = sharedKeyForTable
|
|
||||||
if c.UseSharedKeyLite {
|
|
||||||
t.auth = sharedKeyLiteForTable
|
|
||||||
}
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFileService returns a FileServiceClient which can operate on the file
|
|
||||||
// service of the storage account.
|
|
||||||
func (c Client) GetFileService() FileServiceClient {
|
|
||||||
f := FileServiceClient{
|
|
||||||
client: c,
|
|
||||||
}
|
|
||||||
f.client.AddToUserAgent(fileServiceName)
|
|
||||||
f.auth = sharedKey
|
|
||||||
if c.UseSharedKeyLite {
|
|
||||||
f.auth = sharedKeyLite
|
|
||||||
}
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Client) getStandardHeaders() map[string]string {
|
|
||||||
return map[string]string{
|
|
||||||
userAgentHeader: c.userAgent,
|
|
||||||
"x-ms-version": c.apiVersion,
|
|
||||||
"x-ms-date": currentTimeRfc1123Formatted(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Client) exec(verb, url string, headers map[string]string, body io.Reader, auth authentication) (*storageResponse, error) {
|
|
||||||
headers, err := c.addAuthorizationHeader(verb, url, headers, auth)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest(verb, url, body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("azure/storage: error creating request: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if clstr, ok := headers["Content-Length"]; ok {
|
|
||||||
// content length header is being signed, but completely ignored by golang.
|
|
||||||
// instead we have to use the ContentLength property on the request struct
|
|
||||||
// (see https://golang.org/src/net/http/request.go?s=18140:18370#L536 and
|
|
||||||
// https://golang.org/src/net/http/transfer.go?s=1739:2467#L49)
|
|
||||||
req.ContentLength, err = strconv.ParseInt(clstr, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for k, v := range headers {
|
|
||||||
req.Header.Add(k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.Sender.Send(&c, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
statusCode := resp.StatusCode
|
|
||||||
if statusCode >= 400 && statusCode <= 505 {
|
|
||||||
var respBody []byte
|
|
||||||
respBody, err = readAndCloseBody(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
requestID, date, version := getDebugHeaders(resp.Header)
|
|
||||||
if len(respBody) == 0 {
|
|
||||||
// no error in response body, might happen in HEAD requests
|
|
||||||
err = serviceErrFromStatusCode(resp.StatusCode, resp.Status, requestID, date, version)
|
|
||||||
} else {
|
|
||||||
// response contains storage service error object, unmarshal
|
|
||||||
storageErr, errIn := serviceErrFromXML(respBody, resp.StatusCode, requestID, date, version)
|
|
||||||
if err != nil { // error unmarshaling the error response
|
|
||||||
err = errIn
|
|
||||||
}
|
|
||||||
err = storageErr
|
|
||||||
}
|
|
||||||
return &storageResponse{
|
|
||||||
statusCode: resp.StatusCode,
|
|
||||||
headers: resp.Header,
|
|
||||||
body: ioutil.NopCloser(bytes.NewReader(respBody)), /* restore the body */
|
|
||||||
}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &storageResponse{
|
|
||||||
statusCode: resp.StatusCode,
|
|
||||||
headers: resp.Header,
|
|
||||||
body: resp.Body}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Client) execInternalJSONCommon(verb, url string, headers map[string]string, body io.Reader, auth authentication) (*odataResponse, *http.Request, *http.Response, error) {
|
|
||||||
headers, err := c.addAuthorizationHeader(verb, url, headers, auth)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest(verb, url, body)
|
|
||||||
for k, v := range headers {
|
|
||||||
req.Header.Add(k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.Sender.Send(&c, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
respToRet := &odataResponse{}
|
|
||||||
respToRet.body = resp.Body
|
|
||||||
respToRet.statusCode = resp.StatusCode
|
|
||||||
respToRet.headers = resp.Header
|
|
||||||
|
|
||||||
statusCode := resp.StatusCode
|
|
||||||
if statusCode >= 400 && statusCode <= 505 {
|
|
||||||
var respBody []byte
|
|
||||||
respBody, err = readAndCloseBody(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
requestID, date, version := getDebugHeaders(resp.Header)
|
|
||||||
if len(respBody) == 0 {
|
|
||||||
// no error in response body, might happen in HEAD requests
|
|
||||||
err = serviceErrFromStatusCode(resp.StatusCode, resp.Status, requestID, date, version)
|
|
||||||
return respToRet, req, resp, err
|
|
||||||
}
|
|
||||||
// try unmarshal as odata.error json
|
|
||||||
err = json.Unmarshal(respBody, &respToRet.odata)
|
|
||||||
}
|
|
||||||
|
|
||||||
return respToRet, req, resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Client) execInternalJSON(verb, url string, headers map[string]string, body io.Reader, auth authentication) (*odataResponse, error) {
|
|
||||||
respToRet, _, _, err := c.execInternalJSONCommon(verb, url, headers, body, auth)
|
|
||||||
return respToRet, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Client) execBatchOperationJSON(verb, url string, headers map[string]string, body io.Reader, auth authentication) (*odataResponse, error) {
|
|
||||||
// execute common query, get back generated request, response etc... for more processing.
|
|
||||||
respToRet, req, resp, err := c.execInternalJSONCommon(verb, url, headers, body, auth)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// return the OData in the case of executing batch commands.
|
|
||||||
// In this case we need to read the outer batch boundary and contents.
|
|
||||||
// Then we read the changeset information within the batch
|
|
||||||
var respBody []byte
|
|
||||||
respBody, err = readAndCloseBody(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// outer multipart body
|
|
||||||
_, batchHeader, err := mime.ParseMediaType(resp.Header["Content-Type"][0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// batch details.
|
|
||||||
batchBoundary := batchHeader["boundary"]
|
|
||||||
batchPartBuf, changesetBoundary, err := genBatchReader(batchBoundary, respBody)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// changeset details.
|
|
||||||
err = genChangesetReader(req, respToRet, batchPartBuf, changesetBoundary)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return respToRet, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func genChangesetReader(req *http.Request, respToRet *odataResponse, batchPartBuf io.Reader, changesetBoundary string) error {
|
|
||||||
changesetMultiReader := multipart.NewReader(batchPartBuf, changesetBoundary)
|
|
||||||
changesetPart, err := changesetMultiReader.NextPart()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
changesetPartBufioReader := bufio.NewReader(changesetPart)
|
|
||||||
changesetResp, err := http.ReadResponse(changesetPartBufioReader, req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if changesetResp.StatusCode != http.StatusNoContent {
|
|
||||||
changesetBody, err := readAndCloseBody(changesetResp.Body)
|
|
||||||
err = json.Unmarshal(changesetBody, &respToRet.odata)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
respToRet.statusCode = changesetResp.StatusCode
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func genBatchReader(batchBoundary string, respBody []byte) (io.Reader, string, error) {
|
|
||||||
respBodyString := string(respBody)
|
|
||||||
respBodyReader := strings.NewReader(respBodyString)
|
|
||||||
|
|
||||||
// reading batchresponse
|
|
||||||
batchMultiReader := multipart.NewReader(respBodyReader, batchBoundary)
|
|
||||||
batchPart, err := batchMultiReader.NextPart()
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
batchPartBufioReader := bufio.NewReader(batchPart)
|
|
||||||
|
|
||||||
_, changesetHeader, err := mime.ParseMediaType(batchPart.Header.Get("Content-Type"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
changesetBoundary := changesetHeader["boundary"]
|
|
||||||
return batchPartBufioReader, changesetBoundary, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readAndCloseBody(body io.ReadCloser) ([]byte, error) {
|
|
||||||
defer body.Close()
|
|
||||||
out, err := ioutil.ReadAll(body)
|
|
||||||
if err == io.EOF {
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func serviceErrFromXML(body []byte, statusCode int, requestID, date, version string) (AzureStorageServiceError, error) {
|
|
||||||
storageErr := AzureStorageServiceError{
|
|
||||||
StatusCode: statusCode,
|
|
||||||
RequestID: requestID,
|
|
||||||
Date: date,
|
|
||||||
APIVersion: version,
|
|
||||||
}
|
|
||||||
if err := xml.Unmarshal(body, &storageErr); err != nil {
|
|
||||||
storageErr.Message = fmt.Sprintf("Response body could no be unmarshaled: %v. Body: %v.", err, string(body))
|
|
||||||
return storageErr, err
|
|
||||||
}
|
|
||||||
return storageErr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func serviceErrFromStatusCode(code int, status string, requestID, date, version string) AzureStorageServiceError {
|
|
||||||
return AzureStorageServiceError{
|
|
||||||
StatusCode: code,
|
|
||||||
Code: status,
|
|
||||||
RequestID: requestID,
|
|
||||||
Date: date,
|
|
||||||
APIVersion: version,
|
|
||||||
Message: "no response body was available for error status code",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e AzureStorageServiceError) Error() string {
|
|
||||||
return fmt.Sprintf("storage: service returned error: StatusCode=%d, ErrorCode=%s, ErrorMessage=%s, RequestInitiated=%s, RequestId=%s, API Version=%s, QueryParameterName=%s, QueryParameterValue=%s",
|
|
||||||
e.StatusCode, e.Code, e.Message, e.Date, e.RequestID, e.APIVersion, e.QueryParameterName, e.QueryParameterValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkRespCode returns UnexpectedStatusError if the given response code is not
|
|
||||||
// one of the allowed status codes; otherwise nil.
|
|
||||||
func checkRespCode(respCode int, allowed []int) error {
|
|
||||||
for _, v := range allowed {
|
|
||||||
if respCode == v {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return UnexpectedStatusCodeError{allowed, respCode}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Client) addMetadataToHeaders(h map[string]string, metadata map[string]string) map[string]string {
|
|
||||||
metadata = c.protectUserAgent(metadata)
|
|
||||||
for k, v := range metadata {
|
|
||||||
h[userDefinedMetadataHeaderPrefix+k] = v
|
|
||||||
}
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDebugHeaders(h http.Header) (requestID, date, version string) {
|
|
||||||
requestID = h.Get("x-ms-request-id")
|
|
||||||
version = h.Get("x-ms-version")
|
|
||||||
date = h.Get("Date")
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,450 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Container represents an Azure container.
|
|
||||||
type Container struct {
|
|
||||||
bsc *BlobStorageClient
|
|
||||||
Name string `xml:"Name"`
|
|
||||||
Properties ContainerProperties `xml:"Properties"`
|
|
||||||
Metadata map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Container) buildPath() string {
|
|
||||||
return fmt.Sprintf("/%s", c.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContainerProperties contains various properties of a container returned from
|
|
||||||
// various endpoints like ListContainers.
|
|
||||||
type ContainerProperties struct {
|
|
||||||
LastModified string `xml:"Last-Modified"`
|
|
||||||
Etag string `xml:"Etag"`
|
|
||||||
LeaseStatus string `xml:"LeaseStatus"`
|
|
||||||
LeaseState string `xml:"LeaseState"`
|
|
||||||
LeaseDuration string `xml:"LeaseDuration"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContainerListResponse contains the response fields from
|
|
||||||
// ListContainers call.
|
|
||||||
//
|
|
||||||
// See https://msdn.microsoft.com/en-us/library/azure/dd179352.aspx
|
|
||||||
type ContainerListResponse struct {
|
|
||||||
XMLName xml.Name `xml:"EnumerationResults"`
|
|
||||||
Xmlns string `xml:"xmlns,attr"`
|
|
||||||
Prefix string `xml:"Prefix"`
|
|
||||||
Marker string `xml:"Marker"`
|
|
||||||
NextMarker string `xml:"NextMarker"`
|
|
||||||
MaxResults int64 `xml:"MaxResults"`
|
|
||||||
Containers []Container `xml:"Containers>Container"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// BlobListResponse contains the response fields from ListBlobs call.
|
|
||||||
//
|
|
||||||
// See https://msdn.microsoft.com/en-us/library/azure/dd135734.aspx
|
|
||||||
type BlobListResponse struct {
|
|
||||||
XMLName xml.Name `xml:"EnumerationResults"`
|
|
||||||
Xmlns string `xml:"xmlns,attr"`
|
|
||||||
Prefix string `xml:"Prefix"`
|
|
||||||
Marker string `xml:"Marker"`
|
|
||||||
NextMarker string `xml:"NextMarker"`
|
|
||||||
MaxResults int64 `xml:"MaxResults"`
|
|
||||||
Blobs []Blob `xml:"Blobs>Blob"`
|
|
||||||
|
|
||||||
// BlobPrefix is used to traverse blobs as if it were a file system.
|
|
||||||
// It is returned if ListBlobsParameters.Delimiter is specified.
|
|
||||||
// The list here can be thought of as "folders" that may contain
|
|
||||||
// other folders or blobs.
|
|
||||||
BlobPrefixes []string `xml:"Blobs>BlobPrefix>Name"`
|
|
||||||
|
|
||||||
// Delimiter is used to traverse blobs as if it were a file system.
|
|
||||||
// It is returned if ListBlobsParameters.Delimiter is specified.
|
|
||||||
Delimiter string `xml:"Delimiter"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// IncludeBlobDataset has options to include in a list blobs operation
|
|
||||||
type IncludeBlobDataset struct {
|
|
||||||
Snapshots bool
|
|
||||||
Metadata bool
|
|
||||||
UncommittedBlobs bool
|
|
||||||
Copy bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListBlobsParameters defines the set of customizable
|
|
||||||
// parameters to make a List Blobs call.
|
|
||||||
//
|
|
||||||
// See https://msdn.microsoft.com/en-us/library/azure/dd135734.aspx
|
|
||||||
type ListBlobsParameters struct {
|
|
||||||
Prefix string
|
|
||||||
Delimiter string
|
|
||||||
Marker string
|
|
||||||
Include *IncludeBlobDataset
|
|
||||||
MaxResults uint
|
|
||||||
Timeout uint
|
|
||||||
RequestID string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p ListBlobsParameters) getParameters() url.Values {
|
|
||||||
out := url.Values{}
|
|
||||||
|
|
||||||
if p.Prefix != "" {
|
|
||||||
out.Set("prefix", p.Prefix)
|
|
||||||
}
|
|
||||||
if p.Delimiter != "" {
|
|
||||||
out.Set("delimiter", p.Delimiter)
|
|
||||||
}
|
|
||||||
if p.Marker != "" {
|
|
||||||
out.Set("marker", p.Marker)
|
|
||||||
}
|
|
||||||
if p.Include != nil {
|
|
||||||
include := []string{}
|
|
||||||
include = addString(include, p.Include.Snapshots, "snapshots")
|
|
||||||
include = addString(include, p.Include.Metadata, "metadata")
|
|
||||||
include = addString(include, p.Include.UncommittedBlobs, "uncommittedblobs")
|
|
||||||
include = addString(include, p.Include.Copy, "copy")
|
|
||||||
fullInclude := strings.Join(include, ",")
|
|
||||||
out.Set("include", fullInclude)
|
|
||||||
}
|
|
||||||
if p.MaxResults != 0 {
|
|
||||||
out.Set("maxresults", strconv.FormatUint(uint64(p.MaxResults), 10))
|
|
||||||
}
|
|
||||||
if p.Timeout != 0 {
|
|
||||||
out.Set("timeout", strconv.FormatUint(uint64(p.Timeout), 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func addString(datasets []string, include bool, text string) []string {
|
|
||||||
if include {
|
|
||||||
datasets = append(datasets, text)
|
|
||||||
}
|
|
||||||
return datasets
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContainerAccessType defines the access level to the container from a public
|
|
||||||
// request.
|
|
||||||
//
|
|
||||||
// See https://msdn.microsoft.com/en-us/library/azure/dd179468.aspx and "x-ms-
|
|
||||||
// blob-public-access" header.
|
|
||||||
type ContainerAccessType string
|
|
||||||
|
|
||||||
// Access options for containers
|
|
||||||
const (
|
|
||||||
ContainerAccessTypePrivate ContainerAccessType = ""
|
|
||||||
ContainerAccessTypeBlob ContainerAccessType = "blob"
|
|
||||||
ContainerAccessTypeContainer ContainerAccessType = "container"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ContainerAccessPolicy represents each access policy in the container ACL.
|
|
||||||
type ContainerAccessPolicy struct {
|
|
||||||
ID string
|
|
||||||
StartTime time.Time
|
|
||||||
ExpiryTime time.Time
|
|
||||||
CanRead bool
|
|
||||||
CanWrite bool
|
|
||||||
CanDelete bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContainerPermissions represents the container ACLs.
|
|
||||||
type ContainerPermissions struct {
|
|
||||||
AccessType ContainerAccessType
|
|
||||||
AccessPolicies []ContainerAccessPolicy
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContainerAccessHeader references header used when setting/getting container ACL
|
|
||||||
const (
|
|
||||||
ContainerAccessHeader string = "x-ms-blob-public-access"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetBlobReference returns a Blob object for the specified blob name.
|
|
||||||
func (c *Container) GetBlobReference(name string) *Blob {
|
|
||||||
return &Blob{
|
|
||||||
Container: c,
|
|
||||||
Name: name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateContainerOptions includes the options for a create container operation
|
|
||||||
type CreateContainerOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
Access ContainerAccessType `header:"x-ms-blob-public-access"`
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create creates a blob container within the storage account
|
|
||||||
// with given name and access level. Returns error if container already exists.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Create-Container
|
|
||||||
func (c *Container) Create(options *CreateContainerOptions) error {
|
|
||||||
resp, err := c.create(options)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
readAndCloseBody(resp.body)
|
|
||||||
return checkRespCode(resp.statusCode, []int{http.StatusCreated})
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateIfNotExists creates a blob container if it does not exist. Returns
|
|
||||||
// true if container is newly created or false if container already exists.
|
|
||||||
func (c *Container) CreateIfNotExists(options *CreateContainerOptions) (bool, error) {
|
|
||||||
resp, err := c.create(options)
|
|
||||||
if resp != nil {
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
if resp.statusCode == http.StatusCreated || resp.statusCode == http.StatusConflict {
|
|
||||||
return resp.statusCode == http.StatusCreated, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Container) create(options *CreateContainerOptions) (*storageResponse, error) {
|
|
||||||
query := url.Values{"restype": {"container"}}
|
|
||||||
headers := c.bsc.client.getStandardHeaders()
|
|
||||||
headers = c.bsc.client.addMetadataToHeaders(headers, c.Metadata)
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
query = addTimeout(query, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := c.bsc.client.getEndpoint(blobServiceName, c.buildPath(), query)
|
|
||||||
|
|
||||||
return c.bsc.client.exec(http.MethodPut, uri, headers, nil, c.bsc.auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exists returns true if a container with given name exists
|
|
||||||
// on the storage account, otherwise returns false.
|
|
||||||
func (c *Container) Exists() (bool, error) {
|
|
||||||
uri := c.bsc.client.getEndpoint(blobServiceName, c.buildPath(), url.Values{"restype": {"container"}})
|
|
||||||
headers := c.bsc.client.getStandardHeaders()
|
|
||||||
|
|
||||||
resp, err := c.bsc.client.exec(http.MethodHead, uri, headers, nil, c.bsc.auth)
|
|
||||||
if resp != nil {
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
if resp.statusCode == http.StatusOK || resp.statusCode == http.StatusNotFound {
|
|
||||||
return resp.statusCode == http.StatusOK, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetContainerPermissionOptions includes options for a set container permissions operation
|
|
||||||
type SetContainerPermissionOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
LeaseID string `header:"x-ms-lease-id"`
|
|
||||||
IfModifiedSince *time.Time `header:"If-Modified-Since"`
|
|
||||||
IfUnmodifiedSince *time.Time `header:"If-Unmodified-Since"`
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPermissions sets up container permissions
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Set-Container-ACL
|
|
||||||
func (c *Container) SetPermissions(permissions ContainerPermissions, options *SetContainerPermissionOptions) error {
|
|
||||||
body, length, err := generateContainerACLpayload(permissions.AccessPolicies)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
params := url.Values{
|
|
||||||
"restype": {"container"},
|
|
||||||
"comp": {"acl"},
|
|
||||||
}
|
|
||||||
headers := c.bsc.client.getStandardHeaders()
|
|
||||||
headers = addToHeaders(headers, ContainerAccessHeader, string(permissions.AccessType))
|
|
||||||
headers["Content-Length"] = strconv.Itoa(length)
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := c.bsc.client.getEndpoint(blobServiceName, c.buildPath(), params)
|
|
||||||
|
|
||||||
resp, err := c.bsc.client.exec(http.MethodPut, uri, headers, body, c.bsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
|
||||||
return errors.New("Unable to set permissions")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetContainerPermissionOptions includes options for a get container permissions operation
|
|
||||||
type GetContainerPermissionOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
LeaseID string `header:"x-ms-lease-id"`
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPermissions gets the container permissions as per https://msdn.microsoft.com/en-us/library/azure/dd179469.aspx
|
|
||||||
// If timeout is 0 then it will not be passed to Azure
|
|
||||||
// leaseID will only be passed to Azure if populated
|
|
||||||
func (c *Container) GetPermissions(options *GetContainerPermissionOptions) (*ContainerPermissions, error) {
|
|
||||||
params := url.Values{
|
|
||||||
"restype": {"container"},
|
|
||||||
"comp": {"acl"},
|
|
||||||
}
|
|
||||||
headers := c.bsc.client.getStandardHeaders()
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := c.bsc.client.getEndpoint(blobServiceName, c.buildPath(), params)
|
|
||||||
|
|
||||||
resp, err := c.bsc.client.exec(http.MethodGet, uri, headers, nil, c.bsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.body.Close()
|
|
||||||
|
|
||||||
var ap AccessPolicy
|
|
||||||
err = xmlUnmarshal(resp.body, &ap.SignedIdentifiersList)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return buildAccessPolicy(ap, &resp.headers), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildAccessPolicy(ap AccessPolicy, headers *http.Header) *ContainerPermissions {
|
|
||||||
// containerAccess. Blob, Container, empty
|
|
||||||
containerAccess := headers.Get(http.CanonicalHeaderKey(ContainerAccessHeader))
|
|
||||||
permissions := ContainerPermissions{
|
|
||||||
AccessType: ContainerAccessType(containerAccess),
|
|
||||||
AccessPolicies: []ContainerAccessPolicy{},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, policy := range ap.SignedIdentifiersList.SignedIdentifiers {
|
|
||||||
capd := ContainerAccessPolicy{
|
|
||||||
ID: policy.ID,
|
|
||||||
StartTime: policy.AccessPolicy.StartTime,
|
|
||||||
ExpiryTime: policy.AccessPolicy.ExpiryTime,
|
|
||||||
}
|
|
||||||
capd.CanRead = updatePermissions(policy.AccessPolicy.Permission, "r")
|
|
||||||
capd.CanWrite = updatePermissions(policy.AccessPolicy.Permission, "w")
|
|
||||||
capd.CanDelete = updatePermissions(policy.AccessPolicy.Permission, "d")
|
|
||||||
|
|
||||||
permissions.AccessPolicies = append(permissions.AccessPolicies, capd)
|
|
||||||
}
|
|
||||||
return &permissions
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteContainerOptions includes options for a delete container operation
|
|
||||||
type DeleteContainerOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
LeaseID string `header:"x-ms-lease-id"`
|
|
||||||
IfModifiedSince *time.Time `header:"If-Modified-Since"`
|
|
||||||
IfUnmodifiedSince *time.Time `header:"If-Unmodified-Since"`
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete deletes the container with given name on the storage
|
|
||||||
// account. If the container does not exist returns error.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/delete-container
|
|
||||||
func (c *Container) Delete(options *DeleteContainerOptions) error {
|
|
||||||
resp, err := c.delete(options)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
readAndCloseBody(resp.body)
|
|
||||||
return checkRespCode(resp.statusCode, []int{http.StatusAccepted})
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteIfExists deletes the container with given name on the storage
|
|
||||||
// account if it exists. Returns true if container is deleted with this call, or
|
|
||||||
// false if the container did not exist at the time of the Delete Container
|
|
||||||
// operation.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/delete-container
|
|
||||||
func (c *Container) DeleteIfExists(options *DeleteContainerOptions) (bool, error) {
|
|
||||||
resp, err := c.delete(options)
|
|
||||||
if resp != nil {
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
if resp.statusCode == http.StatusAccepted || resp.statusCode == http.StatusNotFound {
|
|
||||||
return resp.statusCode == http.StatusAccepted, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Container) delete(options *DeleteContainerOptions) (*storageResponse, error) {
|
|
||||||
query := url.Values{"restype": {"container"}}
|
|
||||||
headers := c.bsc.client.getStandardHeaders()
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
query = addTimeout(query, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := c.bsc.client.getEndpoint(blobServiceName, c.buildPath(), query)
|
|
||||||
|
|
||||||
return c.bsc.client.exec(http.MethodDelete, uri, headers, nil, c.bsc.auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListBlobs returns an object that contains list of blobs in the container,
|
|
||||||
// pagination token and other information in the response of List Blobs call.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/List-Blobs
|
|
||||||
func (c *Container) ListBlobs(params ListBlobsParameters) (BlobListResponse, error) {
|
|
||||||
q := mergeParams(params.getParameters(), url.Values{
|
|
||||||
"restype": {"container"},
|
|
||||||
"comp": {"list"}},
|
|
||||||
)
|
|
||||||
uri := c.bsc.client.getEndpoint(blobServiceName, c.buildPath(), q)
|
|
||||||
|
|
||||||
headers := c.bsc.client.getStandardHeaders()
|
|
||||||
headers = addToHeaders(headers, "x-ms-client-request-id", params.RequestID)
|
|
||||||
|
|
||||||
var out BlobListResponse
|
|
||||||
resp, err := c.bsc.client.exec(http.MethodGet, uri, headers, nil, c.bsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
defer resp.body.Close()
|
|
||||||
|
|
||||||
err = xmlUnmarshal(resp.body, &out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateContainerACLpayload(policies []ContainerAccessPolicy) (io.Reader, int, error) {
|
|
||||||
sil := SignedIdentifiers{
|
|
||||||
SignedIdentifiers: []SignedIdentifier{},
|
|
||||||
}
|
|
||||||
for _, capd := range policies {
|
|
||||||
permission := capd.generateContainerPermissions()
|
|
||||||
signedIdentifier := convertAccessPolicyToXMLStructs(capd.ID, capd.StartTime, capd.ExpiryTime, permission)
|
|
||||||
sil.SignedIdentifiers = append(sil.SignedIdentifiers, signedIdentifier)
|
|
||||||
}
|
|
||||||
return xmlMarshal(sil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (capd *ContainerAccessPolicy) generateContainerPermissions() (permissions string) {
|
|
||||||
// generate the permissions string (rwd).
|
|
||||||
// still want the end user API to have bool flags.
|
|
||||||
permissions = ""
|
|
||||||
|
|
||||||
if capd.CanRead {
|
|
||||||
permissions += "r"
|
|
||||||
}
|
|
||||||
|
|
||||||
if capd.CanWrite {
|
|
||||||
permissions += "w"
|
|
||||||
}
|
|
||||||
|
|
||||||
if capd.CanDelete {
|
|
||||||
permissions += "d"
|
|
||||||
}
|
|
||||||
|
|
||||||
return permissions
|
|
||||||
}
|
|
|
@ -1,223 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
blobCopyStatusPending = "pending"
|
|
||||||
blobCopyStatusSuccess = "success"
|
|
||||||
blobCopyStatusAborted = "aborted"
|
|
||||||
blobCopyStatusFailed = "failed"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CopyOptions includes the options for a copy blob operation
|
|
||||||
type CopyOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
Source CopyOptionsConditions
|
|
||||||
Destiny CopyOptionsConditions
|
|
||||||
RequestID string
|
|
||||||
}
|
|
||||||
|
|
||||||
// IncrementalCopyOptions includes the options for an incremental copy blob operation
|
|
||||||
type IncrementalCopyOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
Destination IncrementalCopyOptionsConditions
|
|
||||||
RequestID string
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyOptionsConditions includes some conditional options in a copy blob operation
|
|
||||||
type CopyOptionsConditions struct {
|
|
||||||
LeaseID string
|
|
||||||
IfModifiedSince *time.Time
|
|
||||||
IfUnmodifiedSince *time.Time
|
|
||||||
IfMatch string
|
|
||||||
IfNoneMatch string
|
|
||||||
}
|
|
||||||
|
|
||||||
// IncrementalCopyOptionsConditions includes some conditional options in a copy blob operation
|
|
||||||
type IncrementalCopyOptionsConditions struct {
|
|
||||||
IfModifiedSince *time.Time
|
|
||||||
IfUnmodifiedSince *time.Time
|
|
||||||
IfMatch string
|
|
||||||
IfNoneMatch string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy starts a blob copy operation and waits for the operation to
|
|
||||||
// complete. sourceBlob parameter must be a canonical URL to the blob (can be
|
|
||||||
// obtained using GetBlobURL method.) There is no SLA on blob copy and therefore
|
|
||||||
// this helper method works faster on smaller files.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Copy-Blob
|
|
||||||
func (b *Blob) Copy(sourceBlob string, options *CopyOptions) error {
|
|
||||||
copyID, err := b.StartCopy(sourceBlob, options)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.WaitForCopy(copyID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartCopy starts a blob copy operation.
|
|
||||||
// sourceBlob parameter must be a canonical URL to the blob (can be
|
|
||||||
// obtained using GetBlobURL method.)
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Copy-Blob
|
|
||||||
func (b *Blob) StartCopy(sourceBlob string, options *CopyOptions) (string, error) {
|
|
||||||
params := url.Values{}
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
headers["x-ms-copy-source"] = sourceBlob
|
|
||||||
headers = b.Container.bsc.client.addMetadataToHeaders(headers, b.Metadata)
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
headers = addToHeaders(headers, "x-ms-client-request-id", options.RequestID)
|
|
||||||
// source
|
|
||||||
headers = addToHeaders(headers, "x-ms-source-lease-id", options.Source.LeaseID)
|
|
||||||
headers = addTimeToHeaders(headers, "x-ms-source-if-modified-since", options.Source.IfModifiedSince)
|
|
||||||
headers = addTimeToHeaders(headers, "x-ms-source-if-unmodified-since", options.Source.IfUnmodifiedSince)
|
|
||||||
headers = addToHeaders(headers, "x-ms-source-if-match", options.Source.IfMatch)
|
|
||||||
headers = addToHeaders(headers, "x-ms-source-if-none-match", options.Source.IfNoneMatch)
|
|
||||||
//destiny
|
|
||||||
headers = addToHeaders(headers, "x-ms-lease-id", options.Destiny.LeaseID)
|
|
||||||
headers = addTimeToHeaders(headers, "x-ms-if-modified-since", options.Destiny.IfModifiedSince)
|
|
||||||
headers = addTimeToHeaders(headers, "x-ms-if-unmodified-since", options.Destiny.IfUnmodifiedSince)
|
|
||||||
headers = addToHeaders(headers, "x-ms-if-match", options.Destiny.IfMatch)
|
|
||||||
headers = addToHeaders(headers, "x-ms-if-none-match", options.Destiny.IfNoneMatch)
|
|
||||||
}
|
|
||||||
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
|
|
||||||
|
|
||||||
resp, err := b.Container.bsc.client.exec(http.MethodPut, uri, headers, nil, b.Container.bsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
if err := checkRespCode(resp.statusCode, []int{http.StatusAccepted, http.StatusCreated}); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
copyID := resp.headers.Get("x-ms-copy-id")
|
|
||||||
if copyID == "" {
|
|
||||||
return "", errors.New("Got empty copy id header")
|
|
||||||
}
|
|
||||||
return copyID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AbortCopyOptions includes the options for an abort blob operation
|
|
||||||
type AbortCopyOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
LeaseID string `header:"x-ms-lease-id"`
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AbortCopy aborts a BlobCopy which has already been triggered by the StartBlobCopy function.
|
|
||||||
// copyID is generated from StartBlobCopy function.
|
|
||||||
// currentLeaseID is required IF the destination blob has an active lease on it.
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Abort-Copy-Blob
|
|
||||||
func (b *Blob) AbortCopy(copyID string, options *AbortCopyOptions) error {
|
|
||||||
params := url.Values{
|
|
||||||
"comp": {"copy"},
|
|
||||||
"copyid": {copyID},
|
|
||||||
}
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
headers["x-ms-copy-action"] = "abort"
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
|
|
||||||
|
|
||||||
resp, err := b.Container.bsc.client.exec(http.MethodPut, uri, headers, nil, b.Container.bsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
readAndCloseBody(resp.body)
|
|
||||||
return checkRespCode(resp.statusCode, []int{http.StatusNoContent})
|
|
||||||
}
|
|
||||||
|
|
||||||
// WaitForCopy loops until a BlobCopy operation is completed (or fails with error)
|
|
||||||
func (b *Blob) WaitForCopy(copyID string) error {
|
|
||||||
for {
|
|
||||||
err := b.GetProperties(nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.Properties.CopyID != copyID {
|
|
||||||
return errBlobCopyIDMismatch
|
|
||||||
}
|
|
||||||
|
|
||||||
switch b.Properties.CopyStatus {
|
|
||||||
case blobCopyStatusSuccess:
|
|
||||||
return nil
|
|
||||||
case blobCopyStatusPending:
|
|
||||||
continue
|
|
||||||
case blobCopyStatusAborted:
|
|
||||||
return errBlobCopyAborted
|
|
||||||
case blobCopyStatusFailed:
|
|
||||||
return fmt.Errorf("storage: blob copy failed. Id=%s Description=%s", b.Properties.CopyID, b.Properties.CopyStatusDescription)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("storage: unhandled blob copy status: '%s'", b.Properties.CopyStatus)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IncrementalCopyBlob copies a snapshot of a source blob and copies to referring blob
|
|
||||||
// sourceBlob parameter must be a valid snapshot URL of the original blob.
|
|
||||||
// THe original blob mut be public, or use a Shared Access Signature.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/incremental-copy-blob .
|
|
||||||
func (b *Blob) IncrementalCopyBlob(sourceBlobURL string, snapshotTime time.Time, options *IncrementalCopyOptions) (string, error) {
|
|
||||||
params := url.Values{"comp": {"incrementalcopy"}}
|
|
||||||
|
|
||||||
// need formatting to 7 decimal places so it's friendly to Windows and *nix
|
|
||||||
snapshotTimeFormatted := snapshotTime.Format("2006-01-02T15:04:05.0000000Z")
|
|
||||||
u, err := url.Parse(sourceBlobURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
query := u.Query()
|
|
||||||
query.Add("snapshot", snapshotTimeFormatted)
|
|
||||||
encodedQuery := query.Encode()
|
|
||||||
encodedQuery = strings.Replace(encodedQuery, "%3A", ":", -1)
|
|
||||||
u.RawQuery = encodedQuery
|
|
||||||
snapshotURL := u.String()
|
|
||||||
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
headers["x-ms-copy-source"] = snapshotURL
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
addTimeout(params, options.Timeout)
|
|
||||||
headers = addToHeaders(headers, "x-ms-client-request-id", options.RequestID)
|
|
||||||
headers = addTimeToHeaders(headers, "x-ms-if-modified-since", options.Destination.IfModifiedSince)
|
|
||||||
headers = addTimeToHeaders(headers, "x-ms-if-unmodified-since", options.Destination.IfUnmodifiedSince)
|
|
||||||
headers = addToHeaders(headers, "x-ms-if-match", options.Destination.IfMatch)
|
|
||||||
headers = addToHeaders(headers, "x-ms-if-none-match", options.Destination.IfNoneMatch)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get URI of destination blob
|
|
||||||
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
|
|
||||||
|
|
||||||
resp, err := b.Container.bsc.client.exec(http.MethodPut, uri, headers, nil, b.Container.bsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
if err := checkRespCode(resp.statusCode, []int{http.StatusAccepted}); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
copyID := resp.headers.Get("x-ms-copy-id")
|
|
||||||
if copyID == "" {
|
|
||||||
return "", errors.New("Got empty copy id header")
|
|
||||||
}
|
|
||||||
return copyID, nil
|
|
||||||
}
|
|
|
@ -1,222 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Directory represents a directory on a share.
|
|
||||||
type Directory struct {
|
|
||||||
fsc *FileServiceClient
|
|
||||||
Metadata map[string]string
|
|
||||||
Name string `xml:"Name"`
|
|
||||||
parent *Directory
|
|
||||||
Properties DirectoryProperties
|
|
||||||
share *Share
|
|
||||||
}
|
|
||||||
|
|
||||||
// DirectoryProperties contains various properties of a directory.
|
|
||||||
type DirectoryProperties struct {
|
|
||||||
LastModified string `xml:"Last-Modified"`
|
|
||||||
Etag string `xml:"Etag"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListDirsAndFilesParameters defines the set of customizable parameters to
|
|
||||||
// make a List Files and Directories call.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/List-Directories-and-Files
|
|
||||||
type ListDirsAndFilesParameters struct {
|
|
||||||
Prefix string
|
|
||||||
Marker string
|
|
||||||
MaxResults uint
|
|
||||||
Timeout uint
|
|
||||||
}
|
|
||||||
|
|
||||||
// DirsAndFilesListResponse contains the response fields from
|
|
||||||
// a List Files and Directories call.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/List-Directories-and-Files
|
|
||||||
type DirsAndFilesListResponse struct {
|
|
||||||
XMLName xml.Name `xml:"EnumerationResults"`
|
|
||||||
Xmlns string `xml:"xmlns,attr"`
|
|
||||||
Marker string `xml:"Marker"`
|
|
||||||
MaxResults int64 `xml:"MaxResults"`
|
|
||||||
Directories []Directory `xml:"Entries>Directory"`
|
|
||||||
Files []File `xml:"Entries>File"`
|
|
||||||
NextMarker string `xml:"NextMarker"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// builds the complete directory path for this directory object.
|
|
||||||
func (d *Directory) buildPath() string {
|
|
||||||
path := ""
|
|
||||||
current := d
|
|
||||||
for current.Name != "" {
|
|
||||||
path = "/" + current.Name + path
|
|
||||||
current = current.parent
|
|
||||||
}
|
|
||||||
return d.share.buildPath() + path
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create this directory in the associated share.
|
|
||||||
// If a directory with the same name already exists, the operation fails.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Create-Directory
|
|
||||||
func (d *Directory) Create(options *FileRequestOptions) error {
|
|
||||||
// if this is the root directory exit early
|
|
||||||
if d.parent == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
params := prepareOptions(options)
|
|
||||||
headers, err := d.fsc.createResource(d.buildPath(), resourceDirectory, params, mergeMDIntoExtraHeaders(d.Metadata, nil), []int{http.StatusCreated})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
d.updateEtagAndLastModified(headers)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateIfNotExists creates this directory under the associated share if the
|
|
||||||
// directory does not exists. Returns true if the directory is newly created or
|
|
||||||
// false if the directory already exists.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Create-Directory
|
|
||||||
func (d *Directory) CreateIfNotExists(options *FileRequestOptions) (bool, error) {
|
|
||||||
// if this is the root directory exit early
|
|
||||||
if d.parent == nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
params := prepareOptions(options)
|
|
||||||
resp, err := d.fsc.createResourceNoClose(d.buildPath(), resourceDirectory, params, nil)
|
|
||||||
if resp != nil {
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
if resp.statusCode == http.StatusCreated || resp.statusCode == http.StatusConflict {
|
|
||||||
if resp.statusCode == http.StatusCreated {
|
|
||||||
d.updateEtagAndLastModified(resp.headers)
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, d.FetchAttributes(nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete removes this directory. It must be empty in order to be deleted.
|
|
||||||
// If the directory does not exist the operation fails.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Delete-Directory
|
|
||||||
func (d *Directory) Delete(options *FileRequestOptions) error {
|
|
||||||
return d.fsc.deleteResource(d.buildPath(), resourceDirectory, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteIfExists removes this directory if it exists.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Delete-Directory
|
|
||||||
func (d *Directory) DeleteIfExists(options *FileRequestOptions) (bool, error) {
|
|
||||||
resp, err := d.fsc.deleteResourceNoClose(d.buildPath(), resourceDirectory, options)
|
|
||||||
if resp != nil {
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
if resp.statusCode == http.StatusAccepted || resp.statusCode == http.StatusNotFound {
|
|
||||||
return resp.statusCode == http.StatusAccepted, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exists returns true if this directory exists.
|
|
||||||
func (d *Directory) Exists() (bool, error) {
|
|
||||||
exists, headers, err := d.fsc.resourceExists(d.buildPath(), resourceDirectory)
|
|
||||||
if exists {
|
|
||||||
d.updateEtagAndLastModified(headers)
|
|
||||||
}
|
|
||||||
return exists, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// FetchAttributes retrieves metadata for this directory.
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/get-directory-properties
|
|
||||||
func (d *Directory) FetchAttributes(options *FileRequestOptions) error {
|
|
||||||
params := prepareOptions(options)
|
|
||||||
headers, err := d.fsc.getResourceHeaders(d.buildPath(), compNone, resourceDirectory, params, http.MethodHead)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
d.updateEtagAndLastModified(headers)
|
|
||||||
d.Metadata = getMetadataFromHeaders(headers)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDirectoryReference returns a child Directory object for this directory.
|
|
||||||
func (d *Directory) GetDirectoryReference(name string) *Directory {
|
|
||||||
return &Directory{
|
|
||||||
fsc: d.fsc,
|
|
||||||
Name: name,
|
|
||||||
parent: d,
|
|
||||||
share: d.share,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFileReference returns a child File object for this directory.
|
|
||||||
func (d *Directory) GetFileReference(name string) *File {
|
|
||||||
return &File{
|
|
||||||
fsc: d.fsc,
|
|
||||||
Name: name,
|
|
||||||
parent: d,
|
|
||||||
share: d.share,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListDirsAndFiles returns a list of files and directories under this directory.
|
|
||||||
// It also contains a pagination token and other response details.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/List-Directories-and-Files
|
|
||||||
func (d *Directory) ListDirsAndFiles(params ListDirsAndFilesParameters) (*DirsAndFilesListResponse, error) {
|
|
||||||
q := mergeParams(params.getParameters(), getURLInitValues(compList, resourceDirectory))
|
|
||||||
|
|
||||||
resp, err := d.fsc.listContent(d.buildPath(), q, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer resp.body.Close()
|
|
||||||
var out DirsAndFilesListResponse
|
|
||||||
err = xmlUnmarshal(resp.body, &out)
|
|
||||||
return &out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetMetadata replaces the metadata for this directory.
|
|
||||||
//
|
|
||||||
// Some keys may be converted to Camel-Case before sending. All keys
|
|
||||||
// are returned in lower case by GetDirectoryMetadata. HTTP header names
|
|
||||||
// are case-insensitive so case munging should not matter to other
|
|
||||||
// applications either.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Set-Directory-Metadata
|
|
||||||
func (d *Directory) SetMetadata(options *FileRequestOptions) error {
|
|
||||||
headers, err := d.fsc.setResourceHeaders(d.buildPath(), compMetadata, resourceDirectory, mergeMDIntoExtraHeaders(d.Metadata, nil), options)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
d.updateEtagAndLastModified(headers)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// updates Etag and last modified date
|
|
||||||
func (d *Directory) updateEtagAndLastModified(headers http.Header) {
|
|
||||||
d.Properties.Etag = headers.Get("Etag")
|
|
||||||
d.Properties.LastModified = headers.Get("Last-Modified")
|
|
||||||
}
|
|
||||||
|
|
||||||
// URL gets the canonical URL to this directory.
|
|
||||||
// This method does not create a publicly accessible URL if the directory
|
|
||||||
// is private and this method does not check if the directory exists.
|
|
||||||
func (d *Directory) URL() string {
|
|
||||||
return d.fsc.client.getEndpoint(fileServiceName, d.buildPath(), url.Values{})
|
|
||||||
}
|
|
|
@ -1,439 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/satori/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Annotating as secure for gas scanning
|
|
||||||
/* #nosec */
|
|
||||||
const (
|
|
||||||
partitionKeyNode = "PartitionKey"
|
|
||||||
rowKeyNode = "RowKey"
|
|
||||||
etagErrorTemplate = "Etag didn't match: %v"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errEmptyPayload = errors.New("Empty payload is not a valid metadata level for this operation")
|
|
||||||
errNilPreviousResult = errors.New("The previous results page is nil")
|
|
||||||
errNilNextLink = errors.New("There are no more pages in this query results")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Entity represents an entity inside an Azure table.
|
|
||||||
type Entity struct {
|
|
||||||
Table *Table
|
|
||||||
PartitionKey string
|
|
||||||
RowKey string
|
|
||||||
TimeStamp time.Time
|
|
||||||
OdataMetadata string
|
|
||||||
OdataType string
|
|
||||||
OdataID string
|
|
||||||
OdataEtag string
|
|
||||||
OdataEditLink string
|
|
||||||
Properties map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEntityReference returns an Entity object with the specified
|
|
||||||
// partition key and row key.
|
|
||||||
func (t *Table) GetEntityReference(partitionKey, rowKey string) *Entity {
|
|
||||||
return &Entity{
|
|
||||||
PartitionKey: partitionKey,
|
|
||||||
RowKey: rowKey,
|
|
||||||
Table: t,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// EntityOptions includes options for entity operations.
|
|
||||||
type EntityOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEntityOptions includes options for a get entity operation
|
|
||||||
type GetEntityOptions struct {
|
|
||||||
Select []string
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get gets the referenced entity. Which properties to get can be
|
|
||||||
// specified using the select option.
|
|
||||||
// See:
|
|
||||||
// https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/query-entities
|
|
||||||
// https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/querying-tables-and-entities
|
|
||||||
func (e *Entity) Get(timeout uint, ml MetadataLevel, options *GetEntityOptions) error {
|
|
||||||
if ml == EmptyPayload {
|
|
||||||
return errEmptyPayload
|
|
||||||
}
|
|
||||||
// RowKey and PartitionKey could be lost if not included in the query
|
|
||||||
// As those are the entity identifiers, it is best if they are not lost
|
|
||||||
rk := e.RowKey
|
|
||||||
pk := e.PartitionKey
|
|
||||||
|
|
||||||
query := url.Values{
|
|
||||||
"timeout": {strconv.FormatUint(uint64(timeout), 10)},
|
|
||||||
}
|
|
||||||
headers := e.Table.tsc.client.getStandardHeaders()
|
|
||||||
headers[headerAccept] = string(ml)
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
if len(options.Select) > 0 {
|
|
||||||
query.Add("$select", strings.Join(options.Select, ","))
|
|
||||||
}
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
|
|
||||||
uri := e.Table.tsc.client.getEndpoint(tableServiceName, e.buildPath(), query)
|
|
||||||
resp, err := e.Table.tsc.client.exec(http.MethodGet, uri, headers, nil, e.Table.tsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
if err = checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
respBody, err := ioutil.ReadAll(resp.body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(respBody, e)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
e.PartitionKey = pk
|
|
||||||
e.RowKey = rk
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert inserts the referenced entity in its table.
|
|
||||||
// The function fails if there is an entity with the same
|
|
||||||
// PartitionKey and RowKey in the table.
|
|
||||||
// ml determines the level of detail of metadata in the operation response,
|
|
||||||
// or no data at all.
|
|
||||||
// See: https://docs.microsoft.com/rest/api/storageservices/fileservices/insert-entity
|
|
||||||
func (e *Entity) Insert(ml MetadataLevel, options *EntityOptions) error {
|
|
||||||
query, headers := options.getParameters()
|
|
||||||
headers = mergeHeaders(headers, e.Table.tsc.client.getStandardHeaders())
|
|
||||||
|
|
||||||
body, err := json.Marshal(e)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
headers = addBodyRelatedHeaders(headers, len(body))
|
|
||||||
headers = addReturnContentHeaders(headers, ml)
|
|
||||||
|
|
||||||
uri := e.Table.tsc.client.getEndpoint(tableServiceName, e.Table.buildPath(), query)
|
|
||||||
resp, err := e.Table.tsc.client.exec(http.MethodPost, uri, headers, bytes.NewReader(body), e.Table.tsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.body.Close()
|
|
||||||
|
|
||||||
data, err := ioutil.ReadAll(resp.body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if ml != EmptyPayload {
|
|
||||||
if err = checkRespCode(resp.statusCode, []int{http.StatusCreated}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err = e.UnmarshalJSON(data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err = checkRespCode(resp.statusCode, []int{http.StatusNoContent}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update updates the contents of an entity. The function fails if there is no entity
|
|
||||||
// with the same PartitionKey and RowKey in the table or if the ETag is different
|
|
||||||
// than the one in Azure.
|
|
||||||
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/update-entity2
|
|
||||||
func (e *Entity) Update(force bool, options *EntityOptions) error {
|
|
||||||
return e.updateMerge(force, http.MethodPut, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge merges the contents of entity specified with PartitionKey and RowKey
|
|
||||||
// with the content specified in Properties.
|
|
||||||
// The function fails if there is no entity with the same PartitionKey and
|
|
||||||
// RowKey in the table or if the ETag is different than the one in Azure.
|
|
||||||
// Read more: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/merge-entity
|
|
||||||
func (e *Entity) Merge(force bool, options *EntityOptions) error {
|
|
||||||
return e.updateMerge(force, "MERGE", options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete deletes the entity.
|
|
||||||
// The function fails if there is no entity with the same PartitionKey and
|
|
||||||
// RowKey in the table or if the ETag is different than the one in Azure.
|
|
||||||
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/delete-entity1
|
|
||||||
func (e *Entity) Delete(force bool, options *EntityOptions) error {
|
|
||||||
query, headers := options.getParameters()
|
|
||||||
headers = mergeHeaders(headers, e.Table.tsc.client.getStandardHeaders())
|
|
||||||
|
|
||||||
headers = addIfMatchHeader(headers, force, e.OdataEtag)
|
|
||||||
headers = addReturnContentHeaders(headers, EmptyPayload)
|
|
||||||
|
|
||||||
uri := e.Table.tsc.client.getEndpoint(tableServiceName, e.buildPath(), query)
|
|
||||||
resp, err := e.Table.tsc.client.exec(http.MethodDelete, uri, headers, nil, e.Table.tsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
if resp.statusCode == http.StatusPreconditionFailed {
|
|
||||||
return fmt.Errorf(etagErrorTemplate, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
if err = checkRespCode(resp.statusCode, []int{http.StatusNoContent}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.updateTimestamp(resp.headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertOrReplace inserts an entity or replaces the existing one.
|
|
||||||
// Read more: https://docs.microsoft.com/rest/api/storageservices/fileservices/insert-or-replace-entity
|
|
||||||
func (e *Entity) InsertOrReplace(options *EntityOptions) error {
|
|
||||||
return e.insertOr(http.MethodPut, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertOrMerge inserts an entity or merges the existing one.
|
|
||||||
// Read more: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/insert-or-merge-entity
|
|
||||||
func (e *Entity) InsertOrMerge(options *EntityOptions) error {
|
|
||||||
return e.insertOr("MERGE", options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Entity) buildPath() string {
|
|
||||||
return fmt.Sprintf("%s(PartitionKey='%s', RowKey='%s')", e.Table.buildPath(), e.PartitionKey, e.RowKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON is a custom marshaller for entity
|
|
||||||
func (e *Entity) MarshalJSON() ([]byte, error) {
|
|
||||||
completeMap := map[string]interface{}{}
|
|
||||||
completeMap[partitionKeyNode] = e.PartitionKey
|
|
||||||
completeMap[rowKeyNode] = e.RowKey
|
|
||||||
for k, v := range e.Properties {
|
|
||||||
typeKey := strings.Join([]string{k, OdataTypeSuffix}, "")
|
|
||||||
switch t := v.(type) {
|
|
||||||
case []byte:
|
|
||||||
completeMap[typeKey] = OdataBinary
|
|
||||||
completeMap[k] = string(t)
|
|
||||||
case time.Time:
|
|
||||||
completeMap[typeKey] = OdataDateTime
|
|
||||||
completeMap[k] = t.Format(time.RFC3339Nano)
|
|
||||||
case uuid.UUID:
|
|
||||||
completeMap[typeKey] = OdataGUID
|
|
||||||
completeMap[k] = t.String()
|
|
||||||
case int64:
|
|
||||||
completeMap[typeKey] = OdataInt64
|
|
||||||
completeMap[k] = fmt.Sprintf("%v", v)
|
|
||||||
default:
|
|
||||||
completeMap[k] = v
|
|
||||||
}
|
|
||||||
if strings.HasSuffix(k, OdataTypeSuffix) {
|
|
||||||
if !(completeMap[k] == OdataBinary ||
|
|
||||||
completeMap[k] == OdataDateTime ||
|
|
||||||
completeMap[k] == OdataGUID ||
|
|
||||||
completeMap[k] == OdataInt64) {
|
|
||||||
return nil, fmt.Errorf("Odata.type annotation %v value is not valid", k)
|
|
||||||
}
|
|
||||||
valueKey := strings.TrimSuffix(k, OdataTypeSuffix)
|
|
||||||
if _, ok := completeMap[valueKey]; !ok {
|
|
||||||
return nil, fmt.Errorf("Odata.type annotation %v defined without value defined", k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return json.Marshal(completeMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON is a custom unmarshaller for entities
|
|
||||||
func (e *Entity) UnmarshalJSON(data []byte) error {
|
|
||||||
errorTemplate := "Deserializing error: %v"
|
|
||||||
|
|
||||||
props := map[string]interface{}{}
|
|
||||||
err := json.Unmarshal(data, &props)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// deselialize metadata
|
|
||||||
e.OdataMetadata = stringFromMap(props, "odata.metadata")
|
|
||||||
e.OdataType = stringFromMap(props, "odata.type")
|
|
||||||
e.OdataID = stringFromMap(props, "odata.id")
|
|
||||||
e.OdataEtag = stringFromMap(props, "odata.etag")
|
|
||||||
e.OdataEditLink = stringFromMap(props, "odata.editLink")
|
|
||||||
e.PartitionKey = stringFromMap(props, partitionKeyNode)
|
|
||||||
e.RowKey = stringFromMap(props, rowKeyNode)
|
|
||||||
|
|
||||||
// deserialize timestamp
|
|
||||||
timeStamp, ok := props["Timestamp"]
|
|
||||||
if ok {
|
|
||||||
str, ok := timeStamp.(string)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf(errorTemplate, "Timestamp casting error")
|
|
||||||
}
|
|
||||||
t, err := time.Parse(time.RFC3339Nano, str)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf(errorTemplate, err)
|
|
||||||
}
|
|
||||||
e.TimeStamp = t
|
|
||||||
}
|
|
||||||
delete(props, "Timestamp")
|
|
||||||
delete(props, "Timestamp@odata.type")
|
|
||||||
|
|
||||||
// deserialize entity (user defined fields)
|
|
||||||
for k, v := range props {
|
|
||||||
if strings.HasSuffix(k, OdataTypeSuffix) {
|
|
||||||
valueKey := strings.TrimSuffix(k, OdataTypeSuffix)
|
|
||||||
str, ok := props[valueKey].(string)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf(errorTemplate, fmt.Sprintf("%v casting error", v))
|
|
||||||
}
|
|
||||||
switch v {
|
|
||||||
case OdataBinary:
|
|
||||||
props[valueKey] = []byte(str)
|
|
||||||
case OdataDateTime:
|
|
||||||
t, err := time.Parse("2006-01-02T15:04:05Z", str)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf(errorTemplate, err)
|
|
||||||
}
|
|
||||||
props[valueKey] = t
|
|
||||||
case OdataGUID:
|
|
||||||
props[valueKey] = uuid.FromStringOrNil(str)
|
|
||||||
case OdataInt64:
|
|
||||||
i, err := strconv.ParseInt(str, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf(errorTemplate, err)
|
|
||||||
}
|
|
||||||
props[valueKey] = i
|
|
||||||
default:
|
|
||||||
return fmt.Errorf(errorTemplate, fmt.Sprintf("%v is not supported", v))
|
|
||||||
}
|
|
||||||
delete(props, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Properties = props
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAndDelete(props map[string]interface{}, key string) interface{} {
|
|
||||||
if value, ok := props[key]; ok {
|
|
||||||
delete(props, key)
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func addIfMatchHeader(h map[string]string, force bool, etag string) map[string]string {
|
|
||||||
if force {
|
|
||||||
h[headerIfMatch] = "*"
|
|
||||||
} else {
|
|
||||||
h[headerIfMatch] = etag
|
|
||||||
}
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
// updates Etag and timestamp
|
|
||||||
func (e *Entity) updateEtagAndTimestamp(headers http.Header) error {
|
|
||||||
e.OdataEtag = headers.Get(headerEtag)
|
|
||||||
return e.updateTimestamp(headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Entity) updateTimestamp(headers http.Header) error {
|
|
||||||
str := headers.Get(headerDate)
|
|
||||||
t, err := time.Parse(time.RFC1123, str)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Update timestamp error: %v", err)
|
|
||||||
}
|
|
||||||
e.TimeStamp = t
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Entity) insertOr(verb string, options *EntityOptions) error {
|
|
||||||
query, headers := options.getParameters()
|
|
||||||
headers = mergeHeaders(headers, e.Table.tsc.client.getStandardHeaders())
|
|
||||||
|
|
||||||
body, err := json.Marshal(e)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
headers = addBodyRelatedHeaders(headers, len(body))
|
|
||||||
headers = addReturnContentHeaders(headers, EmptyPayload)
|
|
||||||
|
|
||||||
uri := e.Table.tsc.client.getEndpoint(tableServiceName, e.buildPath(), query)
|
|
||||||
resp, err := e.Table.tsc.client.exec(verb, uri, headers, bytes.NewReader(body), e.Table.tsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
if err = checkRespCode(resp.statusCode, []int{http.StatusNoContent}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.updateEtagAndTimestamp(resp.headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Entity) updateMerge(force bool, verb string, options *EntityOptions) error {
|
|
||||||
query, headers := options.getParameters()
|
|
||||||
headers = mergeHeaders(headers, e.Table.tsc.client.getStandardHeaders())
|
|
||||||
|
|
||||||
body, err := json.Marshal(e)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
headers = addBodyRelatedHeaders(headers, len(body))
|
|
||||||
headers = addIfMatchHeader(headers, force, e.OdataEtag)
|
|
||||||
headers = addReturnContentHeaders(headers, EmptyPayload)
|
|
||||||
|
|
||||||
uri := e.Table.tsc.client.getEndpoint(tableServiceName, e.buildPath(), query)
|
|
||||||
resp, err := e.Table.tsc.client.exec(verb, uri, headers, bytes.NewReader(body), e.Table.tsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
if resp.statusCode == http.StatusPreconditionFailed {
|
|
||||||
return fmt.Errorf(etagErrorTemplate, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
if err = checkRespCode(resp.statusCode, []int{http.StatusNoContent}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.updateEtagAndTimestamp(resp.headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringFromMap(props map[string]interface{}, key string) string {
|
|
||||||
value := getAndDelete(props, key)
|
|
||||||
if value != nil {
|
|
||||||
return value.(string)
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (options *EntityOptions) getParameters() (url.Values, map[string]string) {
|
|
||||||
query := url.Values{}
|
|
||||||
headers := map[string]string{}
|
|
||||||
if options != nil {
|
|
||||||
query = addTimeout(query, options.Timeout)
|
|
||||||
headers = headersFromStruct(*options)
|
|
||||||
}
|
|
||||||
return query, headers
|
|
||||||
}
|
|
|
@ -1,462 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
const fourMB = uint64(4194304)
|
|
||||||
const oneTB = uint64(1099511627776)
|
|
||||||
|
|
||||||
// File represents a file on a share.
|
|
||||||
type File struct {
|
|
||||||
fsc *FileServiceClient
|
|
||||||
Metadata map[string]string
|
|
||||||
Name string `xml:"Name"`
|
|
||||||
parent *Directory
|
|
||||||
Properties FileProperties `xml:"Properties"`
|
|
||||||
share *Share
|
|
||||||
FileCopyProperties FileCopyState
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileProperties contains various properties of a file.
|
|
||||||
type FileProperties struct {
|
|
||||||
CacheControl string `header:"x-ms-cache-control"`
|
|
||||||
Disposition string `header:"x-ms-content-disposition"`
|
|
||||||
Encoding string `header:"x-ms-content-encoding"`
|
|
||||||
Etag string
|
|
||||||
Language string `header:"x-ms-content-language"`
|
|
||||||
LastModified string
|
|
||||||
Length uint64 `xml:"Content-Length" header:"x-ms-content-length"`
|
|
||||||
MD5 string `header:"x-ms-content-md5"`
|
|
||||||
Type string `header:"x-ms-content-type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileCopyState contains various properties of a file copy operation.
|
|
||||||
type FileCopyState struct {
|
|
||||||
CompletionTime string
|
|
||||||
ID string `header:"x-ms-copy-id"`
|
|
||||||
Progress string
|
|
||||||
Source string
|
|
||||||
Status string `header:"x-ms-copy-status"`
|
|
||||||
StatusDesc string
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileStream contains file data returned from a call to GetFile.
|
|
||||||
type FileStream struct {
|
|
||||||
Body io.ReadCloser
|
|
||||||
ContentMD5 string
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileRequestOptions will be passed to misc file operations.
|
|
||||||
// Currently just Timeout (in seconds) but could expand.
|
|
||||||
type FileRequestOptions struct {
|
|
||||||
Timeout uint // timeout duration in seconds.
|
|
||||||
}
|
|
||||||
|
|
||||||
func prepareOptions(options *FileRequestOptions) url.Values {
|
|
||||||
params := url.Values{}
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
}
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileRanges contains a list of file range information for a file.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/List-Ranges
|
|
||||||
type FileRanges struct {
|
|
||||||
ContentLength uint64
|
|
||||||
LastModified string
|
|
||||||
ETag string
|
|
||||||
FileRanges []FileRange `xml:"Range"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileRange contains range information for a file.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/List-Ranges
|
|
||||||
type FileRange struct {
|
|
||||||
Start uint64 `xml:"Start"`
|
|
||||||
End uint64 `xml:"End"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fr FileRange) String() string {
|
|
||||||
return fmt.Sprintf("bytes=%d-%d", fr.Start, fr.End)
|
|
||||||
}
|
|
||||||
|
|
||||||
// builds the complete file path for this file object
|
|
||||||
func (f *File) buildPath() string {
|
|
||||||
return f.parent.buildPath() + "/" + f.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearRange releases the specified range of space in a file.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Put-Range
|
|
||||||
func (f *File) ClearRange(fileRange FileRange, options *FileRequestOptions) error {
|
|
||||||
var timeout *uint
|
|
||||||
if options != nil {
|
|
||||||
timeout = &options.Timeout
|
|
||||||
}
|
|
||||||
headers, err := f.modifyRange(nil, fileRange, timeout, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
f.updateEtagAndLastModified(headers)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create creates a new file or replaces an existing one.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Create-File
|
|
||||||
func (f *File) Create(maxSize uint64, options *FileRequestOptions) error {
|
|
||||||
if maxSize > oneTB {
|
|
||||||
return fmt.Errorf("max file size is 1TB")
|
|
||||||
}
|
|
||||||
params := prepareOptions(options)
|
|
||||||
headers := headersFromStruct(f.Properties)
|
|
||||||
headers["x-ms-content-length"] = strconv.FormatUint(maxSize, 10)
|
|
||||||
headers["x-ms-type"] = "file"
|
|
||||||
|
|
||||||
outputHeaders, err := f.fsc.createResource(f.buildPath(), resourceFile, params, mergeMDIntoExtraHeaders(f.Metadata, headers), []int{http.StatusCreated})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
f.Properties.Length = maxSize
|
|
||||||
f.updateEtagAndLastModified(outputHeaders)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyFile operation copied a file/blob from the sourceURL to the path provided.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/copy-file
|
|
||||||
func (f *File) CopyFile(sourceURL string, options *FileRequestOptions) error {
|
|
||||||
extraHeaders := map[string]string{
|
|
||||||
"x-ms-type": "file",
|
|
||||||
"x-ms-copy-source": sourceURL,
|
|
||||||
}
|
|
||||||
params := prepareOptions(options)
|
|
||||||
|
|
||||||
headers, err := f.fsc.createResource(f.buildPath(), resourceFile, params, mergeMDIntoExtraHeaders(f.Metadata, extraHeaders), []int{http.StatusAccepted})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
f.updateEtagLastModifiedAndCopyHeaders(headers)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete immediately removes this file from the storage account.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Delete-File2
|
|
||||||
func (f *File) Delete(options *FileRequestOptions) error {
|
|
||||||
return f.fsc.deleteResource(f.buildPath(), resourceFile, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteIfExists removes this file if it exists.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Delete-File2
|
|
||||||
func (f *File) DeleteIfExists(options *FileRequestOptions) (bool, error) {
|
|
||||||
resp, err := f.fsc.deleteResourceNoClose(f.buildPath(), resourceFile, options)
|
|
||||||
if resp != nil {
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
if resp.statusCode == http.StatusAccepted || resp.statusCode == http.StatusNotFound {
|
|
||||||
return resp.statusCode == http.StatusAccepted, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFileOptions includes options for a get file operation
|
|
||||||
type GetFileOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
GetContentMD5 bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// DownloadToStream operation downloads the file.
|
|
||||||
//
|
|
||||||
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/get-file
|
|
||||||
func (f *File) DownloadToStream(options *FileRequestOptions) (io.ReadCloser, error) {
|
|
||||||
params := prepareOptions(options)
|
|
||||||
resp, err := f.fsc.getResourceNoClose(f.buildPath(), compNone, resourceFile, params, http.MethodGet, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
|
||||||
readAndCloseBody(resp.body)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return resp.body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DownloadRangeToStream operation downloads the specified range of this file with optional MD5 hash.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/get-file
|
|
||||||
func (f *File) DownloadRangeToStream(fileRange FileRange, options *GetFileOptions) (fs FileStream, err error) {
|
|
||||||
extraHeaders := map[string]string{
|
|
||||||
"Range": fileRange.String(),
|
|
||||||
}
|
|
||||||
params := url.Values{}
|
|
||||||
if options != nil {
|
|
||||||
if options.GetContentMD5 {
|
|
||||||
if isRangeTooBig(fileRange) {
|
|
||||||
return fs, fmt.Errorf("must specify a range less than or equal to 4MB when getContentMD5 is true")
|
|
||||||
}
|
|
||||||
extraHeaders["x-ms-range-get-content-md5"] = "true"
|
|
||||||
}
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := f.fsc.getResourceNoClose(f.buildPath(), compNone, resourceFile, params, http.MethodGet, extraHeaders)
|
|
||||||
if err != nil {
|
|
||||||
return fs, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = checkRespCode(resp.statusCode, []int{http.StatusOK, http.StatusPartialContent}); err != nil {
|
|
||||||
readAndCloseBody(resp.body)
|
|
||||||
return fs, err
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.Body = resp.body
|
|
||||||
if options != nil && options.GetContentMD5 {
|
|
||||||
fs.ContentMD5 = resp.headers.Get("Content-MD5")
|
|
||||||
}
|
|
||||||
return fs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exists returns true if this file exists.
|
|
||||||
func (f *File) Exists() (bool, error) {
|
|
||||||
exists, headers, err := f.fsc.resourceExists(f.buildPath(), resourceFile)
|
|
||||||
if exists {
|
|
||||||
f.updateEtagAndLastModified(headers)
|
|
||||||
f.updateProperties(headers)
|
|
||||||
}
|
|
||||||
return exists, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// FetchAttributes updates metadata and properties for this file.
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/get-file-properties
|
|
||||||
func (f *File) FetchAttributes(options *FileRequestOptions) error {
|
|
||||||
params := prepareOptions(options)
|
|
||||||
headers, err := f.fsc.getResourceHeaders(f.buildPath(), compNone, resourceFile, params, http.MethodHead)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
f.updateEtagAndLastModified(headers)
|
|
||||||
f.updateProperties(headers)
|
|
||||||
f.Metadata = getMetadataFromHeaders(headers)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns true if the range is larger than 4MB
|
|
||||||
func isRangeTooBig(fileRange FileRange) bool {
|
|
||||||
if fileRange.End-fileRange.Start > fourMB {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListRangesOptions includes options for a list file ranges operation
|
|
||||||
type ListRangesOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
ListRange *FileRange
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListRanges returns the list of valid ranges for this file.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/List-Ranges
|
|
||||||
func (f *File) ListRanges(options *ListRangesOptions) (*FileRanges, error) {
|
|
||||||
params := url.Values{"comp": {"rangelist"}}
|
|
||||||
|
|
||||||
// add optional range to list
|
|
||||||
var headers map[string]string
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
if options.ListRange != nil {
|
|
||||||
headers = make(map[string]string)
|
|
||||||
headers["Range"] = options.ListRange.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := f.fsc.listContent(f.buildPath(), params, headers)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer resp.body.Close()
|
|
||||||
var cl uint64
|
|
||||||
cl, err = strconv.ParseUint(resp.headers.Get("x-ms-content-length"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
ioutil.ReadAll(resp.body)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var out FileRanges
|
|
||||||
out.ContentLength = cl
|
|
||||||
out.ETag = resp.headers.Get("ETag")
|
|
||||||
out.LastModified = resp.headers.Get("Last-Modified")
|
|
||||||
|
|
||||||
err = xmlUnmarshal(resp.body, &out)
|
|
||||||
return &out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// modifies a range of bytes in this file
|
|
||||||
func (f *File) modifyRange(bytes io.Reader, fileRange FileRange, timeout *uint, contentMD5 *string) (http.Header, error) {
|
|
||||||
if err := f.fsc.checkForStorageEmulator(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if fileRange.End < fileRange.Start {
|
|
||||||
return nil, errors.New("the value for rangeEnd must be greater than or equal to rangeStart")
|
|
||||||
}
|
|
||||||
if bytes != nil && isRangeTooBig(fileRange) {
|
|
||||||
return nil, errors.New("range cannot exceed 4MB in size")
|
|
||||||
}
|
|
||||||
|
|
||||||
params := url.Values{"comp": {"range"}}
|
|
||||||
if timeout != nil {
|
|
||||||
params = addTimeout(params, *timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
uri := f.fsc.client.getEndpoint(fileServiceName, f.buildPath(), params)
|
|
||||||
|
|
||||||
// default to clear
|
|
||||||
write := "clear"
|
|
||||||
cl := uint64(0)
|
|
||||||
|
|
||||||
// if bytes is not nil then this is an update operation
|
|
||||||
if bytes != nil {
|
|
||||||
write = "update"
|
|
||||||
cl = (fileRange.End - fileRange.Start) + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
extraHeaders := map[string]string{
|
|
||||||
"Content-Length": strconv.FormatUint(cl, 10),
|
|
||||||
"Range": fileRange.String(),
|
|
||||||
"x-ms-write": write,
|
|
||||||
}
|
|
||||||
|
|
||||||
if contentMD5 != nil {
|
|
||||||
extraHeaders["Content-MD5"] = *contentMD5
|
|
||||||
}
|
|
||||||
|
|
||||||
headers := mergeHeaders(f.fsc.client.getStandardHeaders(), extraHeaders)
|
|
||||||
resp, err := f.fsc.client.exec(http.MethodPut, uri, headers, bytes, f.fsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
return resp.headers, checkRespCode(resp.statusCode, []int{http.StatusCreated})
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetMetadata replaces the metadata for this file.
|
|
||||||
//
|
|
||||||
// Some keys may be converted to Camel-Case before sending. All keys
|
|
||||||
// are returned in lower case by GetFileMetadata. HTTP header names
|
|
||||||
// are case-insensitive so case munging should not matter to other
|
|
||||||
// applications either.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Set-File-Metadata
|
|
||||||
func (f *File) SetMetadata(options *FileRequestOptions) error {
|
|
||||||
headers, err := f.fsc.setResourceHeaders(f.buildPath(), compMetadata, resourceFile, mergeMDIntoExtraHeaders(f.Metadata, nil), options)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
f.updateEtagAndLastModified(headers)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetProperties sets system properties on this file.
|
|
||||||
//
|
|
||||||
// Some keys may be converted to Camel-Case before sending. All keys
|
|
||||||
// are returned in lower case by SetFileProperties. HTTP header names
|
|
||||||
// are case-insensitive so case munging should not matter to other
|
|
||||||
// applications either.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Set-File-Properties
|
|
||||||
func (f *File) SetProperties(options *FileRequestOptions) error {
|
|
||||||
headers, err := f.fsc.setResourceHeaders(f.buildPath(), compProperties, resourceFile, headersFromStruct(f.Properties), options)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
f.updateEtagAndLastModified(headers)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// updates Etag and last modified date
|
|
||||||
func (f *File) updateEtagAndLastModified(headers http.Header) {
|
|
||||||
f.Properties.Etag = headers.Get("Etag")
|
|
||||||
f.Properties.LastModified = headers.Get("Last-Modified")
|
|
||||||
}
|
|
||||||
|
|
||||||
// updates Etag, last modified date and x-ms-copy-id
|
|
||||||
func (f *File) updateEtagLastModifiedAndCopyHeaders(headers http.Header) {
|
|
||||||
f.Properties.Etag = headers.Get("Etag")
|
|
||||||
f.Properties.LastModified = headers.Get("Last-Modified")
|
|
||||||
f.FileCopyProperties.ID = headers.Get("X-Ms-Copy-Id")
|
|
||||||
f.FileCopyProperties.Status = headers.Get("X-Ms-Copy-Status")
|
|
||||||
}
|
|
||||||
|
|
||||||
// updates file properties from the specified HTTP header
|
|
||||||
func (f *File) updateProperties(header http.Header) {
|
|
||||||
size, err := strconv.ParseUint(header.Get("Content-Length"), 10, 64)
|
|
||||||
if err == nil {
|
|
||||||
f.Properties.Length = size
|
|
||||||
}
|
|
||||||
|
|
||||||
f.updateEtagAndLastModified(header)
|
|
||||||
f.Properties.CacheControl = header.Get("Cache-Control")
|
|
||||||
f.Properties.Disposition = header.Get("Content-Disposition")
|
|
||||||
f.Properties.Encoding = header.Get("Content-Encoding")
|
|
||||||
f.Properties.Language = header.Get("Content-Language")
|
|
||||||
f.Properties.MD5 = header.Get("Content-MD5")
|
|
||||||
f.Properties.Type = header.Get("Content-Type")
|
|
||||||
}
|
|
||||||
|
|
||||||
// URL gets the canonical URL to this file.
|
|
||||||
// This method does not create a publicly accessible URL if the file
|
|
||||||
// is private and this method does not check if the file exists.
|
|
||||||
func (f *File) URL() string {
|
|
||||||
return f.fsc.client.getEndpoint(fileServiceName, f.buildPath(), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteRangeOptions includes opptions for a write file range operation
|
|
||||||
type WriteRangeOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
ContentMD5 string
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteRange writes a range of bytes to this file with an optional MD5 hash of the content (inside
|
|
||||||
// options parameter). Note that the length of bytes must match (rangeEnd - rangeStart) + 1 with
|
|
||||||
// a maximum size of 4MB.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Put-Range
|
|
||||||
func (f *File) WriteRange(bytes io.Reader, fileRange FileRange, options *WriteRangeOptions) error {
|
|
||||||
if bytes == nil {
|
|
||||||
return errors.New("bytes cannot be nil")
|
|
||||||
}
|
|
||||||
var timeout *uint
|
|
||||||
var md5 *string
|
|
||||||
if options != nil {
|
|
||||||
timeout = &options.Timeout
|
|
||||||
md5 = &options.ContentMD5
|
|
||||||
}
|
|
||||||
|
|
||||||
headers, err := f.modifyRange(bytes, fileRange, timeout, md5)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
f.updateEtagAndLastModified(headers)
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,324 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FileServiceClient contains operations for Microsoft Azure File Service.
|
|
||||||
type FileServiceClient struct {
|
|
||||||
client Client
|
|
||||||
auth authentication
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListSharesParameters defines the set of customizable parameters to make a
|
|
||||||
// List Shares call.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/List-Shares
|
|
||||||
type ListSharesParameters struct {
|
|
||||||
Prefix string
|
|
||||||
Marker string
|
|
||||||
Include string
|
|
||||||
MaxResults uint
|
|
||||||
Timeout uint
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShareListResponse contains the response fields from
|
|
||||||
// ListShares call.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/List-Shares
|
|
||||||
type ShareListResponse struct {
|
|
||||||
XMLName xml.Name `xml:"EnumerationResults"`
|
|
||||||
Xmlns string `xml:"xmlns,attr"`
|
|
||||||
Prefix string `xml:"Prefix"`
|
|
||||||
Marker string `xml:"Marker"`
|
|
||||||
NextMarker string `xml:"NextMarker"`
|
|
||||||
MaxResults int64 `xml:"MaxResults"`
|
|
||||||
Shares []Share `xml:"Shares>Share"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type compType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
compNone compType = ""
|
|
||||||
compList compType = "list"
|
|
||||||
compMetadata compType = "metadata"
|
|
||||||
compProperties compType = "properties"
|
|
||||||
compRangeList compType = "rangelist"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (ct compType) String() string {
|
|
||||||
return string(ct)
|
|
||||||
}
|
|
||||||
|
|
||||||
type resourceType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
resourceDirectory resourceType = "directory"
|
|
||||||
resourceFile resourceType = ""
|
|
||||||
resourceShare resourceType = "share"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (rt resourceType) String() string {
|
|
||||||
return string(rt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p ListSharesParameters) getParameters() url.Values {
|
|
||||||
out := url.Values{}
|
|
||||||
|
|
||||||
if p.Prefix != "" {
|
|
||||||
out.Set("prefix", p.Prefix)
|
|
||||||
}
|
|
||||||
if p.Marker != "" {
|
|
||||||
out.Set("marker", p.Marker)
|
|
||||||
}
|
|
||||||
if p.Include != "" {
|
|
||||||
out.Set("include", p.Include)
|
|
||||||
}
|
|
||||||
if p.MaxResults != 0 {
|
|
||||||
out.Set("maxresults", strconv.FormatUint(uint64(p.MaxResults), 10))
|
|
||||||
}
|
|
||||||
if p.Timeout != 0 {
|
|
||||||
out.Set("timeout", strconv.FormatUint(uint64(p.Timeout), 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p ListDirsAndFilesParameters) getParameters() url.Values {
|
|
||||||
out := url.Values{}
|
|
||||||
|
|
||||||
if p.Prefix != "" {
|
|
||||||
out.Set("prefix", p.Prefix)
|
|
||||||
}
|
|
||||||
if p.Marker != "" {
|
|
||||||
out.Set("marker", p.Marker)
|
|
||||||
}
|
|
||||||
if p.MaxResults != 0 {
|
|
||||||
out.Set("maxresults", strconv.FormatUint(uint64(p.MaxResults), 10))
|
|
||||||
}
|
|
||||||
out = addTimeout(out, p.Timeout)
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns url.Values for the specified types
|
|
||||||
func getURLInitValues(comp compType, res resourceType) url.Values {
|
|
||||||
values := url.Values{}
|
|
||||||
if comp != compNone {
|
|
||||||
values.Set("comp", comp.String())
|
|
||||||
}
|
|
||||||
if res != resourceFile {
|
|
||||||
values.Set("restype", res.String())
|
|
||||||
}
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetShareReference returns a Share object for the specified share name.
|
|
||||||
func (f *FileServiceClient) GetShareReference(name string) *Share {
|
|
||||||
return &Share{
|
|
||||||
fsc: f,
|
|
||||||
Name: name,
|
|
||||||
Properties: ShareProperties{
|
|
||||||
Quota: -1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListShares returns the list of shares in a storage account along with
|
|
||||||
// pagination token and other response details.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/list-shares
|
|
||||||
func (f FileServiceClient) ListShares(params ListSharesParameters) (*ShareListResponse, error) {
|
|
||||||
q := mergeParams(params.getParameters(), url.Values{"comp": {"list"}})
|
|
||||||
|
|
||||||
var out ShareListResponse
|
|
||||||
resp, err := f.listContent("", q, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.body.Close()
|
|
||||||
err = xmlUnmarshal(resp.body, &out)
|
|
||||||
|
|
||||||
// assign our client to the newly created Share objects
|
|
||||||
for i := range out.Shares {
|
|
||||||
out.Shares[i].fsc = &f
|
|
||||||
}
|
|
||||||
return &out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetServiceProperties gets the properties of your storage account's file service.
|
|
||||||
// File service does not support logging
|
|
||||||
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/get-file-service-properties
|
|
||||||
func (f *FileServiceClient) GetServiceProperties() (*ServiceProperties, error) {
|
|
||||||
return f.client.getServiceProperties(fileServiceName, f.auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetServiceProperties sets the properties of your storage account's file service.
|
|
||||||
// File service does not support logging
|
|
||||||
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/set-file-service-properties
|
|
||||||
func (f *FileServiceClient) SetServiceProperties(props ServiceProperties) error {
|
|
||||||
return f.client.setServiceProperties(props, fileServiceName, f.auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
// retrieves directory or share content
|
|
||||||
func (f FileServiceClient) listContent(path string, params url.Values, extraHeaders map[string]string) (*storageResponse, error) {
|
|
||||||
if err := f.checkForStorageEmulator(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
uri := f.client.getEndpoint(fileServiceName, path, params)
|
|
||||||
extraHeaders = f.client.protectUserAgent(extraHeaders)
|
|
||||||
headers := mergeHeaders(f.client.getStandardHeaders(), extraHeaders)
|
|
||||||
|
|
||||||
resp, err := f.client.exec(http.MethodGet, uri, headers, nil, f.auth)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
|
||||||
readAndCloseBody(resp.body)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns true if the specified resource exists
|
|
||||||
func (f FileServiceClient) resourceExists(path string, res resourceType) (bool, http.Header, error) {
|
|
||||||
if err := f.checkForStorageEmulator(); err != nil {
|
|
||||||
return false, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
uri := f.client.getEndpoint(fileServiceName, path, getURLInitValues(compNone, res))
|
|
||||||
headers := f.client.getStandardHeaders()
|
|
||||||
|
|
||||||
resp, err := f.client.exec(http.MethodHead, uri, headers, nil, f.auth)
|
|
||||||
if resp != nil {
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
if resp.statusCode == http.StatusOK || resp.statusCode == http.StatusNotFound {
|
|
||||||
return resp.statusCode == http.StatusOK, resp.headers, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// creates a resource depending on the specified resource type
|
|
||||||
func (f FileServiceClient) createResource(path string, res resourceType, urlParams url.Values, extraHeaders map[string]string, expectedResponseCodes []int) (http.Header, error) {
|
|
||||||
resp, err := f.createResourceNoClose(path, res, urlParams, extraHeaders)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
return resp.headers, checkRespCode(resp.statusCode, expectedResponseCodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// creates a resource depending on the specified resource type, doesn't close the response body
|
|
||||||
func (f FileServiceClient) createResourceNoClose(path string, res resourceType, urlParams url.Values, extraHeaders map[string]string) (*storageResponse, error) {
|
|
||||||
if err := f.checkForStorageEmulator(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
values := getURLInitValues(compNone, res)
|
|
||||||
combinedParams := mergeParams(values, urlParams)
|
|
||||||
uri := f.client.getEndpoint(fileServiceName, path, combinedParams)
|
|
||||||
extraHeaders = f.client.protectUserAgent(extraHeaders)
|
|
||||||
headers := mergeHeaders(f.client.getStandardHeaders(), extraHeaders)
|
|
||||||
|
|
||||||
return f.client.exec(http.MethodPut, uri, headers, nil, f.auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns HTTP header data for the specified directory or share
|
|
||||||
func (f FileServiceClient) getResourceHeaders(path string, comp compType, res resourceType, params url.Values, verb string) (http.Header, error) {
|
|
||||||
resp, err := f.getResourceNoClose(path, comp, res, params, verb, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
if err = checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp.headers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// gets the specified resource, doesn't close the response body
|
|
||||||
func (f FileServiceClient) getResourceNoClose(path string, comp compType, res resourceType, params url.Values, verb string, extraHeaders map[string]string) (*storageResponse, error) {
|
|
||||||
if err := f.checkForStorageEmulator(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
params = mergeParams(params, getURLInitValues(comp, res))
|
|
||||||
uri := f.client.getEndpoint(fileServiceName, path, params)
|
|
||||||
headers := mergeHeaders(f.client.getStandardHeaders(), extraHeaders)
|
|
||||||
|
|
||||||
return f.client.exec(verb, uri, headers, nil, f.auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
// deletes the resource and returns the response
|
|
||||||
func (f FileServiceClient) deleteResource(path string, res resourceType, options *FileRequestOptions) error {
|
|
||||||
resp, err := f.deleteResourceNoClose(path, res, options)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
return checkRespCode(resp.statusCode, []int{http.StatusAccepted})
|
|
||||||
}
|
|
||||||
|
|
||||||
// deletes the resource and returns the response, doesn't close the response body
|
|
||||||
func (f FileServiceClient) deleteResourceNoClose(path string, res resourceType, options *FileRequestOptions) (*storageResponse, error) {
|
|
||||||
if err := f.checkForStorageEmulator(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
values := mergeParams(getURLInitValues(compNone, res), prepareOptions(options))
|
|
||||||
uri := f.client.getEndpoint(fileServiceName, path, values)
|
|
||||||
return f.client.exec(http.MethodDelete, uri, f.client.getStandardHeaders(), nil, f.auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
// merges metadata into extraHeaders and returns extraHeaders
|
|
||||||
func mergeMDIntoExtraHeaders(metadata, extraHeaders map[string]string) map[string]string {
|
|
||||||
if metadata == nil && extraHeaders == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if extraHeaders == nil {
|
|
||||||
extraHeaders = make(map[string]string)
|
|
||||||
}
|
|
||||||
for k, v := range metadata {
|
|
||||||
extraHeaders[userDefinedMetadataHeaderPrefix+k] = v
|
|
||||||
}
|
|
||||||
return extraHeaders
|
|
||||||
}
|
|
||||||
|
|
||||||
// sets extra header data for the specified resource
|
|
||||||
func (f FileServiceClient) setResourceHeaders(path string, comp compType, res resourceType, extraHeaders map[string]string, options *FileRequestOptions) (http.Header, error) {
|
|
||||||
if err := f.checkForStorageEmulator(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
params := mergeParams(getURLInitValues(comp, res), prepareOptions(options))
|
|
||||||
uri := f.client.getEndpoint(fileServiceName, path, params)
|
|
||||||
extraHeaders = f.client.protectUserAgent(extraHeaders)
|
|
||||||
headers := mergeHeaders(f.client.getStandardHeaders(), extraHeaders)
|
|
||||||
|
|
||||||
resp, err := f.client.exec(http.MethodPut, uri, headers, nil, f.auth)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
return resp.headers, checkRespCode(resp.statusCode, []int{http.StatusOK})
|
|
||||||
}
|
|
||||||
|
|
||||||
//checkForStorageEmulator determines if the client is setup for use with
|
|
||||||
//Azure Storage Emulator, and returns a relevant error
|
|
||||||
func (f FileServiceClient) checkForStorageEmulator() error {
|
|
||||||
if f.client.accountName == StorageEmulatorAccountName {
|
|
||||||
return fmt.Errorf("Error: File service is not currently supported by Azure Storage Emulator")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,187 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// lease constants.
|
|
||||||
const (
|
|
||||||
leaseHeaderPrefix = "x-ms-lease-"
|
|
||||||
headerLeaseID = "x-ms-lease-id"
|
|
||||||
leaseAction = "x-ms-lease-action"
|
|
||||||
leaseBreakPeriod = "x-ms-lease-break-period"
|
|
||||||
leaseDuration = "x-ms-lease-duration"
|
|
||||||
leaseProposedID = "x-ms-proposed-lease-id"
|
|
||||||
leaseTime = "x-ms-lease-time"
|
|
||||||
|
|
||||||
acquireLease = "acquire"
|
|
||||||
renewLease = "renew"
|
|
||||||
changeLease = "change"
|
|
||||||
releaseLease = "release"
|
|
||||||
breakLease = "break"
|
|
||||||
)
|
|
||||||
|
|
||||||
// leasePut is common PUT code for the various acquire/release/break etc functions.
|
|
||||||
func (b *Blob) leaseCommonPut(headers map[string]string, expectedStatus int, options *LeaseOptions) (http.Header, error) {
|
|
||||||
params := url.Values{"comp": {"lease"}}
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
|
|
||||||
|
|
||||||
resp, err := b.Container.bsc.client.exec(http.MethodPut, uri, headers, nil, b.Container.bsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
if err := checkRespCode(resp.statusCode, []int{expectedStatus}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp.headers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LeaseOptions includes options for all operations regarding leasing blobs
|
|
||||||
type LeaseOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
Origin string `header:"Origin"`
|
|
||||||
IfMatch string `header:"If-Match"`
|
|
||||||
IfNoneMatch string `header:"If-None-Match"`
|
|
||||||
IfModifiedSince *time.Time `header:"If-Modified-Since"`
|
|
||||||
IfUnmodifiedSince *time.Time `header:"If-Unmodified-Since"`
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AcquireLease creates a lease for a blob
|
|
||||||
// returns leaseID acquired
|
|
||||||
// In API Versions starting on 2012-02-12, the minimum leaseTimeInSeconds is 15, the maximum
|
|
||||||
// non-infinite leaseTimeInSeconds is 60. To specify an infinite lease, provide the value -1.
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Lease-Blob
|
|
||||||
func (b *Blob) AcquireLease(leaseTimeInSeconds int, proposedLeaseID string, options *LeaseOptions) (returnedLeaseID string, err error) {
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
headers[leaseAction] = acquireLease
|
|
||||||
|
|
||||||
if leaseTimeInSeconds == -1 {
|
|
||||||
// Do nothing, but don't trigger the following clauses.
|
|
||||||
} else if leaseTimeInSeconds > 60 || b.Container.bsc.client.apiVersion < "2012-02-12" {
|
|
||||||
leaseTimeInSeconds = 60
|
|
||||||
} else if leaseTimeInSeconds < 15 {
|
|
||||||
leaseTimeInSeconds = 15
|
|
||||||
}
|
|
||||||
|
|
||||||
headers[leaseDuration] = strconv.Itoa(leaseTimeInSeconds)
|
|
||||||
|
|
||||||
if proposedLeaseID != "" {
|
|
||||||
headers[leaseProposedID] = proposedLeaseID
|
|
||||||
}
|
|
||||||
|
|
||||||
respHeaders, err := b.leaseCommonPut(headers, http.StatusCreated, options)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
returnedLeaseID = respHeaders.Get(http.CanonicalHeaderKey(headerLeaseID))
|
|
||||||
|
|
||||||
if returnedLeaseID != "" {
|
|
||||||
return returnedLeaseID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", errors.New("LeaseID not returned")
|
|
||||||
}
|
|
||||||
|
|
||||||
// BreakLease breaks the lease for a blob
|
|
||||||
// Returns the timeout remaining in the lease in seconds
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Lease-Blob
|
|
||||||
func (b *Blob) BreakLease(options *LeaseOptions) (breakTimeout int, err error) {
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
headers[leaseAction] = breakLease
|
|
||||||
return b.breakLeaseCommon(headers, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BreakLeaseWithBreakPeriod breaks the lease for a blob
|
|
||||||
// breakPeriodInSeconds is used to determine how long until new lease can be created.
|
|
||||||
// Returns the timeout remaining in the lease in seconds
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Lease-Blob
|
|
||||||
func (b *Blob) BreakLeaseWithBreakPeriod(breakPeriodInSeconds int, options *LeaseOptions) (breakTimeout int, err error) {
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
headers[leaseAction] = breakLease
|
|
||||||
headers[leaseBreakPeriod] = strconv.Itoa(breakPeriodInSeconds)
|
|
||||||
return b.breakLeaseCommon(headers, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// breakLeaseCommon is common code for both version of BreakLease (with and without break period)
|
|
||||||
func (b *Blob) breakLeaseCommon(headers map[string]string, options *LeaseOptions) (breakTimeout int, err error) {
|
|
||||||
|
|
||||||
respHeaders, err := b.leaseCommonPut(headers, http.StatusAccepted, options)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
breakTimeoutStr := respHeaders.Get(http.CanonicalHeaderKey(leaseTime))
|
|
||||||
if breakTimeoutStr != "" {
|
|
||||||
breakTimeout, err = strconv.Atoi(breakTimeoutStr)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return breakTimeout, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChangeLease changes a lease ID for a blob
|
|
||||||
// Returns the new LeaseID acquired
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Lease-Blob
|
|
||||||
func (b *Blob) ChangeLease(currentLeaseID string, proposedLeaseID string, options *LeaseOptions) (newLeaseID string, err error) {
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
headers[leaseAction] = changeLease
|
|
||||||
headers[headerLeaseID] = currentLeaseID
|
|
||||||
headers[leaseProposedID] = proposedLeaseID
|
|
||||||
|
|
||||||
respHeaders, err := b.leaseCommonPut(headers, http.StatusOK, options)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
newLeaseID = respHeaders.Get(http.CanonicalHeaderKey(headerLeaseID))
|
|
||||||
if newLeaseID != "" {
|
|
||||||
return newLeaseID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", errors.New("LeaseID not returned")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReleaseLease releases the lease for a blob
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Lease-Blob
|
|
||||||
func (b *Blob) ReleaseLease(currentLeaseID string, options *LeaseOptions) error {
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
headers[leaseAction] = releaseLease
|
|
||||||
headers[headerLeaseID] = currentLeaseID
|
|
||||||
|
|
||||||
_, err := b.leaseCommonPut(headers, http.StatusOK, options)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenewLease renews the lease for a blob as per https://msdn.microsoft.com/en-us/library/azure/ee691972.aspx
|
|
||||||
func (b *Blob) RenewLease(currentLeaseID string, options *LeaseOptions) error {
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
headers[leaseAction] = renewLease
|
|
||||||
headers[headerLeaseID] = currentLeaseID
|
|
||||||
|
|
||||||
_, err := b.leaseCommonPut(headers, http.StatusOK, options)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,153 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Message represents an Azure message.
|
|
||||||
type Message struct {
|
|
||||||
Queue *Queue
|
|
||||||
Text string `xml:"MessageText"`
|
|
||||||
ID string `xml:"MessageId"`
|
|
||||||
Insertion TimeRFC1123 `xml:"InsertionTime"`
|
|
||||||
Expiration TimeRFC1123 `xml:"ExpirationTime"`
|
|
||||||
PopReceipt string `xml:"PopReceipt"`
|
|
||||||
NextVisible TimeRFC1123 `xml:"TimeNextVisible"`
|
|
||||||
DequeueCount int `xml:"DequeueCount"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Message) buildPath() string {
|
|
||||||
return fmt.Sprintf("%s/%s", m.Queue.buildPathMessages(), m.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutMessageOptions is the set of options can be specified for Put Messsage
|
|
||||||
// operation. A zero struct does not use any preferences for the request.
|
|
||||||
type PutMessageOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
VisibilityTimeout int
|
|
||||||
MessageTTL int
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put operation adds a new message to the back of the message queue.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Put-Message
|
|
||||||
func (m *Message) Put(options *PutMessageOptions) error {
|
|
||||||
query := url.Values{}
|
|
||||||
headers := m.Queue.qsc.client.getStandardHeaders()
|
|
||||||
|
|
||||||
req := putMessageRequest{MessageText: m.Text}
|
|
||||||
body, nn, err := xmlMarshal(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
headers["Content-Length"] = strconv.Itoa(nn)
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
if options.VisibilityTimeout != 0 {
|
|
||||||
query.Set("visibilitytimeout", strconv.Itoa(options.VisibilityTimeout))
|
|
||||||
}
|
|
||||||
if options.MessageTTL != 0 {
|
|
||||||
query.Set("messagettl", strconv.Itoa(options.MessageTTL))
|
|
||||||
}
|
|
||||||
query = addTimeout(query, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
|
|
||||||
uri := m.Queue.qsc.client.getEndpoint(queueServiceName, m.Queue.buildPathMessages(), query)
|
|
||||||
resp, err := m.Queue.qsc.client.exec(http.MethodPost, uri, headers, body, m.Queue.qsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
err = xmlUnmarshal(resp.body, m)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return checkRespCode(resp.statusCode, []int{http.StatusCreated})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateMessageOptions is the set of options can be specified for Update Messsage
|
|
||||||
// operation. A zero struct does not use any preferences for the request.
|
|
||||||
type UpdateMessageOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
VisibilityTimeout int
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update operation updates the specified message.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Update-Message
|
|
||||||
func (m *Message) Update(options *UpdateMessageOptions) error {
|
|
||||||
query := url.Values{}
|
|
||||||
if m.PopReceipt != "" {
|
|
||||||
query.Set("popreceipt", m.PopReceipt)
|
|
||||||
}
|
|
||||||
|
|
||||||
headers := m.Queue.qsc.client.getStandardHeaders()
|
|
||||||
req := putMessageRequest{MessageText: m.Text}
|
|
||||||
body, nn, err := xmlMarshal(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
headers["Content-Length"] = strconv.Itoa(nn)
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
if options.VisibilityTimeout != 0 {
|
|
||||||
query.Set("visibilitytimeout", strconv.Itoa(options.VisibilityTimeout))
|
|
||||||
}
|
|
||||||
query = addTimeout(query, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := m.Queue.qsc.client.getEndpoint(queueServiceName, m.buildPath(), query)
|
|
||||||
|
|
||||||
resp, err := m.Queue.qsc.client.exec(http.MethodPut, uri, headers, body, m.Queue.qsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
m.PopReceipt = resp.headers.Get("x-ms-popreceipt")
|
|
||||||
nextTimeStr := resp.headers.Get("x-ms-time-next-visible")
|
|
||||||
if nextTimeStr != "" {
|
|
||||||
nextTime, err := time.Parse(time.RFC1123, nextTimeStr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m.NextVisible = TimeRFC1123(nextTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
return checkRespCode(resp.statusCode, []int{http.StatusNoContent})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete operation deletes the specified message.
|
|
||||||
//
|
|
||||||
// See https://msdn.microsoft.com/en-us/library/azure/dd179347.aspx
|
|
||||||
func (m *Message) Delete(options *QueueServiceOptions) error {
|
|
||||||
params := url.Values{"popreceipt": {m.PopReceipt}}
|
|
||||||
headers := m.Queue.qsc.client.getStandardHeaders()
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := m.Queue.qsc.client.getEndpoint(queueServiceName, m.buildPath(), params)
|
|
||||||
|
|
||||||
resp, err := m.Queue.qsc.client.exec(http.MethodDelete, uri, headers, nil, m.Queue.qsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
readAndCloseBody(resp.body)
|
|
||||||
return checkRespCode(resp.statusCode, []int{http.StatusNoContent})
|
|
||||||
}
|
|
||||||
|
|
||||||
type putMessageRequest struct {
|
|
||||||
XMLName xml.Name `xml:"QueueMessage"`
|
|
||||||
MessageText string `xml:"MessageText"`
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
// MetadataLevel determines if operations should return a paylod,
|
|
||||||
// and it level of detail.
|
|
||||||
type MetadataLevel string
|
|
||||||
|
|
||||||
// This consts are meant to help with Odata supported operations
|
|
||||||
const (
|
|
||||||
OdataTypeSuffix = "@odata.type"
|
|
||||||
|
|
||||||
// Types
|
|
||||||
|
|
||||||
OdataBinary = "Edm.Binary"
|
|
||||||
OdataDateTime = "Edm.DateTime"
|
|
||||||
OdataGUID = "Edm.Guid"
|
|
||||||
OdataInt64 = "Edm.Int64"
|
|
||||||
|
|
||||||
// Query options
|
|
||||||
|
|
||||||
OdataFilter = "$filter"
|
|
||||||
OdataOrderBy = "$orderby"
|
|
||||||
OdataTop = "$top"
|
|
||||||
OdataSkip = "$skip"
|
|
||||||
OdataCount = "$count"
|
|
||||||
OdataExpand = "$expand"
|
|
||||||
OdataSelect = "$select"
|
|
||||||
OdataSearch = "$search"
|
|
||||||
|
|
||||||
EmptyPayload MetadataLevel = ""
|
|
||||||
NoMetadata MetadataLevel = "application/json;odata=nometadata"
|
|
||||||
MinimalMetadata MetadataLevel = "application/json;odata=minimalmetadata"
|
|
||||||
FullMetadata MetadataLevel = "application/json;odata=fullmetadata"
|
|
||||||
)
|
|
|
@ -1,189 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetPageRangesResponse contains the response fields from
|
|
||||||
// Get Page Ranges call.
|
|
||||||
//
|
|
||||||
// See https://msdn.microsoft.com/en-us/library/azure/ee691973.aspx
|
|
||||||
type GetPageRangesResponse struct {
|
|
||||||
XMLName xml.Name `xml:"PageList"`
|
|
||||||
PageList []PageRange `xml:"PageRange"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PageRange contains information about a page of a page blob from
|
|
||||||
// Get Pages Range call.
|
|
||||||
//
|
|
||||||
// See https://msdn.microsoft.com/en-us/library/azure/ee691973.aspx
|
|
||||||
type PageRange struct {
|
|
||||||
Start int64 `xml:"Start"`
|
|
||||||
End int64 `xml:"End"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
errBlobCopyAborted = errors.New("storage: blob copy is aborted")
|
|
||||||
errBlobCopyIDMismatch = errors.New("storage: blob copy id is a mismatch")
|
|
||||||
)
|
|
||||||
|
|
||||||
// PutPageOptions includes the options for a put page operation
|
|
||||||
type PutPageOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
LeaseID string `header:"x-ms-lease-id"`
|
|
||||||
IfSequenceNumberLessThanOrEqualTo *int `header:"x-ms-if-sequence-number-le"`
|
|
||||||
IfSequenceNumberLessThan *int `header:"x-ms-if-sequence-number-lt"`
|
|
||||||
IfSequenceNumberEqualTo *int `header:"x-ms-if-sequence-number-eq"`
|
|
||||||
IfModifiedSince *time.Time `header:"If-Modified-Since"`
|
|
||||||
IfUnmodifiedSince *time.Time `header:"If-Unmodified-Since"`
|
|
||||||
IfMatch string `header:"If-Match"`
|
|
||||||
IfNoneMatch string `header:"If-None-Match"`
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteRange writes a range of pages to a page blob.
|
|
||||||
// Ranges must be aligned with 512-byte boundaries and chunk must be of size
|
|
||||||
// multiplies by 512.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Put-Page
|
|
||||||
func (b *Blob) WriteRange(blobRange BlobRange, bytes io.Reader, options *PutPageOptions) error {
|
|
||||||
if bytes == nil {
|
|
||||||
return errors.New("bytes cannot be nil")
|
|
||||||
}
|
|
||||||
return b.modifyRange(blobRange, bytes, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearRange clears the given range in a page blob.
|
|
||||||
// Ranges must be aligned with 512-byte boundaries and chunk must be of size
|
|
||||||
// multiplies by 512.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Put-Page
|
|
||||||
func (b *Blob) ClearRange(blobRange BlobRange, options *PutPageOptions) error {
|
|
||||||
return b.modifyRange(blobRange, nil, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Blob) modifyRange(blobRange BlobRange, bytes io.Reader, options *PutPageOptions) error {
|
|
||||||
if blobRange.End < blobRange.Start {
|
|
||||||
return errors.New("the value for rangeEnd must be greater than or equal to rangeStart")
|
|
||||||
}
|
|
||||||
if blobRange.Start%512 != 0 {
|
|
||||||
return errors.New("the value for rangeStart must be a modulus of 512")
|
|
||||||
}
|
|
||||||
if blobRange.End%512 != 511 {
|
|
||||||
return errors.New("the value for rangeEnd must be a modulus of 511")
|
|
||||||
}
|
|
||||||
|
|
||||||
params := url.Values{"comp": {"page"}}
|
|
||||||
|
|
||||||
// default to clear
|
|
||||||
write := "clear"
|
|
||||||
var cl uint64
|
|
||||||
|
|
||||||
// if bytes is not nil then this is an update operation
|
|
||||||
if bytes != nil {
|
|
||||||
write = "update"
|
|
||||||
cl = (blobRange.End - blobRange.Start) + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
headers["x-ms-blob-type"] = string(BlobTypePage)
|
|
||||||
headers["x-ms-page-write"] = write
|
|
||||||
headers["x-ms-range"] = blobRange.String()
|
|
||||||
headers["Content-Length"] = fmt.Sprintf("%v", cl)
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
|
|
||||||
|
|
||||||
resp, err := b.Container.bsc.client.exec(http.MethodPut, uri, headers, bytes, b.Container.bsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
return checkRespCode(resp.statusCode, []int{http.StatusCreated})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPageRangesOptions includes the options for a get page ranges operation
|
|
||||||
type GetPageRangesOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
Snapshot *time.Time
|
|
||||||
PreviousSnapshot *time.Time
|
|
||||||
Range *BlobRange
|
|
||||||
LeaseID string `header:"x-ms-lease-id"`
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPageRanges returns the list of valid page ranges for a page blob.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Get-Page-Ranges
|
|
||||||
func (b *Blob) GetPageRanges(options *GetPageRangesOptions) (GetPageRangesResponse, error) {
|
|
||||||
params := url.Values{"comp": {"pagelist"}}
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
params = addSnapshot(params, options.Snapshot)
|
|
||||||
if options.PreviousSnapshot != nil {
|
|
||||||
params.Add("prevsnapshot", timeRfc1123Formatted(*options.PreviousSnapshot))
|
|
||||||
}
|
|
||||||
if options.Range != nil {
|
|
||||||
headers["Range"] = options.Range.String()
|
|
||||||
}
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
|
|
||||||
|
|
||||||
var out GetPageRangesResponse
|
|
||||||
resp, err := b.Container.bsc.client.exec(http.MethodGet, uri, headers, nil, b.Container.bsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
defer resp.body.Close()
|
|
||||||
|
|
||||||
if err = checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
err = xmlUnmarshal(resp.body, &out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutPageBlob initializes an empty page blob with specified name and maximum
|
|
||||||
// size in bytes (size must be aligned to a 512-byte boundary). A page blob must
|
|
||||||
// be created using this method before writing pages.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Put-Blob
|
|
||||||
func (b *Blob) PutPageBlob(options *PutBlobOptions) error {
|
|
||||||
if b.Properties.ContentLength%512 != 0 {
|
|
||||||
return errors.New("Content length must be aligned to a 512-byte boundary")
|
|
||||||
}
|
|
||||||
|
|
||||||
params := url.Values{}
|
|
||||||
headers := b.Container.bsc.client.getStandardHeaders()
|
|
||||||
headers["x-ms-blob-type"] = string(BlobTypePage)
|
|
||||||
headers["x-ms-blob-content-length"] = fmt.Sprintf("%v", b.Properties.ContentLength)
|
|
||||||
headers["x-ms-blob-sequence-number"] = fmt.Sprintf("%v", b.Properties.SequenceNumber)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(b.Properties))
|
|
||||||
headers = b.Container.bsc.client.addMetadataToHeaders(headers, b.Metadata)
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := b.Container.bsc.client.getEndpoint(blobServiceName, b.buildPath(), params)
|
|
||||||
|
|
||||||
resp, err := b.Container.bsc.client.exec(http.MethodPut, uri, headers, nil, b.Container.bsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
readAndCloseBody(resp.body)
|
|
||||||
return checkRespCode(resp.statusCode, []int{http.StatusCreated})
|
|
||||||
}
|
|
|
@ -1,427 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// casing is per Golang's http.Header canonicalizing the header names.
|
|
||||||
approximateMessagesCountHeader = "X-Ms-Approximate-Messages-Count"
|
|
||||||
)
|
|
||||||
|
|
||||||
// QueueAccessPolicy represents each access policy in the queue ACL.
|
|
||||||
type QueueAccessPolicy struct {
|
|
||||||
ID string
|
|
||||||
StartTime time.Time
|
|
||||||
ExpiryTime time.Time
|
|
||||||
CanRead bool
|
|
||||||
CanAdd bool
|
|
||||||
CanUpdate bool
|
|
||||||
CanProcess bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueuePermissions represents the queue ACLs.
|
|
||||||
type QueuePermissions struct {
|
|
||||||
AccessPolicies []QueueAccessPolicy
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetQueuePermissionOptions includes options for a set queue permissions operation
|
|
||||||
type SetQueuePermissionOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Queue represents an Azure queue.
|
|
||||||
type Queue struct {
|
|
||||||
qsc *QueueServiceClient
|
|
||||||
Name string
|
|
||||||
Metadata map[string]string
|
|
||||||
AproxMessageCount uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queue) buildPath() string {
|
|
||||||
return fmt.Sprintf("/%s", q.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queue) buildPathMessages() string {
|
|
||||||
return fmt.Sprintf("%s/messages", q.buildPath())
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueueServiceOptions includes options for some queue service operations
|
|
||||||
type QueueServiceOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create operation creates a queue under the given account.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Create-Queue4
|
|
||||||
func (q *Queue) Create(options *QueueServiceOptions) error {
|
|
||||||
params := url.Values{}
|
|
||||||
headers := q.qsc.client.getStandardHeaders()
|
|
||||||
headers = q.qsc.client.addMetadataToHeaders(headers, q.Metadata)
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := q.qsc.client.getEndpoint(queueServiceName, q.buildPath(), params)
|
|
||||||
|
|
||||||
resp, err := q.qsc.client.exec(http.MethodPut, uri, headers, nil, q.qsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
readAndCloseBody(resp.body)
|
|
||||||
return checkRespCode(resp.statusCode, []int{http.StatusCreated})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete operation permanently deletes the specified queue.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Delete-Queue3
|
|
||||||
func (q *Queue) Delete(options *QueueServiceOptions) error {
|
|
||||||
params := url.Values{}
|
|
||||||
headers := q.qsc.client.getStandardHeaders()
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := q.qsc.client.getEndpoint(queueServiceName, q.buildPath(), params)
|
|
||||||
resp, err := q.qsc.client.exec(http.MethodDelete, uri, headers, nil, q.qsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
readAndCloseBody(resp.body)
|
|
||||||
return checkRespCode(resp.statusCode, []int{http.StatusNoContent})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exists returns true if a queue with given name exists.
|
|
||||||
func (q *Queue) Exists() (bool, error) {
|
|
||||||
uri := q.qsc.client.getEndpoint(queueServiceName, q.buildPath(), url.Values{"comp": {"metadata"}})
|
|
||||||
resp, err := q.qsc.client.exec(http.MethodGet, uri, q.qsc.client.getStandardHeaders(), nil, q.qsc.auth)
|
|
||||||
if resp != nil {
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
if resp.statusCode == http.StatusOK || resp.statusCode == http.StatusNotFound {
|
|
||||||
return resp.statusCode == http.StatusOK, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetMetadata operation sets user-defined metadata on the specified queue.
|
|
||||||
// Metadata is associated with the queue as name-value pairs.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Set-Queue-Metadata
|
|
||||||
func (q *Queue) SetMetadata(options *QueueServiceOptions) error {
|
|
||||||
params := url.Values{"comp": {"metadata"}}
|
|
||||||
headers := q.qsc.client.getStandardHeaders()
|
|
||||||
headers = q.qsc.client.addMetadataToHeaders(headers, q.Metadata)
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := q.qsc.client.getEndpoint(queueServiceName, q.buildPath(), params)
|
|
||||||
|
|
||||||
resp, err := q.qsc.client.exec(http.MethodPut, uri, headers, nil, q.qsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
readAndCloseBody(resp.body)
|
|
||||||
return checkRespCode(resp.statusCode, []int{http.StatusNoContent})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMetadata operation retrieves user-defined metadata and queue
|
|
||||||
// properties on the specified queue. Metadata is associated with
|
|
||||||
// the queue as name-values pairs.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Set-Queue-Metadata
|
|
||||||
//
|
|
||||||
// Because the way Golang's http client (and http.Header in particular)
|
|
||||||
// canonicalize header names, the returned metadata names would always
|
|
||||||
// be all lower case.
|
|
||||||
func (q *Queue) GetMetadata(options *QueueServiceOptions) error {
|
|
||||||
params := url.Values{"comp": {"metadata"}}
|
|
||||||
headers := q.qsc.client.getStandardHeaders()
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := q.qsc.client.getEndpoint(queueServiceName, q.buildPath(), url.Values{"comp": {"metadata"}})
|
|
||||||
|
|
||||||
resp, err := q.qsc.client.exec(http.MethodGet, uri, headers, nil, q.qsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
aproxMessagesStr := resp.headers.Get(http.CanonicalHeaderKey(approximateMessagesCountHeader))
|
|
||||||
if aproxMessagesStr != "" {
|
|
||||||
aproxMessages, err := strconv.ParseUint(aproxMessagesStr, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
q.AproxMessageCount = aproxMessages
|
|
||||||
}
|
|
||||||
|
|
||||||
q.Metadata = getMetadataFromHeaders(resp.headers)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMessageReference returns a message object with the specified text.
|
|
||||||
func (q *Queue) GetMessageReference(text string) *Message {
|
|
||||||
return &Message{
|
|
||||||
Queue: q,
|
|
||||||
Text: text,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMessagesOptions is the set of options can be specified for Get
|
|
||||||
// Messsages operation. A zero struct does not use any preferences for the
|
|
||||||
// request.
|
|
||||||
type GetMessagesOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
NumOfMessages int
|
|
||||||
VisibilityTimeout int
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type messages struct {
|
|
||||||
XMLName xml.Name `xml:"QueueMessagesList"`
|
|
||||||
Messages []Message `xml:"QueueMessage"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMessages operation retrieves one or more messages from the front of the
|
|
||||||
// queue.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Get-Messages
|
|
||||||
func (q *Queue) GetMessages(options *GetMessagesOptions) ([]Message, error) {
|
|
||||||
query := url.Values{}
|
|
||||||
headers := q.qsc.client.getStandardHeaders()
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
if options.NumOfMessages != 0 {
|
|
||||||
query.Set("numofmessages", strconv.Itoa(options.NumOfMessages))
|
|
||||||
}
|
|
||||||
if options.VisibilityTimeout != 0 {
|
|
||||||
query.Set("visibilitytimeout", strconv.Itoa(options.VisibilityTimeout))
|
|
||||||
}
|
|
||||||
query = addTimeout(query, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := q.qsc.client.getEndpoint(queueServiceName, q.buildPathMessages(), query)
|
|
||||||
|
|
||||||
resp, err := q.qsc.client.exec(http.MethodGet, uri, headers, nil, q.qsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return []Message{}, err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
var out messages
|
|
||||||
err = xmlUnmarshal(resp.body, &out)
|
|
||||||
if err != nil {
|
|
||||||
return []Message{}, err
|
|
||||||
}
|
|
||||||
for i := range out.Messages {
|
|
||||||
out.Messages[i].Queue = q
|
|
||||||
}
|
|
||||||
return out.Messages, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// PeekMessagesOptions is the set of options can be specified for Peek
|
|
||||||
// Messsage operation. A zero struct does not use any preferences for the
|
|
||||||
// request.
|
|
||||||
type PeekMessagesOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
NumOfMessages int
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PeekMessages retrieves one or more messages from the front of the queue, but
|
|
||||||
// does not alter the visibility of the message.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Peek-Messages
|
|
||||||
func (q *Queue) PeekMessages(options *PeekMessagesOptions) ([]Message, error) {
|
|
||||||
query := url.Values{"peekonly": {"true"}} // Required for peek operation
|
|
||||||
headers := q.qsc.client.getStandardHeaders()
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
if options.NumOfMessages != 0 {
|
|
||||||
query.Set("numofmessages", strconv.Itoa(options.NumOfMessages))
|
|
||||||
}
|
|
||||||
query = addTimeout(query, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := q.qsc.client.getEndpoint(queueServiceName, q.buildPathMessages(), query)
|
|
||||||
|
|
||||||
resp, err := q.qsc.client.exec(http.MethodGet, uri, headers, nil, q.qsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return []Message{}, err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
var out messages
|
|
||||||
err = xmlUnmarshal(resp.body, &out)
|
|
||||||
if err != nil {
|
|
||||||
return []Message{}, err
|
|
||||||
}
|
|
||||||
for i := range out.Messages {
|
|
||||||
out.Messages[i].Queue = q
|
|
||||||
}
|
|
||||||
return out.Messages, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearMessages operation deletes all messages from the specified queue.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Clear-Messages
|
|
||||||
func (q *Queue) ClearMessages(options *QueueServiceOptions) error {
|
|
||||||
params := url.Values{}
|
|
||||||
headers := q.qsc.client.getStandardHeaders()
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := q.qsc.client.getEndpoint(queueServiceName, q.buildPathMessages(), params)
|
|
||||||
|
|
||||||
resp, err := q.qsc.client.exec(http.MethodDelete, uri, headers, nil, q.qsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
readAndCloseBody(resp.body)
|
|
||||||
return checkRespCode(resp.statusCode, []int{http.StatusNoContent})
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPermissions sets up queue permissions
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/set-queue-acl
|
|
||||||
func (q *Queue) SetPermissions(permissions QueuePermissions, options *SetQueuePermissionOptions) error {
|
|
||||||
body, length, err := generateQueueACLpayload(permissions.AccessPolicies)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
params := url.Values{
|
|
||||||
"comp": {"acl"},
|
|
||||||
}
|
|
||||||
headers := q.qsc.client.getStandardHeaders()
|
|
||||||
headers["Content-Length"] = strconv.Itoa(length)
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := q.qsc.client.getEndpoint(queueServiceName, q.buildPath(), params)
|
|
||||||
resp, err := q.qsc.client.exec(http.MethodPut, uri, headers, body, q.qsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
if err := checkRespCode(resp.statusCode, []int{http.StatusNoContent}); err != nil {
|
|
||||||
return errors.New("Unable to set permissions")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateQueueACLpayload(policies []QueueAccessPolicy) (io.Reader, int, error) {
|
|
||||||
sil := SignedIdentifiers{
|
|
||||||
SignedIdentifiers: []SignedIdentifier{},
|
|
||||||
}
|
|
||||||
for _, qapd := range policies {
|
|
||||||
permission := qapd.generateQueuePermissions()
|
|
||||||
signedIdentifier := convertAccessPolicyToXMLStructs(qapd.ID, qapd.StartTime, qapd.ExpiryTime, permission)
|
|
||||||
sil.SignedIdentifiers = append(sil.SignedIdentifiers, signedIdentifier)
|
|
||||||
}
|
|
||||||
return xmlMarshal(sil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (qapd *QueueAccessPolicy) generateQueuePermissions() (permissions string) {
|
|
||||||
// generate the permissions string (raup).
|
|
||||||
// still want the end user API to have bool flags.
|
|
||||||
permissions = ""
|
|
||||||
|
|
||||||
if qapd.CanRead {
|
|
||||||
permissions += "r"
|
|
||||||
}
|
|
||||||
|
|
||||||
if qapd.CanAdd {
|
|
||||||
permissions += "a"
|
|
||||||
}
|
|
||||||
|
|
||||||
if qapd.CanUpdate {
|
|
||||||
permissions += "u"
|
|
||||||
}
|
|
||||||
|
|
||||||
if qapd.CanProcess {
|
|
||||||
permissions += "p"
|
|
||||||
}
|
|
||||||
|
|
||||||
return permissions
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetQueuePermissionOptions includes options for a get queue permissions operation
|
|
||||||
type GetQueuePermissionOptions struct {
|
|
||||||
Timeout uint
|
|
||||||
RequestID string `header:"x-ms-client-request-id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPermissions gets the queue permissions as per https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/get-queue-acl
|
|
||||||
// If timeout is 0 then it will not be passed to Azure
|
|
||||||
func (q *Queue) GetPermissions(options *GetQueuePermissionOptions) (*QueuePermissions, error) {
|
|
||||||
params := url.Values{
|
|
||||||
"comp": {"acl"},
|
|
||||||
}
|
|
||||||
headers := q.qsc.client.getStandardHeaders()
|
|
||||||
|
|
||||||
if options != nil {
|
|
||||||
params = addTimeout(params, options.Timeout)
|
|
||||||
headers = mergeHeaders(headers, headersFromStruct(*options))
|
|
||||||
}
|
|
||||||
uri := q.qsc.client.getEndpoint(queueServiceName, q.buildPath(), params)
|
|
||||||
resp, err := q.qsc.client.exec(http.MethodGet, uri, headers, nil, q.qsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.body.Close()
|
|
||||||
|
|
||||||
var ap AccessPolicy
|
|
||||||
err = xmlUnmarshal(resp.body, &ap.SignedIdentifiersList)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return buildQueueAccessPolicy(ap, &resp.headers), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildQueueAccessPolicy(ap AccessPolicy, headers *http.Header) *QueuePermissions {
|
|
||||||
permissions := QueuePermissions{
|
|
||||||
AccessPolicies: []QueueAccessPolicy{},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, policy := range ap.SignedIdentifiersList.SignedIdentifiers {
|
|
||||||
qapd := QueueAccessPolicy{
|
|
||||||
ID: policy.ID,
|
|
||||||
StartTime: policy.AccessPolicy.StartTime,
|
|
||||||
ExpiryTime: policy.AccessPolicy.ExpiryTime,
|
|
||||||
}
|
|
||||||
qapd.CanRead = updatePermissions(policy.AccessPolicy.Permission, "r")
|
|
||||||
qapd.CanAdd = updatePermissions(policy.AccessPolicy.Permission, "a")
|
|
||||||
qapd.CanUpdate = updatePermissions(policy.AccessPolicy.Permission, "u")
|
|
||||||
qapd.CanProcess = updatePermissions(policy.AccessPolicy.Permission, "p")
|
|
||||||
|
|
||||||
permissions.AccessPolicies = append(permissions.AccessPolicies, qapd)
|
|
||||||
}
|
|
||||||
return &permissions
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
// QueueServiceClient contains operations for Microsoft Azure Queue Storage
|
|
||||||
// Service.
|
|
||||||
type QueueServiceClient struct {
|
|
||||||
client Client
|
|
||||||
auth authentication
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetServiceProperties gets the properties of your storage account's queue service.
|
|
||||||
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/get-queue-service-properties
|
|
||||||
func (q *QueueServiceClient) GetServiceProperties() (*ServiceProperties, error) {
|
|
||||||
return q.client.getServiceProperties(queueServiceName, q.auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetServiceProperties sets the properties of your storage account's queue service.
|
|
||||||
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/set-queue-service-properties
|
|
||||||
func (q *QueueServiceClient) SetServiceProperties(props ServiceProperties) error {
|
|
||||||
return q.client.setServiceProperties(props, queueServiceName, q.auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetQueueReference returns a Container object for the specified queue name.
|
|
||||||
func (q *QueueServiceClient) GetQueueReference(name string) *Queue {
|
|
||||||
return &Queue{
|
|
||||||
qsc: q,
|
|
||||||
Name: name,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,202 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Share represents an Azure file share.
|
|
||||||
type Share struct {
|
|
||||||
fsc *FileServiceClient
|
|
||||||
Name string `xml:"Name"`
|
|
||||||
Properties ShareProperties `xml:"Properties"`
|
|
||||||
Metadata map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShareProperties contains various properties of a share.
|
|
||||||
type ShareProperties struct {
|
|
||||||
LastModified string `xml:"Last-Modified"`
|
|
||||||
Etag string `xml:"Etag"`
|
|
||||||
Quota int `xml:"Quota"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// builds the complete path for this share object.
|
|
||||||
func (s *Share) buildPath() string {
|
|
||||||
return fmt.Sprintf("/%s", s.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create this share under the associated account.
|
|
||||||
// If a share with the same name already exists, the operation fails.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Create-Share
|
|
||||||
func (s *Share) Create(options *FileRequestOptions) error {
|
|
||||||
extraheaders := map[string]string{}
|
|
||||||
if s.Properties.Quota > 0 {
|
|
||||||
extraheaders["x-ms-share-quota"] = strconv.Itoa(s.Properties.Quota)
|
|
||||||
}
|
|
||||||
|
|
||||||
params := prepareOptions(options)
|
|
||||||
headers, err := s.fsc.createResource(s.buildPath(), resourceShare, params, mergeMDIntoExtraHeaders(s.Metadata, extraheaders), []int{http.StatusCreated})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s.updateEtagAndLastModified(headers)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateIfNotExists creates this share under the associated account if
|
|
||||||
// it does not exist. Returns true if the share is newly created or false if
|
|
||||||
// the share already exists.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Create-Share
|
|
||||||
func (s *Share) CreateIfNotExists(options *FileRequestOptions) (bool, error) {
|
|
||||||
extraheaders := map[string]string{}
|
|
||||||
if s.Properties.Quota > 0 {
|
|
||||||
extraheaders["x-ms-share-quota"] = strconv.Itoa(s.Properties.Quota)
|
|
||||||
}
|
|
||||||
|
|
||||||
params := prepareOptions(options)
|
|
||||||
resp, err := s.fsc.createResourceNoClose(s.buildPath(), resourceShare, params, extraheaders)
|
|
||||||
if resp != nil {
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
if resp.statusCode == http.StatusCreated || resp.statusCode == http.StatusConflict {
|
|
||||||
if resp.statusCode == http.StatusCreated {
|
|
||||||
s.updateEtagAndLastModified(resp.headers)
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
return false, s.FetchAttributes(nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete marks this share for deletion. The share along with any files
|
|
||||||
// and directories contained within it are later deleted during garbage
|
|
||||||
// collection. If the share does not exist the operation fails
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Delete-Share
|
|
||||||
func (s *Share) Delete(options *FileRequestOptions) error {
|
|
||||||
return s.fsc.deleteResource(s.buildPath(), resourceShare, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteIfExists operation marks this share for deletion if it exists.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Delete-Share
|
|
||||||
func (s *Share) DeleteIfExists(options *FileRequestOptions) (bool, error) {
|
|
||||||
resp, err := s.fsc.deleteResourceNoClose(s.buildPath(), resourceShare, options)
|
|
||||||
if resp != nil {
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
if resp.statusCode == http.StatusAccepted || resp.statusCode == http.StatusNotFound {
|
|
||||||
return resp.statusCode == http.StatusAccepted, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exists returns true if this share already exists
|
|
||||||
// on the storage account, otherwise returns false.
|
|
||||||
func (s *Share) Exists() (bool, error) {
|
|
||||||
exists, headers, err := s.fsc.resourceExists(s.buildPath(), resourceShare)
|
|
||||||
if exists {
|
|
||||||
s.updateEtagAndLastModified(headers)
|
|
||||||
s.updateQuota(headers)
|
|
||||||
}
|
|
||||||
return exists, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// FetchAttributes retrieves metadata and properties for this share.
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/get-share-properties
|
|
||||||
func (s *Share) FetchAttributes(options *FileRequestOptions) error {
|
|
||||||
params := prepareOptions(options)
|
|
||||||
headers, err := s.fsc.getResourceHeaders(s.buildPath(), compNone, resourceShare, params, http.MethodHead)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s.updateEtagAndLastModified(headers)
|
|
||||||
s.updateQuota(headers)
|
|
||||||
s.Metadata = getMetadataFromHeaders(headers)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRootDirectoryReference returns a Directory object at the root of this share.
|
|
||||||
func (s *Share) GetRootDirectoryReference() *Directory {
|
|
||||||
return &Directory{
|
|
||||||
fsc: s.fsc,
|
|
||||||
share: s,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceClient returns the FileServiceClient associated with this share.
|
|
||||||
func (s *Share) ServiceClient() *FileServiceClient {
|
|
||||||
return s.fsc
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetMetadata replaces the metadata for this share.
|
|
||||||
//
|
|
||||||
// Some keys may be converted to Camel-Case before sending. All keys
|
|
||||||
// are returned in lower case by GetShareMetadata. HTTP header names
|
|
||||||
// are case-insensitive so case munging should not matter to other
|
|
||||||
// applications either.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/set-share-metadata
|
|
||||||
func (s *Share) SetMetadata(options *FileRequestOptions) error {
|
|
||||||
headers, err := s.fsc.setResourceHeaders(s.buildPath(), compMetadata, resourceShare, mergeMDIntoExtraHeaders(s.Metadata, nil), options)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s.updateEtagAndLastModified(headers)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetProperties sets system properties for this share.
|
|
||||||
//
|
|
||||||
// Some keys may be converted to Camel-Case before sending. All keys
|
|
||||||
// are returned in lower case by SetShareProperties. HTTP header names
|
|
||||||
// are case-insensitive so case munging should not matter to other
|
|
||||||
// applications either.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/Set-Share-Properties
|
|
||||||
func (s *Share) SetProperties(options *FileRequestOptions) error {
|
|
||||||
extraheaders := map[string]string{}
|
|
||||||
if s.Properties.Quota > 0 {
|
|
||||||
if s.Properties.Quota > 5120 {
|
|
||||||
return fmt.Errorf("invalid value %v for quota, valid values are [1, 5120]", s.Properties.Quota)
|
|
||||||
}
|
|
||||||
extraheaders["x-ms-share-quota"] = strconv.Itoa(s.Properties.Quota)
|
|
||||||
}
|
|
||||||
|
|
||||||
headers, err := s.fsc.setResourceHeaders(s.buildPath(), compProperties, resourceShare, extraheaders, options)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s.updateEtagAndLastModified(headers)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// updates Etag and last modified date
|
|
||||||
func (s *Share) updateEtagAndLastModified(headers http.Header) {
|
|
||||||
s.Properties.Etag = headers.Get("Etag")
|
|
||||||
s.Properties.LastModified = headers.Get("Last-Modified")
|
|
||||||
}
|
|
||||||
|
|
||||||
// updates quota value
|
|
||||||
func (s *Share) updateQuota(headers http.Header) {
|
|
||||||
quota, err := strconv.Atoi(headers.Get("x-ms-share-quota"))
|
|
||||||
if err == nil {
|
|
||||||
s.Properties.Quota = quota
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// URL gets the canonical URL to this share. This method does not create a publicly accessible
|
|
||||||
// URL if the share is private and this method does not check if the share exists.
|
|
||||||
func (s *Share) URL() string {
|
|
||||||
return s.fsc.client.getEndpoint(fileServiceName, s.buildPath(), url.Values{})
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AccessPolicyDetailsXML has specifics about an access policy
|
|
||||||
// annotated with XML details.
|
|
||||||
type AccessPolicyDetailsXML struct {
|
|
||||||
StartTime time.Time `xml:"Start"`
|
|
||||||
ExpiryTime time.Time `xml:"Expiry"`
|
|
||||||
Permission string `xml:"Permission"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignedIdentifier is a wrapper for a specific policy
|
|
||||||
type SignedIdentifier struct {
|
|
||||||
ID string `xml:"Id"`
|
|
||||||
AccessPolicy AccessPolicyDetailsXML `xml:"AccessPolicy"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignedIdentifiers part of the response from GetPermissions call.
|
|
||||||
type SignedIdentifiers struct {
|
|
||||||
SignedIdentifiers []SignedIdentifier `xml:"SignedIdentifier"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AccessPolicy is the response type from the GetPermissions call.
|
|
||||||
type AccessPolicy struct {
|
|
||||||
SignedIdentifiersList SignedIdentifiers `xml:"SignedIdentifiers"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// convertAccessPolicyToXMLStructs converts between AccessPolicyDetails which is a struct better for API usage to the
|
|
||||||
// AccessPolicy struct which will get converted to XML.
|
|
||||||
func convertAccessPolicyToXMLStructs(id string, startTime time.Time, expiryTime time.Time, permissions string) SignedIdentifier {
|
|
||||||
return SignedIdentifier{
|
|
||||||
ID: id,
|
|
||||||
AccessPolicy: AccessPolicyDetailsXML{
|
|
||||||
StartTime: startTime.UTC().Round(time.Second),
|
|
||||||
ExpiryTime: expiryTime.UTC().Round(time.Second),
|
|
||||||
Permission: permissions,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func updatePermissions(permissions, permission string) bool {
|
|
||||||
return strings.Contains(permissions, permission)
|
|
||||||
}
|
|
|
@ -1,117 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ServiceProperties represents the storage account service properties
|
|
||||||
type ServiceProperties struct {
|
|
||||||
Logging *Logging
|
|
||||||
HourMetrics *Metrics
|
|
||||||
MinuteMetrics *Metrics
|
|
||||||
Cors *Cors
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logging represents the Azure Analytics Logging settings
|
|
||||||
type Logging struct {
|
|
||||||
Version string
|
|
||||||
Delete bool
|
|
||||||
Read bool
|
|
||||||
Write bool
|
|
||||||
RetentionPolicy *RetentionPolicy
|
|
||||||
}
|
|
||||||
|
|
||||||
// RetentionPolicy indicates if retention is enabled and for how many days
|
|
||||||
type RetentionPolicy struct {
|
|
||||||
Enabled bool
|
|
||||||
Days *int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metrics provide request statistics.
|
|
||||||
type Metrics struct {
|
|
||||||
Version string
|
|
||||||
Enabled bool
|
|
||||||
IncludeAPIs *bool
|
|
||||||
RetentionPolicy *RetentionPolicy
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cors includes all the CORS rules
|
|
||||||
type Cors struct {
|
|
||||||
CorsRule []CorsRule
|
|
||||||
}
|
|
||||||
|
|
||||||
// CorsRule includes all settings for a Cors rule
|
|
||||||
type CorsRule struct {
|
|
||||||
AllowedOrigins string
|
|
||||||
AllowedMethods string
|
|
||||||
MaxAgeInSeconds int
|
|
||||||
ExposedHeaders string
|
|
||||||
AllowedHeaders string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Client) getServiceProperties(service string, auth authentication) (*ServiceProperties, error) {
|
|
||||||
query := url.Values{
|
|
||||||
"restype": {"service"},
|
|
||||||
"comp": {"properties"},
|
|
||||||
}
|
|
||||||
uri := c.getEndpoint(service, "", query)
|
|
||||||
headers := c.getStandardHeaders()
|
|
||||||
|
|
||||||
resp, err := c.exec(http.MethodGet, uri, headers, nil, auth)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.body.Close()
|
|
||||||
|
|
||||||
if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var out ServiceProperties
|
|
||||||
err = xmlUnmarshal(resp.body, &out)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Client) setServiceProperties(props ServiceProperties, service string, auth authentication) error {
|
|
||||||
query := url.Values{
|
|
||||||
"restype": {"service"},
|
|
||||||
"comp": {"properties"},
|
|
||||||
}
|
|
||||||
uri := c.getEndpoint(service, "", query)
|
|
||||||
|
|
||||||
// Ideally, StorageServiceProperties would be the output struct
|
|
||||||
// This is to avoid golint stuttering, while generating the correct XML
|
|
||||||
type StorageServiceProperties struct {
|
|
||||||
Logging *Logging
|
|
||||||
HourMetrics *Metrics
|
|
||||||
MinuteMetrics *Metrics
|
|
||||||
Cors *Cors
|
|
||||||
}
|
|
||||||
input := StorageServiceProperties{
|
|
||||||
Logging: props.Logging,
|
|
||||||
HourMetrics: props.HourMetrics,
|
|
||||||
MinuteMetrics: props.MinuteMetrics,
|
|
||||||
Cors: props.Cors,
|
|
||||||
}
|
|
||||||
|
|
||||||
body, length, err := xmlMarshal(input)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
headers := c.getStandardHeaders()
|
|
||||||
headers["Content-Length"] = strconv.Itoa(length)
|
|
||||||
|
|
||||||
resp, err := c.exec(http.MethodPut, uri, headers, body, auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
readAndCloseBody(resp.body)
|
|
||||||
return checkRespCode(resp.statusCode, []int{http.StatusAccepted})
|
|
||||||
}
|
|
|
@ -1,412 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
tablesURIPath = "/Tables"
|
|
||||||
nextTableQueryParameter = "NextTableName"
|
|
||||||
headerNextPartitionKey = "x-ms-continuation-NextPartitionKey"
|
|
||||||
headerNextRowKey = "x-ms-continuation-NextRowKey"
|
|
||||||
nextPartitionKeyQueryParameter = "NextPartitionKey"
|
|
||||||
nextRowKeyQueryParameter = "NextRowKey"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TableAccessPolicy are used for SETTING table policies
|
|
||||||
type TableAccessPolicy struct {
|
|
||||||
ID string
|
|
||||||
StartTime time.Time
|
|
||||||
ExpiryTime time.Time
|
|
||||||
CanRead bool
|
|
||||||
CanAppend bool
|
|
||||||
CanUpdate bool
|
|
||||||
CanDelete bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Table represents an Azure table.
|
|
||||||
type Table struct {
|
|
||||||
tsc *TableServiceClient
|
|
||||||
Name string `json:"TableName"`
|
|
||||||
OdataEditLink string `json:"odata.editLink"`
|
|
||||||
OdataID string `json:"odata.id"`
|
|
||||||
OdataMetadata string `json:"odata.metadata"`
|
|
||||||
OdataType string `json:"odata.type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// EntityQueryResult contains the response from
|
|
||||||
// ExecuteQuery and ExecuteQueryNextResults functions.
|
|
||||||
type EntityQueryResult struct {
|
|
||||||
OdataMetadata string `json:"odata.metadata"`
|
|
||||||
Entities []*Entity `json:"value"`
|
|
||||||
QueryNextLink
|
|
||||||
table *Table
|
|
||||||
}
|
|
||||||
|
|
||||||
type continuationToken struct {
|
|
||||||
NextPartitionKey string
|
|
||||||
NextRowKey string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Table) buildPath() string {
|
|
||||||
return fmt.Sprintf("/%s", t.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Table) buildSpecificPath() string {
|
|
||||||
return fmt.Sprintf("%s('%s')", tablesURIPath, t.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get gets the referenced table.
|
|
||||||
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/querying-tables-and-entities
|
|
||||||
func (t *Table) Get(timeout uint, ml MetadataLevel) error {
|
|
||||||
if ml == EmptyPayload {
|
|
||||||
return errEmptyPayload
|
|
||||||
}
|
|
||||||
|
|
||||||
query := url.Values{
|
|
||||||
"timeout": {strconv.FormatUint(uint64(timeout), 10)},
|
|
||||||
}
|
|
||||||
headers := t.tsc.client.getStandardHeaders()
|
|
||||||
headers[headerAccept] = string(ml)
|
|
||||||
|
|
||||||
uri := t.tsc.client.getEndpoint(tableServiceName, t.buildSpecificPath(), query)
|
|
||||||
resp, err := t.tsc.client.exec(http.MethodGet, uri, headers, nil, t.tsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
if err = checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
respBody, err := ioutil.ReadAll(resp.body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(respBody, t)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create creates the referenced table.
|
|
||||||
// This function fails if the name is not compliant
|
|
||||||
// with the specification or the tables already exists.
|
|
||||||
// ml determines the level of detail of metadata in the operation response,
|
|
||||||
// or no data at all.
|
|
||||||
// See https://docs.microsoft.com/rest/api/storageservices/fileservices/create-table
|
|
||||||
func (t *Table) Create(timeout uint, ml MetadataLevel, options *TableOptions) error {
|
|
||||||
uri := t.tsc.client.getEndpoint(tableServiceName, tablesURIPath, url.Values{
|
|
||||||
"timeout": {strconv.FormatUint(uint64(timeout), 10)},
|
|
||||||
})
|
|
||||||
|
|
||||||
type createTableRequest struct {
|
|
||||||
TableName string `json:"TableName"`
|
|
||||||
}
|
|
||||||
req := createTableRequest{TableName: t.Name}
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
if err := json.NewEncoder(buf).Encode(req); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
headers := t.tsc.client.getStandardHeaders()
|
|
||||||
headers = addReturnContentHeaders(headers, ml)
|
|
||||||
headers = addBodyRelatedHeaders(headers, buf.Len())
|
|
||||||
headers = options.addToHeaders(headers)
|
|
||||||
|
|
||||||
resp, err := t.tsc.client.exec(http.MethodPost, uri, headers, buf, t.tsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
if ml == EmptyPayload {
|
|
||||||
if err := checkRespCode(resp.statusCode, []int{http.StatusNoContent}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := checkRespCode(resp.statusCode, []int{http.StatusCreated}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ml != EmptyPayload {
|
|
||||||
data, err := ioutil.ReadAll(resp.body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(data, t)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete deletes the referenced table.
|
|
||||||
// This function fails if the table is not present.
|
|
||||||
// Be advised: Delete deletes all the entries that may be present.
|
|
||||||
// See https://docs.microsoft.com/rest/api/storageservices/fileservices/delete-table
|
|
||||||
func (t *Table) Delete(timeout uint, options *TableOptions) error {
|
|
||||||
uri := t.tsc.client.getEndpoint(tableServiceName, t.buildSpecificPath(), url.Values{
|
|
||||||
"timeout": {strconv.Itoa(int(timeout))},
|
|
||||||
})
|
|
||||||
|
|
||||||
headers := t.tsc.client.getStandardHeaders()
|
|
||||||
headers = addReturnContentHeaders(headers, EmptyPayload)
|
|
||||||
headers = options.addToHeaders(headers)
|
|
||||||
|
|
||||||
resp, err := t.tsc.client.exec(http.MethodDelete, uri, headers, nil, t.tsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
if err := checkRespCode(resp.statusCode, []int{http.StatusNoContent}); err != nil {
|
|
||||||
return err
|
|
||||||
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryOptions includes options for a query entities operation.
|
|
||||||
// Top, filter and select are OData query options.
|
|
||||||
type QueryOptions struct {
|
|
||||||
Top uint
|
|
||||||
Filter string
|
|
||||||
Select []string
|
|
||||||
RequestID string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (options *QueryOptions) getParameters() (url.Values, map[string]string) {
|
|
||||||
query := url.Values{}
|
|
||||||
headers := map[string]string{}
|
|
||||||
if options != nil {
|
|
||||||
if options.Top > 0 {
|
|
||||||
query.Add(OdataTop, strconv.FormatUint(uint64(options.Top), 10))
|
|
||||||
}
|
|
||||||
if options.Filter != "" {
|
|
||||||
query.Add(OdataFilter, options.Filter)
|
|
||||||
}
|
|
||||||
if len(options.Select) > 0 {
|
|
||||||
query.Add(OdataSelect, strings.Join(options.Select, ","))
|
|
||||||
}
|
|
||||||
headers = addToHeaders(headers, "x-ms-client-request-id", options.RequestID)
|
|
||||||
}
|
|
||||||
return query, headers
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryEntities returns the entities in the table.
|
|
||||||
// You can use query options defined by the OData Protocol specification.
|
|
||||||
//
|
|
||||||
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/query-entities
|
|
||||||
func (t *Table) QueryEntities(timeout uint, ml MetadataLevel, options *QueryOptions) (*EntityQueryResult, error) {
|
|
||||||
if ml == EmptyPayload {
|
|
||||||
return nil, errEmptyPayload
|
|
||||||
}
|
|
||||||
query, headers := options.getParameters()
|
|
||||||
query = addTimeout(query, timeout)
|
|
||||||
uri := t.tsc.client.getEndpoint(tableServiceName, t.buildPath(), query)
|
|
||||||
return t.queryEntities(uri, headers, ml)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NextResults returns the next page of results
|
|
||||||
// from a QueryEntities or NextResults operation.
|
|
||||||
//
|
|
||||||
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/query-entities
|
|
||||||
// See https://docs.microsoft.com/rest/api/storageservices/fileservices/query-timeout-and-pagination
|
|
||||||
func (eqr *EntityQueryResult) NextResults(options *TableOptions) (*EntityQueryResult, error) {
|
|
||||||
if eqr == nil {
|
|
||||||
return nil, errNilPreviousResult
|
|
||||||
}
|
|
||||||
if eqr.NextLink == nil {
|
|
||||||
return nil, errNilNextLink
|
|
||||||
}
|
|
||||||
headers := options.addToHeaders(map[string]string{})
|
|
||||||
return eqr.table.queryEntities(*eqr.NextLink, headers, eqr.ml)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPermissions sets up table ACL permissions
|
|
||||||
// See https://docs.microsoft.com/rest/api/storageservices/fileservices/Set-Table-ACL
|
|
||||||
func (t *Table) SetPermissions(tap []TableAccessPolicy, timeout uint, options *TableOptions) error {
|
|
||||||
params := url.Values{"comp": {"acl"},
|
|
||||||
"timeout": {strconv.Itoa(int(timeout))},
|
|
||||||
}
|
|
||||||
|
|
||||||
uri := t.tsc.client.getEndpoint(tableServiceName, t.Name, params)
|
|
||||||
headers := t.tsc.client.getStandardHeaders()
|
|
||||||
headers = options.addToHeaders(headers)
|
|
||||||
|
|
||||||
body, length, err := generateTableACLPayload(tap)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
headers["Content-Length"] = strconv.Itoa(length)
|
|
||||||
|
|
||||||
resp, err := t.tsc.client.exec(http.MethodPut, uri, headers, body, t.tsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer readAndCloseBody(resp.body)
|
|
||||||
|
|
||||||
if err := checkRespCode(resp.statusCode, []int{http.StatusNoContent}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateTableACLPayload(policies []TableAccessPolicy) (io.Reader, int, error) {
|
|
||||||
sil := SignedIdentifiers{
|
|
||||||
SignedIdentifiers: []SignedIdentifier{},
|
|
||||||
}
|
|
||||||
for _, tap := range policies {
|
|
||||||
permission := generateTablePermissions(&tap)
|
|
||||||
signedIdentifier := convertAccessPolicyToXMLStructs(tap.ID, tap.StartTime, tap.ExpiryTime, permission)
|
|
||||||
sil.SignedIdentifiers = append(sil.SignedIdentifiers, signedIdentifier)
|
|
||||||
}
|
|
||||||
return xmlMarshal(sil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPermissions gets the table ACL permissions
|
|
||||||
// See https://docs.microsoft.com/rest/api/storageservices/fileservices/get-table-acl
|
|
||||||
func (t *Table) GetPermissions(timeout int, options *TableOptions) ([]TableAccessPolicy, error) {
|
|
||||||
params := url.Values{"comp": {"acl"},
|
|
||||||
"timeout": {strconv.Itoa(int(timeout))},
|
|
||||||
}
|
|
||||||
|
|
||||||
uri := t.tsc.client.getEndpoint(tableServiceName, t.Name, params)
|
|
||||||
headers := t.tsc.client.getStandardHeaders()
|
|
||||||
headers = options.addToHeaders(headers)
|
|
||||||
|
|
||||||
resp, err := t.tsc.client.exec(http.MethodGet, uri, headers, nil, t.tsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.body.Close()
|
|
||||||
|
|
||||||
if err = checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var ap AccessPolicy
|
|
||||||
err = xmlUnmarshal(resp.body, &ap.SignedIdentifiersList)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return updateTableAccessPolicy(ap), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Table) queryEntities(uri string, headers map[string]string, ml MetadataLevel) (*EntityQueryResult, error) {
|
|
||||||
headers = mergeHeaders(headers, t.tsc.client.getStandardHeaders())
|
|
||||||
if ml != EmptyPayload {
|
|
||||||
headers[headerAccept] = string(ml)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := t.tsc.client.exec(http.MethodGet, uri, headers, nil, t.tsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.body.Close()
|
|
||||||
|
|
||||||
if err = checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := ioutil.ReadAll(resp.body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var entities EntityQueryResult
|
|
||||||
err = json.Unmarshal(data, &entities)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range entities.Entities {
|
|
||||||
entities.Entities[i].Table = t
|
|
||||||
}
|
|
||||||
entities.table = t
|
|
||||||
|
|
||||||
contToken := extractContinuationTokenFromHeaders(resp.headers)
|
|
||||||
if contToken == nil {
|
|
||||||
entities.NextLink = nil
|
|
||||||
} else {
|
|
||||||
originalURI, err := url.Parse(uri)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
v := originalURI.Query()
|
|
||||||
v.Set(nextPartitionKeyQueryParameter, contToken.NextPartitionKey)
|
|
||||||
v.Set(nextRowKeyQueryParameter, contToken.NextRowKey)
|
|
||||||
newURI := t.tsc.client.getEndpoint(tableServiceName, t.buildPath(), v)
|
|
||||||
entities.NextLink = &newURI
|
|
||||||
entities.ml = ml
|
|
||||||
}
|
|
||||||
|
|
||||||
return &entities, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractContinuationTokenFromHeaders(h http.Header) *continuationToken {
|
|
||||||
ct := continuationToken{
|
|
||||||
NextPartitionKey: h.Get(headerNextPartitionKey),
|
|
||||||
NextRowKey: h.Get(headerNextRowKey),
|
|
||||||
}
|
|
||||||
|
|
||||||
if ct.NextPartitionKey != "" && ct.NextRowKey != "" {
|
|
||||||
return &ct
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateTableAccessPolicy(ap AccessPolicy) []TableAccessPolicy {
|
|
||||||
taps := []TableAccessPolicy{}
|
|
||||||
for _, policy := range ap.SignedIdentifiersList.SignedIdentifiers {
|
|
||||||
tap := TableAccessPolicy{
|
|
||||||
ID: policy.ID,
|
|
||||||
StartTime: policy.AccessPolicy.StartTime,
|
|
||||||
ExpiryTime: policy.AccessPolicy.ExpiryTime,
|
|
||||||
}
|
|
||||||
tap.CanRead = updatePermissions(policy.AccessPolicy.Permission, "r")
|
|
||||||
tap.CanAppend = updatePermissions(policy.AccessPolicy.Permission, "a")
|
|
||||||
tap.CanUpdate = updatePermissions(policy.AccessPolicy.Permission, "u")
|
|
||||||
tap.CanDelete = updatePermissions(policy.AccessPolicy.Permission, "d")
|
|
||||||
|
|
||||||
taps = append(taps, tap)
|
|
||||||
}
|
|
||||||
return taps
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateTablePermissions(tap *TableAccessPolicy) (permissions string) {
|
|
||||||
// generate the permissions string (raud).
|
|
||||||
// still want the end user API to have bool flags.
|
|
||||||
permissions = ""
|
|
||||||
|
|
||||||
if tap.CanRead {
|
|
||||||
permissions += "r"
|
|
||||||
}
|
|
||||||
|
|
||||||
if tap.CanAppend {
|
|
||||||
permissions += "a"
|
|
||||||
}
|
|
||||||
|
|
||||||
if tap.CanUpdate {
|
|
||||||
permissions += "u"
|
|
||||||
}
|
|
||||||
|
|
||||||
if tap.CanDelete {
|
|
||||||
permissions += "d"
|
|
||||||
}
|
|
||||||
return permissions
|
|
||||||
}
|
|
|
@ -1,302 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
|
||||||
"net/textproto"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/satori/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Operation type. Insert, Delete, Replace etc.
|
|
||||||
type Operation int
|
|
||||||
|
|
||||||
// consts for batch operations.
|
|
||||||
const (
|
|
||||||
InsertOp = Operation(1)
|
|
||||||
DeleteOp = Operation(2)
|
|
||||||
ReplaceOp = Operation(3)
|
|
||||||
MergeOp = Operation(4)
|
|
||||||
InsertOrReplaceOp = Operation(5)
|
|
||||||
InsertOrMergeOp = Operation(6)
|
|
||||||
)
|
|
||||||
|
|
||||||
// BatchEntity used for tracking Entities to operate on and
|
|
||||||
// whether operations (replace/merge etc) should be forced.
|
|
||||||
// Wrapper for regular Entity with additional data specific for the entity.
|
|
||||||
type BatchEntity struct {
|
|
||||||
*Entity
|
|
||||||
Force bool
|
|
||||||
Op Operation
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableBatch stores all the entities that will be operated on during a batch process.
|
|
||||||
// Entities can be inserted, replaced or deleted.
|
|
||||||
type TableBatch struct {
|
|
||||||
BatchEntitySlice []BatchEntity
|
|
||||||
|
|
||||||
// reference to table we're operating on.
|
|
||||||
Table *Table
|
|
||||||
}
|
|
||||||
|
|
||||||
// defaultChangesetHeaders for changeSets
|
|
||||||
var defaultChangesetHeaders = map[string]string{
|
|
||||||
"Accept": "application/json;odata=minimalmetadata",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Prefer": "return-no-content",
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBatch return new TableBatch for populating.
|
|
||||||
func (t *Table) NewBatch() *TableBatch {
|
|
||||||
return &TableBatch{
|
|
||||||
Table: t,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertEntity adds an entity in preparation for a batch insert.
|
|
||||||
func (t *TableBatch) InsertEntity(entity *Entity) {
|
|
||||||
be := BatchEntity{Entity: entity, Force: false, Op: InsertOp}
|
|
||||||
t.BatchEntitySlice = append(t.BatchEntitySlice, be)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertOrReplaceEntity adds an entity in preparation for a batch insert or replace.
|
|
||||||
func (t *TableBatch) InsertOrReplaceEntity(entity *Entity, force bool) {
|
|
||||||
be := BatchEntity{Entity: entity, Force: false, Op: InsertOrReplaceOp}
|
|
||||||
t.BatchEntitySlice = append(t.BatchEntitySlice, be)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertOrReplaceEntityByForce adds an entity in preparation for a batch insert or replace. Forces regardless of ETag
|
|
||||||
func (t *TableBatch) InsertOrReplaceEntityByForce(entity *Entity) {
|
|
||||||
t.InsertOrReplaceEntity(entity, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertOrMergeEntity adds an entity in preparation for a batch insert or merge.
|
|
||||||
func (t *TableBatch) InsertOrMergeEntity(entity *Entity, force bool) {
|
|
||||||
be := BatchEntity{Entity: entity, Force: false, Op: InsertOrMergeOp}
|
|
||||||
t.BatchEntitySlice = append(t.BatchEntitySlice, be)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertOrMergeEntityByForce adds an entity in preparation for a batch insert or merge. Forces regardless of ETag
|
|
||||||
func (t *TableBatch) InsertOrMergeEntityByForce(entity *Entity) {
|
|
||||||
t.InsertOrMergeEntity(entity, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReplaceEntity adds an entity in preparation for a batch replace.
|
|
||||||
func (t *TableBatch) ReplaceEntity(entity *Entity) {
|
|
||||||
be := BatchEntity{Entity: entity, Force: false, Op: ReplaceOp}
|
|
||||||
t.BatchEntitySlice = append(t.BatchEntitySlice, be)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteEntity adds an entity in preparation for a batch delete
|
|
||||||
func (t *TableBatch) DeleteEntity(entity *Entity, force bool) {
|
|
||||||
be := BatchEntity{Entity: entity, Force: false, Op: DeleteOp}
|
|
||||||
t.BatchEntitySlice = append(t.BatchEntitySlice, be)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteEntityByForce adds an entity in preparation for a batch delete. Forces regardless of ETag
|
|
||||||
func (t *TableBatch) DeleteEntityByForce(entity *Entity, force bool) {
|
|
||||||
t.DeleteEntity(entity, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MergeEntity adds an entity in preparation for a batch merge
|
|
||||||
func (t *TableBatch) MergeEntity(entity *Entity) {
|
|
||||||
be := BatchEntity{Entity: entity, Force: false, Op: MergeOp}
|
|
||||||
t.BatchEntitySlice = append(t.BatchEntitySlice, be)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecuteBatch executes many table operations in one request to Azure.
|
|
||||||
// The operations can be combinations of Insert, Delete, Replace and Merge
|
|
||||||
// Creates the inner changeset body (various operations, Insert, Delete etc) then creates the outer request packet that encompasses
|
|
||||||
// the changesets.
|
|
||||||
// As per document https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/performing-entity-group-transactions
|
|
||||||
func (t *TableBatch) ExecuteBatch() error {
|
|
||||||
changesetBoundary := fmt.Sprintf("changeset_%s", uuid.NewV1())
|
|
||||||
uri := t.Table.tsc.client.getEndpoint(tableServiceName, "$batch", nil)
|
|
||||||
changesetBody, err := t.generateChangesetBody(changesetBoundary)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
boundary := fmt.Sprintf("batch_%s", uuid.NewV1())
|
|
||||||
body, err := generateBody(changesetBody, changesetBoundary, boundary)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
headers := t.Table.tsc.client.getStandardHeaders()
|
|
||||||
headers[headerContentType] = fmt.Sprintf("multipart/mixed; boundary=%s", boundary)
|
|
||||||
|
|
||||||
resp, err := t.Table.tsc.client.execBatchOperationJSON(http.MethodPost, uri, headers, bytes.NewReader(body.Bytes()), t.Table.tsc.auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.body.Close()
|
|
||||||
|
|
||||||
if err = checkRespCode(resp.statusCode, []int{http.StatusAccepted}); err != nil {
|
|
||||||
|
|
||||||
// check which batch failed.
|
|
||||||
operationFailedMessage := t.getFailedOperation(resp.odata.Err.Message.Value)
|
|
||||||
requestID, date, version := getDebugHeaders(resp.headers)
|
|
||||||
return AzureStorageServiceError{
|
|
||||||
StatusCode: resp.statusCode,
|
|
||||||
Code: resp.odata.Err.Code,
|
|
||||||
RequestID: requestID,
|
|
||||||
Date: date,
|
|
||||||
APIVersion: version,
|
|
||||||
Message: operationFailedMessage,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getFailedOperation parses the original Azure error string and determines which operation failed
|
|
||||||
// and generates appropriate message.
|
|
||||||
func (t *TableBatch) getFailedOperation(errorMessage string) string {
|
|
||||||
// errorMessage consists of "number:string" we just need the number.
|
|
||||||
sp := strings.Split(errorMessage, ":")
|
|
||||||
if len(sp) > 1 {
|
|
||||||
msg := fmt.Sprintf("Element %s in the batch returned an unexpected response code.\n%s", sp[0], errorMessage)
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
|
|
||||||
// cant parse the message, just return the original message to client
|
|
||||||
return errorMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateBody generates the complete body for the batch request.
|
|
||||||
func generateBody(changeSetBody *bytes.Buffer, changesetBoundary string, boundary string) (*bytes.Buffer, error) {
|
|
||||||
|
|
||||||
body := new(bytes.Buffer)
|
|
||||||
writer := multipart.NewWriter(body)
|
|
||||||
writer.SetBoundary(boundary)
|
|
||||||
h := make(textproto.MIMEHeader)
|
|
||||||
h.Set(headerContentType, fmt.Sprintf("multipart/mixed; boundary=%s\r\n", changesetBoundary))
|
|
||||||
batchWriter, err := writer.CreatePart(h)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
batchWriter.Write(changeSetBody.Bytes())
|
|
||||||
writer.Close()
|
|
||||||
return body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateChangesetBody generates the individual changesets for the various operations within the batch request.
|
|
||||||
// There is a changeset for Insert, Delete, Merge etc.
|
|
||||||
func (t *TableBatch) generateChangesetBody(changesetBoundary string) (*bytes.Buffer, error) {
|
|
||||||
|
|
||||||
body := new(bytes.Buffer)
|
|
||||||
writer := multipart.NewWriter(body)
|
|
||||||
writer.SetBoundary(changesetBoundary)
|
|
||||||
|
|
||||||
for _, be := range t.BatchEntitySlice {
|
|
||||||
t.generateEntitySubset(&be, writer)
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.Close()
|
|
||||||
return body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateVerb generates the HTTP request VERB required for each changeset.
|
|
||||||
func generateVerb(op Operation) (string, error) {
|
|
||||||
switch op {
|
|
||||||
case InsertOp:
|
|
||||||
return http.MethodPost, nil
|
|
||||||
case DeleteOp:
|
|
||||||
return http.MethodDelete, nil
|
|
||||||
case ReplaceOp, InsertOrReplaceOp:
|
|
||||||
return http.MethodPut, nil
|
|
||||||
case MergeOp, InsertOrMergeOp:
|
|
||||||
return "MERGE", nil
|
|
||||||
default:
|
|
||||||
return "", errors.New("Unable to detect operation")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateQueryPath generates the query path for within the changesets
|
|
||||||
// For inserts it will just be a table query path (table name)
|
|
||||||
// but for other operations (modifying an existing entity) then
|
|
||||||
// the partition/row keys need to be generated.
|
|
||||||
func (t *TableBatch) generateQueryPath(op Operation, entity *Entity) string {
|
|
||||||
if op == InsertOp {
|
|
||||||
return entity.Table.buildPath()
|
|
||||||
}
|
|
||||||
return entity.buildPath()
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateGenericOperationHeaders generates common headers for a given operation.
|
|
||||||
func generateGenericOperationHeaders(be *BatchEntity) map[string]string {
|
|
||||||
retval := map[string]string{}
|
|
||||||
|
|
||||||
for k, v := range defaultChangesetHeaders {
|
|
||||||
retval[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
if be.Op == DeleteOp || be.Op == ReplaceOp || be.Op == MergeOp {
|
|
||||||
if be.Force || be.Entity.OdataEtag == "" {
|
|
||||||
retval["If-Match"] = "*"
|
|
||||||
} else {
|
|
||||||
retval["If-Match"] = be.Entity.OdataEtag
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return retval
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateEntitySubset generates body payload for particular batch entity
|
|
||||||
func (t *TableBatch) generateEntitySubset(batchEntity *BatchEntity, writer *multipart.Writer) error {
|
|
||||||
|
|
||||||
h := make(textproto.MIMEHeader)
|
|
||||||
h.Set(headerContentType, "application/http")
|
|
||||||
h.Set(headerContentTransferEncoding, "binary")
|
|
||||||
|
|
||||||
verb, err := generateVerb(batchEntity.Op)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
genericOpHeadersMap := generateGenericOperationHeaders(batchEntity)
|
|
||||||
queryPath := t.generateQueryPath(batchEntity.Op, batchEntity.Entity)
|
|
||||||
uri := t.Table.tsc.client.getEndpoint(tableServiceName, queryPath, nil)
|
|
||||||
|
|
||||||
operationWriter, err := writer.CreatePart(h)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
urlAndVerb := fmt.Sprintf("%s %s HTTP/1.1\r\n", verb, uri)
|
|
||||||
operationWriter.Write([]byte(urlAndVerb))
|
|
||||||
writeHeaders(genericOpHeadersMap, &operationWriter)
|
|
||||||
operationWriter.Write([]byte("\r\n")) // additional \r\n is needed per changeset separating the "headers" and the body.
|
|
||||||
|
|
||||||
// delete operation doesn't need a body.
|
|
||||||
if batchEntity.Op != DeleteOp {
|
|
||||||
//var e Entity = batchEntity.Entity
|
|
||||||
body, err := json.Marshal(batchEntity.Entity)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
operationWriter.Write(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeHeaders(h map[string]string, writer *io.Writer) {
|
|
||||||
// This way it is guaranteed the headers will be written in a sorted order
|
|
||||||
var keys []string
|
|
||||||
for k := range h {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
for _, k := range keys {
|
|
||||||
(*writer).Write([]byte(fmt.Sprintf("%s: %s\r\n", k, h[k])))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,190 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
headerAccept = "Accept"
|
|
||||||
headerEtag = "Etag"
|
|
||||||
headerPrefer = "Prefer"
|
|
||||||
headerXmsContinuation = "x-ms-Continuation-NextTableName"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TableServiceClient contains operations for Microsoft Azure Table Storage
|
|
||||||
// Service.
|
|
||||||
type TableServiceClient struct {
|
|
||||||
client Client
|
|
||||||
auth authentication
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableOptions includes options for some table operations
|
|
||||||
type TableOptions struct {
|
|
||||||
RequestID string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (options *TableOptions) addToHeaders(h map[string]string) map[string]string {
|
|
||||||
if options != nil {
|
|
||||||
h = addToHeaders(h, "x-ms-client-request-id", options.RequestID)
|
|
||||||
}
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryNextLink includes information for getting the next page of
|
|
||||||
// results in query operations
|
|
||||||
type QueryNextLink struct {
|
|
||||||
NextLink *string
|
|
||||||
ml MetadataLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetServiceProperties gets the properties of your storage account's table service.
|
|
||||||
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/get-table-service-properties
|
|
||||||
func (t *TableServiceClient) GetServiceProperties() (*ServiceProperties, error) {
|
|
||||||
return t.client.getServiceProperties(tableServiceName, t.auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetServiceProperties sets the properties of your storage account's table service.
|
|
||||||
// See: https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/set-table-service-properties
|
|
||||||
func (t *TableServiceClient) SetServiceProperties(props ServiceProperties) error {
|
|
||||||
return t.client.setServiceProperties(props, tableServiceName, t.auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTableReference returns a Table object for the specified table name.
|
|
||||||
func (t *TableServiceClient) GetTableReference(name string) *Table {
|
|
||||||
return &Table{
|
|
||||||
tsc: t,
|
|
||||||
Name: name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryTablesOptions includes options for some table operations
|
|
||||||
type QueryTablesOptions struct {
|
|
||||||
Top uint
|
|
||||||
Filter string
|
|
||||||
RequestID string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (options *QueryTablesOptions) getParameters() (url.Values, map[string]string) {
|
|
||||||
query := url.Values{}
|
|
||||||
headers := map[string]string{}
|
|
||||||
if options != nil {
|
|
||||||
if options.Top > 0 {
|
|
||||||
query.Add(OdataTop, strconv.FormatUint(uint64(options.Top), 10))
|
|
||||||
}
|
|
||||||
if options.Filter != "" {
|
|
||||||
query.Add(OdataFilter, options.Filter)
|
|
||||||
}
|
|
||||||
headers = addToHeaders(headers, "x-ms-client-request-id", options.RequestID)
|
|
||||||
}
|
|
||||||
return query, headers
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryTables returns the tables in the storage account.
|
|
||||||
// You can use query options defined by the OData Protocol specification.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/query-tables
|
|
||||||
func (t *TableServiceClient) QueryTables(ml MetadataLevel, options *QueryTablesOptions) (*TableQueryResult, error) {
|
|
||||||
query, headers := options.getParameters()
|
|
||||||
uri := t.client.getEndpoint(tableServiceName, tablesURIPath, query)
|
|
||||||
return t.queryTables(uri, headers, ml)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NextResults returns the next page of results
|
|
||||||
// from a QueryTables or a NextResults operation.
|
|
||||||
//
|
|
||||||
// See https://docs.microsoft.com/rest/api/storageservices/fileservices/query-tables
|
|
||||||
// See https://docs.microsoft.com/rest/api/storageservices/fileservices/query-timeout-and-pagination
|
|
||||||
func (tqr *TableQueryResult) NextResults(options *TableOptions) (*TableQueryResult, error) {
|
|
||||||
if tqr == nil {
|
|
||||||
return nil, errNilPreviousResult
|
|
||||||
}
|
|
||||||
if tqr.NextLink == nil {
|
|
||||||
return nil, errNilNextLink
|
|
||||||
}
|
|
||||||
headers := options.addToHeaders(map[string]string{})
|
|
||||||
|
|
||||||
return tqr.tsc.queryTables(*tqr.NextLink, headers, tqr.ml)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableQueryResult contains the response from
|
|
||||||
// QueryTables and QueryTablesNextResults functions.
|
|
||||||
type TableQueryResult struct {
|
|
||||||
OdataMetadata string `json:"odata.metadata"`
|
|
||||||
Tables []Table `json:"value"`
|
|
||||||
QueryNextLink
|
|
||||||
tsc *TableServiceClient
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TableServiceClient) queryTables(uri string, headers map[string]string, ml MetadataLevel) (*TableQueryResult, error) {
|
|
||||||
if ml == EmptyPayload {
|
|
||||||
return nil, errEmptyPayload
|
|
||||||
}
|
|
||||||
headers = mergeHeaders(headers, t.client.getStandardHeaders())
|
|
||||||
headers[headerAccept] = string(ml)
|
|
||||||
|
|
||||||
resp, err := t.client.exec(http.MethodGet, uri, headers, nil, t.auth)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.body.Close()
|
|
||||||
|
|
||||||
if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
respBody, err := ioutil.ReadAll(resp.body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var out TableQueryResult
|
|
||||||
err = json.Unmarshal(respBody, &out)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range out.Tables {
|
|
||||||
out.Tables[i].tsc = t
|
|
||||||
}
|
|
||||||
out.tsc = t
|
|
||||||
|
|
||||||
nextLink := resp.headers.Get(http.CanonicalHeaderKey(headerXmsContinuation))
|
|
||||||
if nextLink == "" {
|
|
||||||
out.NextLink = nil
|
|
||||||
} else {
|
|
||||||
originalURI, err := url.Parse(uri)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
v := originalURI.Query()
|
|
||||||
v.Set(nextTableQueryParameter, nextLink)
|
|
||||||
newURI := t.client.getEndpoint(tableServiceName, tablesURIPath, v)
|
|
||||||
out.NextLink = &newURI
|
|
||||||
out.ml = ml
|
|
||||||
}
|
|
||||||
|
|
||||||
return &out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func addBodyRelatedHeaders(h map[string]string, length int) map[string]string {
|
|
||||||
h[headerContentType] = "application/json"
|
|
||||||
h[headerContentLength] = fmt.Sprintf("%v", length)
|
|
||||||
h[headerAcceptCharset] = "UTF-8"
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
func addReturnContentHeaders(h map[string]string, ml MetadataLevel) map[string]string {
|
|
||||||
if ml != EmptyPayload {
|
|
||||||
h[headerPrefer] = "return-content"
|
|
||||||
h[headerAccept] = string(ml)
|
|
||||||
} else {
|
|
||||||
h[headerPrefer] = "return-no-content"
|
|
||||||
// From API version 2015-12-11 onwards, Accept header is required
|
|
||||||
h[headerAccept] = string(NoMetadata)
|
|
||||||
}
|
|
||||||
return h
|
|
||||||
}
|
|
|
@ -1,199 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
fixedTime = time.Date(2050, time.December, 20, 21, 55, 0, 0, time.FixedZone("GMT", -6))
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c Client) computeHmac256(message string) string {
|
|
||||||
h := hmac.New(sha256.New, c.accountKey)
|
|
||||||
h.Write([]byte(message))
|
|
||||||
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func currentTimeRfc1123Formatted() string {
|
|
||||||
return timeRfc1123Formatted(time.Now().UTC())
|
|
||||||
}
|
|
||||||
|
|
||||||
func timeRfc1123Formatted(t time.Time) string {
|
|
||||||
return t.Format(http.TimeFormat)
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeParams(v1, v2 url.Values) url.Values {
|
|
||||||
out := url.Values{}
|
|
||||||
for k, v := range v1 {
|
|
||||||
out[k] = v
|
|
||||||
}
|
|
||||||
for k, v := range v2 {
|
|
||||||
vals, ok := out[k]
|
|
||||||
if ok {
|
|
||||||
vals = append(vals, v...)
|
|
||||||
out[k] = vals
|
|
||||||
} else {
|
|
||||||
out[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func prepareBlockListRequest(blocks []Block) string {
|
|
||||||
s := `<?xml version="1.0" encoding="utf-8"?><BlockList>`
|
|
||||||
for _, v := range blocks {
|
|
||||||
s += fmt.Sprintf("<%s>%s</%s>", v.Status, v.ID, v.Status)
|
|
||||||
}
|
|
||||||
s += `</BlockList>`
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func xmlUnmarshal(body io.Reader, v interface{}) error {
|
|
||||||
data, err := ioutil.ReadAll(body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return xml.Unmarshal(data, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func xmlMarshal(v interface{}) (io.Reader, int, error) {
|
|
||||||
b, err := xml.Marshal(v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
return bytes.NewReader(b), len(b), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func headersFromStruct(v interface{}) map[string]string {
|
|
||||||
headers := make(map[string]string)
|
|
||||||
value := reflect.ValueOf(v)
|
|
||||||
for i := 0; i < value.NumField(); i++ {
|
|
||||||
key := value.Type().Field(i).Tag.Get("header")
|
|
||||||
if key != "" {
|
|
||||||
reflectedValue := reflect.Indirect(value.Field(i))
|
|
||||||
var val string
|
|
||||||
if reflectedValue.IsValid() {
|
|
||||||
switch reflectedValue.Type() {
|
|
||||||
case reflect.TypeOf(fixedTime):
|
|
||||||
val = timeRfc1123Formatted(reflectedValue.Interface().(time.Time))
|
|
||||||
case reflect.TypeOf(uint64(0)), reflect.TypeOf(uint(0)):
|
|
||||||
val = strconv.FormatUint(reflectedValue.Uint(), 10)
|
|
||||||
case reflect.TypeOf(int(0)):
|
|
||||||
val = strconv.FormatInt(reflectedValue.Int(), 10)
|
|
||||||
default:
|
|
||||||
val = reflectedValue.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if val != "" {
|
|
||||||
headers[key] = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
|
||||||
// merges extraHeaders into headers and returns headers
|
|
||||||
func mergeHeaders(headers, extraHeaders map[string]string) map[string]string {
|
|
||||||
for k, v := range extraHeaders {
|
|
||||||
headers[k] = v
|
|
||||||
}
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
|
||||||
func addToHeaders(h map[string]string, key, value string) map[string]string {
|
|
||||||
if value != "" {
|
|
||||||
h[key] = value
|
|
||||||
}
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
func addTimeToHeaders(h map[string]string, key string, value *time.Time) map[string]string {
|
|
||||||
if value != nil {
|
|
||||||
h = addToHeaders(h, key, timeRfc1123Formatted(*value))
|
|
||||||
}
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
func addTimeout(params url.Values, timeout uint) url.Values {
|
|
||||||
if timeout > 0 {
|
|
||||||
params.Add("timeout", fmt.Sprintf("%v", timeout))
|
|
||||||
}
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
|
|
||||||
func addSnapshot(params url.Values, snapshot *time.Time) url.Values {
|
|
||||||
if snapshot != nil {
|
|
||||||
params.Add("snapshot", timeRfc1123Formatted(*snapshot))
|
|
||||||
}
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTimeFromHeaders(h http.Header, key string) (*time.Time, error) {
|
|
||||||
var out time.Time
|
|
||||||
var err error
|
|
||||||
outStr := h.Get(key)
|
|
||||||
if outStr != "" {
|
|
||||||
out, err = time.Parse(time.RFC1123, outStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TimeRFC1123 is an alias for time.Time needed for custom Unmarshalling
|
|
||||||
type TimeRFC1123 time.Time
|
|
||||||
|
|
||||||
// UnmarshalXML is a custom unmarshaller that overrides the default time unmarshal which uses a different time layout.
|
|
||||||
func (t *TimeRFC1123) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
||||||
var value string
|
|
||||||
d.DecodeElement(&value, &start)
|
|
||||||
parse, err := time.Parse(time.RFC1123, value)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*t = TimeRFC1123(parse)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns a map of custom metadata values from the specified HTTP header
|
|
||||||
func getMetadataFromHeaders(header http.Header) map[string]string {
|
|
||||||
metadata := make(map[string]string)
|
|
||||||
for k, v := range header {
|
|
||||||
// Can't trust CanonicalHeaderKey() to munge case
|
|
||||||
// reliably. "_" is allowed in identifiers:
|
|
||||||
// https://msdn.microsoft.com/en-us/library/azure/dd179414.aspx
|
|
||||||
// https://msdn.microsoft.com/library/aa664670(VS.71).aspx
|
|
||||||
// http://tools.ietf.org/html/rfc7230#section-3.2
|
|
||||||
// ...but "_" is considered invalid by
|
|
||||||
// CanonicalMIMEHeaderKey in
|
|
||||||
// https://golang.org/src/net/textproto/reader.go?s=14615:14659#L542
|
|
||||||
// so k can be "X-Ms-Meta-Lol" or "x-ms-meta-lol_rofl".
|
|
||||||
k = strings.ToLower(k)
|
|
||||||
if len(v) == 0 || !strings.HasPrefix(k, strings.ToLower(userDefinedMetadataHeaderPrefix)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// metadata["lol"] = content of the last X-Ms-Meta-Lol header
|
|
||||||
k = k[len(userDefinedMetadataHeaderPrefix):]
|
|
||||||
metadata[k] = v[len(v)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(metadata) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return metadata
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
package storage
|
|
||||||
|
|
||||||
var (
|
|
||||||
sdkVersion = "10.0.2"
|
|
||||||
)
|
|
|
@ -1,191 +0,0 @@
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Copyright 2015 Microsoft Corporation
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
|
@ -1,253 +0,0 @@
|
||||||
# Azure Active Directory library for Go
|
|
||||||
|
|
||||||
This project provides a stand alone Azure Active Directory library for Go. The code was extracted
|
|
||||||
from [go-autorest](https://github.com/Azure/go-autorest/) project, which is used as a base for
|
|
||||||
[azure-sdk-for-go](https://github.com/Azure/azure-sdk-for-go).
|
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```
|
|
||||||
go get -u github.com/Azure/go-autorest/autorest/adal
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
An Active Directory application is required in order to use this library. An application can be registered in the [Azure Portal](https://portal.azure.com/) follow these [guidelines](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-integrating-applications) or using the [Azure CLI](https://github.com/Azure/azure-cli).
|
|
||||||
|
|
||||||
### Register an Azure AD Application with secret
|
|
||||||
|
|
||||||
|
|
||||||
1. Register a new application with a `secret` credential
|
|
||||||
|
|
||||||
```
|
|
||||||
az ad app create \
|
|
||||||
--display-name example-app \
|
|
||||||
--homepage https://example-app/home \
|
|
||||||
--identifier-uris https://example-app/app \
|
|
||||||
--password secret
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Create a service principal using the `Application ID` from previous step
|
|
||||||
|
|
||||||
```
|
|
||||||
az ad sp create --id "Application ID"
|
|
||||||
```
|
|
||||||
|
|
||||||
* Replace `Application ID` with `appId` from step 1.
|
|
||||||
|
|
||||||
### Register an Azure AD Application with certificate
|
|
||||||
|
|
||||||
1. Create a private key
|
|
||||||
|
|
||||||
```
|
|
||||||
openssl genrsa -out "example-app.key" 2048
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Create the certificate
|
|
||||||
|
|
||||||
```
|
|
||||||
openssl req -new -key "example-app.key" -subj "/CN=example-app" -out "example-app.csr"
|
|
||||||
openssl x509 -req -in "example-app.csr" -signkey "example-app.key" -out "example-app.crt" -days 10000
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Create the PKCS12 version of the certificate containing also the private key
|
|
||||||
|
|
||||||
```
|
|
||||||
openssl pkcs12 -export -out "example-app.pfx" -inkey "example-app.key" -in "example-app.crt" -passout pass:
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Register a new application with the certificate content form `example-app.crt`
|
|
||||||
|
|
||||||
```
|
|
||||||
certificateContents="$(tail -n+2 "example-app.crt" | head -n-1)"
|
|
||||||
|
|
||||||
az ad app create \
|
|
||||||
--display-name example-app \
|
|
||||||
--homepage https://example-app/home \
|
|
||||||
--identifier-uris https://example-app/app \
|
|
||||||
--key-usage Verify --end-date 2018-01-01 \
|
|
||||||
--key-value "${certificateContents}"
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Create a service principal using the `Application ID` from previous step
|
|
||||||
|
|
||||||
```
|
|
||||||
az ad sp create --id "APPLICATION_ID"
|
|
||||||
```
|
|
||||||
|
|
||||||
* Replace `APPLICATION_ID` with `appId` from step 4.
|
|
||||||
|
|
||||||
|
|
||||||
### Grant the necessary permissions
|
|
||||||
|
|
||||||
Azure relies on a Role-Based Access Control (RBAC) model to manage the access to resources at a fine-grained
|
|
||||||
level. There is a set of [pre-defined roles](https://docs.microsoft.com/en-us/azure/active-directory/role-based-access-built-in-roles)
|
|
||||||
which can be assigned to a service principal of an Azure AD application depending of your needs.
|
|
||||||
|
|
||||||
```
|
|
||||||
az role assignment create --assigner "SERVICE_PRINCIPAL_ID" --role "ROLE_NAME"
|
|
||||||
```
|
|
||||||
|
|
||||||
* Replace the `SERVICE_PRINCIPAL_ID` with the `appId` from previous step.
|
|
||||||
* Replace the `ROLE_NAME` with a role name of your choice.
|
|
||||||
|
|
||||||
It is also possible to define custom role definitions.
|
|
||||||
|
|
||||||
```
|
|
||||||
az role definition create --role-definition role-definition.json
|
|
||||||
```
|
|
||||||
|
|
||||||
* Check [custom roles](https://docs.microsoft.com/en-us/azure/active-directory/role-based-access-control-custom-roles) for more details regarding the content of `role-definition.json` file.
|
|
||||||
|
|
||||||
|
|
||||||
### Acquire Access Token
|
|
||||||
|
|
||||||
The common configuration used by all flows:
|
|
||||||
|
|
||||||
```Go
|
|
||||||
const activeDirectoryEndpoint = "https://login.microsoftonline.com/"
|
|
||||||
tenantID := "TENANT_ID"
|
|
||||||
oauthConfig, err := adal.NewOAuthConfig(activeDirectoryEndpoint, tenantID)
|
|
||||||
|
|
||||||
applicationID := "APPLICATION_ID"
|
|
||||||
|
|
||||||
callback := func(token adal.Token) error {
|
|
||||||
// This is called after the token is acquired
|
|
||||||
}
|
|
||||||
|
|
||||||
// The resource for which the token is acquired
|
|
||||||
resource := "https://management.core.windows.net/"
|
|
||||||
```
|
|
||||||
|
|
||||||
* Replace the `TENANT_ID` with your tenant ID.
|
|
||||||
* Replace the `APPLICATION_ID` with the value from previous section.
|
|
||||||
|
|
||||||
#### Client Credentials
|
|
||||||
|
|
||||||
```Go
|
|
||||||
applicationSecret := "APPLICATION_SECRET"
|
|
||||||
|
|
||||||
spt, err := adal.NewServicePrincipalToken(
|
|
||||||
oauthConfig,
|
|
||||||
appliationID,
|
|
||||||
applicationSecret,
|
|
||||||
resource,
|
|
||||||
callbacks...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Acquire a new access token
|
|
||||||
err = spt.Refresh()
|
|
||||||
if (err == nil) {
|
|
||||||
token := spt.Token
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* Replace the `APPLICATION_SECRET` with the `password` value from previous section.
|
|
||||||
|
|
||||||
#### Client Certificate
|
|
||||||
|
|
||||||
```Go
|
|
||||||
certificatePath := "./example-app.pfx"
|
|
||||||
|
|
||||||
certData, err := ioutil.ReadFile(certificatePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read the certificate file (%s): %v", certificatePath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the certificate and private key from pfx file
|
|
||||||
certificate, rsaPrivateKey, err := decodePkcs12(certData, "")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to decode pkcs12 certificate while creating spt: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
spt, err := adal.NewServicePrincipalTokenFromCertificate(
|
|
||||||
oauthConfig,
|
|
||||||
applicationID,
|
|
||||||
certificate,
|
|
||||||
rsaPrivateKey,
|
|
||||||
resource,
|
|
||||||
callbacks...)
|
|
||||||
|
|
||||||
// Acquire a new access token
|
|
||||||
err = spt.Refresh()
|
|
||||||
if (err == nil) {
|
|
||||||
token := spt.Token
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* Update the certificate path to point to the example-app.pfx file which was created in previous section.
|
|
||||||
|
|
||||||
|
|
||||||
#### Device Code
|
|
||||||
|
|
||||||
```Go
|
|
||||||
oauthClient := &http.Client{}
|
|
||||||
|
|
||||||
// Acquire the device code
|
|
||||||
deviceCode, err := adal.InitiateDeviceAuth(
|
|
||||||
oauthClient,
|
|
||||||
oauthConfig,
|
|
||||||
applicationID,
|
|
||||||
resource)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to start device auth flow: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display the authentication message
|
|
||||||
fmt.Println(*deviceCode.Message)
|
|
||||||
|
|
||||||
// Wait here until the user is authenticated
|
|
||||||
token, err := adal.WaitForUserCompletion(oauthClient, deviceCode)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to finish device auth flow: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
spt, err := adal.NewServicePrincipalTokenFromManualToken(
|
|
||||||
oauthConfig,
|
|
||||||
applicationID,
|
|
||||||
resource,
|
|
||||||
*token,
|
|
||||||
callbacks...)
|
|
||||||
|
|
||||||
if (err == nil) {
|
|
||||||
token := spt.Token
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Command Line Tool
|
|
||||||
|
|
||||||
A command line tool is available in `cmd/adal.go` that can acquire a token for a given resource. It supports all flows mentioned above.
|
|
||||||
|
|
||||||
```
|
|
||||||
adal -h
|
|
||||||
|
|
||||||
Usage of ./adal:
|
|
||||||
-applicationId string
|
|
||||||
application id
|
|
||||||
-certificatePath string
|
|
||||||
path to pk12/PFC application certificate
|
|
||||||
-mode string
|
|
||||||
authentication mode (device, secret, cert, refresh) (default "device")
|
|
||||||
-resource string
|
|
||||||
resource for which the token is requested
|
|
||||||
-secret string
|
|
||||||
application secret
|
|
||||||
-tenantId string
|
|
||||||
tenant id
|
|
||||||
-tokenCachePath string
|
|
||||||
location of oath token cache (default "/home/cgc/.adal/accessToken.json")
|
|
||||||
```
|
|
||||||
|
|
||||||
Example acquire a token for `https://management.core.windows.net/` using device code flow:
|
|
||||||
|
|
||||||
```
|
|
||||||
adal -mode device \
|
|
||||||
-applicationId "APPLICATION_ID" \
|
|
||||||
-tenantId "TENANT_ID" \
|
|
||||||
-resource https://management.core.windows.net/
|
|
||||||
|
|
||||||
```
|
|
|
@ -1,51 +0,0 @@
|
||||||
package adal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
activeDirectoryAPIVersion = "1.0"
|
|
||||||
)
|
|
||||||
|
|
||||||
// OAuthConfig represents the endpoints needed
|
|
||||||
// in OAuth operations
|
|
||||||
type OAuthConfig struct {
|
|
||||||
AuthorityEndpoint url.URL
|
|
||||||
AuthorizeEndpoint url.URL
|
|
||||||
TokenEndpoint url.URL
|
|
||||||
DeviceCodeEndpoint url.URL
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewOAuthConfig returns an OAuthConfig with tenant specific urls
|
|
||||||
func NewOAuthConfig(activeDirectoryEndpoint, tenantID string) (*OAuthConfig, error) {
|
|
||||||
const activeDirectoryEndpointTemplate = "%s/oauth2/%s?api-version=%s"
|
|
||||||
u, err := url.Parse(activeDirectoryEndpoint)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
authorityURL, err := u.Parse(tenantID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
authorizeURL, err := u.Parse(fmt.Sprintf(activeDirectoryEndpointTemplate, tenantID, "authorize", activeDirectoryAPIVersion))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tokenURL, err := u.Parse(fmt.Sprintf(activeDirectoryEndpointTemplate, tenantID, "token", activeDirectoryAPIVersion))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
deviceCodeURL, err := u.Parse(fmt.Sprintf(activeDirectoryEndpointTemplate, tenantID, "devicecode", activeDirectoryAPIVersion))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &OAuthConfig{
|
|
||||||
AuthorityEndpoint: *authorityURL,
|
|
||||||
AuthorizeEndpoint: *authorizeURL,
|
|
||||||
TokenEndpoint: *tokenURL,
|
|
||||||
DeviceCodeEndpoint: *deviceCodeURL,
|
|
||||||
}, nil
|
|
||||||
}
|
|
|
@ -1,228 +0,0 @@
|
||||||
package adal
|
|
||||||
|
|
||||||
/*
|
|
||||||
This file is largely based on rjw57/oauth2device's code, with the follow differences:
|
|
||||||
* scope -> resource, and only allow a single one
|
|
||||||
* receive "Message" in the DeviceCode struct and show it to users as the prompt
|
|
||||||
* azure-xplat-cli has the following behavior that this emulates:
|
|
||||||
- does not send client_secret during the token exchange
|
|
||||||
- sends resource again in the token exchange request
|
|
||||||
*/
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
logPrefix = "autorest/adal/devicetoken:"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrDeviceGeneric represents an unknown error from the token endpoint when using device flow
|
|
||||||
ErrDeviceGeneric = fmt.Errorf("%s Error while retrieving OAuth token: Unknown Error", logPrefix)
|
|
||||||
|
|
||||||
// ErrDeviceAccessDenied represents an access denied error from the token endpoint when using device flow
|
|
||||||
ErrDeviceAccessDenied = fmt.Errorf("%s Error while retrieving OAuth token: Access Denied", logPrefix)
|
|
||||||
|
|
||||||
// ErrDeviceAuthorizationPending represents the server waiting on the user to complete the device flow
|
|
||||||
ErrDeviceAuthorizationPending = fmt.Errorf("%s Error while retrieving OAuth token: Authorization Pending", logPrefix)
|
|
||||||
|
|
||||||
// ErrDeviceCodeExpired represents the server timing out and expiring the code during device flow
|
|
||||||
ErrDeviceCodeExpired = fmt.Errorf("%s Error while retrieving OAuth token: Code Expired", logPrefix)
|
|
||||||
|
|
||||||
// ErrDeviceSlowDown represents the service telling us we're polling too often during device flow
|
|
||||||
ErrDeviceSlowDown = fmt.Errorf("%s Error while retrieving OAuth token: Slow Down", logPrefix)
|
|
||||||
|
|
||||||
// ErrDeviceCodeEmpty represents an empty device code from the device endpoint while using device flow
|
|
||||||
ErrDeviceCodeEmpty = fmt.Errorf("%s Error while retrieving device code: Device Code Empty", logPrefix)
|
|
||||||
|
|
||||||
// ErrOAuthTokenEmpty represents an empty OAuth token from the token endpoint when using device flow
|
|
||||||
ErrOAuthTokenEmpty = fmt.Errorf("%s Error while retrieving OAuth token: Token Empty", logPrefix)
|
|
||||||
|
|
||||||
errCodeSendingFails = "Error occurred while sending request for Device Authorization Code"
|
|
||||||
errCodeHandlingFails = "Error occurred while handling response from the Device Endpoint"
|
|
||||||
errTokenSendingFails = "Error occurred while sending request with device code for a token"
|
|
||||||
errTokenHandlingFails = "Error occurred while handling response from the Token Endpoint (during device flow)"
|
|
||||||
errStatusNotOK = "Error HTTP status != 200"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DeviceCode is the object returned by the device auth endpoint
|
|
||||||
// It contains information to instruct the user to complete the auth flow
|
|
||||||
type DeviceCode struct {
|
|
||||||
DeviceCode *string `json:"device_code,omitempty"`
|
|
||||||
UserCode *string `json:"user_code,omitempty"`
|
|
||||||
VerificationURL *string `json:"verification_url,omitempty"`
|
|
||||||
ExpiresIn *int64 `json:"expires_in,string,omitempty"`
|
|
||||||
Interval *int64 `json:"interval,string,omitempty"`
|
|
||||||
|
|
||||||
Message *string `json:"message"` // Azure specific
|
|
||||||
Resource string // store the following, stored when initiating, used when exchanging
|
|
||||||
OAuthConfig OAuthConfig
|
|
||||||
ClientID string
|
|
||||||
}
|
|
||||||
|
|
||||||
// TokenError is the object returned by the token exchange endpoint
|
|
||||||
// when something is amiss
|
|
||||||
type TokenError struct {
|
|
||||||
Error *string `json:"error,omitempty"`
|
|
||||||
ErrorCodes []int `json:"error_codes,omitempty"`
|
|
||||||
ErrorDescription *string `json:"error_description,omitempty"`
|
|
||||||
Timestamp *string `json:"timestamp,omitempty"`
|
|
||||||
TraceID *string `json:"trace_id,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeviceToken is the object return by the token exchange endpoint
|
|
||||||
// It can either look like a Token or an ErrorToken, so put both here
|
|
||||||
// and check for presence of "Error" to know if we are in error state
|
|
||||||
type deviceToken struct {
|
|
||||||
Token
|
|
||||||
TokenError
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitiateDeviceAuth initiates a device auth flow. It returns a DeviceCode
|
|
||||||
// that can be used with CheckForUserCompletion or WaitForUserCompletion.
|
|
||||||
func InitiateDeviceAuth(sender Sender, oauthConfig OAuthConfig, clientID, resource string) (*DeviceCode, error) {
|
|
||||||
v := url.Values{
|
|
||||||
"client_id": []string{clientID},
|
|
||||||
"resource": []string{resource},
|
|
||||||
}
|
|
||||||
|
|
||||||
s := v.Encode()
|
|
||||||
body := ioutil.NopCloser(strings.NewReader(s))
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPost, oauthConfig.DeviceCodeEndpoint.String(), body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s %s: %s", logPrefix, errCodeSendingFails, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
req.ContentLength = int64(len(s))
|
|
||||||
req.Header.Set(contentType, mimeTypeFormPost)
|
|
||||||
resp, err := sender.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s %s: %s", logPrefix, errCodeSendingFails, err.Error())
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
rb, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s %s: %s", logPrefix, errCodeHandlingFails, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("%s %s: %s", logPrefix, errCodeHandlingFails, errStatusNotOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(strings.Trim(string(rb), " ")) == 0 {
|
|
||||||
return nil, ErrDeviceCodeEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
var code DeviceCode
|
|
||||||
err = json.Unmarshal(rb, &code)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s %s: %s", logPrefix, errCodeHandlingFails, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
code.ClientID = clientID
|
|
||||||
code.Resource = resource
|
|
||||||
code.OAuthConfig = oauthConfig
|
|
||||||
|
|
||||||
return &code, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckForUserCompletion takes a DeviceCode and checks with the Azure AD OAuth endpoint
|
|
||||||
// to see if the device flow has: been completed, timed out, or otherwise failed
|
|
||||||
func CheckForUserCompletion(sender Sender, code *DeviceCode) (*Token, error) {
|
|
||||||
v := url.Values{
|
|
||||||
"client_id": []string{code.ClientID},
|
|
||||||
"code": []string{*code.DeviceCode},
|
|
||||||
"grant_type": []string{OAuthGrantTypeDeviceCode},
|
|
||||||
"resource": []string{code.Resource},
|
|
||||||
}
|
|
||||||
|
|
||||||
s := v.Encode()
|
|
||||||
body := ioutil.NopCloser(strings.NewReader(s))
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPost, code.OAuthConfig.TokenEndpoint.String(), body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s %s: %s", logPrefix, errTokenSendingFails, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
req.ContentLength = int64(len(s))
|
|
||||||
req.Header.Set(contentType, mimeTypeFormPost)
|
|
||||||
resp, err := sender.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s %s: %s", logPrefix, errTokenSendingFails, err.Error())
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
rb, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s %s: %s", logPrefix, errTokenHandlingFails, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK && len(strings.Trim(string(rb), " ")) == 0 {
|
|
||||||
return nil, fmt.Errorf("%s %s: %s", logPrefix, errTokenHandlingFails, errStatusNotOK)
|
|
||||||
}
|
|
||||||
if len(strings.Trim(string(rb), " ")) == 0 {
|
|
||||||
return nil, ErrOAuthTokenEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
var token deviceToken
|
|
||||||
err = json.Unmarshal(rb, &token)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s %s: %s", logPrefix, errTokenHandlingFails, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if token.Error == nil {
|
|
||||||
return &token.Token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch *token.Error {
|
|
||||||
case "authorization_pending":
|
|
||||||
return nil, ErrDeviceAuthorizationPending
|
|
||||||
case "slow_down":
|
|
||||||
return nil, ErrDeviceSlowDown
|
|
||||||
case "access_denied":
|
|
||||||
return nil, ErrDeviceAccessDenied
|
|
||||||
case "code_expired":
|
|
||||||
return nil, ErrDeviceCodeExpired
|
|
||||||
default:
|
|
||||||
return nil, ErrDeviceGeneric
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WaitForUserCompletion calls CheckForUserCompletion repeatedly until a token is granted or an error state occurs.
|
|
||||||
// This prevents the user from looping and checking against 'ErrDeviceAuthorizationPending'.
|
|
||||||
func WaitForUserCompletion(sender Sender, code *DeviceCode) (*Token, error) {
|
|
||||||
intervalDuration := time.Duration(*code.Interval) * time.Second
|
|
||||||
waitDuration := intervalDuration
|
|
||||||
|
|
||||||
for {
|
|
||||||
token, err := CheckForUserCompletion(sender, code)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
return token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch err {
|
|
||||||
case ErrDeviceSlowDown:
|
|
||||||
waitDuration += waitDuration
|
|
||||||
case ErrDeviceAuthorizationPending:
|
|
||||||
// noop
|
|
||||||
default: // everything else is "fatal" to us
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if waitDuration > (intervalDuration * 3) {
|
|
||||||
return nil, fmt.Errorf("%s Error waiting for user to complete device flow. Server told us to slow_down too much", logPrefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(waitDuration)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
package adal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LoadToken restores a Token object from a file located at 'path'.
|
|
||||||
func LoadToken(path string) (*Token, error) {
|
|
||||||
file, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to open file (%s) while loading token: %v", path, err)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
var token Token
|
|
||||||
|
|
||||||
dec := json.NewDecoder(file)
|
|
||||||
if err = dec.Decode(&token); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to decode contents of file (%s) into Token representation: %v", path, err)
|
|
||||||
}
|
|
||||||
return &token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveToken persists an oauth token at the given location on disk.
|
|
||||||
// It moves the new file into place so it can safely be used to replace an existing file
|
|
||||||
// that maybe accessed by multiple processes.
|
|
||||||
func SaveToken(path string, mode os.FileMode, token Token) error {
|
|
||||||
dir := filepath.Dir(path)
|
|
||||||
err := os.MkdirAll(dir, os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create directory (%s) to store token in: %v", dir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
newFile, err := ioutil.TempFile(dir, "token")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create the temp file to write the token: %v", err)
|
|
||||||
}
|
|
||||||
tempPath := newFile.Name()
|
|
||||||
|
|
||||||
if err := json.NewEncoder(newFile).Encode(token); err != nil {
|
|
||||||
return fmt.Errorf("failed to encode token to file (%s) while saving token: %v", tempPath, err)
|
|
||||||
}
|
|
||||||
if err := newFile.Close(); err != nil {
|
|
||||||
return fmt.Errorf("failed to close temp file %s: %v", tempPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Atomic replace to avoid multi-writer file corruptions
|
|
||||||
if err := os.Rename(tempPath, path); err != nil {
|
|
||||||
return fmt.Errorf("failed to move temporary token to desired output location. src=%s dst=%s: %v", tempPath, path, err)
|
|
||||||
}
|
|
||||||
if err := os.Chmod(path, mode); err != nil {
|
|
||||||
return fmt.Errorf("failed to chmod the token file %s: %v", path, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
package adal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
contentType = "Content-Type"
|
|
||||||
mimeTypeFormPost = "application/x-www-form-urlencoded"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Sender is the interface that wraps the Do method to send HTTP requests.
|
|
||||||
//
|
|
||||||
// The standard http.Client conforms to this interface.
|
|
||||||
type Sender interface {
|
|
||||||
Do(*http.Request) (*http.Response, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SenderFunc is a method that implements the Sender interface.
|
|
||||||
type SenderFunc func(*http.Request) (*http.Response, error)
|
|
||||||
|
|
||||||
// Do implements the Sender interface on SenderFunc.
|
|
||||||
func (sf SenderFunc) Do(r *http.Request) (*http.Response, error) {
|
|
||||||
return sf(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendDecorator takes and possibily decorates, by wrapping, a Sender. Decorators may affect the
|
|
||||||
// http.Request and pass it along or, first, pass the http.Request along then react to the
|
|
||||||
// http.Response result.
|
|
||||||
type SendDecorator func(Sender) Sender
|
|
||||||
|
|
||||||
// CreateSender creates, decorates, and returns, as a Sender, the default http.Client.
|
|
||||||
func CreateSender(decorators ...SendDecorator) Sender {
|
|
||||||
return DecorateSender(&http.Client{}, decorators...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecorateSender accepts a Sender and a, possibly empty, set of SendDecorators, which is applies to
|
|
||||||
// the Sender. Decorators are applied in the order received, but their affect upon the request
|
|
||||||
// depends on whether they are a pre-decorator (change the http.Request and then pass it along) or a
|
|
||||||
// post-decorator (pass the http.Request along and react to the results in http.Response).
|
|
||||||
func DecorateSender(s Sender, decorators ...SendDecorator) Sender {
|
|
||||||
for _, decorate := range decorators {
|
|
||||||
s = decorate(s)
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
|
@ -1,408 +0,0 @@
|
||||||
package adal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/sha1"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/dgrijalva/jwt-go"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultRefresh = 5 * time.Minute
|
|
||||||
tokenBaseDate = "1970-01-01T00:00:00Z"
|
|
||||||
|
|
||||||
// OAuthGrantTypeDeviceCode is the "grant_type" identifier used in device flow
|
|
||||||
OAuthGrantTypeDeviceCode = "device_code"
|
|
||||||
|
|
||||||
// OAuthGrantTypeClientCredentials is the "grant_type" identifier used in credential flows
|
|
||||||
OAuthGrantTypeClientCredentials = "client_credentials"
|
|
||||||
|
|
||||||
// OAuthGrantTypeRefreshToken is the "grant_type" identifier used in refresh token flows
|
|
||||||
OAuthGrantTypeRefreshToken = "refresh_token"
|
|
||||||
|
|
||||||
// managedIdentitySettingsPath is the path to the MSI Extension settings file (to discover the endpoint)
|
|
||||||
managedIdentitySettingsPath = "/var/lib/waagent/ManagedIdentity-Settings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var expirationBase time.Time
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
expirationBase, _ = time.Parse(time.RFC3339, tokenBaseDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OAuthTokenProvider is an interface which should be implemented by an access token retriever
|
|
||||||
type OAuthTokenProvider interface {
|
|
||||||
OAuthToken() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresher is an interface for token refresh functionality
|
|
||||||
type Refresher interface {
|
|
||||||
Refresh() error
|
|
||||||
RefreshExchange(resource string) error
|
|
||||||
EnsureFresh() error
|
|
||||||
}
|
|
||||||
|
|
||||||
// TokenRefreshCallback is the type representing callbacks that will be called after
|
|
||||||
// a successful token refresh
|
|
||||||
type TokenRefreshCallback func(Token) error
|
|
||||||
|
|
||||||
// Token encapsulates the access token used to authorize Azure requests.
|
|
||||||
type Token struct {
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
|
||||||
|
|
||||||
ExpiresIn string `json:"expires_in"`
|
|
||||||
ExpiresOn string `json:"expires_on"`
|
|
||||||
NotBefore string `json:"not_before"`
|
|
||||||
|
|
||||||
Resource string `json:"resource"`
|
|
||||||
Type string `json:"token_type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expires returns the time.Time when the Token expires.
|
|
||||||
func (t Token) Expires() time.Time {
|
|
||||||
s, err := strconv.Atoi(t.ExpiresOn)
|
|
||||||
if err != nil {
|
|
||||||
s = -3600
|
|
||||||
}
|
|
||||||
return expirationBase.Add(time.Duration(s) * time.Second).UTC()
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsExpired returns true if the Token is expired, false otherwise.
|
|
||||||
func (t Token) IsExpired() bool {
|
|
||||||
return t.WillExpireIn(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WillExpireIn returns true if the Token will expire after the passed time.Duration interval
|
|
||||||
// from now, false otherwise.
|
|
||||||
func (t Token) WillExpireIn(d time.Duration) bool {
|
|
||||||
return !t.Expires().After(time.Now().Add(d))
|
|
||||||
}
|
|
||||||
|
|
||||||
//OAuthToken return the current access token
|
|
||||||
func (t *Token) OAuthToken() string {
|
|
||||||
return t.AccessToken
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServicePrincipalNoSecret represents a secret type that contains no secret
|
|
||||||
// meaning it is not valid for fetching a fresh token. This is used by Manual
|
|
||||||
type ServicePrincipalNoSecret struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAuthenticationValues is a method of the interface ServicePrincipalSecret
|
|
||||||
// It only returns an error for the ServicePrincipalNoSecret type
|
|
||||||
func (noSecret *ServicePrincipalNoSecret) SetAuthenticationValues(spt *ServicePrincipalToken, v *url.Values) error {
|
|
||||||
return fmt.Errorf("Manually created ServicePrincipalToken does not contain secret material to retrieve a new access token")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServicePrincipalSecret is an interface that allows various secret mechanism to fill the form
|
|
||||||
// that is submitted when acquiring an oAuth token.
|
|
||||||
type ServicePrincipalSecret interface {
|
|
||||||
SetAuthenticationValues(spt *ServicePrincipalToken, values *url.Values) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServicePrincipalTokenSecret implements ServicePrincipalSecret for client_secret type authorization.
|
|
||||||
type ServicePrincipalTokenSecret struct {
|
|
||||||
ClientSecret string
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAuthenticationValues is a method of the interface ServicePrincipalSecret.
|
|
||||||
// It will populate the form submitted during oAuth Token Acquisition using the client_secret.
|
|
||||||
func (tokenSecret *ServicePrincipalTokenSecret) SetAuthenticationValues(spt *ServicePrincipalToken, v *url.Values) error {
|
|
||||||
v.Set("client_secret", tokenSecret.ClientSecret)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServicePrincipalCertificateSecret implements ServicePrincipalSecret for generic RSA cert auth with signed JWTs.
|
|
||||||
type ServicePrincipalCertificateSecret struct {
|
|
||||||
Certificate *x509.Certificate
|
|
||||||
PrivateKey *rsa.PrivateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServicePrincipalMSISecret implements ServicePrincipalSecret for machines running the MSI Extension.
|
|
||||||
type ServicePrincipalMSISecret struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAuthenticationValues is a method of the interface ServicePrincipalSecret.
|
|
||||||
// MSI extension requires the authority field to be set to the real tenant authority endpoint
|
|
||||||
func (msiSecret *ServicePrincipalMSISecret) SetAuthenticationValues(spt *ServicePrincipalToken, v *url.Values) error {
|
|
||||||
v.Set("authority", spt.oauthConfig.AuthorityEndpoint.String())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignJwt returns the JWT signed with the certificate's private key.
|
|
||||||
func (secret *ServicePrincipalCertificateSecret) SignJwt(spt *ServicePrincipalToken) (string, error) {
|
|
||||||
hasher := sha1.New()
|
|
||||||
_, err := hasher.Write(secret.Certificate.Raw)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
thumbprint := base64.URLEncoding.EncodeToString(hasher.Sum(nil))
|
|
||||||
|
|
||||||
// The jti (JWT ID) claim provides a unique identifier for the JWT.
|
|
||||||
jti := make([]byte, 20)
|
|
||||||
_, err = rand.Read(jti)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
token := jwt.New(jwt.SigningMethodRS256)
|
|
||||||
token.Header["x5t"] = thumbprint
|
|
||||||
token.Claims = jwt.MapClaims{
|
|
||||||
"aud": spt.oauthConfig.TokenEndpoint.String(),
|
|
||||||
"iss": spt.clientID,
|
|
||||||
"sub": spt.clientID,
|
|
||||||
"jti": base64.URLEncoding.EncodeToString(jti),
|
|
||||||
"nbf": time.Now().Unix(),
|
|
||||||
"exp": time.Now().Add(time.Hour * 24).Unix(),
|
|
||||||
}
|
|
||||||
|
|
||||||
signedString, err := token.SignedString(secret.PrivateKey)
|
|
||||||
return signedString, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAuthenticationValues is a method of the interface ServicePrincipalSecret.
|
|
||||||
// It will populate the form submitted during oAuth Token Acquisition using a JWT signed with a certificate.
|
|
||||||
func (secret *ServicePrincipalCertificateSecret) SetAuthenticationValues(spt *ServicePrincipalToken, v *url.Values) error {
|
|
||||||
jwt, err := secret.SignJwt(spt)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
v.Set("client_assertion", jwt)
|
|
||||||
v.Set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServicePrincipalToken encapsulates a Token created for a Service Principal.
|
|
||||||
type ServicePrincipalToken struct {
|
|
||||||
Token
|
|
||||||
|
|
||||||
secret ServicePrincipalSecret
|
|
||||||
oauthConfig OAuthConfig
|
|
||||||
clientID string
|
|
||||||
resource string
|
|
||||||
autoRefresh bool
|
|
||||||
refreshWithin time.Duration
|
|
||||||
sender Sender
|
|
||||||
|
|
||||||
refreshCallbacks []TokenRefreshCallback
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewServicePrincipalTokenWithSecret create a ServicePrincipalToken using the supplied ServicePrincipalSecret implementation.
|
|
||||||
func NewServicePrincipalTokenWithSecret(oauthConfig OAuthConfig, id string, resource string, secret ServicePrincipalSecret, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
|
|
||||||
spt := &ServicePrincipalToken{
|
|
||||||
oauthConfig: oauthConfig,
|
|
||||||
secret: secret,
|
|
||||||
clientID: id,
|
|
||||||
resource: resource,
|
|
||||||
autoRefresh: true,
|
|
||||||
refreshWithin: defaultRefresh,
|
|
||||||
sender: &http.Client{},
|
|
||||||
refreshCallbacks: callbacks,
|
|
||||||
}
|
|
||||||
return spt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewServicePrincipalTokenFromManualToken creates a ServicePrincipalToken using the supplied token
|
|
||||||
func NewServicePrincipalTokenFromManualToken(oauthConfig OAuthConfig, clientID string, resource string, token Token, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
|
|
||||||
spt, err := NewServicePrincipalTokenWithSecret(
|
|
||||||
oauthConfig,
|
|
||||||
clientID,
|
|
||||||
resource,
|
|
||||||
&ServicePrincipalNoSecret{},
|
|
||||||
callbacks...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
spt.Token = token
|
|
||||||
|
|
||||||
return spt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewServicePrincipalToken creates a ServicePrincipalToken from the supplied Service Principal
|
|
||||||
// credentials scoped to the named resource.
|
|
||||||
func NewServicePrincipalToken(oauthConfig OAuthConfig, clientID string, secret string, resource string, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
|
|
||||||
return NewServicePrincipalTokenWithSecret(
|
|
||||||
oauthConfig,
|
|
||||||
clientID,
|
|
||||||
resource,
|
|
||||||
&ServicePrincipalTokenSecret{
|
|
||||||
ClientSecret: secret,
|
|
||||||
},
|
|
||||||
callbacks...,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewServicePrincipalTokenFromCertificate create a ServicePrincipalToken from the supplied pkcs12 bytes.
|
|
||||||
func NewServicePrincipalTokenFromCertificate(oauthConfig OAuthConfig, clientID string, certificate *x509.Certificate, privateKey *rsa.PrivateKey, resource string, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
|
|
||||||
return NewServicePrincipalTokenWithSecret(
|
|
||||||
oauthConfig,
|
|
||||||
clientID,
|
|
||||||
resource,
|
|
||||||
&ServicePrincipalCertificateSecret{
|
|
||||||
PrivateKey: privateKey,
|
|
||||||
Certificate: certificate,
|
|
||||||
},
|
|
||||||
callbacks...,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewServicePrincipalTokenFromMSI creates a ServicePrincipalToken via the MSI VM Extension.
|
|
||||||
func NewServicePrincipalTokenFromMSI(oauthConfig OAuthConfig, resource string, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
|
|
||||||
return newServicePrincipalTokenFromMSI(oauthConfig, resource, managedIdentitySettingsPath, callbacks...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newServicePrincipalTokenFromMSI(oauthConfig OAuthConfig, resource, settingsPath string, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
|
|
||||||
// Read MSI settings
|
|
||||||
bytes, err := ioutil.ReadFile(settingsPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
msiSettings := struct {
|
|
||||||
URL string `json:"url"`
|
|
||||||
}{}
|
|
||||||
err = json.Unmarshal(bytes, &msiSettings)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// We set the oauth config token endpoint to be MSI's endpoint
|
|
||||||
// We leave the authority as-is so MSI can POST it with the token request
|
|
||||||
msiEndpointURL, err := url.Parse(msiSettings.URL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
msiTokenEndpointURL, err := msiEndpointURL.Parse("/oauth2/token")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
oauthConfig.TokenEndpoint = *msiTokenEndpointURL
|
|
||||||
|
|
||||||
spt := &ServicePrincipalToken{
|
|
||||||
oauthConfig: oauthConfig,
|
|
||||||
secret: &ServicePrincipalMSISecret{},
|
|
||||||
resource: resource,
|
|
||||||
autoRefresh: true,
|
|
||||||
refreshWithin: defaultRefresh,
|
|
||||||
sender: &http.Client{},
|
|
||||||
refreshCallbacks: callbacks,
|
|
||||||
}
|
|
||||||
|
|
||||||
return spt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnsureFresh will refresh the token if it will expire within the refresh window (as set by
|
|
||||||
// RefreshWithin) and autoRefresh flag is on.
|
|
||||||
func (spt *ServicePrincipalToken) EnsureFresh() error {
|
|
||||||
if spt.autoRefresh && spt.WillExpireIn(spt.refreshWithin) {
|
|
||||||
return spt.Refresh()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// InvokeRefreshCallbacks calls any TokenRefreshCallbacks that were added to the SPT during initialization
|
|
||||||
func (spt *ServicePrincipalToken) InvokeRefreshCallbacks(token Token) error {
|
|
||||||
if spt.refreshCallbacks != nil {
|
|
||||||
for _, callback := range spt.refreshCallbacks {
|
|
||||||
err := callback(spt.Token)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("adal: TokenRefreshCallback handler failed. Error = '%v'", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh obtains a fresh token for the Service Principal.
|
|
||||||
func (spt *ServicePrincipalToken) Refresh() error {
|
|
||||||
return spt.refreshInternal(spt.resource)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RefreshExchange refreshes the token, but for a different resource.
|
|
||||||
func (spt *ServicePrincipalToken) RefreshExchange(resource string) error {
|
|
||||||
return spt.refreshInternal(resource)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (spt *ServicePrincipalToken) refreshInternal(resource string) error {
|
|
||||||
v := url.Values{}
|
|
||||||
v.Set("client_id", spt.clientID)
|
|
||||||
v.Set("resource", resource)
|
|
||||||
|
|
||||||
if spt.RefreshToken != "" {
|
|
||||||
v.Set("grant_type", OAuthGrantTypeRefreshToken)
|
|
||||||
v.Set("refresh_token", spt.RefreshToken)
|
|
||||||
} else {
|
|
||||||
v.Set("grant_type", OAuthGrantTypeClientCredentials)
|
|
||||||
err := spt.secret.SetAuthenticationValues(spt, &v)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s := v.Encode()
|
|
||||||
body := ioutil.NopCloser(strings.NewReader(s))
|
|
||||||
req, err := http.NewRequest(http.MethodPost, spt.oauthConfig.TokenEndpoint.String(), body)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("adal: Failed to build the refresh request. Error = '%v'", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.ContentLength = int64(len(s))
|
|
||||||
req.Header.Set(contentType, mimeTypeFormPost)
|
|
||||||
resp, err := spt.sender.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("adal: Failed to execute the refresh request. Error = '%v'", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("adal: Refresh request failed. Status Code = '%d'", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
rb, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("adal: Failed to read a new service principal token during refresh. Error = '%v'", err)
|
|
||||||
}
|
|
||||||
if len(strings.Trim(string(rb), " ")) == 0 {
|
|
||||||
return fmt.Errorf("adal: Empty service principal token received during refresh")
|
|
||||||
}
|
|
||||||
var token Token
|
|
||||||
err = json.Unmarshal(rb, &token)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("adal: Failed to unmarshal the service principal token during refresh. Error = '%v' JSON = '%s'", err, string(rb))
|
|
||||||
}
|
|
||||||
|
|
||||||
spt.Token = token
|
|
||||||
|
|
||||||
return spt.InvokeRefreshCallbacks(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAutoRefresh enables or disables automatic refreshing of stale tokens.
|
|
||||||
func (spt *ServicePrincipalToken) SetAutoRefresh(autoRefresh bool) {
|
|
||||||
spt.autoRefresh = autoRefresh
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetRefreshWithin sets the interval within which if the token will expire, EnsureFresh will
|
|
||||||
// refresh the token.
|
|
||||||
func (spt *ServicePrincipalToken) SetRefreshWithin(d time.Duration) {
|
|
||||||
spt.refreshWithin = d
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSender sets the http.Client used when obtaining the Service Principal token. An
|
|
||||||
// undecorated http.Client is used by default.
|
|
||||||
func (spt *ServicePrincipalToken) SetSender(s Sender) { spt.sender = s }
|
|
|
@ -1,57 +0,0 @@
|
||||||
package autorest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/Azure/go-autorest/autorest/adal"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Authorizer is the interface that provides a PrepareDecorator used to supply request
|
|
||||||
// authorization. Most often, the Authorizer decorator runs last so it has access to the full
|
|
||||||
// state of the formed HTTP request.
|
|
||||||
type Authorizer interface {
|
|
||||||
WithAuthorization() PrepareDecorator
|
|
||||||
}
|
|
||||||
|
|
||||||
// NullAuthorizer implements a default, "do nothing" Authorizer.
|
|
||||||
type NullAuthorizer struct{}
|
|
||||||
|
|
||||||
// WithAuthorization returns a PrepareDecorator that does nothing.
|
|
||||||
func (na NullAuthorizer) WithAuthorization() PrepareDecorator {
|
|
||||||
return WithNothing()
|
|
||||||
}
|
|
||||||
|
|
||||||
// BearerAuthorizer implements the bearer authorization
|
|
||||||
type BearerAuthorizer struct {
|
|
||||||
tokenProvider adal.OAuthTokenProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBearerAuthorizer crates a BearerAuthorizer using the given token provider
|
|
||||||
func NewBearerAuthorizer(tp adal.OAuthTokenProvider) *BearerAuthorizer {
|
|
||||||
return &BearerAuthorizer{tokenProvider: tp}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ba *BearerAuthorizer) withBearerAuthorization() PrepareDecorator {
|
|
||||||
return WithHeader(headerAuthorization, fmt.Sprintf("Bearer %s", ba.tokenProvider.OAuthToken()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithAuthorization returns a PrepareDecorator that adds an HTTP Authorization header whose
|
|
||||||
// value is "Bearer " followed by the token.
|
|
||||||
//
|
|
||||||
// By default, the token will be automatically refreshed through the Refresher interface.
|
|
||||||
func (ba *BearerAuthorizer) WithAuthorization() PrepareDecorator {
|
|
||||||
return func(p Preparer) Preparer {
|
|
||||||
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
|
||||||
refresher, ok := ba.tokenProvider.(adal.Refresher)
|
|
||||||
if ok {
|
|
||||||
err := refresher.EnsureFresh()
|
|
||||||
if err != nil {
|
|
||||||
return r, NewErrorWithError(err, "azure.BearerAuthorizer", "WithAuthorization", nil,
|
|
||||||
"Failed to refresh the Token for request to %s", r.URL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (ba.withBearerAuthorization()(p)).Prepare(r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,115 +0,0 @@
|
||||||
/*
|
|
||||||
Package autorest implements an HTTP request pipeline suitable for use across multiple go-routines
|
|
||||||
and provides the shared routines relied on by AutoRest (see https://github.com/Azure/autorest/)
|
|
||||||
generated Go code.
|
|
||||||
|
|
||||||
The package breaks sending and responding to HTTP requests into three phases: Preparing, Sending,
|
|
||||||
and Responding. A typical pattern is:
|
|
||||||
|
|
||||||
req, err := Prepare(&http.Request{},
|
|
||||||
token.WithAuthorization())
|
|
||||||
|
|
||||||
resp, err := Send(req,
|
|
||||||
WithLogging(logger),
|
|
||||||
DoErrorIfStatusCode(http.StatusInternalServerError),
|
|
||||||
DoCloseIfError(),
|
|
||||||
DoRetryForAttempts(5, time.Second))
|
|
||||||
|
|
||||||
err = Respond(resp,
|
|
||||||
ByDiscardingBody(),
|
|
||||||
ByClosing())
|
|
||||||
|
|
||||||
Each phase relies on decorators to modify and / or manage processing. Decorators may first modify
|
|
||||||
and then pass the data along, pass the data first and then modify the result, or wrap themselves
|
|
||||||
around passing the data (such as a logger might do). Decorators run in the order provided. For
|
|
||||||
example, the following:
|
|
||||||
|
|
||||||
req, err := Prepare(&http.Request{},
|
|
||||||
WithBaseURL("https://microsoft.com/"),
|
|
||||||
WithPath("a"),
|
|
||||||
WithPath("b"),
|
|
||||||
WithPath("c"))
|
|
||||||
|
|
||||||
will set the URL to:
|
|
||||||
|
|
||||||
https://microsoft.com/a/b/c
|
|
||||||
|
|
||||||
Preparers and Responders may be shared and re-used (assuming the underlying decorators support
|
|
||||||
sharing and re-use). Performant use is obtained by creating one or more Preparers and Responders
|
|
||||||
shared among multiple go-routines, and a single Sender shared among multiple sending go-routines,
|
|
||||||
all bound together by means of input / output channels.
|
|
||||||
|
|
||||||
Decorators hold their passed state within a closure (such as the path components in the example
|
|
||||||
above). Be careful to share Preparers and Responders only in a context where such held state
|
|
||||||
applies. For example, it may not make sense to share a Preparer that applies a query string from a
|
|
||||||
fixed set of values. Similarly, sharing a Responder that reads the response body into a passed
|
|
||||||
struct (e.g., ByUnmarshallingJson) is likely incorrect.
|
|
||||||
|
|
||||||
Lastly, the Swagger specification (https://swagger.io) that drives AutoRest
|
|
||||||
(https://github.com/Azure/autorest/) precisely defines two date forms: date and date-time. The
|
|
||||||
github.com/Azure/go-autorest/autorest/date package provides time.Time derivations to ensure
|
|
||||||
correct parsing and formatting.
|
|
||||||
|
|
||||||
Errors raised by autorest objects and methods will conform to the autorest.Error interface.
|
|
||||||
|
|
||||||
See the included examples for more detail. For details on the suggested use of this package by
|
|
||||||
generated clients, see the Client described below.
|
|
||||||
*/
|
|
||||||
package autorest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// HeaderLocation specifies the HTTP Location header.
|
|
||||||
HeaderLocation = "Location"
|
|
||||||
|
|
||||||
// HeaderRetryAfter specifies the HTTP Retry-After header.
|
|
||||||
HeaderRetryAfter = "Retry-After"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ResponseHasStatusCode returns true if the status code in the HTTP Response is in the passed set
|
|
||||||
// and false otherwise.
|
|
||||||
func ResponseHasStatusCode(resp *http.Response, codes ...int) bool {
|
|
||||||
return containsInt(codes, resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLocation retrieves the URL from the Location header of the passed response.
|
|
||||||
func GetLocation(resp *http.Response) string {
|
|
||||||
return resp.Header.Get(HeaderLocation)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRetryAfter extracts the retry delay from the Retry-After header of the passed response. If
|
|
||||||
// the header is absent or is malformed, it will return the supplied default delay time.Duration.
|
|
||||||
func GetRetryAfter(resp *http.Response, defaultDelay time.Duration) time.Duration {
|
|
||||||
retry := resp.Header.Get(HeaderRetryAfter)
|
|
||||||
if retry == "" {
|
|
||||||
return defaultDelay
|
|
||||||
}
|
|
||||||
|
|
||||||
d, err := time.ParseDuration(retry + "s")
|
|
||||||
if err != nil {
|
|
||||||
return defaultDelay
|
|
||||||
}
|
|
||||||
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPollingRequest allocates and returns a new http.Request to poll for the passed response.
|
|
||||||
func NewPollingRequest(resp *http.Response, cancel <-chan struct{}) (*http.Request, error) {
|
|
||||||
location := GetLocation(resp)
|
|
||||||
if location == "" {
|
|
||||||
return nil, NewErrorWithResponse("autorest", "NewPollingRequest", resp, "Location header missing from response that requires polling")
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := Prepare(&http.Request{Cancel: cancel},
|
|
||||||
AsGet(),
|
|
||||||
WithBaseURL(location))
|
|
||||||
if err != nil {
|
|
||||||
return nil, NewErrorWithError(err, "autorest", "NewPollingRequest", nil, "Failure creating poll request to %s", location)
|
|
||||||
}
|
|
||||||
|
|
||||||
return req, nil
|
|
||||||
}
|
|
|
@ -1,302 +0,0 @@
|
||||||
package azure
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Azure/go-autorest/autorest"
|
|
||||||
"github.com/Azure/go-autorest/autorest/date"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
headerAsyncOperation = "Azure-AsyncOperation"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
operationInProgress string = "InProgress"
|
|
||||||
operationCanceled string = "Canceled"
|
|
||||||
operationFailed string = "Failed"
|
|
||||||
operationSucceeded string = "Succeeded"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DoPollForAsynchronous returns a SendDecorator that polls if the http.Response is for an Azure
|
|
||||||
// long-running operation. It will delay between requests for the duration specified in the
|
|
||||||
// RetryAfter header or, if the header is absent, the passed delay. Polling may be canceled by
|
|
||||||
// closing the optional channel on the http.Request.
|
|
||||||
func DoPollForAsynchronous(delay time.Duration) autorest.SendDecorator {
|
|
||||||
return func(s autorest.Sender) autorest.Sender {
|
|
||||||
return autorest.SenderFunc(func(r *http.Request) (resp *http.Response, err error) {
|
|
||||||
resp, err = s.Do(r)
|
|
||||||
if err != nil {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
pollingCodes := []int{http.StatusAccepted, http.StatusCreated, http.StatusOK}
|
|
||||||
if !autorest.ResponseHasStatusCode(resp, pollingCodes...) {
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ps := pollingState{}
|
|
||||||
for err == nil {
|
|
||||||
err = updatePollingState(resp, &ps)
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if ps.hasTerminated() {
|
|
||||||
if !ps.hasSucceeded() {
|
|
||||||
err = ps
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err = newPollingRequest(resp, ps)
|
|
||||||
if err != nil {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
delay = autorest.GetRetryAfter(resp, delay)
|
|
||||||
resp, err = autorest.SendWithSender(s, r,
|
|
||||||
autorest.AfterDelay(delay))
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAsyncOperation(resp *http.Response) string {
|
|
||||||
return resp.Header.Get(http.CanonicalHeaderKey(headerAsyncOperation))
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasSucceeded(state string) bool {
|
|
||||||
return state == operationSucceeded
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasTerminated(state string) bool {
|
|
||||||
switch state {
|
|
||||||
case operationCanceled, operationFailed, operationSucceeded:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasFailed(state string) bool {
|
|
||||||
return state == operationFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
type provisioningTracker interface {
|
|
||||||
state() string
|
|
||||||
hasSucceeded() bool
|
|
||||||
hasTerminated() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type operationResource struct {
|
|
||||||
// Note:
|
|
||||||
// The specification states services should return the "id" field. However some return it as
|
|
||||||
// "operationId".
|
|
||||||
ID string `json:"id"`
|
|
||||||
OperationID string `json:"operationId"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Properties map[string]interface{} `json:"properties"`
|
|
||||||
OperationError ServiceError `json:"error"`
|
|
||||||
StartTime date.Time `json:"startTime"`
|
|
||||||
EndTime date.Time `json:"endTime"`
|
|
||||||
PercentComplete float64 `json:"percentComplete"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (or operationResource) state() string {
|
|
||||||
return or.Status
|
|
||||||
}
|
|
||||||
|
|
||||||
func (or operationResource) hasSucceeded() bool {
|
|
||||||
return hasSucceeded(or.state())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (or operationResource) hasTerminated() bool {
|
|
||||||
return hasTerminated(or.state())
|
|
||||||
}
|
|
||||||
|
|
||||||
type provisioningProperties struct {
|
|
||||||
ProvisioningState string `json:"provisioningState"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type provisioningStatus struct {
|
|
||||||
Properties provisioningProperties `json:"properties,omitempty"`
|
|
||||||
ProvisioningError ServiceError `json:"error,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ps provisioningStatus) state() string {
|
|
||||||
return ps.Properties.ProvisioningState
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ps provisioningStatus) hasSucceeded() bool {
|
|
||||||
return hasSucceeded(ps.state())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ps provisioningStatus) hasTerminated() bool {
|
|
||||||
return hasTerminated(ps.state())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ps provisioningStatus) hasProvisioningError() bool {
|
|
||||||
return ps.ProvisioningError != ServiceError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type pollingResponseFormat string
|
|
||||||
|
|
||||||
const (
|
|
||||||
usesOperationResponse pollingResponseFormat = "OperationResponse"
|
|
||||||
usesProvisioningStatus pollingResponseFormat = "ProvisioningStatus"
|
|
||||||
formatIsUnknown pollingResponseFormat = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
type pollingState struct {
|
|
||||||
responseFormat pollingResponseFormat
|
|
||||||
uri string
|
|
||||||
state string
|
|
||||||
code string
|
|
||||||
message string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ps pollingState) hasSucceeded() bool {
|
|
||||||
return hasSucceeded(ps.state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ps pollingState) hasTerminated() bool {
|
|
||||||
return hasTerminated(ps.state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ps pollingState) hasFailed() bool {
|
|
||||||
return hasFailed(ps.state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ps pollingState) Error() string {
|
|
||||||
return fmt.Sprintf("Long running operation terminated with status '%s': Code=%q Message=%q", ps.state, ps.code, ps.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// updatePollingState maps the operation status -- retrieved from either a provisioningState
|
|
||||||
// field, the status field of an OperationResource, or inferred from the HTTP status code --
|
|
||||||
// into a well-known states. Since the process begins from the initial request, the state
|
|
||||||
// always comes from either a the provisioningState returned or is inferred from the HTTP
|
|
||||||
// status code. Subsequent requests will read an Azure OperationResource object if the
|
|
||||||
// service initially returned the Azure-AsyncOperation header. The responseFormat field notes
|
|
||||||
// the expected response format.
|
|
||||||
func updatePollingState(resp *http.Response, ps *pollingState) error {
|
|
||||||
// Determine the response shape
|
|
||||||
// -- The first response will always be a provisioningStatus response; only the polling requests,
|
|
||||||
// depending on the header returned, may be something otherwise.
|
|
||||||
var pt provisioningTracker
|
|
||||||
if ps.responseFormat == usesOperationResponse {
|
|
||||||
pt = &operationResource{}
|
|
||||||
} else {
|
|
||||||
pt = &provisioningStatus{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this is the first request (that is, the polling response shape is unknown), determine how
|
|
||||||
// to poll and what to expect
|
|
||||||
if ps.responseFormat == formatIsUnknown {
|
|
||||||
req := resp.Request
|
|
||||||
if req == nil {
|
|
||||||
return autorest.NewError("azure", "updatePollingState", "Azure Polling Error - Original HTTP request is missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefer the Azure-AsyncOperation header
|
|
||||||
ps.uri = getAsyncOperation(resp)
|
|
||||||
if ps.uri != "" {
|
|
||||||
ps.responseFormat = usesOperationResponse
|
|
||||||
} else {
|
|
||||||
ps.responseFormat = usesProvisioningStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
// Else, use the Location header
|
|
||||||
if ps.uri == "" {
|
|
||||||
ps.uri = autorest.GetLocation(resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lastly, requests against an existing resource, use the last request URI
|
|
||||||
if ps.uri == "" {
|
|
||||||
m := strings.ToUpper(req.Method)
|
|
||||||
if m == http.MethodPatch || m == http.MethodPut || m == http.MethodGet {
|
|
||||||
ps.uri = req.URL.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read and interpret the response (saving the Body in case no polling is necessary)
|
|
||||||
b := &bytes.Buffer{}
|
|
||||||
err := autorest.Respond(resp,
|
|
||||||
autorest.ByCopying(b),
|
|
||||||
autorest.ByUnmarshallingJSON(pt),
|
|
||||||
autorest.ByClosing())
|
|
||||||
resp.Body = ioutil.NopCloser(b)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interpret the results
|
|
||||||
// -- Terminal states apply regardless
|
|
||||||
// -- Unknown states are per-service inprogress states
|
|
||||||
// -- Otherwise, infer state from HTTP status code
|
|
||||||
if pt.hasTerminated() {
|
|
||||||
ps.state = pt.state()
|
|
||||||
} else if pt.state() != "" {
|
|
||||||
ps.state = operationInProgress
|
|
||||||
} else {
|
|
||||||
switch resp.StatusCode {
|
|
||||||
case http.StatusAccepted:
|
|
||||||
ps.state = operationInProgress
|
|
||||||
|
|
||||||
case http.StatusNoContent, http.StatusCreated, http.StatusOK:
|
|
||||||
ps.state = operationSucceeded
|
|
||||||
|
|
||||||
default:
|
|
||||||
ps.state = operationFailed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ps.state == operationInProgress && ps.uri == "" {
|
|
||||||
return autorest.NewError("azure", "updatePollingState", "Azure Polling Error - Unable to obtain polling URI for %s %s", resp.Request.Method, resp.Request.URL)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For failed operation, check for error code and message in
|
|
||||||
// -- Operation resource
|
|
||||||
// -- Response
|
|
||||||
// -- Otherwise, Unknown
|
|
||||||
if ps.hasFailed() {
|
|
||||||
if ps.responseFormat == usesOperationResponse {
|
|
||||||
or := pt.(*operationResource)
|
|
||||||
ps.code = or.OperationError.Code
|
|
||||||
ps.message = or.OperationError.Message
|
|
||||||
} else {
|
|
||||||
p := pt.(*provisioningStatus)
|
|
||||||
if p.hasProvisioningError() {
|
|
||||||
ps.code = p.ProvisioningError.Code
|
|
||||||
ps.message = p.ProvisioningError.Message
|
|
||||||
} else {
|
|
||||||
ps.code = "Unknown"
|
|
||||||
ps.message = "None"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPollingRequest(resp *http.Response, ps pollingState) (*http.Request, error) {
|
|
||||||
req := resp.Request
|
|
||||||
if req == nil {
|
|
||||||
return nil, autorest.NewError("azure", "newPollingRequest", "Azure Polling Error - Original HTTP request is missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
reqPoll, err := autorest.Prepare(&http.Request{Cancel: req.Cancel},
|
|
||||||
autorest.AsGet(),
|
|
||||||
autorest.WithBaseURL(ps.uri))
|
|
||||||
if err != nil {
|
|
||||||
return nil, autorest.NewErrorWithError(err, "azure", "newPollingRequest", nil, "Failure creating poll request to %s", ps.uri)
|
|
||||||
}
|
|
||||||
|
|
||||||
return reqPoll, nil
|
|
||||||
}
|
|
|
@ -1,180 +0,0 @@
|
||||||
/*
|
|
||||||
Package azure provides Azure-specific implementations used with AutoRest.
|
|
||||||
|
|
||||||
See the included examples for more detail.
|
|
||||||
*/
|
|
||||||
package azure
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/Azure/go-autorest/autorest"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// HeaderClientID is the Azure extension header to set a user-specified request ID.
|
|
||||||
HeaderClientID = "x-ms-client-request-id"
|
|
||||||
|
|
||||||
// HeaderReturnClientID is the Azure extension header to set if the user-specified request ID
|
|
||||||
// should be included in the response.
|
|
||||||
HeaderReturnClientID = "x-ms-return-client-request-id"
|
|
||||||
|
|
||||||
// HeaderRequestID is the Azure extension header of the service generated request ID returned
|
|
||||||
// in the response.
|
|
||||||
HeaderRequestID = "x-ms-request-id"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ServiceError encapsulates the error response from an Azure service.
|
|
||||||
type ServiceError struct {
|
|
||||||
Code string `json:"code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Details *[]interface{} `json:"details"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (se ServiceError) Error() string {
|
|
||||||
if se.Details != nil {
|
|
||||||
d, err := json.Marshal(*(se.Details))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Sprintf("Code=%q Message=%q Details=%v", se.Code, se.Message, *se.Details)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("Code=%q Message=%q Details=%v", se.Code, se.Message, string(d))
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("Code=%q Message=%q", se.Code, se.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestError describes an error response returned by Azure service.
|
|
||||||
type RequestError struct {
|
|
||||||
autorest.DetailedError
|
|
||||||
|
|
||||||
// The error returned by the Azure service.
|
|
||||||
ServiceError *ServiceError `json:"error"`
|
|
||||||
|
|
||||||
// The request id (from the x-ms-request-id-header) of the request.
|
|
||||||
RequestID string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error returns a human-friendly error message from service error.
|
|
||||||
func (e RequestError) Error() string {
|
|
||||||
return fmt.Sprintf("autorest/azure: Service returned an error. Status=%v %v",
|
|
||||||
e.StatusCode, e.ServiceError)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsAzureError returns true if the passed error is an Azure Service error; false otherwise.
|
|
||||||
func IsAzureError(e error) bool {
|
|
||||||
_, ok := e.(*RequestError)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewErrorWithError creates a new Error conforming object from the
|
|
||||||
// passed packageType, method, statusCode of the given resp (UndefinedStatusCode
|
|
||||||
// if resp is nil), message, and original error. message is treated as a format
|
|
||||||
// string to which the optional args apply.
|
|
||||||
func NewErrorWithError(original error, packageType string, method string, resp *http.Response, message string, args ...interface{}) RequestError {
|
|
||||||
if v, ok := original.(*RequestError); ok {
|
|
||||||
return *v
|
|
||||||
}
|
|
||||||
|
|
||||||
statusCode := autorest.UndefinedStatusCode
|
|
||||||
if resp != nil {
|
|
||||||
statusCode = resp.StatusCode
|
|
||||||
}
|
|
||||||
return RequestError{
|
|
||||||
DetailedError: autorest.DetailedError{
|
|
||||||
Original: original,
|
|
||||||
PackageType: packageType,
|
|
||||||
Method: method,
|
|
||||||
StatusCode: statusCode,
|
|
||||||
Message: fmt.Sprintf(message, args...),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithReturningClientID returns a PrepareDecorator that adds an HTTP extension header of
|
|
||||||
// x-ms-client-request-id whose value is the passed, undecorated UUID (e.g.,
|
|
||||||
// "0F39878C-5F76-4DB8-A25D-61D2C193C3CA"). It also sets the x-ms-return-client-request-id
|
|
||||||
// header to true such that UUID accompanies the http.Response.
|
|
||||||
func WithReturningClientID(uuid string) autorest.PrepareDecorator {
|
|
||||||
preparer := autorest.CreatePreparer(
|
|
||||||
WithClientID(uuid),
|
|
||||||
WithReturnClientID(true))
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
return preparer.Prepare(r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithClientID returns a PrepareDecorator that adds an HTTP extension header of
|
|
||||||
// x-ms-client-request-id whose value is passed, undecorated UUID (e.g.,
|
|
||||||
// "0F39878C-5F76-4DB8-A25D-61D2C193C3CA").
|
|
||||||
func WithClientID(uuid string) autorest.PrepareDecorator {
|
|
||||||
return autorest.WithHeader(HeaderClientID, uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithReturnClientID returns a PrepareDecorator that adds an HTTP extension header of
|
|
||||||
// x-ms-return-client-request-id whose boolean value indicates if the value of the
|
|
||||||
// x-ms-client-request-id header should be included in the http.Response.
|
|
||||||
func WithReturnClientID(b bool) autorest.PrepareDecorator {
|
|
||||||
return autorest.WithHeader(HeaderReturnClientID, strconv.FormatBool(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtractClientID extracts the client identifier from the x-ms-client-request-id header set on the
|
|
||||||
// http.Request sent to the service (and returned in the http.Response)
|
|
||||||
func ExtractClientID(resp *http.Response) string {
|
|
||||||
return autorest.ExtractHeaderValue(HeaderClientID, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtractRequestID extracts the Azure server generated request identifier from the
|
|
||||||
// x-ms-request-id header.
|
|
||||||
func ExtractRequestID(resp *http.Response) string {
|
|
||||||
return autorest.ExtractHeaderValue(HeaderRequestID, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 RequestError
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// 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(autorest.EncodedAsJSON, 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)
|
|
||||||
} else if e.ServiceError == nil {
|
|
||||||
e.ServiceError = &ServiceError{Code: "Unknown", Message: "Unknown service error"}
|
|
||||||
}
|
|
||||||
|
|
||||||
e.RequestID = ExtractRequestID(resp)
|
|
||||||
if e.StatusCode == nil {
|
|
||||||
e.StatusCode = resp.StatusCode
|
|
||||||
}
|
|
||||||
err = &e
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,130 +0,0 @@
|
||||||
package azure
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var environments = map[string]Environment{
|
|
||||||
"AZURECHINACLOUD": ChinaCloud,
|
|
||||||
"AZUREGERMANCLOUD": GermanCloud,
|
|
||||||
"AZUREPUBLICCLOUD": PublicCloud,
|
|
||||||
"AZUREUSGOVERNMENTCLOUD": USGovernmentCloud,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Environment represents a set of endpoints for each of Azure's Clouds.
|
|
||||||
type Environment struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
ManagementPortalURL string `json:"managementPortalURL"`
|
|
||||||
PublishSettingsURL string `json:"publishSettingsURL"`
|
|
||||||
ServiceManagementEndpoint string `json:"serviceManagementEndpoint"`
|
|
||||||
ResourceManagerEndpoint string `json:"resourceManagerEndpoint"`
|
|
||||||
ActiveDirectoryEndpoint string `json:"activeDirectoryEndpoint"`
|
|
||||||
GalleryEndpoint string `json:"galleryEndpoint"`
|
|
||||||
KeyVaultEndpoint string `json:"keyVaultEndpoint"`
|
|
||||||
GraphEndpoint string `json:"graphEndpoint"`
|
|
||||||
StorageEndpointSuffix string `json:"storageEndpointSuffix"`
|
|
||||||
SQLDatabaseDNSSuffix string `json:"sqlDatabaseDNSSuffix"`
|
|
||||||
TrafficManagerDNSSuffix string `json:"trafficManagerDNSSuffix"`
|
|
||||||
KeyVaultDNSSuffix string `json:"keyVaultDNSSuffix"`
|
|
||||||
ServiceBusEndpointSuffix string `json:"serviceBusEndpointSuffix"`
|
|
||||||
ServiceManagementVMDNSSuffix string `json:"serviceManagementVMDNSSuffix"`
|
|
||||||
ResourceManagerVMDNSSuffix string `json:"resourceManagerVMDNSSuffix"`
|
|
||||||
ContainerRegistryDNSSuffix string `json:"containerRegistryDNSSuffix"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
// PublicCloud is the default public Azure cloud environment
|
|
||||||
PublicCloud = Environment{
|
|
||||||
Name: "AzurePublicCloud",
|
|
||||||
ManagementPortalURL: "https://manage.windowsazure.com/",
|
|
||||||
PublishSettingsURL: "https://manage.windowsazure.com/publishsettings/index",
|
|
||||||
ServiceManagementEndpoint: "https://management.core.windows.net/",
|
|
||||||
ResourceManagerEndpoint: "https://management.azure.com/",
|
|
||||||
ActiveDirectoryEndpoint: "https://login.microsoftonline.com/",
|
|
||||||
GalleryEndpoint: "https://gallery.azure.com/",
|
|
||||||
KeyVaultEndpoint: "https://vault.azure.net/",
|
|
||||||
GraphEndpoint: "https://graph.windows.net/",
|
|
||||||
StorageEndpointSuffix: "core.windows.net",
|
|
||||||
SQLDatabaseDNSSuffix: "database.windows.net",
|
|
||||||
TrafficManagerDNSSuffix: "trafficmanager.net",
|
|
||||||
KeyVaultDNSSuffix: "vault.azure.net",
|
|
||||||
ServiceBusEndpointSuffix: "servicebus.azure.com",
|
|
||||||
ServiceManagementVMDNSSuffix: "cloudapp.net",
|
|
||||||
ResourceManagerVMDNSSuffix: "cloudapp.azure.com",
|
|
||||||
ContainerRegistryDNSSuffix: "azurecr.io",
|
|
||||||
}
|
|
||||||
|
|
||||||
// USGovernmentCloud is the cloud environment for the US Government
|
|
||||||
USGovernmentCloud = Environment{
|
|
||||||
Name: "AzureUSGovernmentCloud",
|
|
||||||
ManagementPortalURL: "https://manage.windowsazure.us/",
|
|
||||||
PublishSettingsURL: "https://manage.windowsazure.us/publishsettings/index",
|
|
||||||
ServiceManagementEndpoint: "https://management.core.usgovcloudapi.net/",
|
|
||||||
ResourceManagerEndpoint: "https://management.usgovcloudapi.net/",
|
|
||||||
ActiveDirectoryEndpoint: "https://login.microsoftonline.com/",
|
|
||||||
GalleryEndpoint: "https://gallery.usgovcloudapi.net/",
|
|
||||||
KeyVaultEndpoint: "https://vault.usgovcloudapi.net/",
|
|
||||||
GraphEndpoint: "https://graph.usgovcloudapi.net/",
|
|
||||||
StorageEndpointSuffix: "core.usgovcloudapi.net",
|
|
||||||
SQLDatabaseDNSSuffix: "database.usgovcloudapi.net",
|
|
||||||
TrafficManagerDNSSuffix: "usgovtrafficmanager.net",
|
|
||||||
KeyVaultDNSSuffix: "vault.usgovcloudapi.net",
|
|
||||||
ServiceBusEndpointSuffix: "servicebus.usgovcloudapi.net",
|
|
||||||
ServiceManagementVMDNSSuffix: "usgovcloudapp.net",
|
|
||||||
ResourceManagerVMDNSSuffix: "cloudapp.windowsazure.us",
|
|
||||||
ContainerRegistryDNSSuffix: "azurecr.io",
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChinaCloud is the cloud environment operated in China
|
|
||||||
ChinaCloud = Environment{
|
|
||||||
Name: "AzureChinaCloud",
|
|
||||||
ManagementPortalURL: "https://manage.chinacloudapi.com/",
|
|
||||||
PublishSettingsURL: "https://manage.chinacloudapi.com/publishsettings/index",
|
|
||||||
ServiceManagementEndpoint: "https://management.core.chinacloudapi.cn/",
|
|
||||||
ResourceManagerEndpoint: "https://management.chinacloudapi.cn/",
|
|
||||||
ActiveDirectoryEndpoint: "https://login.chinacloudapi.cn/",
|
|
||||||
GalleryEndpoint: "https://gallery.chinacloudapi.cn/",
|
|
||||||
KeyVaultEndpoint: "https://vault.azure.cn/",
|
|
||||||
GraphEndpoint: "https://graph.chinacloudapi.cn/",
|
|
||||||
StorageEndpointSuffix: "core.chinacloudapi.cn",
|
|
||||||
SQLDatabaseDNSSuffix: "database.chinacloudapi.cn",
|
|
||||||
TrafficManagerDNSSuffix: "trafficmanager.cn",
|
|
||||||
KeyVaultDNSSuffix: "vault.azure.cn",
|
|
||||||
ServiceBusEndpointSuffix: "servicebus.chinacloudapi.net",
|
|
||||||
ServiceManagementVMDNSSuffix: "chinacloudapp.cn",
|
|
||||||
ResourceManagerVMDNSSuffix: "cloudapp.azure.cn",
|
|
||||||
ContainerRegistryDNSSuffix: "azurecr.io",
|
|
||||||
}
|
|
||||||
|
|
||||||
// GermanCloud is the cloud environment operated in Germany
|
|
||||||
GermanCloud = Environment{
|
|
||||||
Name: "AzureGermanCloud",
|
|
||||||
ManagementPortalURL: "http://portal.microsoftazure.de/",
|
|
||||||
PublishSettingsURL: "https://manage.microsoftazure.de/publishsettings/index",
|
|
||||||
ServiceManagementEndpoint: "https://management.core.cloudapi.de/",
|
|
||||||
ResourceManagerEndpoint: "https://management.microsoftazure.de/",
|
|
||||||
ActiveDirectoryEndpoint: "https://login.microsoftonline.de/",
|
|
||||||
GalleryEndpoint: "https://gallery.cloudapi.de/",
|
|
||||||
KeyVaultEndpoint: "https://vault.microsoftazure.de/",
|
|
||||||
GraphEndpoint: "https://graph.cloudapi.de/",
|
|
||||||
StorageEndpointSuffix: "core.cloudapi.de",
|
|
||||||
SQLDatabaseDNSSuffix: "database.cloudapi.de",
|
|
||||||
TrafficManagerDNSSuffix: "azuretrafficmanager.de",
|
|
||||||
KeyVaultDNSSuffix: "vault.microsoftazure.de",
|
|
||||||
ServiceBusEndpointSuffix: "servicebus.cloudapi.de",
|
|
||||||
ServiceManagementVMDNSSuffix: "azurecloudapp.de",
|
|
||||||
ResourceManagerVMDNSSuffix: "cloudapp.microsoftazure.de",
|
|
||||||
ContainerRegistryDNSSuffix: "azurecr.io",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// EnvironmentFromName returns an Environment based on the common name specified
|
|
||||||
func EnvironmentFromName(name string) (Environment, error) {
|
|
||||||
name = strings.ToUpper(name)
|
|
||||||
env, ok := environments[name]
|
|
||||||
if !ok {
|
|
||||||
return env, fmt.Errorf("autorest/azure: There is no cloud environment matching the name %q", name)
|
|
||||||
}
|
|
||||||
return env, nil
|
|
||||||
}
|
|
|
@ -1,235 +0,0 @@
|
||||||
package autorest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/http/cookiejar"
|
|
||||||
"runtime"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DefaultPollingDelay is a reasonable delay between polling requests.
|
|
||||||
DefaultPollingDelay = 60 * time.Second
|
|
||||||
|
|
||||||
// DefaultPollingDuration is a reasonable total polling duration.
|
|
||||||
DefaultPollingDuration = 15 * time.Minute
|
|
||||||
|
|
||||||
// DefaultRetryAttempts is number of attempts for retry status codes (5xx).
|
|
||||||
DefaultRetryAttempts = 3
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// defaultUserAgent builds a string containing the Go version, system archityecture and OS,
|
|
||||||
// and the go-autorest version.
|
|
||||||
defaultUserAgent = fmt.Sprintf("Go/%s (%s-%s) go-autorest/%s",
|
|
||||||
runtime.Version(),
|
|
||||||
runtime.GOARCH,
|
|
||||||
runtime.GOOS,
|
|
||||||
Version(),
|
|
||||||
)
|
|
||||||
|
|
||||||
statusCodesForRetry = []int{
|
|
||||||
http.StatusRequestTimeout, // 408
|
|
||||||
http.StatusInternalServerError, // 500
|
|
||||||
http.StatusBadGateway, // 502
|
|
||||||
http.StatusServiceUnavailable, // 503
|
|
||||||
http.StatusGatewayTimeout, // 504
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
requestFormat = `HTTP Request Begin ===================================================
|
|
||||||
%s
|
|
||||||
===================================================== HTTP Request End
|
|
||||||
`
|
|
||||||
responseFormat = `HTTP Response Begin ===================================================
|
|
||||||
%s
|
|
||||||
===================================================== HTTP Response End
|
|
||||||
`
|
|
||||||
)
|
|
||||||
|
|
||||||
// Response serves as the base for all responses from generated clients. It provides access to the
|
|
||||||
// last http.Response.
|
|
||||||
type Response struct {
|
|
||||||
*http.Response `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoggingInspector implements request and response inspectors that log the full request and
|
|
||||||
// response to a supplied log.
|
|
||||||
type LoggingInspector struct {
|
|
||||||
Logger *log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithInspection returns a PrepareDecorator that emits the http.Request to the supplied logger. The
|
|
||||||
// body is restored after being emitted.
|
|
||||||
//
|
|
||||||
// Note: Since it reads the entire Body, this decorator should not be used where body streaming is
|
|
||||||
// important. It is best used to trace JSON or similar body values.
|
|
||||||
func (li LoggingInspector) WithInspection() PrepareDecorator {
|
|
||||||
return func(p Preparer) Preparer {
|
|
||||||
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
|
||||||
var body, b bytes.Buffer
|
|
||||||
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
r.Body = ioutil.NopCloser(io.TeeReader(r.Body, &body))
|
|
||||||
if err := r.Write(&b); err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to write response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
li.Logger.Printf(requestFormat, b.String())
|
|
||||||
|
|
||||||
r.Body = ioutil.NopCloser(&body)
|
|
||||||
return p.Prepare(r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByInspecting returns a RespondDecorator that emits the http.Response to the supplied logger. The
|
|
||||||
// body is restored after being emitted.
|
|
||||||
//
|
|
||||||
// Note: Since it reads the entire Body, this decorator should not be used where body streaming is
|
|
||||||
// important. It is best used to trace JSON or similar body values.
|
|
||||||
func (li LoggingInspector) ByInspecting() RespondDecorator {
|
|
||||||
return func(r Responder) Responder {
|
|
||||||
return ResponderFunc(func(resp *http.Response) error {
|
|
||||||
var body, b bytes.Buffer
|
|
||||||
defer resp.Body.Close()
|
|
||||||
resp.Body = ioutil.NopCloser(io.TeeReader(resp.Body, &body))
|
|
||||||
if err := resp.Write(&b); err != nil {
|
|
||||||
return fmt.Errorf("Failed to write response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
li.Logger.Printf(responseFormat, b.String())
|
|
||||||
|
|
||||||
resp.Body = ioutil.NopCloser(&body)
|
|
||||||
return r.Respond(resp)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client is the base for autorest generated clients. It provides default, "do nothing"
|
|
||||||
// implementations of an Authorizer, RequestInspector, and ResponseInspector. It also returns the
|
|
||||||
// standard, undecorated http.Client as a default Sender.
|
|
||||||
//
|
|
||||||
// Generated clients should also use Error (see NewError and NewErrorWithError) for errors and
|
|
||||||
// return responses that compose with Response.
|
|
||||||
//
|
|
||||||
// Most customization of generated clients is best achieved by supplying a custom Authorizer, custom
|
|
||||||
// RequestInspector, and / or custom ResponseInspector. Users may log requests, implement circuit
|
|
||||||
// breakers (see https://msdn.microsoft.com/en-us/library/dn589784.aspx) or otherwise influence
|
|
||||||
// sending the request by providing a decorated Sender.
|
|
||||||
type Client struct {
|
|
||||||
Authorizer Authorizer
|
|
||||||
Sender Sender
|
|
||||||
RequestInspector PrepareDecorator
|
|
||||||
ResponseInspector RespondDecorator
|
|
||||||
|
|
||||||
// PollingDelay sets the polling frequency used in absence of a Retry-After HTTP header
|
|
||||||
PollingDelay time.Duration
|
|
||||||
|
|
||||||
// PollingDuration sets the maximum polling time after which an error is returned.
|
|
||||||
PollingDuration time.Duration
|
|
||||||
|
|
||||||
// RetryAttempts sets the default number of retry attempts for client.
|
|
||||||
RetryAttempts int
|
|
||||||
|
|
||||||
// RetryDuration sets the delay duration for retries.
|
|
||||||
RetryDuration time.Duration
|
|
||||||
|
|
||||||
// UserAgent, if not empty, will be set as the HTTP User-Agent header on all requests sent
|
|
||||||
// through the Do method.
|
|
||||||
UserAgent string
|
|
||||||
|
|
||||||
Jar http.CookieJar
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClientWithUserAgent returns an instance of a Client with the UserAgent set to the passed
|
|
||||||
// string.
|
|
||||||
func NewClientWithUserAgent(ua string) Client {
|
|
||||||
c := Client{
|
|
||||||
PollingDelay: DefaultPollingDelay,
|
|
||||||
PollingDuration: DefaultPollingDuration,
|
|
||||||
RetryAttempts: DefaultRetryAttempts,
|
|
||||||
RetryDuration: 30 * time.Second,
|
|
||||||
UserAgent: defaultUserAgent,
|
|
||||||
}
|
|
||||||
c.AddToUserAgent(ua)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddToUserAgent adds an extension to the current user agent
|
|
||||||
func (c *Client) AddToUserAgent(extension string) error {
|
|
||||||
if extension != "" {
|
|
||||||
c.UserAgent = fmt.Sprintf("%s %s", c.UserAgent, extension)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("Extension was empty, User Agent stayed as %s", c.UserAgent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do implements the Sender interface by invoking the active Sender after applying authorization.
|
|
||||||
// If Sender is not set, it uses a new instance of http.Client. In both cases it will, if UserAgent
|
|
||||||
// is set, apply set the User-Agent header.
|
|
||||||
func (c Client) Do(r *http.Request) (*http.Response, error) {
|
|
||||||
if r.UserAgent() == "" {
|
|
||||||
r, _ = Prepare(r,
|
|
||||||
WithUserAgent(c.UserAgent))
|
|
||||||
}
|
|
||||||
r, err := Prepare(r,
|
|
||||||
c.WithInspection(),
|
|
||||||
c.WithAuthorization())
|
|
||||||
if err != nil {
|
|
||||||
return nil, NewErrorWithError(err, "autorest/Client", "Do", nil, "Preparing request failed")
|
|
||||||
}
|
|
||||||
resp, err := SendWithSender(c.sender(), r,
|
|
||||||
DoRetryForStatusCodes(c.RetryAttempts, c.RetryDuration, statusCodesForRetry...))
|
|
||||||
Respond(resp,
|
|
||||||
c.ByInspecting())
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// sender returns the Sender to which to send requests.
|
|
||||||
func (c Client) sender() Sender {
|
|
||||||
if c.Sender == nil {
|
|
||||||
j, _ := cookiejar.New(nil)
|
|
||||||
return &http.Client{Jar: j}
|
|
||||||
}
|
|
||||||
return c.Sender
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithAuthorization is a convenience method that returns the WithAuthorization PrepareDecorator
|
|
||||||
// from the current Authorizer. If not Authorizer is set, it uses the NullAuthorizer.
|
|
||||||
func (c Client) WithAuthorization() PrepareDecorator {
|
|
||||||
return c.authorizer().WithAuthorization()
|
|
||||||
}
|
|
||||||
|
|
||||||
// authorizer returns the Authorizer to use.
|
|
||||||
func (c Client) authorizer() Authorizer {
|
|
||||||
if c.Authorizer == nil {
|
|
||||||
return NullAuthorizer{}
|
|
||||||
}
|
|
||||||
return c.Authorizer
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithInspection is a convenience method that passes the request to the supplied RequestInspector,
|
|
||||||
// if present, or returns the WithNothing PrepareDecorator otherwise.
|
|
||||||
func (c Client) WithInspection() PrepareDecorator {
|
|
||||||
if c.RequestInspector == nil {
|
|
||||||
return WithNothing()
|
|
||||||
}
|
|
||||||
return c.RequestInspector
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByInspecting is a convenience method that passes the response to the supplied ResponseInspector,
|
|
||||||
// if present, or returns the ByIgnoring RespondDecorator otherwise.
|
|
||||||
func (c Client) ByInspecting() RespondDecorator {
|
|
||||||
if c.ResponseInspector == nil {
|
|
||||||
return ByIgnoring()
|
|
||||||
}
|
|
||||||
return c.ResponseInspector
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
/*
|
|
||||||
Package date provides time.Time derivatives that conform to the Swagger.io (https://swagger.io/)
|
|
||||||
defined date formats: Date and DateTime. Both types may, in most cases, be used in lieu of
|
|
||||||
time.Time types. And both convert to time.Time through a ToTime method.
|
|
||||||
*/
|
|
||||||
package date
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
fullDate = "2006-01-02"
|
|
||||||
fullDateJSON = `"2006-01-02"`
|
|
||||||
dateFormat = "%04d-%02d-%02d"
|
|
||||||
jsonFormat = `"%04d-%02d-%02d"`
|
|
||||||
)
|
|
||||||
|
|
||||||
// Date defines a type similar to time.Time but assumes a layout of RFC3339 full-date (i.e.,
|
|
||||||
// 2006-01-02).
|
|
||||||
type Date struct {
|
|
||||||
time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseDate create a new Date from the passed string.
|
|
||||||
func ParseDate(date string) (d Date, err error) {
|
|
||||||
return parseDate(date, fullDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseDate(date string, format string) (Date, error) {
|
|
||||||
d, err := time.Parse(format, date)
|
|
||||||
return Date{Time: d}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary preserves the Date as a byte array conforming to RFC3339 full-date (i.e.,
|
|
||||||
// 2006-01-02).
|
|
||||||
func (d Date) MarshalBinary() ([]byte, error) {
|
|
||||||
return d.MarshalText()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBinary reconstitutes a Date saved as a byte array conforming to RFC3339 full-date (i.e.,
|
|
||||||
// 2006-01-02).
|
|
||||||
func (d *Date) UnmarshalBinary(data []byte) error {
|
|
||||||
return d.UnmarshalText(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON preserves the Date as a JSON string conforming to RFC3339 full-date (i.e.,
|
|
||||||
// 2006-01-02).
|
|
||||||
func (d Date) MarshalJSON() (json []byte, err error) {
|
|
||||||
return []byte(fmt.Sprintf(jsonFormat, d.Year(), d.Month(), d.Day())), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON reconstitutes the Date from a JSON string conforming to RFC3339 full-date (i.e.,
|
|
||||||
// 2006-01-02).
|
|
||||||
func (d *Date) UnmarshalJSON(data []byte) (err error) {
|
|
||||||
d.Time, err = time.Parse(fullDateJSON, string(data))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalText preserves the Date as a byte array conforming to RFC3339 full-date (i.e.,
|
|
||||||
// 2006-01-02).
|
|
||||||
func (d Date) MarshalText() (text []byte, err error) {
|
|
||||||
return []byte(fmt.Sprintf(dateFormat, d.Year(), d.Month(), d.Day())), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalText reconstitutes a Date saved as a byte array conforming to RFC3339 full-date (i.e.,
|
|
||||||
// 2006-01-02).
|
|
||||||
func (d *Date) UnmarshalText(data []byte) (err error) {
|
|
||||||
d.Time, err = time.Parse(fullDate, string(data))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the Date formatted as an RFC3339 full-date string (i.e., 2006-01-02).
|
|
||||||
func (d Date) String() string {
|
|
||||||
return fmt.Sprintf(dateFormat, d.Year(), d.Month(), d.Day())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToTime returns a Date as a time.Time
|
|
||||||
func (d Date) ToTime() time.Time {
|
|
||||||
return d.Time
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
package date
|
|
||||||
|
|
||||||
import (
|
|
||||||
"regexp"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Azure reports time in UTC but it doesn't include the 'Z' time zone suffix in some cases.
|
|
||||||
const (
|
|
||||||
azureUtcFormatJSON = `"2006-01-02T15:04:05.999999999"`
|
|
||||||
azureUtcFormat = "2006-01-02T15:04:05.999999999"
|
|
||||||
rfc3339JSON = `"` + time.RFC3339Nano + `"`
|
|
||||||
rfc3339 = time.RFC3339Nano
|
|
||||||
tzOffsetRegex = `(Z|z|\+|-)(\d+:\d+)*"*$`
|
|
||||||
)
|
|
||||||
|
|
||||||
// Time defines a type similar to time.Time but assumes a layout of RFC3339 date-time (i.e.,
|
|
||||||
// 2006-01-02T15:04:05Z).
|
|
||||||
type Time struct {
|
|
||||||
time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary preserves the Time as a byte array conforming to RFC3339 date-time (i.e.,
|
|
||||||
// 2006-01-02T15:04:05Z).
|
|
||||||
func (t Time) MarshalBinary() ([]byte, error) {
|
|
||||||
return t.Time.MarshalText()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBinary reconstitutes a Time saved as a byte array conforming to RFC3339 date-time
|
|
||||||
// (i.e., 2006-01-02T15:04:05Z).
|
|
||||||
func (t *Time) UnmarshalBinary(data []byte) error {
|
|
||||||
return t.UnmarshalText(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON preserves the Time as a JSON string conforming to RFC3339 date-time (i.e.,
|
|
||||||
// 2006-01-02T15:04:05Z).
|
|
||||||
func (t Time) MarshalJSON() (json []byte, err error) {
|
|
||||||
return t.Time.MarshalJSON()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON reconstitutes the Time from a JSON string conforming to RFC3339 date-time
|
|
||||||
// (i.e., 2006-01-02T15:04:05Z).
|
|
||||||
func (t *Time) UnmarshalJSON(data []byte) (err error) {
|
|
||||||
timeFormat := azureUtcFormatJSON
|
|
||||||
match, err := regexp.Match(tzOffsetRegex, data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if match {
|
|
||||||
timeFormat = rfc3339JSON
|
|
||||||
}
|
|
||||||
t.Time, err = ParseTime(timeFormat, string(data))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalText preserves the Time as a byte array conforming to RFC3339 date-time (i.e.,
|
|
||||||
// 2006-01-02T15:04:05Z).
|
|
||||||
func (t Time) MarshalText() (text []byte, err error) {
|
|
||||||
return t.Time.MarshalText()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalText reconstitutes a Time saved as a byte array conforming to RFC3339 date-time
|
|
||||||
// (i.e., 2006-01-02T15:04:05Z).
|
|
||||||
func (t *Time) UnmarshalText(data []byte) (err error) {
|
|
||||||
timeFormat := azureUtcFormat
|
|
||||||
match, err := regexp.Match(tzOffsetRegex, data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if match {
|
|
||||||
timeFormat = rfc3339
|
|
||||||
}
|
|
||||||
t.Time, err = ParseTime(timeFormat, string(data))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the Time formatted as an RFC3339 date-time string (i.e.,
|
|
||||||
// 2006-01-02T15:04:05Z).
|
|
||||||
func (t Time) String() string {
|
|
||||||
// Note: time.Time.String does not return an RFC3339 compliant string, time.Time.MarshalText does.
|
|
||||||
b, err := t.MarshalText()
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToTime returns a Time as a time.Time
|
|
||||||
func (t Time) ToTime() time.Time {
|
|
||||||
return t.Time
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
package date
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
rfc1123JSON = `"` + time.RFC1123 + `"`
|
|
||||||
rfc1123 = time.RFC1123
|
|
||||||
)
|
|
||||||
|
|
||||||
// TimeRFC1123 defines a type similar to time.Time but assumes a layout of RFC1123 date-time (i.e.,
|
|
||||||
// Mon, 02 Jan 2006 15:04:05 MST).
|
|
||||||
type TimeRFC1123 struct {
|
|
||||||
time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON reconstitutes the Time from a JSON string conforming to RFC1123 date-time
|
|
||||||
// (i.e., Mon, 02 Jan 2006 15:04:05 MST).
|
|
||||||
func (t *TimeRFC1123) UnmarshalJSON(data []byte) (err error) {
|
|
||||||
t.Time, err = ParseTime(rfc1123JSON, string(data))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON preserves the Time as a JSON string conforming to RFC1123 date-time (i.e.,
|
|
||||||
// Mon, 02 Jan 2006 15:04:05 MST).
|
|
||||||
func (t TimeRFC1123) MarshalJSON() ([]byte, error) {
|
|
||||||
if y := t.Year(); y < 0 || y >= 10000 {
|
|
||||||
return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]")
|
|
||||||
}
|
|
||||||
b := []byte(t.Format(rfc1123JSON))
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalText preserves the Time as a byte array conforming to RFC1123 date-time (i.e.,
|
|
||||||
// Mon, 02 Jan 2006 15:04:05 MST).
|
|
||||||
func (t TimeRFC1123) MarshalText() ([]byte, error) {
|
|
||||||
if y := t.Year(); y < 0 || y >= 10000 {
|
|
||||||
return nil, errors.New("Time.MarshalText: year outside of range [0,9999]")
|
|
||||||
}
|
|
||||||
|
|
||||||
b := []byte(t.Format(rfc1123))
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalText reconstitutes a Time saved as a byte array conforming to RFC1123 date-time
|
|
||||||
// (i.e., Mon, 02 Jan 2006 15:04:05 MST).
|
|
||||||
func (t *TimeRFC1123) UnmarshalText(data []byte) (err error) {
|
|
||||||
t.Time, err = ParseTime(rfc1123, string(data))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary preserves the Time as a byte array conforming to RFC1123 date-time (i.e.,
|
|
||||||
// Mon, 02 Jan 2006 15:04:05 MST).
|
|
||||||
func (t TimeRFC1123) MarshalBinary() ([]byte, error) {
|
|
||||||
return t.MarshalText()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBinary reconstitutes a Time saved as a byte array conforming to RFC1123 date-time
|
|
||||||
// (i.e., Mon, 02 Jan 2006 15:04:05 MST).
|
|
||||||
func (t *TimeRFC1123) UnmarshalBinary(data []byte) error {
|
|
||||||
return t.UnmarshalText(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToTime returns a Time as a time.Time
|
|
||||||
func (t TimeRFC1123) ToTime() time.Time {
|
|
||||||
return t.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the Time formatted as an RFC1123 date-time string (i.e.,
|
|
||||||
// Mon, 02 Jan 2006 15:04:05 MST).
|
|
||||||
func (t TimeRFC1123) String() string {
|
|
||||||
// Note: time.Time.String does not return an RFC1123 compliant string, time.Time.MarshalText does.
|
|
||||||
b, err := t.MarshalText()
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return string(b)
|
|
||||||
}
|
|
|
@ -1,109 +0,0 @@
|
||||||
package date
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"encoding/json"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// unixEpoch is the moment in time that should be treated as timestamp 0.
|
|
||||||
var unixEpoch = time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC)
|
|
||||||
|
|
||||||
// UnixTime marshals and unmarshals a time that is represented as the number
|
|
||||||
// of seconds (ignoring skip-seconds) since the Unix Epoch.
|
|
||||||
type UnixTime time.Time
|
|
||||||
|
|
||||||
// Duration returns the time as a Duration since the UnixEpoch.
|
|
||||||
func (t UnixTime) Duration() time.Duration {
|
|
||||||
return time.Time(t).Sub(unixEpoch)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewUnixTimeFromSeconds creates a UnixTime as a number of seconds from the UnixEpoch.
|
|
||||||
func NewUnixTimeFromSeconds(seconds float64) UnixTime {
|
|
||||||
return NewUnixTimeFromDuration(time.Duration(seconds * float64(time.Second)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewUnixTimeFromNanoseconds creates a UnixTime as a number of nanoseconds from the UnixEpoch.
|
|
||||||
func NewUnixTimeFromNanoseconds(nanoseconds int64) UnixTime {
|
|
||||||
return NewUnixTimeFromDuration(time.Duration(nanoseconds))
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewUnixTimeFromDuration creates a UnixTime as a duration of time since the UnixEpoch.
|
|
||||||
func NewUnixTimeFromDuration(dur time.Duration) UnixTime {
|
|
||||||
return UnixTime(unixEpoch.Add(dur))
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnixEpoch retreives the moment considered the Unix Epoch. I.e. The time represented by '0'
|
|
||||||
func UnixEpoch() time.Time {
|
|
||||||
return unixEpoch
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON preserves the UnixTime as a JSON number conforming to Unix Timestamp requirements.
|
|
||||||
// (i.e. the number of seconds since midnight January 1st, 1970 not considering leap seconds.)
|
|
||||||
func (t UnixTime) MarshalJSON() ([]byte, error) {
|
|
||||||
buffer := &bytes.Buffer{}
|
|
||||||
enc := json.NewEncoder(buffer)
|
|
||||||
err := enc.Encode(float64(time.Time(t).UnixNano()) / 1e9)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return buffer.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalJSON reconstitures a UnixTime saved as a JSON number of the number of seconds since
|
|
||||||
// midnight January 1st, 1970.
|
|
||||||
func (t *UnixTime) UnmarshalJSON(text []byte) error {
|
|
||||||
dec := json.NewDecoder(bytes.NewReader(text))
|
|
||||||
|
|
||||||
var secondsSinceEpoch float64
|
|
||||||
if err := dec.Decode(&secondsSinceEpoch); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*t = NewUnixTimeFromSeconds(secondsSinceEpoch)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalText stores the number of seconds since the Unix Epoch as a textual floating point number.
|
|
||||||
func (t UnixTime) MarshalText() ([]byte, error) {
|
|
||||||
cast := time.Time(t)
|
|
||||||
return cast.MarshalText()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalText populates a UnixTime with a value stored textually as a floating point number of seconds since the Unix Epoch.
|
|
||||||
func (t *UnixTime) UnmarshalText(raw []byte) error {
|
|
||||||
var unmarshaled time.Time
|
|
||||||
|
|
||||||
if err := unmarshaled.UnmarshalText(raw); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*t = UnixTime(unmarshaled)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalBinary converts a UnixTime into a binary.LittleEndian float64 of nanoseconds since the epoch.
|
|
||||||
func (t UnixTime) MarshalBinary() ([]byte, error) {
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
|
|
||||||
payload := int64(t.Duration())
|
|
||||||
|
|
||||||
if err := binary.Write(buf, binary.LittleEndian, &payload); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBinary converts a from a binary.LittleEndian float64 of nanoseconds since the epoch into a UnixTime.
|
|
||||||
func (t *UnixTime) UnmarshalBinary(raw []byte) error {
|
|
||||||
var nanosecondsSinceEpoch int64
|
|
||||||
|
|
||||||
if err := binary.Read(bytes.NewReader(raw), binary.LittleEndian, &nanosecondsSinceEpoch); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*t = NewUnixTimeFromNanoseconds(nanosecondsSinceEpoch)
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
package date
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ParseTime to parse Time string to specified format.
|
|
||||||
func ParseTime(format string, t string) (d time.Time, err error) {
|
|
||||||
return time.Parse(format, strings.ToUpper(t))
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
package autorest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// UndefinedStatusCode is used when HTTP status code is not available for an error.
|
|
||||||
UndefinedStatusCode = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
// DetailedError encloses a error with details of the package, method, and associated HTTP
|
|
||||||
// status code (if any).
|
|
||||||
type DetailedError struct {
|
|
||||||
Original error
|
|
||||||
|
|
||||||
// PackageType is the package type of the object emitting the error. For types, the value
|
|
||||||
// matches that produced the the '%T' format specifier of the fmt package. For other elements,
|
|
||||||
// such as functions, it is just the package name (e.g., "autorest").
|
|
||||||
PackageType string
|
|
||||||
|
|
||||||
// Method is the name of the method raising the error.
|
|
||||||
Method string
|
|
||||||
|
|
||||||
// StatusCode is the HTTP Response StatusCode (if non-zero) that led to the error.
|
|
||||||
StatusCode interface{}
|
|
||||||
|
|
||||||
// Message is the error message.
|
|
||||||
Message string
|
|
||||||
|
|
||||||
// Service Error is the response body of failed API in bytes
|
|
||||||
ServiceError []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewError creates a new Error conforming object from the passed packageType, method, and
|
|
||||||
// message. message is treated as a format string to which the optional args apply.
|
|
||||||
func NewError(packageType string, method string, message string, args ...interface{}) DetailedError {
|
|
||||||
return NewErrorWithError(nil, packageType, method, nil, message, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewErrorWithResponse creates a new Error conforming object from the passed
|
|
||||||
// packageType, method, statusCode of the given resp (UndefinedStatusCode if
|
|
||||||
// resp is nil), and message. message is treated as a format string to which the
|
|
||||||
// optional args apply.
|
|
||||||
func NewErrorWithResponse(packageType string, method string, resp *http.Response, message string, args ...interface{}) DetailedError {
|
|
||||||
return NewErrorWithError(nil, packageType, method, resp, message, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewErrorWithError creates a new Error conforming object from the
|
|
||||||
// passed packageType, method, statusCode of the given resp (UndefinedStatusCode
|
|
||||||
// if resp is nil), message, and original error. message is treated as a format
|
|
||||||
// string to which the optional args apply.
|
|
||||||
func NewErrorWithError(original error, packageType string, method string, resp *http.Response, message string, args ...interface{}) DetailedError {
|
|
||||||
if v, ok := original.(DetailedError); ok {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
statusCode := UndefinedStatusCode
|
|
||||||
if resp != nil {
|
|
||||||
statusCode = resp.StatusCode
|
|
||||||
}
|
|
||||||
|
|
||||||
return DetailedError{
|
|
||||||
Original: original,
|
|
||||||
PackageType: packageType,
|
|
||||||
Method: method,
|
|
||||||
StatusCode: statusCode,
|
|
||||||
Message: fmt.Sprintf(message, args...),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error returns a formatted containing all available details (i.e., PackageType, Method,
|
|
||||||
// StatusCode, Message, and original error (if any)).
|
|
||||||
func (e DetailedError) Error() string {
|
|
||||||
if e.Original == nil {
|
|
||||||
return fmt.Sprintf("%s#%s: %s: StatusCode=%d", e.PackageType, e.Method, e.Message, e.StatusCode)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s#%s: %s: StatusCode=%d -- Original Error: %v", e.PackageType, e.Method, e.Message, e.StatusCode, e.Original)
|
|
||||||
}
|
|
|
@ -1,428 +0,0 @@
|
||||||
package autorest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
mimeTypeJSON = "application/json"
|
|
||||||
mimeTypeFormPost = "application/x-www-form-urlencoded"
|
|
||||||
|
|
||||||
headerAuthorization = "Authorization"
|
|
||||||
headerContentType = "Content-Type"
|
|
||||||
headerUserAgent = "User-Agent"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Preparer is the interface that wraps the Prepare method.
|
|
||||||
//
|
|
||||||
// Prepare accepts and possibly modifies an http.Request (e.g., adding Headers). Implementations
|
|
||||||
// must ensure to not share or hold per-invocation state since Preparers may be shared and re-used.
|
|
||||||
type Preparer interface {
|
|
||||||
Prepare(*http.Request) (*http.Request, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PreparerFunc is a method that implements the Preparer interface.
|
|
||||||
type PreparerFunc func(*http.Request) (*http.Request, error)
|
|
||||||
|
|
||||||
// Prepare implements the Preparer interface on PreparerFunc.
|
|
||||||
func (pf PreparerFunc) Prepare(r *http.Request) (*http.Request, error) {
|
|
||||||
return pf(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrepareDecorator takes and possibly decorates, by wrapping, a Preparer. Decorators may affect the
|
|
||||||
// http.Request and pass it along or, first, pass the http.Request along then affect the result.
|
|
||||||
type PrepareDecorator func(Preparer) Preparer
|
|
||||||
|
|
||||||
// CreatePreparer creates, decorates, and returns a Preparer.
|
|
||||||
// Without decorators, the returned Preparer returns the passed http.Request unmodified.
|
|
||||||
// Preparers are safe to share and re-use.
|
|
||||||
func CreatePreparer(decorators ...PrepareDecorator) Preparer {
|
|
||||||
return DecoratePreparer(
|
|
||||||
Preparer(PreparerFunc(func(r *http.Request) (*http.Request, error) { return r, nil })),
|
|
||||||
decorators...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecoratePreparer accepts a Preparer and a, possibly empty, set of PrepareDecorators, which it
|
|
||||||
// applies to the Preparer. Decorators are applied in the order received, but their affect upon the
|
|
||||||
// request depends on whether they are a pre-decorator (change the http.Request and then pass it
|
|
||||||
// along) or a post-decorator (pass the http.Request along and alter it on return).
|
|
||||||
func DecoratePreparer(p Preparer, decorators ...PrepareDecorator) Preparer {
|
|
||||||
for _, decorate := range decorators {
|
|
||||||
p = decorate(p)
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare accepts an http.Request and a, possibly empty, set of PrepareDecorators.
|
|
||||||
// It creates a Preparer from the decorators which it then applies to the passed http.Request.
|
|
||||||
func Prepare(r *http.Request, decorators ...PrepareDecorator) (*http.Request, error) {
|
|
||||||
if r == nil {
|
|
||||||
return nil, NewError("autorest", "Prepare", "Invoked without an http.Request")
|
|
||||||
}
|
|
||||||
return CreatePreparer(decorators...).Prepare(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithNothing returns a "do nothing" PrepareDecorator that makes no changes to the passed
|
|
||||||
// http.Request.
|
|
||||||
func WithNothing() PrepareDecorator {
|
|
||||||
return func(p Preparer) Preparer {
|
|
||||||
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
|
||||||
return p.Prepare(r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithHeader returns a PrepareDecorator that sets the specified HTTP header of the http.Request to
|
|
||||||
// the passed value. It canonicalizes the passed header name (via http.CanonicalHeaderKey) before
|
|
||||||
// adding the header.
|
|
||||||
func WithHeader(header string, value string) PrepareDecorator {
|
|
||||||
return func(p Preparer) Preparer {
|
|
||||||
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
|
||||||
r, err := p.Prepare(r)
|
|
||||||
if err == nil {
|
|
||||||
if r.Header == nil {
|
|
||||||
r.Header = make(http.Header)
|
|
||||||
}
|
|
||||||
r.Header.Set(http.CanonicalHeaderKey(header), value)
|
|
||||||
}
|
|
||||||
return r, err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithBearerAuthorization returns a PrepareDecorator that adds an HTTP Authorization header whose
|
|
||||||
// value is "Bearer " followed by the supplied token.
|
|
||||||
func WithBearerAuthorization(token string) PrepareDecorator {
|
|
||||||
return WithHeader(headerAuthorization, fmt.Sprintf("Bearer %s", token))
|
|
||||||
}
|
|
||||||
|
|
||||||
// AsContentType returns a PrepareDecorator that adds an HTTP Content-Type header whose value
|
|
||||||
// is the passed contentType.
|
|
||||||
func AsContentType(contentType string) PrepareDecorator {
|
|
||||||
return WithHeader(headerContentType, contentType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithUserAgent returns a PrepareDecorator that adds an HTTP User-Agent header whose value is the
|
|
||||||
// passed string.
|
|
||||||
func WithUserAgent(ua string) PrepareDecorator {
|
|
||||||
return WithHeader(headerUserAgent, ua)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AsFormURLEncoded returns a PrepareDecorator that adds an HTTP Content-Type header whose value is
|
|
||||||
// "application/x-www-form-urlencoded".
|
|
||||||
func AsFormURLEncoded() PrepareDecorator {
|
|
||||||
return AsContentType(mimeTypeFormPost)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AsJSON returns a PrepareDecorator that adds an HTTP Content-Type header whose value is
|
|
||||||
// "application/json".
|
|
||||||
func AsJSON() PrepareDecorator {
|
|
||||||
return AsContentType(mimeTypeJSON)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithMethod returns a PrepareDecorator that sets the HTTP method of the passed request. The
|
|
||||||
// decorator does not validate that the passed method string is a known HTTP method.
|
|
||||||
func WithMethod(method string) PrepareDecorator {
|
|
||||||
return func(p Preparer) Preparer {
|
|
||||||
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
|
||||||
r.Method = method
|
|
||||||
return p.Prepare(r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AsDelete returns a PrepareDecorator that sets the HTTP method to DELETE.
|
|
||||||
func AsDelete() PrepareDecorator { return WithMethod("DELETE") }
|
|
||||||
|
|
||||||
// AsGet returns a PrepareDecorator that sets the HTTP method to GET.
|
|
||||||
func AsGet() PrepareDecorator { return WithMethod("GET") }
|
|
||||||
|
|
||||||
// AsHead returns a PrepareDecorator that sets the HTTP method to HEAD.
|
|
||||||
func AsHead() PrepareDecorator { return WithMethod("HEAD") }
|
|
||||||
|
|
||||||
// AsOptions returns a PrepareDecorator that sets the HTTP method to OPTIONS.
|
|
||||||
func AsOptions() PrepareDecorator { return WithMethod("OPTIONS") }
|
|
||||||
|
|
||||||
// AsPatch returns a PrepareDecorator that sets the HTTP method to PATCH.
|
|
||||||
func AsPatch() PrepareDecorator { return WithMethod("PATCH") }
|
|
||||||
|
|
||||||
// AsPost returns a PrepareDecorator that sets the HTTP method to POST.
|
|
||||||
func AsPost() PrepareDecorator { return WithMethod("POST") }
|
|
||||||
|
|
||||||
// AsPut returns a PrepareDecorator that sets the HTTP method to PUT.
|
|
||||||
func AsPut() PrepareDecorator { return WithMethod("PUT") }
|
|
||||||
|
|
||||||
// WithBaseURL returns a PrepareDecorator that populates the http.Request with a url.URL constructed
|
|
||||||
// from the supplied baseUrl.
|
|
||||||
func WithBaseURL(baseURL string) PrepareDecorator {
|
|
||||||
return func(p Preparer) Preparer {
|
|
||||||
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
|
||||||
r, err := p.Prepare(r)
|
|
||||||
if err == nil {
|
|
||||||
var u *url.URL
|
|
||||||
if u, err = url.Parse(baseURL); err != nil {
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
if u.Scheme == "" {
|
|
||||||
err = fmt.Errorf("autorest: No scheme detected in URL %s", baseURL)
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
r.URL = u
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return r, err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithCustomBaseURL returns a PrepareDecorator that replaces brace-enclosed keys within the
|
|
||||||
// request base URL (i.e., http.Request.URL) with the corresponding values from the passed map.
|
|
||||||
func WithCustomBaseURL(baseURL string, urlParameters map[string]interface{}) PrepareDecorator {
|
|
||||||
parameters := ensureValueStrings(urlParameters)
|
|
||||||
for key, value := range parameters {
|
|
||||||
baseURL = strings.Replace(baseURL, "{"+key+"}", value, -1)
|
|
||||||
}
|
|
||||||
return WithBaseURL(baseURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithFormData returns a PrepareDecoratore that "URL encodes" (e.g., bar=baz&foo=quux) into the
|
|
||||||
// http.Request body.
|
|
||||||
func WithFormData(v url.Values) PrepareDecorator {
|
|
||||||
return func(p Preparer) Preparer {
|
|
||||||
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
|
||||||
r, err := p.Prepare(r)
|
|
||||||
if err == nil {
|
|
||||||
s := v.Encode()
|
|
||||||
r.ContentLength = int64(len(s))
|
|
||||||
r.Body = ioutil.NopCloser(strings.NewReader(s))
|
|
||||||
}
|
|
||||||
return r, err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithMultiPartFormData returns a PrepareDecoratore that "URL encodes" (e.g., bar=baz&foo=quux) form parameters
|
|
||||||
// into the http.Request body.
|
|
||||||
func WithMultiPartFormData(formDataParameters map[string]interface{}) PrepareDecorator {
|
|
||||||
return func(p Preparer) Preparer {
|
|
||||||
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
|
||||||
r, err := p.Prepare(r)
|
|
||||||
if err == nil {
|
|
||||||
var body bytes.Buffer
|
|
||||||
writer := multipart.NewWriter(&body)
|
|
||||||
for key, value := range formDataParameters {
|
|
||||||
if rc, ok := value.(io.ReadCloser); ok {
|
|
||||||
var fd io.Writer
|
|
||||||
if fd, err = writer.CreateFormFile(key, key); err != nil {
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
if _, err = io.Copy(fd, rc); err != nil {
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err = writer.WriteField(key, ensureValueString(value)); err != nil {
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err = writer.Close(); err != nil {
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
if r.Header == nil {
|
|
||||||
r.Header = make(http.Header)
|
|
||||||
}
|
|
||||||
r.Header.Set(http.CanonicalHeaderKey(headerContentType), writer.FormDataContentType())
|
|
||||||
r.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes()))
|
|
||||||
r.ContentLength = int64(body.Len())
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
return r, err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithFile returns a PrepareDecorator that sends file in request body.
|
|
||||||
func WithFile(f io.ReadCloser) PrepareDecorator {
|
|
||||||
return func(p Preparer) Preparer {
|
|
||||||
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
|
||||||
r, err := p.Prepare(r)
|
|
||||||
if err == nil {
|
|
||||||
b, err := ioutil.ReadAll(f)
|
|
||||||
if err != nil {
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
r.Body = ioutil.NopCloser(bytes.NewReader(b))
|
|
||||||
r.ContentLength = int64(len(b))
|
|
||||||
}
|
|
||||||
return r, err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithBool returns a PrepareDecorator that encodes the passed bool into the body of the request
|
|
||||||
// and sets the Content-Length header.
|
|
||||||
func WithBool(v bool) PrepareDecorator {
|
|
||||||
return WithString(fmt.Sprintf("%v", v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithFloat32 returns a PrepareDecorator that encodes the passed float32 into the body of the
|
|
||||||
// request and sets the Content-Length header.
|
|
||||||
func WithFloat32(v float32) PrepareDecorator {
|
|
||||||
return WithString(fmt.Sprintf("%v", v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithFloat64 returns a PrepareDecorator that encodes the passed float64 into the body of the
|
|
||||||
// request and sets the Content-Length header.
|
|
||||||
func WithFloat64(v float64) PrepareDecorator {
|
|
||||||
return WithString(fmt.Sprintf("%v", v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithInt32 returns a PrepareDecorator that encodes the passed int32 into the body of the request
|
|
||||||
// and sets the Content-Length header.
|
|
||||||
func WithInt32(v int32) PrepareDecorator {
|
|
||||||
return WithString(fmt.Sprintf("%v", v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithInt64 returns a PrepareDecorator that encodes the passed int64 into the body of the request
|
|
||||||
// and sets the Content-Length header.
|
|
||||||
func WithInt64(v int64) PrepareDecorator {
|
|
||||||
return WithString(fmt.Sprintf("%v", v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithString returns a PrepareDecorator that encodes the passed string into the body of the request
|
|
||||||
// and sets the Content-Length header.
|
|
||||||
func WithString(v string) PrepareDecorator {
|
|
||||||
return func(p Preparer) Preparer {
|
|
||||||
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
|
||||||
r, err := p.Prepare(r)
|
|
||||||
if err == nil {
|
|
||||||
r.ContentLength = int64(len(v))
|
|
||||||
r.Body = ioutil.NopCloser(strings.NewReader(v))
|
|
||||||
}
|
|
||||||
return r, err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithJSON returns a PrepareDecorator that encodes the data passed as JSON into the body of the
|
|
||||||
// request and sets the Content-Length header.
|
|
||||||
func WithJSON(v interface{}) PrepareDecorator {
|
|
||||||
return func(p Preparer) Preparer {
|
|
||||||
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
|
||||||
r, err := p.Prepare(r)
|
|
||||||
if err == nil {
|
|
||||||
b, err := json.Marshal(v)
|
|
||||||
if err == nil {
|
|
||||||
r.ContentLength = int64(len(b))
|
|
||||||
r.Body = ioutil.NopCloser(bytes.NewReader(b))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return r, err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithPath returns a PrepareDecorator that adds the supplied path to the request URL. If the path
|
|
||||||
// is absolute (that is, it begins with a "/"), it replaces the existing path.
|
|
||||||
func WithPath(path string) PrepareDecorator {
|
|
||||||
return func(p Preparer) Preparer {
|
|
||||||
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
|
||||||
r, err := p.Prepare(r)
|
|
||||||
if err == nil {
|
|
||||||
if r.URL == nil {
|
|
||||||
return r, NewError("autorest", "WithPath", "Invoked with a nil URL")
|
|
||||||
}
|
|
||||||
if r.URL, err = parseURL(r.URL, path); err != nil {
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return r, err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithEscapedPathParameters returns a PrepareDecorator that replaces brace-enclosed keys within the
|
|
||||||
// request path (i.e., http.Request.URL.Path) with the corresponding values from the passed map. The
|
|
||||||
// values will be escaped (aka URL encoded) before insertion into the path.
|
|
||||||
func WithEscapedPathParameters(path string, pathParameters map[string]interface{}) PrepareDecorator {
|
|
||||||
parameters := escapeValueStrings(ensureValueStrings(pathParameters))
|
|
||||||
return func(p Preparer) Preparer {
|
|
||||||
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
|
||||||
r, err := p.Prepare(r)
|
|
||||||
if err == nil {
|
|
||||||
if r.URL == nil {
|
|
||||||
return r, NewError("autorest", "WithEscapedPathParameters", "Invoked with a nil URL")
|
|
||||||
}
|
|
||||||
for key, value := range parameters {
|
|
||||||
path = strings.Replace(path, "{"+key+"}", value, -1)
|
|
||||||
}
|
|
||||||
if r.URL, err = parseURL(r.URL, path); err != nil {
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return r, err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithPathParameters returns a PrepareDecorator that replaces brace-enclosed keys within the
|
|
||||||
// request path (i.e., http.Request.URL.Path) with the corresponding values from the passed map.
|
|
||||||
func WithPathParameters(path string, pathParameters map[string]interface{}) PrepareDecorator {
|
|
||||||
parameters := ensureValueStrings(pathParameters)
|
|
||||||
return func(p Preparer) Preparer {
|
|
||||||
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
|
||||||
r, err := p.Prepare(r)
|
|
||||||
if err == nil {
|
|
||||||
if r.URL == nil {
|
|
||||||
return r, NewError("autorest", "WithPathParameters", "Invoked with a nil URL")
|
|
||||||
}
|
|
||||||
for key, value := range parameters {
|
|
||||||
path = strings.Replace(path, "{"+key+"}", value, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.URL, err = parseURL(r.URL, path); err != nil {
|
|
||||||
return r, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return r, err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseURL(u *url.URL, path string) (*url.URL, error) {
|
|
||||||
p := strings.TrimRight(u.String(), "/")
|
|
||||||
if !strings.HasPrefix(path, "/") {
|
|
||||||
path = "/" + path
|
|
||||||
}
|
|
||||||
return url.Parse(p + path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithQueryParameters returns a PrepareDecorators that encodes and applies the query parameters
|
|
||||||
// given in the supplied map (i.e., key=value).
|
|
||||||
func WithQueryParameters(queryParameters map[string]interface{}) PrepareDecorator {
|
|
||||||
parameters := ensureValueStrings(queryParameters)
|
|
||||||
return func(p Preparer) Preparer {
|
|
||||||
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
|
||||||
r, err := p.Prepare(r)
|
|
||||||
if err == nil {
|
|
||||||
if r.URL == nil {
|
|
||||||
return r, NewError("autorest", "WithQueryParameters", "Invoked with a nil URL")
|
|
||||||
}
|
|
||||||
v := r.URL.Query()
|
|
||||||
for key, value := range parameters {
|
|
||||||
v.Add(key, value)
|
|
||||||
}
|
|
||||||
r.URL.RawQuery = createQuery(v)
|
|
||||||
}
|
|
||||||
return r, err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,236 +0,0 @@
|
||||||
package autorest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Responder is the interface that wraps the Respond method.
|
|
||||||
//
|
|
||||||
// Respond accepts and reacts to an http.Response. Implementations must ensure to not share or hold
|
|
||||||
// state since Responders may be shared and re-used.
|
|
||||||
type Responder interface {
|
|
||||||
Respond(*http.Response) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResponderFunc is a method that implements the Responder interface.
|
|
||||||
type ResponderFunc func(*http.Response) error
|
|
||||||
|
|
||||||
// Respond implements the Responder interface on ResponderFunc.
|
|
||||||
func (rf ResponderFunc) Respond(r *http.Response) error {
|
|
||||||
return rf(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RespondDecorator takes and possibly decorates, by wrapping, a Responder. Decorators may react to
|
|
||||||
// the http.Response and pass it along or, first, pass the http.Response along then react.
|
|
||||||
type RespondDecorator func(Responder) Responder
|
|
||||||
|
|
||||||
// CreateResponder creates, decorates, and returns a Responder. Without decorators, the returned
|
|
||||||
// Responder returns the passed http.Response unmodified. Responders may or may not be safe to share
|
|
||||||
// and re-used: It depends on the applied decorators. For example, a standard decorator that closes
|
|
||||||
// the response body is fine to share whereas a decorator that reads the body into a passed struct
|
|
||||||
// is not.
|
|
||||||
//
|
|
||||||
// To prevent memory leaks, ensure that at least one Responder closes the response body.
|
|
||||||
func CreateResponder(decorators ...RespondDecorator) Responder {
|
|
||||||
return DecorateResponder(
|
|
||||||
Responder(ResponderFunc(func(r *http.Response) error { return nil })),
|
|
||||||
decorators...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecorateResponder accepts a Responder and a, possibly empty, set of RespondDecorators, which it
|
|
||||||
// applies to the Responder. Decorators are applied in the order received, but their affect upon the
|
|
||||||
// request depends on whether they are a pre-decorator (react to the http.Response and then pass it
|
|
||||||
// along) or a post-decorator (pass the http.Response along and then react).
|
|
||||||
func DecorateResponder(r Responder, decorators ...RespondDecorator) Responder {
|
|
||||||
for _, decorate := range decorators {
|
|
||||||
r = decorate(r)
|
|
||||||
}
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
// Respond accepts an http.Response and a, possibly empty, set of RespondDecorators.
|
|
||||||
// It creates a Responder from the decorators it then applies to the passed http.Response.
|
|
||||||
func Respond(r *http.Response, decorators ...RespondDecorator) error {
|
|
||||||
if r == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return CreateResponder(decorators...).Respond(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByIgnoring returns a RespondDecorator that ignores the passed http.Response passing it unexamined
|
|
||||||
// to the next RespondDecorator.
|
|
||||||
func ByIgnoring() RespondDecorator {
|
|
||||||
return func(r Responder) Responder {
|
|
||||||
return ResponderFunc(func(resp *http.Response) error {
|
|
||||||
return r.Respond(resp)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByCopying copies the contents of the http.Response Body into the passed bytes.Buffer as
|
|
||||||
// the Body is read.
|
|
||||||
func ByCopying(b *bytes.Buffer) RespondDecorator {
|
|
||||||
return func(r Responder) Responder {
|
|
||||||
return ResponderFunc(func(resp *http.Response) error {
|
|
||||||
err := r.Respond(resp)
|
|
||||||
if err == nil && resp != nil && resp.Body != nil {
|
|
||||||
resp.Body = TeeReadCloser(resp.Body, b)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByDiscardingBody returns a RespondDecorator that first invokes the passed Responder after which
|
|
||||||
// it copies the remaining bytes (if any) in the response body to ioutil.Discard. Since the passed
|
|
||||||
// Responder is invoked prior to discarding the response body, the decorator may occur anywhere
|
|
||||||
// within the set.
|
|
||||||
func ByDiscardingBody() RespondDecorator {
|
|
||||||
return func(r Responder) Responder {
|
|
||||||
return ResponderFunc(func(resp *http.Response) error {
|
|
||||||
err := r.Respond(resp)
|
|
||||||
if err == nil && resp != nil && resp.Body != nil {
|
|
||||||
if _, err := io.Copy(ioutil.Discard, resp.Body); err != nil {
|
|
||||||
return fmt.Errorf("Error discarding the response body: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByClosing returns a RespondDecorator that first invokes the passed Responder after which it
|
|
||||||
// closes the response body. Since the passed Responder is invoked prior to closing the response
|
|
||||||
// body, the decorator may occur anywhere within the set.
|
|
||||||
func ByClosing() RespondDecorator {
|
|
||||||
return func(r Responder) Responder {
|
|
||||||
return ResponderFunc(func(resp *http.Response) error {
|
|
||||||
err := r.Respond(resp)
|
|
||||||
if resp != nil && resp.Body != nil {
|
|
||||||
if err := resp.Body.Close(); err != nil {
|
|
||||||
return fmt.Errorf("Error closing the response body: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByClosingIfError returns a RespondDecorator that first invokes the passed Responder after which
|
|
||||||
// it closes the response if the passed Responder returns an error and the response body exists.
|
|
||||||
func ByClosingIfError() RespondDecorator {
|
|
||||||
return func(r Responder) Responder {
|
|
||||||
return ResponderFunc(func(resp *http.Response) error {
|
|
||||||
err := r.Respond(resp)
|
|
||||||
if err != nil && resp != nil && resp.Body != nil {
|
|
||||||
if err := resp.Body.Close(); err != nil {
|
|
||||||
return fmt.Errorf("Error closing the response body: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByUnmarshallingJSON returns a RespondDecorator that decodes a JSON document returned in the
|
|
||||||
// response Body into the value pointed to by v.
|
|
||||||
func ByUnmarshallingJSON(v interface{}) RespondDecorator {
|
|
||||||
return func(r Responder) Responder {
|
|
||||||
return ResponderFunc(func(resp *http.Response) error {
|
|
||||||
err := r.Respond(resp)
|
|
||||||
if err == nil {
|
|
||||||
b, errInner := ioutil.ReadAll(resp.Body)
|
|
||||||
// Some responses might include a BOM, remove for successful unmarshalling
|
|
||||||
b = bytes.TrimPrefix(b, []byte("\xef\xbb\xbf"))
|
|
||||||
if errInner != nil {
|
|
||||||
err = fmt.Errorf("Error occurred reading http.Response#Body - Error = '%v'", errInner)
|
|
||||||
} else if len(strings.Trim(string(b), " ")) > 0 {
|
|
||||||
errInner = json.Unmarshal(b, v)
|
|
||||||
if errInner != nil {
|
|
||||||
err = fmt.Errorf("Error occurred unmarshalling JSON - Error = '%v' JSON = '%s'", errInner, string(b))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByUnmarshallingXML returns a RespondDecorator that decodes a XML document returned in the
|
|
||||||
// response Body into the value pointed to by v.
|
|
||||||
func ByUnmarshallingXML(v interface{}) RespondDecorator {
|
|
||||||
return func(r Responder) Responder {
|
|
||||||
return ResponderFunc(func(resp *http.Response) error {
|
|
||||||
err := r.Respond(resp)
|
|
||||||
if err == nil {
|
|
||||||
b, errInner := ioutil.ReadAll(resp.Body)
|
|
||||||
if errInner != nil {
|
|
||||||
err = fmt.Errorf("Error occurred reading http.Response#Body - Error = '%v'", errInner)
|
|
||||||
} else {
|
|
||||||
errInner = xml.Unmarshal(b, v)
|
|
||||||
if errInner != nil {
|
|
||||||
err = fmt.Errorf("Error occurred unmarshalling Xml - Error = '%v' Xml = '%s'", errInner, string(b))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithErrorUnlessStatusCode returns a RespondDecorator that emits an error unless the response
|
|
||||||
// StatusCode is among the set passed. On error, response body is fully read into a buffer and
|
|
||||||
// presented in the returned error, as well as in the response body.
|
|
||||||
func WithErrorUnlessStatusCode(codes ...int) RespondDecorator {
|
|
||||||
return func(r Responder) Responder {
|
|
||||||
return ResponderFunc(func(resp *http.Response) error {
|
|
||||||
err := r.Respond(resp)
|
|
||||||
if err == nil && !ResponseHasStatusCode(resp, codes...) {
|
|
||||||
derr := NewErrorWithResponse("autorest", "WithErrorUnlessStatusCode", resp, "%v %v failed with %s",
|
|
||||||
resp.Request.Method,
|
|
||||||
resp.Request.URL,
|
|
||||||
resp.Status)
|
|
||||||
if resp.Body != nil {
|
|
||||||
defer resp.Body.Close()
|
|
||||||
b, _ := ioutil.ReadAll(resp.Body)
|
|
||||||
derr.ServiceError = b
|
|
||||||
resp.Body = ioutil.NopCloser(bytes.NewReader(b))
|
|
||||||
}
|
|
||||||
err = derr
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithErrorUnlessOK returns a RespondDecorator that emits an error if the response StatusCode is
|
|
||||||
// anything other than HTTP 200.
|
|
||||||
func WithErrorUnlessOK() RespondDecorator {
|
|
||||||
return WithErrorUnlessStatusCode(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtractHeader extracts all values of the specified header from the http.Response. It returns an
|
|
||||||
// empty string slice if the passed http.Response is nil or the header does not exist.
|
|
||||||
func ExtractHeader(header string, resp *http.Response) []string {
|
|
||||||
if resp != nil && resp.Header != nil {
|
|
||||||
return resp.Header[http.CanonicalHeaderKey(header)]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtractHeaderValue extracts the first value of the specified header from the http.Response. It
|
|
||||||
// returns an empty string if the passed http.Response is nil or the header does not exist.
|
|
||||||
func ExtractHeaderValue(header string, resp *http.Response) string {
|
|
||||||
h := ExtractHeader(header, resp)
|
|
||||||
if len(h) > 0 {
|
|
||||||
return h[0]
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
|
@ -1,270 +0,0 @@
|
||||||
package autorest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"math"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Sender is the interface that wraps the Do method to send HTTP requests.
|
|
||||||
//
|
|
||||||
// The standard http.Client conforms to this interface.
|
|
||||||
type Sender interface {
|
|
||||||
Do(*http.Request) (*http.Response, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SenderFunc is a method that implements the Sender interface.
|
|
||||||
type SenderFunc func(*http.Request) (*http.Response, error)
|
|
||||||
|
|
||||||
// Do implements the Sender interface on SenderFunc.
|
|
||||||
func (sf SenderFunc) Do(r *http.Request) (*http.Response, error) {
|
|
||||||
return sf(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendDecorator takes and possibily decorates, by wrapping, a Sender. Decorators may affect the
|
|
||||||
// http.Request and pass it along or, first, pass the http.Request along then react to the
|
|
||||||
// http.Response result.
|
|
||||||
type SendDecorator func(Sender) Sender
|
|
||||||
|
|
||||||
// CreateSender creates, decorates, and returns, as a Sender, the default http.Client.
|
|
||||||
func CreateSender(decorators ...SendDecorator) Sender {
|
|
||||||
return DecorateSender(&http.Client{}, decorators...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecorateSender accepts a Sender and a, possibly empty, set of SendDecorators, which is applies to
|
|
||||||
// the Sender. Decorators are applied in the order received, but their affect upon the request
|
|
||||||
// depends on whether they are a pre-decorator (change the http.Request and then pass it along) or a
|
|
||||||
// post-decorator (pass the http.Request along and react to the results in http.Response).
|
|
||||||
func DecorateSender(s Sender, decorators ...SendDecorator) Sender {
|
|
||||||
for _, decorate := range decorators {
|
|
||||||
s = decorate(s)
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send sends, by means of the default http.Client, the passed http.Request, returning the
|
|
||||||
// http.Response and possible error. It also accepts a, possibly empty, set of SendDecorators which
|
|
||||||
// it will apply the http.Client before invoking the Do method.
|
|
||||||
//
|
|
||||||
// Send is a convenience method and not recommended for production. Advanced users should use
|
|
||||||
// SendWithSender, passing and sharing their own Sender (e.g., instance of http.Client).
|
|
||||||
//
|
|
||||||
// Send will not poll or retry requests.
|
|
||||||
func Send(r *http.Request, decorators ...SendDecorator) (*http.Response, error) {
|
|
||||||
return SendWithSender(&http.Client{}, r, decorators...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendWithSender sends the passed http.Request, through the provided Sender, returning the
|
|
||||||
// http.Response and possible error. It also accepts a, possibly empty, set of SendDecorators which
|
|
||||||
// it will apply the http.Client before invoking the Do method.
|
|
||||||
//
|
|
||||||
// SendWithSender will not poll or retry requests.
|
|
||||||
func SendWithSender(s Sender, r *http.Request, decorators ...SendDecorator) (*http.Response, error) {
|
|
||||||
return DecorateSender(s, decorators...).Do(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AfterDelay returns a SendDecorator that delays for the passed time.Duration before
|
|
||||||
// invoking the Sender. The delay may be terminated by closing the optional channel on the
|
|
||||||
// http.Request. If canceled, no further Senders are invoked.
|
|
||||||
func AfterDelay(d time.Duration) SendDecorator {
|
|
||||||
return func(s Sender) Sender {
|
|
||||||
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
|
||||||
if !DelayForBackoff(d, 0, r.Cancel) {
|
|
||||||
return nil, fmt.Errorf("autorest: AfterDelay canceled before full delay")
|
|
||||||
}
|
|
||||||
return s.Do(r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AsIs returns a SendDecorator that invokes the passed Sender without modifying the http.Request.
|
|
||||||
func AsIs() SendDecorator {
|
|
||||||
return func(s Sender) Sender {
|
|
||||||
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
|
||||||
return s.Do(r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoCloseIfError returns a SendDecorator that first invokes the passed Sender after which
|
|
||||||
// it closes the response if the passed Sender returns an error and the response body exists.
|
|
||||||
func DoCloseIfError() SendDecorator {
|
|
||||||
return func(s Sender) Sender {
|
|
||||||
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
|
||||||
resp, err := s.Do(r)
|
|
||||||
if err != nil {
|
|
||||||
Respond(resp, ByDiscardingBody(), ByClosing())
|
|
||||||
}
|
|
||||||
return resp, err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoErrorIfStatusCode returns a SendDecorator that emits an error if the response StatusCode is
|
|
||||||
// among the set passed. Since these are artificial errors, the response body may still require
|
|
||||||
// closing.
|
|
||||||
func DoErrorIfStatusCode(codes ...int) SendDecorator {
|
|
||||||
return func(s Sender) Sender {
|
|
||||||
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
|
||||||
resp, err := s.Do(r)
|
|
||||||
if err == nil && ResponseHasStatusCode(resp, codes...) {
|
|
||||||
err = NewErrorWithResponse("autorest", "DoErrorIfStatusCode", resp, "%v %v failed with %s",
|
|
||||||
resp.Request.Method,
|
|
||||||
resp.Request.URL,
|
|
||||||
resp.Status)
|
|
||||||
}
|
|
||||||
return resp, err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoErrorUnlessStatusCode returns a SendDecorator that emits an error unless the response
|
|
||||||
// StatusCode is among the set passed. Since these are artificial errors, the response body
|
|
||||||
// may still require closing.
|
|
||||||
func DoErrorUnlessStatusCode(codes ...int) SendDecorator {
|
|
||||||
return func(s Sender) Sender {
|
|
||||||
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
|
||||||
resp, err := s.Do(r)
|
|
||||||
if err == nil && !ResponseHasStatusCode(resp, codes...) {
|
|
||||||
err = NewErrorWithResponse("autorest", "DoErrorUnlessStatusCode", resp, "%v %v failed with %s",
|
|
||||||
resp.Request.Method,
|
|
||||||
resp.Request.URL,
|
|
||||||
resp.Status)
|
|
||||||
}
|
|
||||||
return resp, err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoPollForStatusCodes returns a SendDecorator that polls if the http.Response contains one of the
|
|
||||||
// passed status codes. It expects the http.Response to contain a Location header providing the
|
|
||||||
// URL at which to poll (using GET) and will poll until the time passed is equal to or greater than
|
|
||||||
// the supplied duration. It will delay between requests for the duration specified in the
|
|
||||||
// RetryAfter header or, if the header is absent, the passed delay. Polling may be canceled by
|
|
||||||
// closing the optional channel on the http.Request.
|
|
||||||
func DoPollForStatusCodes(duration time.Duration, delay time.Duration, codes ...int) SendDecorator {
|
|
||||||
return func(s Sender) Sender {
|
|
||||||
return SenderFunc(func(r *http.Request) (resp *http.Response, err error) {
|
|
||||||
resp, err = s.Do(r)
|
|
||||||
|
|
||||||
if err == nil && ResponseHasStatusCode(resp, codes...) {
|
|
||||||
r, err = NewPollingRequest(resp, r.Cancel)
|
|
||||||
|
|
||||||
for err == nil && ResponseHasStatusCode(resp, codes...) {
|
|
||||||
Respond(resp,
|
|
||||||
ByDiscardingBody(),
|
|
||||||
ByClosing())
|
|
||||||
resp, err = SendWithSender(s, r,
|
|
||||||
AfterDelay(GetRetryAfter(resp, delay)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoRetryForAttempts returns a SendDecorator that retries a failed request for up to the specified
|
|
||||||
// number of attempts, exponentially backing off between requests using the supplied backoff
|
|
||||||
// time.Duration (which may be zero). Retrying may be canceled by closing the optional channel on
|
|
||||||
// the http.Request.
|
|
||||||
func DoRetryForAttempts(attempts int, backoff time.Duration) SendDecorator {
|
|
||||||
return func(s Sender) Sender {
|
|
||||||
return SenderFunc(func(r *http.Request) (resp *http.Response, err error) {
|
|
||||||
for attempt := 0; attempt < attempts; attempt++ {
|
|
||||||
resp, err = s.Do(r)
|
|
||||||
if err == nil {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
DelayForBackoff(backoff, attempt, r.Cancel)
|
|
||||||
}
|
|
||||||
return resp, err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoRetryForStatusCodes returns a SendDecorator that retries for specified statusCodes for up to the specified
|
|
||||||
// number of attempts, exponentially backing off between requests using the supplied backoff
|
|
||||||
// time.Duration (which may be zero). Retrying may be canceled by closing the optional channel on
|
|
||||||
// the http.Request.
|
|
||||||
func DoRetryForStatusCodes(attempts int, backoff time.Duration, codes ...int) SendDecorator {
|
|
||||||
return func(s Sender) Sender {
|
|
||||||
return SenderFunc(func(r *http.Request) (resp *http.Response, err error) {
|
|
||||||
b := []byte{}
|
|
||||||
if r.Body != nil {
|
|
||||||
b, err = ioutil.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Increment to add the first call (attempts denotes number of retries)
|
|
||||||
attempts++
|
|
||||||
for attempt := 0; attempt < attempts; attempt++ {
|
|
||||||
r.Body = ioutil.NopCloser(bytes.NewBuffer(b))
|
|
||||||
resp, err = s.Do(r)
|
|
||||||
if err != nil || !ResponseHasStatusCode(resp, codes...) {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
DelayForBackoff(backoff, attempt, r.Cancel)
|
|
||||||
}
|
|
||||||
return resp, err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoRetryForDuration returns a SendDecorator that retries the request until the total time is equal
|
|
||||||
// to or greater than the specified duration, exponentially backing off between requests using the
|
|
||||||
// supplied backoff time.Duration (which may be zero). Retrying may be canceled by closing the
|
|
||||||
// optional channel on the http.Request.
|
|
||||||
func DoRetryForDuration(d time.Duration, backoff time.Duration) SendDecorator {
|
|
||||||
return func(s Sender) Sender {
|
|
||||||
return SenderFunc(func(r *http.Request) (resp *http.Response, err error) {
|
|
||||||
end := time.Now().Add(d)
|
|
||||||
for attempt := 0; time.Now().Before(end); attempt++ {
|
|
||||||
resp, err = s.Do(r)
|
|
||||||
if err == nil {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
DelayForBackoff(backoff, attempt, r.Cancel)
|
|
||||||
}
|
|
||||||
return resp, err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithLogging returns a SendDecorator that implements simple before and after logging of the
|
|
||||||
// request.
|
|
||||||
func WithLogging(logger *log.Logger) SendDecorator {
|
|
||||||
return func(s Sender) Sender {
|
|
||||||
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
|
||||||
logger.Printf("Sending %s %s", r.Method, r.URL)
|
|
||||||
resp, err := s.Do(r)
|
|
||||||
if err != nil {
|
|
||||||
logger.Printf("%s %s received error '%v'", r.Method, r.URL, err)
|
|
||||||
} else {
|
|
||||||
logger.Printf("%s %s received %s", r.Method, r.URL, resp.Status)
|
|
||||||
}
|
|
||||||
return resp, err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DelayForBackoff invokes time.After for the supplied backoff duration raised to the power of
|
|
||||||
// passed attempt (i.e., an exponential backoff delay). Backoff duration is in seconds and can set
|
|
||||||
// to zero for no delay. The delay may be canceled by closing the passed channel. If terminated early,
|
|
||||||
// returns false.
|
|
||||||
// Note: Passing attempt 1 will result in doubling "backoff" duration. Treat this as a zero-based attempt
|
|
||||||
// count.
|
|
||||||
func DelayForBackoff(backoff time.Duration, attempt int, cancel <-chan struct{}) bool {
|
|
||||||
select {
|
|
||||||
case <-time.After(time.Duration(backoff.Seconds()*math.Pow(2, float64(attempt))) * time.Second):
|
|
||||||
return true
|
|
||||||
case <-cancel:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,178 +0,0 @@
|
||||||
package autorest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/url"
|
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EncodedAs is a series of constants specifying various data encodings
|
|
||||||
type EncodedAs string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// EncodedAsJSON states that data is encoded as JSON
|
|
||||||
EncodedAsJSON EncodedAs = "JSON"
|
|
||||||
|
|
||||||
// EncodedAsXML states that data is encoded as Xml
|
|
||||||
EncodedAsXML EncodedAs = "XML"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Decoder defines the decoding method json.Decoder and xml.Decoder share
|
|
||||||
type Decoder interface {
|
|
||||||
Decode(v interface{}) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDecoder creates a new decoder appropriate to the passed encoding.
|
|
||||||
// encodedAs specifies the type of encoding and r supplies the io.Reader containing the
|
|
||||||
// encoded data.
|
|
||||||
func NewDecoder(encodedAs EncodedAs, r io.Reader) Decoder {
|
|
||||||
if encodedAs == EncodedAsJSON {
|
|
||||||
return json.NewDecoder(r)
|
|
||||||
} else if encodedAs == EncodedAsXML {
|
|
||||||
return xml.NewDecoder(r)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyAndDecode decodes the data from the passed io.Reader while making a copy. Having a copy
|
|
||||||
// is especially useful if there is a chance the data will fail to decode.
|
|
||||||
// encodedAs specifies the expected encoding, r provides the io.Reader to the data, and v
|
|
||||||
// is the decoding destination.
|
|
||||||
func CopyAndDecode(encodedAs EncodedAs, r io.Reader, v interface{}) (bytes.Buffer, error) {
|
|
||||||
b := bytes.Buffer{}
|
|
||||||
return b, NewDecoder(encodedAs, io.TeeReader(r, &b)).Decode(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TeeReadCloser returns a ReadCloser that writes to w what it reads from rc.
|
|
||||||
// It utilizes io.TeeReader to copy the data read and has the same behavior when reading.
|
|
||||||
// Further, when it is closed, it ensures that rc is closed as well.
|
|
||||||
func TeeReadCloser(rc io.ReadCloser, w io.Writer) io.ReadCloser {
|
|
||||||
return &teeReadCloser{rc, io.TeeReader(rc, w)}
|
|
||||||
}
|
|
||||||
|
|
||||||
type teeReadCloser struct {
|
|
||||||
rc io.ReadCloser
|
|
||||||
r io.Reader
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *teeReadCloser) Read(p []byte) (int, error) {
|
|
||||||
return t.r.Read(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *teeReadCloser) Close() error {
|
|
||||||
return t.rc.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func containsInt(ints []int, n int) bool {
|
|
||||||
for _, i := range ints {
|
|
||||||
if i == n {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func escapeValueStrings(m map[string]string) map[string]string {
|
|
||||||
for key, value := range m {
|
|
||||||
m[key] = url.QueryEscape(value)
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureValueStrings(mapOfInterface map[string]interface{}) map[string]string {
|
|
||||||
mapOfStrings := make(map[string]string)
|
|
||||||
for key, value := range mapOfInterface {
|
|
||||||
mapOfStrings[key] = ensureValueString(value)
|
|
||||||
}
|
|
||||||
return mapOfStrings
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureValueString(value interface{}) string {
|
|
||||||
if value == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
switch v := value.(type) {
|
|
||||||
case string:
|
|
||||||
return v
|
|
||||||
case []byte:
|
|
||||||
return string(v)
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("%v", v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MapToValues method converts map[string]interface{} to url.Values.
|
|
||||||
func MapToValues(m map[string]interface{}) url.Values {
|
|
||||||
v := url.Values{}
|
|
||||||
for key, value := range m {
|
|
||||||
x := reflect.ValueOf(value)
|
|
||||||
if x.Kind() == reflect.Array || x.Kind() == reflect.Slice {
|
|
||||||
for i := 0; i < x.Len(); i++ {
|
|
||||||
v.Add(key, ensureValueString(x.Index(i)))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
v.Add(key, ensureValueString(value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// String method converts interface v to string. If interface is a list, it
|
|
||||||
// joins list elements using separator.
|
|
||||||
func String(v interface{}, sep ...string) string {
|
|
||||||
if len(sep) > 0 {
|
|
||||||
return ensureValueString(strings.Join(v.([]string), sep[0]))
|
|
||||||
}
|
|
||||||
return ensureValueString(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode method encodes url path and query parameters.
|
|
||||||
func Encode(location string, v interface{}, sep ...string) string {
|
|
||||||
s := String(v, sep...)
|
|
||||||
switch strings.ToLower(location) {
|
|
||||||
case "path":
|
|
||||||
return pathEscape(s)
|
|
||||||
case "query":
|
|
||||||
return queryEscape(s)
|
|
||||||
default:
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func pathEscape(s string) string {
|
|
||||||
return strings.Replace(url.QueryEscape(s), "+", "%20", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func queryEscape(s string) string {
|
|
||||||
return url.QueryEscape(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This method is same as Encode() method of "net/url" go package,
|
|
||||||
// except it does not encode the query parameters because they
|
|
||||||
// already come encoded. It formats values map in query format (bar=foo&a=b).
|
|
||||||
func createQuery(v url.Values) string {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
keys := make([]string, 0, len(v))
|
|
||||||
for k := range v {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
for _, k := range keys {
|
|
||||||
vs := v[k]
|
|
||||||
prefix := url.QueryEscape(k) + "="
|
|
||||||
for _, v := range vs {
|
|
||||||
if buf.Len() > 0 {
|
|
||||||
buf.WriteByte('&')
|
|
||||||
}
|
|
||||||
buf.WriteString(prefix)
|
|
||||||
buf.WriteString(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
|
@ -1,373 +0,0 @@
|
||||||
/*
|
|
||||||
Package validation provides methods for validating parameter value using reflection.
|
|
||||||
*/
|
|
||||||
package validation
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Constraint stores constraint name, target field name
|
|
||||||
// Rule and chain validations.
|
|
||||||
type Constraint struct {
|
|
||||||
|
|
||||||
// Target field name for validation.
|
|
||||||
Target string
|
|
||||||
|
|
||||||
// Constraint name e.g. minLength, MaxLength, Pattern, etc.
|
|
||||||
Name string
|
|
||||||
|
|
||||||
// Rule for constraint e.g. greater than 10, less than 5 etc.
|
|
||||||
Rule interface{}
|
|
||||||
|
|
||||||
// Chain Validations for struct type
|
|
||||||
Chain []Constraint
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validation stores parameter-wise validation.
|
|
||||||
type Validation struct {
|
|
||||||
TargetValue interface{}
|
|
||||||
Constraints []Constraint
|
|
||||||
}
|
|
||||||
|
|
||||||
// Constraint list
|
|
||||||
const (
|
|
||||||
Empty = "Empty"
|
|
||||||
Null = "Null"
|
|
||||||
ReadOnly = "ReadOnly"
|
|
||||||
Pattern = "Pattern"
|
|
||||||
MaxLength = "MaxLength"
|
|
||||||
MinLength = "MinLength"
|
|
||||||
MaxItems = "MaxItems"
|
|
||||||
MinItems = "MinItems"
|
|
||||||
MultipleOf = "MultipleOf"
|
|
||||||
UniqueItems = "UniqueItems"
|
|
||||||
InclusiveMaximum = "InclusiveMaximum"
|
|
||||||
ExclusiveMaximum = "ExclusiveMaximum"
|
|
||||||
ExclusiveMinimum = "ExclusiveMinimum"
|
|
||||||
InclusiveMinimum = "InclusiveMinimum"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Validate method validates constraints on parameter
|
|
||||||
// passed in validation array.
|
|
||||||
func Validate(m []Validation) error {
|
|
||||||
for _, item := range m {
|
|
||||||
v := reflect.ValueOf(item.TargetValue)
|
|
||||||
for _, constraint := range item.Constraints {
|
|
||||||
var err error
|
|
||||||
switch v.Kind() {
|
|
||||||
case reflect.Ptr:
|
|
||||||
err = validatePtr(v, constraint)
|
|
||||||
case reflect.String:
|
|
||||||
err = validateString(v, constraint)
|
|
||||||
case reflect.Struct:
|
|
||||||
err = validateStruct(v, constraint)
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
||||||
err = validateInt(v, constraint)
|
|
||||||
case reflect.Float32, reflect.Float64:
|
|
||||||
err = validateFloat(v, constraint)
|
|
||||||
case reflect.Array, reflect.Slice, reflect.Map:
|
|
||||||
err = validateArrayMap(v, constraint)
|
|
||||||
default:
|
|
||||||
err = createError(v, constraint, fmt.Sprintf("unknown type %v", v.Kind()))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateStruct(x reflect.Value, v Constraint, name ...string) error {
|
|
||||||
//Get field name from target name which is in format a.b.c
|
|
||||||
s := strings.Split(v.Target, ".")
|
|
||||||
f := x.FieldByName(s[len(s)-1])
|
|
||||||
if isZero(f) {
|
|
||||||
return createError(x, v, fmt.Sprintf("field %q doesn't exist", v.Target))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := Validate([]Validation{
|
|
||||||
{
|
|
||||||
TargetValue: getInterfaceValue(f),
|
|
||||||
Constraints: []Constraint{v},
|
|
||||||
},
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validatePtr(x reflect.Value, v Constraint) error {
|
|
||||||
if v.Name == ReadOnly {
|
|
||||||
if !x.IsNil() {
|
|
||||||
return createError(x.Elem(), v, "readonly parameter; must send as nil or empty in request")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if x.IsNil() {
|
|
||||||
return checkNil(x, v)
|
|
||||||
}
|
|
||||||
if v.Chain != nil {
|
|
||||||
return Validate([]Validation{
|
|
||||||
{
|
|
||||||
TargetValue: getInterfaceValue(x.Elem()),
|
|
||||||
Constraints: v.Chain,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateInt(x reflect.Value, v Constraint) error {
|
|
||||||
i := x.Int()
|
|
||||||
r, ok := v.Rule.(int)
|
|
||||||
if !ok {
|
|
||||||
return createError(x, v, fmt.Sprintf("rule must be integer value for %v constraint; got: %v", v.Name, v.Rule))
|
|
||||||
}
|
|
||||||
switch v.Name {
|
|
||||||
case MultipleOf:
|
|
||||||
if i%int64(r) != 0 {
|
|
||||||
return createError(x, v, fmt.Sprintf("value must be a multiple of %v", r))
|
|
||||||
}
|
|
||||||
case ExclusiveMinimum:
|
|
||||||
if i <= int64(r) {
|
|
||||||
return createError(x, v, fmt.Sprintf("value must be greater than %v", r))
|
|
||||||
}
|
|
||||||
case ExclusiveMaximum:
|
|
||||||
if i >= int64(r) {
|
|
||||||
return createError(x, v, fmt.Sprintf("value must be less than %v", r))
|
|
||||||
}
|
|
||||||
case InclusiveMinimum:
|
|
||||||
if i < int64(r) {
|
|
||||||
return createError(x, v, fmt.Sprintf("value must be greater than or equal to %v", r))
|
|
||||||
}
|
|
||||||
case InclusiveMaximum:
|
|
||||||
if i > int64(r) {
|
|
||||||
return createError(x, v, fmt.Sprintf("value must be less than or equal to %v", r))
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return createError(x, v, fmt.Sprintf("constraint %v is not applicable for type integer", v.Name))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateFloat(x reflect.Value, v Constraint) error {
|
|
||||||
f := x.Float()
|
|
||||||
r, ok := v.Rule.(float64)
|
|
||||||
if !ok {
|
|
||||||
return createError(x, v, fmt.Sprintf("rule must be float value for %v constraint; got: %v", v.Name, v.Rule))
|
|
||||||
}
|
|
||||||
switch v.Name {
|
|
||||||
case ExclusiveMinimum:
|
|
||||||
if f <= r {
|
|
||||||
return createError(x, v, fmt.Sprintf("value must be greater than %v", r))
|
|
||||||
}
|
|
||||||
case ExclusiveMaximum:
|
|
||||||
if f >= r {
|
|
||||||
return createError(x, v, fmt.Sprintf("value must be less than %v", r))
|
|
||||||
}
|
|
||||||
case InclusiveMinimum:
|
|
||||||
if f < r {
|
|
||||||
return createError(x, v, fmt.Sprintf("value must be greater than or equal to %v", r))
|
|
||||||
}
|
|
||||||
case InclusiveMaximum:
|
|
||||||
if f > r {
|
|
||||||
return createError(x, v, fmt.Sprintf("value must be less than or equal to %v", r))
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return createError(x, v, fmt.Sprintf("constraint %s is not applicable for type float", v.Name))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateString(x reflect.Value, v Constraint) error {
|
|
||||||
s := x.String()
|
|
||||||
switch v.Name {
|
|
||||||
case Empty:
|
|
||||||
if len(s) == 0 {
|
|
||||||
return checkEmpty(x, v)
|
|
||||||
}
|
|
||||||
case Pattern:
|
|
||||||
reg, err := regexp.Compile(v.Rule.(string))
|
|
||||||
if err != nil {
|
|
||||||
return createError(x, v, err.Error())
|
|
||||||
}
|
|
||||||
if !reg.MatchString(s) {
|
|
||||||
return createError(x, v, fmt.Sprintf("value doesn't match pattern %v", v.Rule))
|
|
||||||
}
|
|
||||||
case MaxLength:
|
|
||||||
if _, ok := v.Rule.(int); !ok {
|
|
||||||
return createError(x, v, fmt.Sprintf("rule must be integer value for %v constraint; got: %v", v.Name, v.Rule))
|
|
||||||
}
|
|
||||||
if len(s) > v.Rule.(int) {
|
|
||||||
return createError(x, v, fmt.Sprintf("value length must be less than %v", v.Rule))
|
|
||||||
}
|
|
||||||
case MinLength:
|
|
||||||
if _, ok := v.Rule.(int); !ok {
|
|
||||||
return createError(x, v, fmt.Sprintf("rule must be integer value for %v constraint; got: %v", v.Name, v.Rule))
|
|
||||||
}
|
|
||||||
if len(s) < v.Rule.(int) {
|
|
||||||
return createError(x, v, fmt.Sprintf("value length must be greater than %v", v.Rule))
|
|
||||||
}
|
|
||||||
case ReadOnly:
|
|
||||||
if len(s) > 0 {
|
|
||||||
return createError(reflect.ValueOf(s), v, "readonly parameter; must send as nil or empty in request")
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return createError(x, v, fmt.Sprintf("constraint %s is not applicable to string type", v.Name))
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.Chain != nil {
|
|
||||||
return Validate([]Validation{
|
|
||||||
{
|
|
||||||
TargetValue: getInterfaceValue(x),
|
|
||||||
Constraints: v.Chain,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateArrayMap(x reflect.Value, v Constraint) error {
|
|
||||||
switch v.Name {
|
|
||||||
case Null:
|
|
||||||
if x.IsNil() {
|
|
||||||
return checkNil(x, v)
|
|
||||||
}
|
|
||||||
case Empty:
|
|
||||||
if x.IsNil() || x.Len() == 0 {
|
|
||||||
return checkEmpty(x, v)
|
|
||||||
}
|
|
||||||
case MaxItems:
|
|
||||||
if _, ok := v.Rule.(int); !ok {
|
|
||||||
return createError(x, v, fmt.Sprintf("rule must be integer for %v constraint; got: %v", v.Name, v.Rule))
|
|
||||||
}
|
|
||||||
if x.Len() > v.Rule.(int) {
|
|
||||||
return createError(x, v, fmt.Sprintf("maximum item limit is %v; got: %v", v.Rule, x.Len()))
|
|
||||||
}
|
|
||||||
case MinItems:
|
|
||||||
if _, ok := v.Rule.(int); !ok {
|
|
||||||
return createError(x, v, fmt.Sprintf("rule must be integer for %v constraint; got: %v", v.Name, v.Rule))
|
|
||||||
}
|
|
||||||
if x.Len() < v.Rule.(int) {
|
|
||||||
return createError(x, v, fmt.Sprintf("minimum item limit is %v; got: %v", v.Rule, x.Len()))
|
|
||||||
}
|
|
||||||
case UniqueItems:
|
|
||||||
if x.Kind() == reflect.Array || x.Kind() == reflect.Slice {
|
|
||||||
if !checkForUniqueInArray(x) {
|
|
||||||
return createError(x, v, fmt.Sprintf("all items in parameter %q must be unique; got:%v", v.Target, x))
|
|
||||||
}
|
|
||||||
} else if x.Kind() == reflect.Map {
|
|
||||||
if !checkForUniqueInMap(x) {
|
|
||||||
return createError(x, v, fmt.Sprintf("all items in parameter %q must be unique; got:%v", v.Target, x))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return createError(x, v, fmt.Sprintf("type must be array, slice or map for constraint %v; got: %v", v.Name, x.Kind()))
|
|
||||||
}
|
|
||||||
case ReadOnly:
|
|
||||||
if x.Len() != 0 {
|
|
||||||
return createError(x, v, "readonly parameter; must send as nil or empty in request")
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return createError(x, v, fmt.Sprintf("constraint %v is not applicable to array, slice and map type", v.Name))
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.Chain != nil {
|
|
||||||
return Validate([]Validation{
|
|
||||||
{
|
|
||||||
TargetValue: getInterfaceValue(x),
|
|
||||||
Constraints: v.Chain,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkNil(x reflect.Value, v Constraint) error {
|
|
||||||
if _, ok := v.Rule.(bool); !ok {
|
|
||||||
return createError(x, v, fmt.Sprintf("rule must be bool value for %v constraint; got: %v", v.Name, v.Rule))
|
|
||||||
}
|
|
||||||
if v.Rule.(bool) {
|
|
||||||
return createError(x, v, "value can not be null; required parameter")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkEmpty(x reflect.Value, v Constraint) error {
|
|
||||||
if _, ok := v.Rule.(bool); !ok {
|
|
||||||
return createError(x, v, fmt.Sprintf("rule must be bool value for %v constraint; got: %v", v.Name, v.Rule))
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.Rule.(bool) {
|
|
||||||
return createError(x, v, "value can not be null or empty; required parameter")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkForUniqueInArray(x reflect.Value) bool {
|
|
||||||
if x == reflect.Zero(reflect.TypeOf(x)) || x.Len() == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
arrOfInterface := make([]interface{}, x.Len())
|
|
||||||
|
|
||||||
for i := 0; i < x.Len(); i++ {
|
|
||||||
arrOfInterface[i] = x.Index(i).Interface()
|
|
||||||
}
|
|
||||||
|
|
||||||
m := make(map[interface{}]bool)
|
|
||||||
for _, val := range arrOfInterface {
|
|
||||||
if m[val] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
m[val] = true
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkForUniqueInMap(x reflect.Value) bool {
|
|
||||||
if x == reflect.Zero(reflect.TypeOf(x)) || x.Len() == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
mapOfInterface := make(map[interface{}]interface{}, x.Len())
|
|
||||||
|
|
||||||
keys := x.MapKeys()
|
|
||||||
for _, k := range keys {
|
|
||||||
mapOfInterface[k.Interface()] = x.MapIndex(k).Interface()
|
|
||||||
}
|
|
||||||
|
|
||||||
m := make(map[interface{}]bool)
|
|
||||||
for _, val := range mapOfInterface {
|
|
||||||
if m[val] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
m[val] = true
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func getInterfaceValue(x reflect.Value) interface{} {
|
|
||||||
if x.Kind() == reflect.Invalid {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return x.Interface()
|
|
||||||
}
|
|
||||||
|
|
||||||
func isZero(x interface{}) bool {
|
|
||||||
return x == reflect.Zero(reflect.TypeOf(x)).Interface()
|
|
||||||
}
|
|
||||||
|
|
||||||
func createError(x reflect.Value, v Constraint, err string) error {
|
|
||||||
return fmt.Errorf("autorest/validation: validation failed: parameter=%s constraint=%s value=%#v details: %s",
|
|
||||||
v.Target, v.Name, getInterfaceValue(x), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewErrorWithValidationError appends package type and method name in
|
|
||||||
// validation error.
|
|
||||||
func NewErrorWithValidationError(err error, packageType, method string) error {
|
|
||||||
return fmt.Errorf("%s#%s: Invalid input: %v", packageType, method, err)
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
package autorest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
major = 8
|
|
||||||
minor = 0
|
|
||||||
patch = 0
|
|
||||||
tag = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
var once sync.Once
|
|
||||||
var version string
|
|
||||||
|
|
||||||
// Version returns the semantic version (see http://semver.org).
|
|
||||||
func Version() string {
|
|
||||||
once.Do(func() {
|
|
||||||
semver := fmt.Sprintf("%d.%d.%d", major, minor, patch)
|
|
||||||
verBuilder := bytes.NewBufferString(semver)
|
|
||||||
if tag != "" && tag != "-" {
|
|
||||||
updated := strings.TrimPrefix(tag, "-")
|
|
||||||
_, err := verBuilder.WriteString("-" + updated)
|
|
||||||
if err == nil {
|
|
||||||
verBuilder = bytes.NewBufferString(semver)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
version = verBuilder.String()
|
|
||||||
})
|
|
||||||
return version
|
|
||||||
}
|
|
|
@ -1,191 +0,0 @@
|
||||||
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:
|
|
||||||
|
|
||||||
You must give any other recipients of the Work or Derivative Works a copy of
|
|
||||||
this License; and
|
|
||||||
You must cause any modified files to carry prominent notices stating that You
|
|
||||||
changed the files; and
|
|
||||||
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
|
|
||||||
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.
|
|
|
@ -1,20 +0,0 @@
|
||||||
Common Functions
|
|
||||||
================
|
|
||||||
|
|
||||||
[![Build Status](https://travis-ci.org/Unknwon/com.svg)](https://travis-ci.org/Unknwon/com) [![Go Walker](http://gowalker.org/api/v1/badge)](http://gowalker.org/github.com/Unknwon/com)
|
|
||||||
|
|
||||||
This is an open source project for commonly used functions for the Go programming language.
|
|
||||||
|
|
||||||
This package need >= **go 1.2**
|
|
||||||
|
|
||||||
Code Convention: based on [Go Code Convention](https://github.com/Unknwon/go-code-convention).
|
|
||||||
|
|
||||||
## Contribute
|
|
||||||
|
|
||||||
Your contribute is welcome, but you have to check following steps after you added some functions and commit them:
|
|
||||||
|
|
||||||
1. Make sure you wrote user-friendly comments for **all functions** .
|
|
||||||
2. Make sure you wrote test cases with any possible condition for **all functions** in file `*_test.go`.
|
|
||||||
3. Make sure you wrote benchmarks for **all functions** in file `*_test.go`.
|
|
||||||
4. Make sure you wrote useful examples for **all functions** in file `example_test.go`.
|
|
||||||
5. Make sure you ran `go test` and got **PASS** .
|
|
|
@ -1,161 +0,0 @@
|
||||||
// +build go1.2
|
|
||||||
|
|
||||||
// Copyright 2013 com authors
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
// Package com is an open source project for commonly used functions for the Go programming language.
|
|
||||||
package com
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ExecCmdDirBytes executes system command in given directory
|
|
||||||
// and return stdout, stderr in bytes type, along with possible error.
|
|
||||||
func ExecCmdDirBytes(dir, cmdName string, args ...string) ([]byte, []byte, error) {
|
|
||||||
bufOut := new(bytes.Buffer)
|
|
||||||
bufErr := new(bytes.Buffer)
|
|
||||||
|
|
||||||
cmd := exec.Command(cmdName, args...)
|
|
||||||
cmd.Dir = dir
|
|
||||||
cmd.Stdout = bufOut
|
|
||||||
cmd.Stderr = bufErr
|
|
||||||
|
|
||||||
err := cmd.Run()
|
|
||||||
return bufOut.Bytes(), bufErr.Bytes(), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecCmdBytes executes system command
|
|
||||||
// and return stdout, stderr in bytes type, along with possible error.
|
|
||||||
func ExecCmdBytes(cmdName string, args ...string) ([]byte, []byte, error) {
|
|
||||||
return ExecCmdDirBytes("", cmdName, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecCmdDir executes system command in given directory
|
|
||||||
// and return stdout, stderr in string type, along with possible error.
|
|
||||||
func ExecCmdDir(dir, cmdName string, args ...string) (string, string, error) {
|
|
||||||
bufOut, bufErr, err := ExecCmdDirBytes(dir, cmdName, args...)
|
|
||||||
return string(bufOut), string(bufErr), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecCmd executes system command
|
|
||||||
// and return stdout, stderr in string type, along with possible error.
|
|
||||||
func ExecCmd(cmdName string, args ...string) (string, string, error) {
|
|
||||||
return ExecCmdDir("", cmdName, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// _________ .__ .____
|
|
||||||
// \_ ___ \ ____ | | ___________ | | ____ ____
|
|
||||||
// / \ \/ / _ \| | / _ \_ __ \ | | / _ \ / ___\
|
|
||||||
// \ \___( <_> ) |_( <_> ) | \/ | |__( <_> ) /_/ >
|
|
||||||
// \______ /\____/|____/\____/|__| |_______ \____/\___ /
|
|
||||||
// \/ \/ /_____/
|
|
||||||
|
|
||||||
// Color number constants.
|
|
||||||
const (
|
|
||||||
Gray = uint8(iota + 90)
|
|
||||||
Red
|
|
||||||
Green
|
|
||||||
Yellow
|
|
||||||
Blue
|
|
||||||
Magenta
|
|
||||||
//NRed = uint8(31) // Normal
|
|
||||||
EndColor = "\033[0m"
|
|
||||||
)
|
|
||||||
|
|
||||||
// getColorLevel returns colored level string by given level.
|
|
||||||
func getColorLevel(level string) string {
|
|
||||||
level = strings.ToUpper(level)
|
|
||||||
switch level {
|
|
||||||
case "TRAC":
|
|
||||||
return fmt.Sprintf("\033[%dm%s\033[0m", Blue, level)
|
|
||||||
case "ERRO":
|
|
||||||
return fmt.Sprintf("\033[%dm%s\033[0m", Red, level)
|
|
||||||
case "WARN":
|
|
||||||
return fmt.Sprintf("\033[%dm%s\033[0m", Magenta, level)
|
|
||||||
case "SUCC":
|
|
||||||
return fmt.Sprintf("\033[%dm%s\033[0m", Green, level)
|
|
||||||
default:
|
|
||||||
return level
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ColorLogS colors log and return colored content.
|
|
||||||
// Log format: <level> <content [highlight][path]> [ error ].
|
|
||||||
// Level: TRAC -> blue; ERRO -> red; WARN -> Magenta; SUCC -> green; others -> default.
|
|
||||||
// Content: default; path: yellow; error -> red.
|
|
||||||
// Level has to be surrounded by "[" and "]".
|
|
||||||
// Highlights have to be surrounded by "# " and " #"(space), "#" will be deleted.
|
|
||||||
// Paths have to be surrounded by "( " and " )"(space).
|
|
||||||
// Errors have to be surrounded by "[ " and " ]"(space).
|
|
||||||
// Note: it hasn't support windows yet, contribute is welcome.
|
|
||||||
func ColorLogS(format string, a ...interface{}) string {
|
|
||||||
log := fmt.Sprintf(format, a...)
|
|
||||||
|
|
||||||
var clog string
|
|
||||||
|
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
// Level.
|
|
||||||
i := strings.Index(log, "]")
|
|
||||||
if log[0] == '[' && i > -1 {
|
|
||||||
clog += "[" + getColorLevel(log[1:i]) + "]"
|
|
||||||
}
|
|
||||||
|
|
||||||
log = log[i+1:]
|
|
||||||
|
|
||||||
// Error.
|
|
||||||
log = strings.Replace(log, "[ ", fmt.Sprintf("[\033[%dm", Red), -1)
|
|
||||||
log = strings.Replace(log, " ]", EndColor+"]", -1)
|
|
||||||
|
|
||||||
// Path.
|
|
||||||
log = strings.Replace(log, "( ", fmt.Sprintf("(\033[%dm", Yellow), -1)
|
|
||||||
log = strings.Replace(log, " )", EndColor+")", -1)
|
|
||||||
|
|
||||||
// Highlights.
|
|
||||||
log = strings.Replace(log, "# ", fmt.Sprintf("\033[%dm", Gray), -1)
|
|
||||||
log = strings.Replace(log, " #", EndColor, -1)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Level.
|
|
||||||
i := strings.Index(log, "]")
|
|
||||||
if log[0] == '[' && i > -1 {
|
|
||||||
clog += "[" + log[1:i] + "]"
|
|
||||||
}
|
|
||||||
|
|
||||||
log = log[i+1:]
|
|
||||||
|
|
||||||
// Error.
|
|
||||||
log = strings.Replace(log, "[ ", "[", -1)
|
|
||||||
log = strings.Replace(log, " ]", "]", -1)
|
|
||||||
|
|
||||||
// Path.
|
|
||||||
log = strings.Replace(log, "( ", "(", -1)
|
|
||||||
log = strings.Replace(log, " )", ")", -1)
|
|
||||||
|
|
||||||
// Highlights.
|
|
||||||
log = strings.Replace(log, "# ", "", -1)
|
|
||||||
log = strings.Replace(log, " #", "", -1)
|
|
||||||
}
|
|
||||||
return clog + log
|
|
||||||
}
|
|
||||||
|
|
||||||
// ColorLog prints colored log to stdout.
|
|
||||||
// See color rules in function 'ColorLogS'.
|
|
||||||
func ColorLog(format string, a ...interface{}) {
|
|
||||||
fmt.Print(ColorLogS(format, a...))
|
|
||||||
}
|
|
|
@ -1,157 +0,0 @@
|
||||||
// Copyright 2014 com authors
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
package com
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Convert string to specify type.
|
|
||||||
type StrTo string
|
|
||||||
|
|
||||||
func (f StrTo) Exist() bool {
|
|
||||||
return string(f) != string(0x1E)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f StrTo) Uint8() (uint8, error) {
|
|
||||||
v, err := strconv.ParseUint(f.String(), 10, 8)
|
|
||||||
return uint8(v), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f StrTo) Int() (int, error) {
|
|
||||||
v, err := strconv.ParseInt(f.String(), 10, 0)
|
|
||||||
return int(v), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f StrTo) Int64() (int64, error) {
|
|
||||||
v, err := strconv.ParseInt(f.String(), 10, 64)
|
|
||||||
return int64(v), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f StrTo) MustUint8() uint8 {
|
|
||||||
v, _ := f.Uint8()
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f StrTo) MustInt() int {
|
|
||||||
v, _ := f.Int()
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f StrTo) MustInt64() int64 {
|
|
||||||
v, _ := f.Int64()
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f StrTo) String() string {
|
|
||||||
if f.Exist() {
|
|
||||||
return string(f)
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert any type to string.
|
|
||||||
func ToStr(value interface{}, args ...int) (s string) {
|
|
||||||
switch v := value.(type) {
|
|
||||||
case bool:
|
|
||||||
s = strconv.FormatBool(v)
|
|
||||||
case float32:
|
|
||||||
s = strconv.FormatFloat(float64(v), 'f', argInt(args).Get(0, -1), argInt(args).Get(1, 32))
|
|
||||||
case float64:
|
|
||||||
s = strconv.FormatFloat(v, 'f', argInt(args).Get(0, -1), argInt(args).Get(1, 64))
|
|
||||||
case int:
|
|
||||||
s = strconv.FormatInt(int64(v), argInt(args).Get(0, 10))
|
|
||||||
case int8:
|
|
||||||
s = strconv.FormatInt(int64(v), argInt(args).Get(0, 10))
|
|
||||||
case int16:
|
|
||||||
s = strconv.FormatInt(int64(v), argInt(args).Get(0, 10))
|
|
||||||
case int32:
|
|
||||||
s = strconv.FormatInt(int64(v), argInt(args).Get(0, 10))
|
|
||||||
case int64:
|
|
||||||
s = strconv.FormatInt(v, argInt(args).Get(0, 10))
|
|
||||||
case uint:
|
|
||||||
s = strconv.FormatUint(uint64(v), argInt(args).Get(0, 10))
|
|
||||||
case uint8:
|
|
||||||
s = strconv.FormatUint(uint64(v), argInt(args).Get(0, 10))
|
|
||||||
case uint16:
|
|
||||||
s = strconv.FormatUint(uint64(v), argInt(args).Get(0, 10))
|
|
||||||
case uint32:
|
|
||||||
s = strconv.FormatUint(uint64(v), argInt(args).Get(0, 10))
|
|
||||||
case uint64:
|
|
||||||
s = strconv.FormatUint(v, argInt(args).Get(0, 10))
|
|
||||||
case string:
|
|
||||||
s = v
|
|
||||||
case []byte:
|
|
||||||
s = string(v)
|
|
||||||
default:
|
|
||||||
s = fmt.Sprintf("%v", v)
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
type argInt []int
|
|
||||||
|
|
||||||
func (a argInt) Get(i int, args ...int) (r int) {
|
|
||||||
if i >= 0 && i < len(a) {
|
|
||||||
r = a[i]
|
|
||||||
} else if len(args) > 0 {
|
|
||||||
r = args[0]
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// HexStr2int converts hex format string to decimal number.
|
|
||||||
func HexStr2int(hexStr string) (int, error) {
|
|
||||||
num := 0
|
|
||||||
length := len(hexStr)
|
|
||||||
for i := 0; i < length; i++ {
|
|
||||||
char := hexStr[length-i-1]
|
|
||||||
factor := -1
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case char >= '0' && char <= '9':
|
|
||||||
factor = int(char) - '0'
|
|
||||||
case char >= 'a' && char <= 'f':
|
|
||||||
factor = int(char) - 'a' + 10
|
|
||||||
default:
|
|
||||||
return -1, fmt.Errorf("invalid hex: %s", string(char))
|
|
||||||
}
|
|
||||||
|
|
||||||
num += factor * PowInt(16, i)
|
|
||||||
}
|
|
||||||
return num, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Int2HexStr converts decimal number to hex format string.
|
|
||||||
func Int2HexStr(num int) (hex string) {
|
|
||||||
if num == 0 {
|
|
||||||
return "0"
|
|
||||||
}
|
|
||||||
|
|
||||||
for num > 0 {
|
|
||||||
r := num % 16
|
|
||||||
|
|
||||||
c := "?"
|
|
||||||
if r >= 0 && r <= 9 {
|
|
||||||
c = string(r + '0')
|
|
||||||
} else {
|
|
||||||
c = string(r + 'a' - 10)
|
|
||||||
}
|
|
||||||
hex = c + hex
|
|
||||||
num = num / 16
|
|
||||||
}
|
|
||||||
return hex
|
|
||||||
}
|
|
|
@ -1,173 +0,0 @@
|
||||||
// Copyright 2013 com authors
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
package com
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IsDir returns true if given path is a directory,
|
|
||||||
// or returns false when it's a file or does not exist.
|
|
||||||
func IsDir(dir string) bool {
|
|
||||||
f, e := os.Stat(dir)
|
|
||||||
if e != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return f.IsDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
func statDir(dirPath, recPath string, includeDir, isDirOnly bool) ([]string, error) {
|
|
||||||
dir, err := os.Open(dirPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer dir.Close()
|
|
||||||
|
|
||||||
fis, err := dir.Readdir(0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
statList := make([]string, 0)
|
|
||||||
for _, fi := range fis {
|
|
||||||
if strings.Contains(fi.Name(), ".DS_Store") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
relPath := path.Join(recPath, fi.Name())
|
|
||||||
curPath := path.Join(dirPath, fi.Name())
|
|
||||||
if fi.IsDir() {
|
|
||||||
if includeDir {
|
|
||||||
statList = append(statList, relPath+"/")
|
|
||||||
}
|
|
||||||
s, err := statDir(curPath, relPath, includeDir, isDirOnly)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
statList = append(statList, s...)
|
|
||||||
} else if !isDirOnly {
|
|
||||||
statList = append(statList, relPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return statList, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// StatDir gathers information of given directory by depth-first.
|
|
||||||
// It returns slice of file list and includes subdirectories if enabled;
|
|
||||||
// it returns error and nil slice when error occurs in underlying functions,
|
|
||||||
// or given path is not a directory or does not exist.
|
|
||||||
//
|
|
||||||
// Slice does not include given path itself.
|
|
||||||
// If subdirectories is enabled, they will have suffix '/'.
|
|
||||||
func StatDir(rootPath string, includeDir ...bool) ([]string, error) {
|
|
||||||
if !IsDir(rootPath) {
|
|
||||||
return nil, errors.New("not a directory or does not exist: " + rootPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
isIncludeDir := false
|
|
||||||
if len(includeDir) >= 1 {
|
|
||||||
isIncludeDir = includeDir[0]
|
|
||||||
}
|
|
||||||
return statDir(rootPath, "", isIncludeDir, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllSubDirs returns all subdirectories of given root path.
|
|
||||||
// Slice does not include given path itself.
|
|
||||||
func GetAllSubDirs(rootPath string) ([]string, error) {
|
|
||||||
if !IsDir(rootPath) {
|
|
||||||
return nil, errors.New("not a directory or does not exist: " + rootPath)
|
|
||||||
}
|
|
||||||
return statDir(rootPath, "", true, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFileListBySuffix returns an ordered list of file paths.
|
|
||||||
// It recognize if given path is a file, and don't do recursive find.
|
|
||||||
func GetFileListBySuffix(dirPath, suffix string) ([]string, error) {
|
|
||||||
if !IsExist(dirPath) {
|
|
||||||
return nil, fmt.Errorf("given path does not exist: %s", dirPath)
|
|
||||||
} else if IsFile(dirPath) {
|
|
||||||
return []string{dirPath}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given path is a directory.
|
|
||||||
dir, err := os.Open(dirPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
fis, err := dir.Readdir(0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
files := make([]string, 0, len(fis))
|
|
||||||
for _, fi := range fis {
|
|
||||||
if strings.HasSuffix(fi.Name(), suffix) {
|
|
||||||
files = append(files, path.Join(dirPath, fi.Name()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return files, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyDir copy files recursively from source to target directory.
|
|
||||||
//
|
|
||||||
// The filter accepts a function that process the path info.
|
|
||||||
// and should return true for need to filter.
|
|
||||||
//
|
|
||||||
// It returns error when error occurs in underlying functions.
|
|
||||||
func CopyDir(srcPath, destPath string, filters ...func(filePath string) bool) error {
|
|
||||||
// Check if target directory exists.
|
|
||||||
if IsExist(destPath) {
|
|
||||||
return errors.New("file or directory alreay exists: " + destPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := os.MkdirAll(destPath, os.ModePerm)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gather directory info.
|
|
||||||
infos, err := StatDir(srcPath, true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var filter func(filePath string) bool
|
|
||||||
if len(filters) > 0 {
|
|
||||||
filter = filters[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, info := range infos {
|
|
||||||
if filter != nil && filter(info) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
curPath := path.Join(destPath, info)
|
|
||||||
if strings.HasSuffix(info, "/") {
|
|
||||||
err = os.MkdirAll(curPath, os.ModePerm)
|
|
||||||
} else {
|
|
||||||
err = Copy(path.Join(srcPath, info), curPath)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,145 +0,0 @@
|
||||||
// Copyright 2013 com authors
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
package com
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"math"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Storage unit constants.
|
|
||||||
const (
|
|
||||||
Byte = 1
|
|
||||||
KByte = Byte * 1024
|
|
||||||
MByte = KByte * 1024
|
|
||||||
GByte = MByte * 1024
|
|
||||||
TByte = GByte * 1024
|
|
||||||
PByte = TByte * 1024
|
|
||||||
EByte = PByte * 1024
|
|
||||||
)
|
|
||||||
|
|
||||||
func logn(n, b float64) float64 {
|
|
||||||
return math.Log(n) / math.Log(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func humanateBytes(s uint64, base float64, sizes []string) string {
|
|
||||||
if s < 10 {
|
|
||||||
return fmt.Sprintf("%dB", s)
|
|
||||||
}
|
|
||||||
e := math.Floor(logn(float64(s), base))
|
|
||||||
suffix := sizes[int(e)]
|
|
||||||
val := float64(s) / math.Pow(base, math.Floor(e))
|
|
||||||
f := "%.0f"
|
|
||||||
if val < 10 {
|
|
||||||
f = "%.1f"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf(f+"%s", val, suffix)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HumaneFileSize calculates the file size and generate user-friendly string.
|
|
||||||
func HumaneFileSize(s uint64) string {
|
|
||||||
sizes := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"}
|
|
||||||
return humanateBytes(s, 1024, sizes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileMTime returns file modified time and possible error.
|
|
||||||
func FileMTime(file string) (int64, error) {
|
|
||||||
f, err := os.Stat(file)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return f.ModTime().Unix(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileSize returns file size in bytes and possible error.
|
|
||||||
func FileSize(file string) (int64, error) {
|
|
||||||
f, err := os.Stat(file)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return f.Size(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy copies file from source to target path.
|
|
||||||
func Copy(src, dest string) error {
|
|
||||||
// Gather file information to set back later.
|
|
||||||
si, err := os.Lstat(src)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle symbolic link.
|
|
||||||
if si.Mode()&os.ModeSymlink != 0 {
|
|
||||||
target, err := os.Readlink(src)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// NOTE: os.Chmod and os.Chtimes don't recoganize symbolic link,
|
|
||||||
// which will lead "no such file or directory" error.
|
|
||||||
return os.Symlink(target, dest)
|
|
||||||
}
|
|
||||||
|
|
||||||
sr, err := os.Open(src)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer sr.Close()
|
|
||||||
|
|
||||||
dw, err := os.Create(dest)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer dw.Close()
|
|
||||||
|
|
||||||
if _, err = io.Copy(dw, sr); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set back file information.
|
|
||||||
if err = os.Chtimes(dest, si.ModTime(), si.ModTime()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.Chmod(dest, si.Mode())
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteFile writes data to a file named by filename.
|
|
||||||
// If the file does not exist, WriteFile creates it
|
|
||||||
// and its upper level paths.
|
|
||||||
func WriteFile(filename string, data []byte) error {
|
|
||||||
os.MkdirAll(path.Dir(filename), os.ModePerm)
|
|
||||||
return ioutil.WriteFile(filename, data, 0655)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsFile returns true if given path is a file,
|
|
||||||
// or returns false when it's a directory or does not exist.
|
|
||||||
func IsFile(filePath string) bool {
|
|
||||||
f, e := os.Stat(filePath)
|
|
||||||
if e != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return !f.IsDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsExist checks whether a file or directory exists.
|
|
||||||
// It returns false when the file or directory does not exist.
|
|
||||||
func IsExist(path string) bool {
|
|
||||||
_, err := os.Stat(path)
|
|
||||||
return err == nil || os.IsExist(err)
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
// Copyright 2013 com authors
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
package com
|
|
||||||
|
|
||||||
import (
|
|
||||||
"html"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Html2JS converts []byte type of HTML content into JS format.
|
|
||||||
func Html2JS(data []byte) []byte {
|
|
||||||
s := string(data)
|
|
||||||
s = strings.Replace(s, `\`, `\\`, -1)
|
|
||||||
s = strings.Replace(s, "\n", `\n`, -1)
|
|
||||||
s = strings.Replace(s, "\r", "", -1)
|
|
||||||
s = strings.Replace(s, "\"", `\"`, -1)
|
|
||||||
s = strings.Replace(s, "<table>", "<table>", -1)
|
|
||||||
return []byte(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// encode html chars to string
|
|
||||||
func HtmlEncode(str string) string {
|
|
||||||
return html.EscapeString(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
// decode string to html chars
|
|
||||||
func HtmlDecode(str string) string {
|
|
||||||
return html.UnescapeString(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
// strip tags in html string
|
|
||||||
func StripTags(src string) string {
|
|
||||||
//去除style,script,html tag
|
|
||||||
re := regexp.MustCompile(`(?s)<(?:style|script)[^<>]*>.*?</(?:style|script)>|</?[a-z][a-z0-9]*[^<>]*>|<!--.*?-->`)
|
|
||||||
src = re.ReplaceAllString(src, "")
|
|
||||||
|
|
||||||
//trim all spaces(2+) into \n
|
|
||||||
re = regexp.MustCompile(`\s{2,}`)
|
|
||||||
src = re.ReplaceAllString(src, "\n")
|
|
||||||
|
|
||||||
return strings.TrimSpace(src)
|
|
||||||
}
|
|
||||||
|
|
||||||
// change \n to <br/>
|
|
||||||
func Nl2br(str string) string {
|
|
||||||
return strings.Replace(str, "\n", "<br/>", -1)
|
|
||||||
}
|
|
|
@ -1,201 +0,0 @@
|
||||||
// Copyright 2013 com authors
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
package com
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NotFoundError struct {
|
|
||||||
Message string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e NotFoundError) Error() string {
|
|
||||||
return e.Message
|
|
||||||
}
|
|
||||||
|
|
||||||
type RemoteError struct {
|
|
||||||
Host string
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *RemoteError) Error() string {
|
|
||||||
return e.Err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
var UserAgent = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1541.0 Safari/537.36"
|
|
||||||
|
|
||||||
// HttpCall makes HTTP method call.
|
|
||||||
func HttpCall(client *http.Client, method, url string, header http.Header, body io.Reader) (io.ReadCloser, error) {
|
|
||||||
req, err := http.NewRequest(method, url, body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.Header.Set("User-Agent", UserAgent)
|
|
||||||
for k, vs := range header {
|
|
||||||
req.Header[k] = vs
|
|
||||||
}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resp.StatusCode == 200 {
|
|
||||||
return resp.Body, nil
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
if resp.StatusCode == 404 { // 403 can be rate limit error. || resp.StatusCode == 403 {
|
|
||||||
err = fmt.Errorf("resource not found: %s", url)
|
|
||||||
} else {
|
|
||||||
err = fmt.Errorf("%s %s -> %d", method, url, resp.StatusCode)
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// HttpGet gets the specified resource.
|
|
||||||
// ErrNotFound is returned if the server responds with status 404.
|
|
||||||
func HttpGet(client *http.Client, url string, header http.Header) (io.ReadCloser, error) {
|
|
||||||
return HttpCall(client, "GET", url, header, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HttpPost posts the specified resource.
|
|
||||||
// ErrNotFound is returned if the server responds with status 404.
|
|
||||||
func HttpPost(client *http.Client, url string, header http.Header, body []byte) (io.ReadCloser, error) {
|
|
||||||
return HttpCall(client, "POST", url, header, bytes.NewBuffer(body))
|
|
||||||
}
|
|
||||||
|
|
||||||
// HttpGetToFile gets the specified resource and writes to file.
|
|
||||||
// ErrNotFound is returned if the server responds with status 404.
|
|
||||||
func HttpGetToFile(client *http.Client, url string, header http.Header, fileName string) error {
|
|
||||||
rc, err := HttpGet(client, url, header)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer rc.Close()
|
|
||||||
|
|
||||||
os.MkdirAll(path.Dir(fileName), os.ModePerm)
|
|
||||||
f, err := os.Create(fileName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
_, err = io.Copy(f, rc)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// HttpGetBytes gets the specified resource. ErrNotFound is returned if the server
|
|
||||||
// responds with status 404.
|
|
||||||
func HttpGetBytes(client *http.Client, url string, header http.Header) ([]byte, error) {
|
|
||||||
rc, err := HttpGet(client, url, header)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rc.Close()
|
|
||||||
return ioutil.ReadAll(rc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HttpGetJSON gets the specified resource and mapping to struct.
|
|
||||||
// ErrNotFound is returned if the server responds with status 404.
|
|
||||||
func HttpGetJSON(client *http.Client, url string, v interface{}) error {
|
|
||||||
rc, err := HttpGet(client, url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer rc.Close()
|
|
||||||
err = json.NewDecoder(rc).Decode(v)
|
|
||||||
if _, ok := err.(*json.SyntaxError); ok {
|
|
||||||
return fmt.Errorf("JSON syntax error at %s", url)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HttpPostJSON posts the specified resource with struct values,
|
|
||||||
// and maps results to struct.
|
|
||||||
// ErrNotFound is returned if the server responds with status 404.
|
|
||||||
func HttpPostJSON(client *http.Client, url string, body, v interface{}) error {
|
|
||||||
data, err := json.Marshal(body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rc, err := HttpPost(client, url, http.Header{"content-type": []string{"application/json"}}, data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer rc.Close()
|
|
||||||
err = json.NewDecoder(rc).Decode(v)
|
|
||||||
if _, ok := err.(*json.SyntaxError); ok {
|
|
||||||
return fmt.Errorf("JSON syntax error at %s", url)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// A RawFile describes a file that can be downloaded.
|
|
||||||
type RawFile interface {
|
|
||||||
Name() string
|
|
||||||
RawUrl() string
|
|
||||||
Data() []byte
|
|
||||||
SetData([]byte)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FetchFiles fetches files specified by the rawURL field in parallel.
|
|
||||||
func FetchFiles(client *http.Client, files []RawFile, header http.Header) error {
|
|
||||||
ch := make(chan error, len(files))
|
|
||||||
for i := range files {
|
|
||||||
go func(i int) {
|
|
||||||
p, err := HttpGetBytes(client, files[i].RawUrl(), nil)
|
|
||||||
if err != nil {
|
|
||||||
ch <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
files[i].SetData(p)
|
|
||||||
ch <- nil
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
for _ = range files {
|
|
||||||
if err := <-ch; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FetchFiles uses command `curl` to fetch files specified by the rawURL field in parallel.
|
|
||||||
func FetchFilesCurl(files []RawFile, curlOptions ...string) error {
|
|
||||||
ch := make(chan error, len(files))
|
|
||||||
for i := range files {
|
|
||||||
go func(i int) {
|
|
||||||
stdout, _, err := ExecCmd("curl", append(curlOptions, files[i].RawUrl())...)
|
|
||||||
if err != nil {
|
|
||||||
ch <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
files[i].SetData([]byte(stdout))
|
|
||||||
ch <- nil
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
for _ = range files {
|
|
||||||
if err := <-ch; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
// Copyright 2014 com authors
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
package com
|
|
||||||
|
|
||||||
// PowInt is int type of math.Pow function.
|
|
||||||
func PowInt(x int, y int) int {
|
|
||||||
if y <= 0 {
|
|
||||||
return 1
|
|
||||||
} else {
|
|
||||||
if y % 2 == 0 {
|
|
||||||
sqrt := PowInt(x, y/2)
|
|
||||||
return sqrt * sqrt
|
|
||||||
} else {
|
|
||||||
return PowInt(x, y-1) * x
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
// Copyright 2013 com authors
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
package com
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetGOPATHs returns all paths in GOPATH variable.
|
|
||||||
func GetGOPATHs() []string {
|
|
||||||
gopath := os.Getenv("GOPATH")
|
|
||||||
var paths []string
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
gopath = strings.Replace(gopath, "\\", "/", -1)
|
|
||||||
paths = strings.Split(gopath, ";")
|
|
||||||
} else {
|
|
||||||
paths = strings.Split(gopath, ":")
|
|
||||||
}
|
|
||||||
return paths
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSrcPath returns app. source code path.
|
|
||||||
// It only works when you have src. folder in GOPATH,
|
|
||||||
// it returns error not able to locate source folder path.
|
|
||||||
func GetSrcPath(importPath string) (appPath string, err error) {
|
|
||||||
paths := GetGOPATHs()
|
|
||||||
for _, p := range paths {
|
|
||||||
if IsExist(p + "/src/" + importPath + "/") {
|
|
||||||
appPath = p + "/src/" + importPath + "/"
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(appPath) == 0 {
|
|
||||||
return "", errors.New("Unable to locate source folder path")
|
|
||||||
}
|
|
||||||
|
|
||||||
appPath = filepath.Dir(appPath) + "/"
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
// Replace all '\' to '/'.
|
|
||||||
appPath = strings.Replace(appPath, "\\", "/", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return appPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HomeDir returns path of '~'(in Linux) on Windows,
|
|
||||||
// it returns error when the variable does not exist.
|
|
||||||
func HomeDir() (home string, err error) {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
home = os.Getenv("USERPROFILE")
|
|
||||||
if len(home) == 0 {
|
|
||||||
home = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
home = os.Getenv("HOME")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(home) == 0 {
|
|
||||||
return "", errors.New("Cannot specify home directory because it's empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
return home, nil
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
// Copyright 2013 com authors
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
package com
|
|
||||||
|
|
||||||
import "regexp"
|
|
||||||
|
|
||||||
const (
|
|
||||||
regex_email_pattern = `(?i)[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}`
|
|
||||||
regex_strict_email_pattern = `(?i)[A-Z0-9!#$%&'*+/=?^_{|}~-]+` +
|
|
||||||
`(?:\.[A-Z0-9!#$%&'*+/=?^_{|}~-]+)*` +
|
|
||||||
`@(?:[A-Z0-9](?:[A-Z0-9-]*[A-Z0-9])?\.)+` +
|
|
||||||
`[A-Z0-9](?:[A-Z0-9-]*[A-Z0-9])?`
|
|
||||||
regex_url_pattern = `(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?`
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
regex_email *regexp.Regexp
|
|
||||||
regex_strict_email *regexp.Regexp
|
|
||||||
regex_url *regexp.Regexp
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
regex_email = regexp.MustCompile(regex_email_pattern)
|
|
||||||
regex_strict_email = regexp.MustCompile(regex_strict_email_pattern)
|
|
||||||
regex_url = regexp.MustCompile(regex_url_pattern)
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate string is an email address, if not return false
|
|
||||||
// basically validation can match 99% cases
|
|
||||||
func IsEmail(email string) bool {
|
|
||||||
return regex_email.MatchString(email)
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate string is an email address, if not return false
|
|
||||||
// this validation omits RFC 2822
|
|
||||||
func IsEmailRFC(email string) bool {
|
|
||||||
return regex_strict_email.MatchString(email)
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate string is a url link, if not return false
|
|
||||||
// simple validation can match 99% cases
|
|
||||||
func IsUrl(url string) bool {
|
|
||||||
return regex_url.MatchString(url)
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
// Copyright 2013 com authors
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
package com
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AppendStr appends string to slice with no duplicates.
|
|
||||||
func AppendStr(strs []string, str string) []string {
|
|
||||||
for _, s := range strs {
|
|
||||||
if s == str {
|
|
||||||
return strs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return append(strs, str)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CompareSliceStr compares two 'string' type slices.
|
|
||||||
// It returns true if elements and order are both the same.
|
|
||||||
func CompareSliceStr(s1, s2 []string) bool {
|
|
||||||
if len(s1) != len(s2) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range s1 {
|
|
||||||
if s1[i] != s2[i] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// CompareSliceStr compares two 'string' type slices.
|
|
||||||
// It returns true if elements are the same, and ignores the order.
|
|
||||||
func CompareSliceStrU(s1, s2 []string) bool {
|
|
||||||
if len(s1) != len(s2) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range s1 {
|
|
||||||
for j := len(s2) - 1; j >= 0; j-- {
|
|
||||||
if s1[i] == s2[j] {
|
|
||||||
s2 = append(s2[:j], s2[j+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(s2) > 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsSliceContainsStr returns true if the string exists in given slice, ignore case.
|
|
||||||
func IsSliceContainsStr(sl []string, str string) bool {
|
|
||||||
str = strings.ToLower(str)
|
|
||||||
for _, s := range sl {
|
|
||||||
if strings.ToLower(s) == str {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsSliceContainsInt64 returns true if the int64 exists in given slice.
|
|
||||||
func IsSliceContainsInt64(sl []int64, i int64) bool {
|
|
||||||
for _, s := range sl {
|
|
||||||
if s == i {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
|
@ -1,243 +0,0 @@
|
||||||
// Copyright 2013 com authors
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
package com
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/aes"
|
|
||||||
"crypto/cipher"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
r "math/rand"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
"unicode"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AESEncrypt encrypts text and given key with AES.
|
|
||||||
func AESEncrypt(key, text []byte) ([]byte, error) {
|
|
||||||
block, err := aes.NewCipher(key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
b := base64.StdEncoding.EncodeToString(text)
|
|
||||||
ciphertext := make([]byte, aes.BlockSize+len(b))
|
|
||||||
iv := ciphertext[:aes.BlockSize]
|
|
||||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
cfb := cipher.NewCFBEncrypter(block, iv)
|
|
||||||
cfb.XORKeyStream(ciphertext[aes.BlockSize:], []byte(b))
|
|
||||||
return ciphertext, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AESDecrypt decrypts text and given key with AES.
|
|
||||||
func AESDecrypt(key, text []byte) ([]byte, error) {
|
|
||||||
block, err := aes.NewCipher(key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(text) < aes.BlockSize {
|
|
||||||
return nil, errors.New("ciphertext too short")
|
|
||||||
}
|
|
||||||
iv := text[:aes.BlockSize]
|
|
||||||
text = text[aes.BlockSize:]
|
|
||||||
cfb := cipher.NewCFBDecrypter(block, iv)
|
|
||||||
cfb.XORKeyStream(text, text)
|
|
||||||
data, err := base64.StdEncoding.DecodeString(string(text))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsLetter returns true if the 'l' is an English letter.
|
|
||||||
func IsLetter(l uint8) bool {
|
|
||||||
n := (l | 0x20) - 'a'
|
|
||||||
if n >= 0 && n < 26 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expand replaces {k} in template with match[k] or subs[atoi(k)] if k is not in match.
|
|
||||||
func Expand(template string, match map[string]string, subs ...string) string {
|
|
||||||
var p []byte
|
|
||||||
var i int
|
|
||||||
for {
|
|
||||||
i = strings.Index(template, "{")
|
|
||||||
if i < 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
p = append(p, template[:i]...)
|
|
||||||
template = template[i+1:]
|
|
||||||
i = strings.Index(template, "}")
|
|
||||||
if s, ok := match[template[:i]]; ok {
|
|
||||||
p = append(p, s...)
|
|
||||||
} else {
|
|
||||||
j, _ := strconv.Atoi(template[:i])
|
|
||||||
if j >= len(subs) {
|
|
||||||
p = append(p, []byte("Missing")...)
|
|
||||||
} else {
|
|
||||||
p = append(p, subs[j]...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
template = template[i+1:]
|
|
||||||
}
|
|
||||||
p = append(p, template...)
|
|
||||||
return string(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reverse s string, support unicode
|
|
||||||
func Reverse(s string) string {
|
|
||||||
n := len(s)
|
|
||||||
runes := make([]rune, n)
|
|
||||||
for _, rune := range s {
|
|
||||||
n--
|
|
||||||
runes[n] = rune
|
|
||||||
}
|
|
||||||
return string(runes[n:])
|
|
||||||
}
|
|
||||||
|
|
||||||
// RandomCreateBytes generate random []byte by specify chars.
|
|
||||||
func RandomCreateBytes(n int, alphabets ...byte) []byte {
|
|
||||||
const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
||||||
var bytes = make([]byte, n)
|
|
||||||
var randby bool
|
|
||||||
if num, err := rand.Read(bytes); num != n || err != nil {
|
|
||||||
r.Seed(time.Now().UnixNano())
|
|
||||||
randby = true
|
|
||||||
}
|
|
||||||
for i, b := range bytes {
|
|
||||||
if len(alphabets) == 0 {
|
|
||||||
if randby {
|
|
||||||
bytes[i] = alphanum[r.Intn(len(alphanum))]
|
|
||||||
} else {
|
|
||||||
bytes[i] = alphanum[b%byte(len(alphanum))]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if randby {
|
|
||||||
bytes[i] = alphabets[r.Intn(len(alphabets))]
|
|
||||||
} else {
|
|
||||||
bytes[i] = alphabets[b%byte(len(alphabets))]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToSnakeCase can convert all upper case characters in a string to
|
|
||||||
// underscore format.
|
|
||||||
//
|
|
||||||
// Some samples.
|
|
||||||
// "FirstName" => "first_name"
|
|
||||||
// "HTTPServer" => "http_server"
|
|
||||||
// "NoHTTPS" => "no_https"
|
|
||||||
// "GO_PATH" => "go_path"
|
|
||||||
// "GO PATH" => "go_path" // space is converted to underscore.
|
|
||||||
// "GO-PATH" => "go_path" // hyphen is converted to underscore.
|
|
||||||
//
|
|
||||||
// From https://github.com/huandu/xstrings
|
|
||||||
func ToSnakeCase(str string) string {
|
|
||||||
if len(str) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
var prev, r0, r1 rune
|
|
||||||
var size int
|
|
||||||
|
|
||||||
r0 = '_'
|
|
||||||
|
|
||||||
for len(str) > 0 {
|
|
||||||
prev = r0
|
|
||||||
r0, size = utf8.DecodeRuneInString(str)
|
|
||||||
str = str[size:]
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case r0 == utf8.RuneError:
|
|
||||||
buf.WriteByte(byte(str[0]))
|
|
||||||
|
|
||||||
case unicode.IsUpper(r0):
|
|
||||||
if prev != '_' {
|
|
||||||
buf.WriteRune('_')
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.WriteRune(unicode.ToLower(r0))
|
|
||||||
|
|
||||||
if len(str) == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
r0, size = utf8.DecodeRuneInString(str)
|
|
||||||
str = str[size:]
|
|
||||||
|
|
||||||
if !unicode.IsUpper(r0) {
|
|
||||||
buf.WriteRune(r0)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// find next non-upper-case character and insert `_` properly.
|
|
||||||
// it's designed to convert `HTTPServer` to `http_server`.
|
|
||||||
// if there are more than 2 adjacent upper case characters in a word,
|
|
||||||
// treat them as an abbreviation plus a normal word.
|
|
||||||
for len(str) > 0 {
|
|
||||||
r1 = r0
|
|
||||||
r0, size = utf8.DecodeRuneInString(str)
|
|
||||||
str = str[size:]
|
|
||||||
|
|
||||||
if r0 == utf8.RuneError {
|
|
||||||
buf.WriteRune(unicode.ToLower(r1))
|
|
||||||
buf.WriteByte(byte(str[0]))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if !unicode.IsUpper(r0) {
|
|
||||||
if r0 == '_' || r0 == ' ' || r0 == '-' {
|
|
||||||
r0 = '_'
|
|
||||||
|
|
||||||
buf.WriteRune(unicode.ToLower(r1))
|
|
||||||
} else {
|
|
||||||
buf.WriteRune('_')
|
|
||||||
buf.WriteRune(unicode.ToLower(r1))
|
|
||||||
buf.WriteRune(r0)
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.WriteRune(unicode.ToLower(r1))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(str) == 0 || r0 == '_' {
|
|
||||||
buf.WriteRune(unicode.ToLower(r0))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
if r0 == ' ' || r0 == '-' {
|
|
||||||
r0 = '_'
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.WriteRune(r0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
|
@ -1,115 +0,0 @@
|
||||||
// Copyright 2013 com authors
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
package com
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Format unix time int64 to string
|
|
||||||
func Date(ti int64, format string) string {
|
|
||||||
t := time.Unix(int64(ti), 0)
|
|
||||||
return DateT(t, format)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format unix time string to string
|
|
||||||
func DateS(ts string, format string) string {
|
|
||||||
i, _ := strconv.ParseInt(ts, 10, 64)
|
|
||||||
return Date(i, format)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format time.Time struct to string
|
|
||||||
// MM - month - 01
|
|
||||||
// M - month - 1, single bit
|
|
||||||
// DD - day - 02
|
|
||||||
// D - day 2
|
|
||||||
// YYYY - year - 2006
|
|
||||||
// YY - year - 06
|
|
||||||
// HH - 24 hours - 03
|
|
||||||
// H - 24 hours - 3
|
|
||||||
// hh - 12 hours - 03
|
|
||||||
// h - 12 hours - 3
|
|
||||||
// mm - minute - 04
|
|
||||||
// m - minute - 4
|
|
||||||
// ss - second - 05
|
|
||||||
// s - second = 5
|
|
||||||
func DateT(t time.Time, format string) string {
|
|
||||||
res := strings.Replace(format, "MM", t.Format("01"), -1)
|
|
||||||
res = strings.Replace(res, "M", t.Format("1"), -1)
|
|
||||||
res = strings.Replace(res, "DD", t.Format("02"), -1)
|
|
||||||
res = strings.Replace(res, "D", t.Format("2"), -1)
|
|
||||||
res = strings.Replace(res, "YYYY", t.Format("2006"), -1)
|
|
||||||
res = strings.Replace(res, "YY", t.Format("06"), -1)
|
|
||||||
res = strings.Replace(res, "HH", fmt.Sprintf("%02d", t.Hour()), -1)
|
|
||||||
res = strings.Replace(res, "H", fmt.Sprintf("%d", t.Hour()), -1)
|
|
||||||
res = strings.Replace(res, "hh", t.Format("03"), -1)
|
|
||||||
res = strings.Replace(res, "h", t.Format("3"), -1)
|
|
||||||
res = strings.Replace(res, "mm", t.Format("04"), -1)
|
|
||||||
res = strings.Replace(res, "m", t.Format("4"), -1)
|
|
||||||
res = strings.Replace(res, "ss", t.Format("05"), -1)
|
|
||||||
res = strings.Replace(res, "s", t.Format("5"), -1)
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// DateFormat pattern rules.
|
|
||||||
var datePatterns = []string{
|
|
||||||
// year
|
|
||||||
"Y", "2006", // A full numeric representation of a year, 4 digits Examples: 1999 or 2003
|
|
||||||
"y", "06", //A two digit representation of a year Examples: 99 or 03
|
|
||||||
|
|
||||||
// month
|
|
||||||
"m", "01", // Numeric representation of a month, with leading zeros 01 through 12
|
|
||||||
"n", "1", // Numeric representation of a month, without leading zeros 1 through 12
|
|
||||||
"M", "Jan", // A short textual representation of a month, three letters Jan through Dec
|
|
||||||
"F", "January", // A full textual representation of a month, such as January or March January through December
|
|
||||||
|
|
||||||
// day
|
|
||||||
"d", "02", // Day of the month, 2 digits with leading zeros 01 to 31
|
|
||||||
"j", "2", // Day of the month without leading zeros 1 to 31
|
|
||||||
|
|
||||||
// week
|
|
||||||
"D", "Mon", // A textual representation of a day, three letters Mon through Sun
|
|
||||||
"l", "Monday", // A full textual representation of the day of the week Sunday through Saturday
|
|
||||||
|
|
||||||
// time
|
|
||||||
"g", "3", // 12-hour format of an hour without leading zeros 1 through 12
|
|
||||||
"G", "15", // 24-hour format of an hour without leading zeros 0 through 23
|
|
||||||
"h", "03", // 12-hour format of an hour with leading zeros 01 through 12
|
|
||||||
"H", "15", // 24-hour format of an hour with leading zeros 00 through 23
|
|
||||||
|
|
||||||
"a", "pm", // Lowercase Ante meridiem and Post meridiem am or pm
|
|
||||||
"A", "PM", // Uppercase Ante meridiem and Post meridiem AM or PM
|
|
||||||
|
|
||||||
"i", "04", // Minutes with leading zeros 00 to 59
|
|
||||||
"s", "05", // Seconds, with leading zeros 00 through 59
|
|
||||||
|
|
||||||
// time zone
|
|
||||||
"T", "MST",
|
|
||||||
"P", "-07:00",
|
|
||||||
"O", "-0700",
|
|
||||||
|
|
||||||
// RFC 2822
|
|
||||||
"r", time.RFC1123Z,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse Date use PHP time format.
|
|
||||||
func DateParse(dateString, format string) (time.Time, error) {
|
|
||||||
replacer := strings.NewReplacer(datePatterns...)
|
|
||||||
format = replacer.Replace(format)
|
|
||||||
return time.ParseInLocation(format, dateString, time.Local)
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
// Copyright 2013 com authors
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
package com
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"net/url"
|
|
||||||
)
|
|
||||||
|
|
||||||
// url encode string, is + not %20
|
|
||||||
func UrlEncode(str string) string {
|
|
||||||
return url.QueryEscape(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
// url decode string
|
|
||||||
func UrlDecode(str string) (string, error) {
|
|
||||||
return url.QueryUnescape(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
// base64 encode
|
|
||||||
func Base64Encode(str string) string {
|
|
||||||
return base64.StdEncoding.EncodeToString([]byte(str))
|
|
||||||
}
|
|
||||||
|
|
||||||
// base64 decode
|
|
||||||
func Base64Decode(str string) (string, error) {
|
|
||||||
s, e := base64.StdEncoding.DecodeString(str)
|
|
||||||
return string(s), e
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
Developer Certificate of Origin
|
|
||||||
Version 1.1
|
|
||||||
|
|
||||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
|
||||||
660 York Street, Suite 102,
|
|
||||||
San Francisco, CA 94110 USA
|
|
||||||
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies of this
|
|
||||||
license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
|
|
||||||
Developer's Certificate of Origin 1.1
|
|
||||||
|
|
||||||
By making a contribution to this project, I certify that:
|
|
||||||
|
|
||||||
(a) The contribution was created in whole or in part by me and I
|
|
||||||
have the right to submit it under the open source license
|
|
||||||
indicated in the file; or
|
|
||||||
|
|
||||||
(b) The contribution is based upon previous work that, to the best
|
|
||||||
of my knowledge, is covered under an appropriate open source
|
|
||||||
license and I have the right under that license to submit that
|
|
||||||
work with modifications, whether created in whole or in part
|
|
||||||
by me, under the same open source license (unless I am
|
|
||||||
permitted to submit under a different license), as indicated
|
|
||||||
in the file; or
|
|
||||||
|
|
||||||
(c) The contribution was provided directly to me by some other
|
|
||||||
person who certified (a), (b) or (c) and I have not modified
|
|
||||||
it.
|
|
||||||
|
|
||||||
(d) I understand and agree that this project and the contribution
|
|
||||||
are public and that a record of the contribution (including all
|
|
||||||
personal information I submit with it, including my sign-off) is
|
|
||||||
maintained indefinitely and may be redistributed consistent with
|
|
||||||
this project or the open source license(s) involved.
|
|
|
@ -1,201 +0,0 @@
|
||||||
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.
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue