From c8f98588a07ae3d59f92d096616d1cdb4c0167c7 Mon Sep 17 00:00:00 2001 From: Outer-God <39217235+Outer-God@users.noreply.github.com> Date: Mon, 15 Mar 2021 20:02:56 +0800 Subject: [PATCH] Create ossClient.go (#574) * Create ossClient.go * Update ossClient.go * comments * Update ossClient.go * Update ossClient.go * Update ossClient.go * Update ossClient.go * Create vendor.go * vendor * del * vendor oss * del * aliyun * vendor Co-authored-by: Welt --- .../aliyun/aliyun-oss-go-sdk/.travis.yml | 45 + .../aliyun/aliyun-oss-go-sdk/CHANGELOG.md | 247 + vendor/github.com/aliyun/aliyun-oss-go-sdk/LICENSE | 14 + .../aliyun/aliyun-oss-go-sdk/README-CN.md | 169 + .../github.com/aliyun/aliyun-oss-go-sdk/README.md | 168 + .../aliyun/aliyun-oss-go-sdk/oss/auth.go | 190 + .../aliyun/aliyun-oss-go-sdk/oss/bucket.go | 1289 +++++ .../oss/bucket_credential_test.go | 616 +++ .../aliyun/aliyun-oss-go-sdk/oss/bucket_test.go | 5313 ++++++++++++++++++++ .../aliyun/aliyun-oss-go-sdk/oss/client.go | 1722 +++++++ .../aliyun/aliyun-oss-go-sdk/oss/client_test.go | 3788 ++++++++++++++ .../aliyun/aliyun-oss-go-sdk/oss/conf.go | 185 + .../aliyun/aliyun-oss-go-sdk/oss/conn.go | 793 +++ .../aliyun/aliyun-oss-go-sdk/oss/conn_test.go | 192 + .../aliyun/aliyun-oss-go-sdk/oss/const.go | 247 + .../github.com/aliyun/aliyun-oss-go-sdk/oss/crc.go | 123 + .../aliyun/aliyun-oss-go-sdk/oss/crc_test.go | 492 ++ .../aliyun/aliyun-oss-go-sdk/oss/crypto/aes_ctr.go | 68 + .../aliyun-oss-go-sdk/oss/crypto/aes_ctr_cipher.go | 153 + .../oss/crypto/aes_ctr_cipher_test.go | 41 + .../aliyun/aliyun-oss-go-sdk/oss/crypto/cipher.go | 69 + .../aliyun-oss-go-sdk/oss/crypto/cipher_test.go | 32 + .../aliyun-oss-go-sdk/oss/crypto/crypto_bucket.go | 539 ++ .../oss/crypto/crypto_bucket_test.go | 1226 +++++ .../aliyun-oss-go-sdk/oss/crypto/crypto_const.go | 26 + .../oss/crypto/crypto_download.go | 12 + .../oss/crypto/crypto_multicopy.go | 12 + .../oss/crypto/crypto_multipart.go | 174 + .../oss/crypto/crypto_multipart_test.go | 758 +++ .../aliyun-oss-go-sdk/oss/crypto/crypto_type.go | 128 + .../aliyun-oss-go-sdk/oss/crypto/crypto_upload.go | 12 + .../oss/crypto/master_alikms_cipher.go | 85 + .../oss/crypto/master_alikms_cipher_test.go | 89 + .../oss/crypto/master_rsa_cipher.go | 102 + .../oss/crypto/master_rsa_cipher_test.go | 41 + .../aliyun/aliyun-oss-go-sdk/oss/download.go | 567 +++ .../aliyun/aliyun-oss-go-sdk/oss/download_test.go | 979 ++++ .../aliyun/aliyun-oss-go-sdk/oss/error.go | 94 + .../aliyun/aliyun-oss-go-sdk/oss/error_test.go | 111 + .../aliyun-oss-go-sdk/oss/limit_reader_1_6.go | 28 + .../aliyun-oss-go-sdk/oss/limit_reader_1_7.go | 90 + .../aliyun/aliyun-oss-go-sdk/oss/livechannel.go | 257 + .../aliyun-oss-go-sdk/oss/livechannel_test.go | 428 ++ .../aliyun/aliyun-oss-go-sdk/oss/mime.go | 572 +++ .../aliyun/aliyun-oss-go-sdk/oss/model.go | 69 + .../aliyun/aliyun-oss-go-sdk/oss/multicopy.go | 474 ++ .../aliyun/aliyun-oss-go-sdk/oss/multicopy_test.go | 669 +++ .../aliyun/aliyun-oss-go-sdk/oss/multipart.go | 308 ++ .../aliyun/aliyun-oss-go-sdk/oss/multipart_test.go | 986 ++++ .../aliyun/aliyun-oss-go-sdk/oss/option.go | 638 +++ .../aliyun/aliyun-oss-go-sdk/oss/option_test.go | 317 ++ .../aliyun/aliyun-oss-go-sdk/oss/progress.go | 114 + .../aliyun/aliyun-oss-go-sdk/oss/progress_test.go | 582 +++ .../aliyun/aliyun-oss-go-sdk/oss/redirect_1_6.go | 11 + .../aliyun/aliyun-oss-go-sdk/oss/redirect_1_7.go | 12 + .../oss/select_csv_objcet_test.go | 627 +++ .../oss/select_json_object_test.go | 488 ++ .../aliyun/aliyun-oss-go-sdk/oss/select_object.go | 197 + .../oss/select_object_read_file_test.go | 542 ++ .../aliyun-oss-go-sdk/oss/select_object_type.go | 364 ++ .../aliyun/aliyun-oss-go-sdk/oss/transport_1_6.go | 34 + .../aliyun/aliyun-oss-go-sdk/oss/transport_1_7.go | 36 + .../aliyun/aliyun-oss-go-sdk/oss/type.go | 1247 +++++ .../aliyun/aliyun-oss-go-sdk/oss/type_test.go | 391 ++ .../aliyun/aliyun-oss-go-sdk/oss/upload.go | 552 ++ .../aliyun/aliyun-oss-go-sdk/oss/upload_test.go | 741 +++ .../aliyun/aliyun-oss-go-sdk/oss/utils.go | 512 ++ .../aliyun/aliyun-oss-go-sdk/oss/utils_test.go | 259 + .../github.com/aliyun/aliyun-oss-go-sdk/sample.go | 59 + .../sample/BingWallpaper-2015-11-07.jpg | Bin 0 -> 482048 bytes .../sample/The Go Programming Language.html | 3663 ++++++++++++++ .../aliyun-oss-go-sdk/sample/append_object.go | 164 + .../aliyun/aliyun-oss-go-sdk/sample/archive.go | 74 + .../aliyun/aliyun-oss-go-sdk/sample/bucket_acl.go | 43 + .../aliyun/aliyun-oss-go-sdk/sample/bucket_cors.go | 71 + .../aliyun-oss-go-sdk/sample/bucket_encryption.go | 51 + .../aliyun-oss-go-sdk/sample/bucket_inventory.go | 102 + .../aliyun-oss-go-sdk/sample/bucket_lifecycle.go | 121 + .../aliyun-oss-go-sdk/sample/bucket_logging.go | 90 + .../aliyun-oss-go-sdk/sample/bucket_policy.go | 67 + .../aliyun-oss-go-sdk/sample/bucket_qosInfo.go | 65 + .../aliyun-oss-go-sdk/sample/bucket_referer.go | 57 + .../sample/bucket_requestpayment.go | 117 + .../aliyun-oss-go-sdk/sample/bucket_website.go | 104 + .../aliyun-oss-go-sdk/sample/cname_sample.go | 95 + .../aliyun/aliyun-oss-go-sdk/sample/comm.go | 181 + .../aliyun/aliyun-oss-go-sdk/sample/config.go | 36 + .../aliyun/aliyun-oss-go-sdk/sample/copy_object.go | 138 + .../aliyun-oss-go-sdk/sample/create_bucket.go | 50 + .../aliyun-oss-go-sdk/sample/delete_object.go | 108 + .../aliyun/aliyun-oss-go-sdk/sample/get_object.go | 156 + .../aliyun-oss-go-sdk/sample/list_buckets.go | 129 + .../aliyun-oss-go-sdk/sample/list_objects.go | 148 + .../aliyun/aliyun-oss-go-sdk/sample/livechannel.go | 445 ++ .../aliyun/aliyun-oss-go-sdk/sample/new_bucket.go | 50 + .../aliyun/aliyun-oss-go-sdk/sample/object_acl.go | 44 + .../aliyun/aliyun-oss-go-sdk/sample/object_meta.go | 73 + .../aliyun-oss-go-sdk/sample/object_tagging.go | 75 + .../aliyun/aliyun-oss-go-sdk/sample/put_object.go | 145 + .../aliyun-oss-go-sdk/sample/sample_data.csv | 304 ++ .../aliyun-oss-go-sdk/sample/sample_json.json | 5154 +++++++++++++++++++ .../sample/sample_json_lines.json | 5145 +++++++++++++++++++ .../aliyun-oss-go-sdk/sample/select_object.go | 203 + .../aliyun/aliyun-oss-go-sdk/sample/sign_url.go | 80 + .../test-client-encryption-crypto-cpp-rsa.jpg | Bin 0 -> 21839 bytes .../test-client-encryption-crypto-python-rsa.jpg | Bin 0 -> 21839 bytes .../sample/test-client-encryption-src.jpg | Bin 0 -> 21839 bytes .../sample_crypto/sample_crypto.go | 304 ++ vendor/golang.org/x/time/AUTHORS | 3 + vendor/golang.org/x/time/CONTRIBUTING.md | 26 + vendor/golang.org/x/time/CONTRIBUTORS | 3 + vendor/golang.org/x/time/LICENSE | 27 + vendor/golang.org/x/time/PATENTS | 22 + vendor/golang.org/x/time/README.md | 19 + vendor/golang.org/x/time/go.mod | 1 + vendor/golang.org/x/time/rate/rate.go | 402 ++ vendor/golang.org/x/time/rate/rate_test.go | 477 ++ 117 files changed, 51347 insertions(+) create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/.travis.yml create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/CHANGELOG.md create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/LICENSE create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/README-CN.md create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/README.md create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/auth.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/bucket.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/bucket_credential_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/bucket_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/client.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/client_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/conf.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/conn.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/conn_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/const.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crc.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crc_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/aes_ctr.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/aes_ctr_cipher.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/aes_ctr_cipher_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/cipher.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/cipher_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_bucket.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_bucket_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_const.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_download.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_multicopy.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_multipart.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_multipart_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_type.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_upload.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/master_alikms_cipher.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/master_alikms_cipher_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/master_rsa_cipher.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/master_rsa_cipher_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/download.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/download_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/error.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/error_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/limit_reader_1_6.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/limit_reader_1_7.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/livechannel.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/livechannel_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/mime.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/model.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/multicopy.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/multicopy_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/multipart.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/multipart_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/option.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/option_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/progress.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/progress_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/redirect_1_6.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/redirect_1_7.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/select_csv_objcet_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/select_json_object_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/select_object.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/select_object_read_file_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/select_object_type.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/transport_1_6.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/transport_1_7.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/type.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/type_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/upload.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/upload_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/utils.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/utils_test.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/BingWallpaper-2015-11-07.jpg create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/The Go Programming Language.html create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/append_object.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/archive.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_acl.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_cors.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_encryption.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_inventory.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_lifecycle.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_logging.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_policy.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_qosInfo.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_referer.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_requestpayment.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_website.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/cname_sample.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/comm.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/config.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/copy_object.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/create_bucket.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/delete_object.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/get_object.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/list_buckets.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/list_objects.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/livechannel.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/new_bucket.go create mode 100755 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/object_acl.go create mode 100755 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/object_meta.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/object_tagging.go create mode 100755 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/put_object.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/sample_data.csv create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/sample_json.json create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/sample_json_lines.json create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/select_object.go create mode 100755 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/sign_url.go create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/test-client-encryption-crypto-cpp-rsa.jpg create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/test-client-encryption-crypto-python-rsa.jpg create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/test-client-encryption-src.jpg create mode 100644 vendor/github.com/aliyun/aliyun-oss-go-sdk/sample_crypto/sample_crypto.go create mode 100644 vendor/golang.org/x/time/AUTHORS create mode 100644 vendor/golang.org/x/time/CONTRIBUTING.md create mode 100644 vendor/golang.org/x/time/CONTRIBUTORS create mode 100644 vendor/golang.org/x/time/LICENSE create mode 100644 vendor/golang.org/x/time/PATENTS create mode 100644 vendor/golang.org/x/time/README.md create mode 100644 vendor/golang.org/x/time/go.mod create mode 100644 vendor/golang.org/x/time/rate/rate.go create mode 100644 vendor/golang.org/x/time/rate/rate_test.go diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/.travis.yml b/vendor/github.com/aliyun/aliyun-oss-go-sdk/.travis.yml new file mode 100644 index 00000000..516f1a2c --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/.travis.yml @@ -0,0 +1,45 @@ +language: go +go: +- 1.5 +- 1.6 +- 1.7 +- 1.8 +install: +- go get golang.org/x/tools/cmd/cover +- go get github.com/mattn/goveralls +- go get gopkg.in/check.v1 +- go get github.com/satori/go.uuid +- go get github.com/baiyubin/aliyun-sts-go-sdk/sts +- if [[ $TRAVIS_GO_VERSION = '1.7' || $TRAVIS_GO_VERSION > '1.7' ]]; then go get golang.org/x/time/rate + ; fi +script: +- if [[ ! -n "$OSS_TEST_ACCESS_KEY_ID" ]]; then exit 0 + ; fi + +#- cd oss +#- travis_wait 50 go test -v -covermode=count -coverprofile=coverage.out -timeout=50m +#- "$HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci" + +env: + global: + - secure: ZCL5egxJmZA+o1ujsaJe//AIh1ag/exSUJ2FzoKPF3O9c84hMc2k5EYO2rGzTNn1ML6M89Mo5hAvHQhyJEHjRuMtjc1QrfxAaA3mqm4scGXIXesPMqYGuvuPSh++6/fkAwaVBAhrk5GaDG1/FuxHE5zusGx3SvGegnCwO7n/2YCfXco6DWgVCdrz4p1EpPkAM3JIdHFUzsDWiimVuiNAvJmAT8+IeOPTT+WgusCJj4ORS3X3LddTjttBP+hRrp/pGSoNqPMzfysWybtaL2SJ8URtvsxW0Mo5BwocHAxAhPP+M2OscQbDzthSAezCLngYvrfBplfIyWlahlgzNz/FjXz5pQwWdYVNoibyxLLMOH685n75LNONN/xVO/GFmVPx7DMGapkN5NzIWS62D4v8QrRkwtms42OUkyEUHjDh8Evui3K2MNJVXA3TI9zOAR+C0krD7OEyS37qrppodhRxJSqFUlgXnk//wLldMC7vleDd7L2UQSWjqyBHqFOgsVaiLU2KRTY3zvv7ke+dqb5VF31mH6qAr8lJTR9un8M1att0VwCEKxoIRT4cKJCpEtZd8ovXOVt1uE695ThVXE9I5e00GXdTzqXOuv6zT4hv/dgmbz9JN9MYeCwmokEoIUmJKNYERa/bNVVefdnJt7h+dm+KpyPAS+XvPLzjbnWdYNA= + - secure: GdrPX7nUoZhB1DYTVD6x/vgA7H9dOlQc4n7nCsqEDyif+Y1XdPT83Ic3gSOt+cfy0/Kjh0/TT5xmLqpSh7wr7eyTpBPZGjz4ZbxBOcSLTfrf/spacgzla9I1335CvaTmpvrnvGUlOuVS8rb3J/+19dHlN6dfxX+ucjdfShR504d2JEcCLpTc1CEXAl+HEt3hM9gztOX5ykxyrtibDr0OPkNF7QjZ485V6UJkfyVlBM6JL59ywgh2dhdZn6JwmexHjVPsw6V8Ka07GzbpOs1e5eis42RUJe8eSqRRToCcTUbA9HOgWXswuu5k7nAwErygX2ub3hZ+yIjc+9JLsiy6F69RaUPVFlxfw8s5NLeInTIt28+A6iaf3X2k4lOaVFytgTl7lkYGNWz4eV/vXf2H4wZmaZn5OI0WKd3WuEJ04rsm7xzx9rC8znnsI3R1BHfapU/y6z2QGjgJsHqZmgfvXNKgSOurM6O/nlDnEsYOwYLQLhpeXVFNmbo+M77HAVicKD4yL08+5uBZaeYYipzyC1O4DEHX5BNdl34NpNxUdMqUb4MfNEnYeqvmemvZkOO6BO+xucP1S2LSKNXfFxH5iVfKbz4+VJ/2kt5R77672lkG3bXPUJFk4t1CTHBOLizEJNTTD8uzRIsW7Io+XFk6oyoEqXF0sT6Lbp/4pUHJQQM= + - secure: DGIgOKinCvYcLdcaIOKcecidQe5q/K4aGAjTyl8/fCp3mRWwFTrlv5gPMy9sHQEsiRjzQehpubMO1d0XFVO+0LvqdGLnXyM6lSnMhN6voQMnF1GaIXmxxBfvP3BHwyN8kMyY+4oqgMROvpxDKvq0IH/GE8opWRJhQdNkRscbtfHUvnqSk62oNziqIBxXrYBIuezuNcgZkCwEoZQwPu674efIAwVr01BmRfd/8Q5W27dJbCJFF9ceyOQrsjG2QGmW6GWraaOpqYfQ82A5ROB2saVU8mEs/f+mGoJgOP2aH3ErFBkJWBCUNzajluAyGyU9VgHGRQ+GMbSr/HqRILd5aYpDCGA0oFer4/jP32CqeEt1Jdqs2lWar5mFX1sae6PIxFyl5lnENgjTfbOt4oiGGye6t0mgI4+psRdCCQV5FQ7WfobEZ2ryfQxbiVU1X219jMoHhHmFzC83e/T6V/mkHrh8OR67k9pieH+DqNGvWFtv7BBs/ihfoo2ONNgsHcofsPj85I6odWAhsBnsYm6FsR31N19nObnggeDyqiCyh5qUFvSGPkH4fXTKthKETIRdsdOEDOcbCD6kUpZqIWyuk6TKeOD8PxjgKzm3hZjlugU1x3amVv71EKjA5/FOVyIVuekLKoLn7pt+n3PVlT30IZfWorEfqjeVAKp2SglE8nA= + - secure: LnL0Hl9yZEie1aYngEO5QK332cn3W2i4f6R5+kxX3tncdqBDFhsp5tQfMvlKHIzFlK94DI/G0diAl8zJmYQfAYARe4uvW5FAINRCkOUz2jwIA/gtQDr+oONqHK0OLPWYuZ6KJM4t7dmuPUR2/frKe0/6r5XeFkeAz9l8r1Gw9o42jFDQPDkhBj7k5EkmB1DpuAY74vXy0tVBCJsd57/kuaRqbX4euhx3zFrDcr+xQEiDWHKzNHlJd6DZnruy0KDuWmIbUWhR2rd8YKAnzP7OHzpbTHsnbXvrVpaN1Mv8lXz2dpPTr7I2LMCrMEtfECu9U+LDIqhbHVMsp9rZ+fNQ048fREoj8HZrorIxmsRJzV0ZQzjdW9Q6EVaiYcLZPFOASsyuTNBbSJ2AIrE/izo4EUKme8BY+0mFzTJwMk7XwAtatItVhEUXb2wXWZm8GR8wIrNmbeSzle5NkwXpdpW3QzZ2EADL63/pP80aV1aMBmoAuLMIHxeHEnXOTAgQj6SOiloY+II/iJE4cE5vo9UNtZsqnJZqdd22s3kLdQV0kbFMWq8S3qmxtDFPeoZAy03xhTVnJUBkdjSL2UER5WAacZIr85M6Z2APc6dzMUlWEE+4QkkM1UAbwDBTXFrrmfDVYc0LrePRuoHQiOmSvTus5+WV9iIQF7rM4BcSLnOEW5U= + - secure: SgvbTYTbMEkmqDXP8MW6lbERkHUjBRwg477hUL11Ok1TiRdHCbEDrq3mfUP+Tl2sS1x5qQ2JFg2NyWS2ikCAd0zjO3QEfmhfQFRpmfgb5O67wY2oEAsbRDanjIXTwpDAZn87KFIPB6ohVsX4LEztfR8zqXKIfXrVFs6lyDHS1LIgbgQhJl+XfJRsfPlWRq7QydANTCY34raUXgXgBxtbv43b232LT8UusizUvZS4HJbrbo4oXhVfhkUH46B2o0ct2Xt6EIlxyOtxtZOmnai8O7kIFHoG+GcHxeZ7++X6FaHR3Cdv3rr7EEg3MwsOIZ6SmgeQ0gcs32RZf0giFMwo1LkgiB9KrJTBXkU0CSYysxfeLUCEd11q4h4lZyhxU8CMvgs+1m3s6A2/5uYDYSSqJuAHTHQgntMn7/baXKVXuWXrSSERxkUiqwlKjFFHz0kshj3ZXiTZ3EhjBZgXeeGzGEEbZBQCEJgXKUpl+C0D07PLKt6f2ya4TVTZ3WOjh7dKq14+0nC3w+5z0ZtGtv+IS1LFfajNs/LsT4fDmKsIoEQg2Kf5S//ckeuzaR4bkMGCm3qquJrNE6Uqq0MZzSUdUpnsILjfLiVlrtHk/9So7ulRc8XyhBIWDy9lTS7NxOfpw5cnVJRcjEwrZ4Q6iNwdsh4vPZLVgavwfFRW87DjF6A= + - secure: Jef94P0QfHuMT5GQNrzfMdtVUVvV9dEGsvOLFqPvIkPLJZWGqwaIFUG5t0mCPEgn8uct3XPOiIYivgxnOURa1JNegnTbjRLXBOAjhE5hw2x2IRWj2xT+ylYCNqh9jtIEJdAjUzJnXXj1iasZGCKa4DNwNLgsuQ8d5Yl3bY5l5YI4MtTsdzbBUT9WewDWgnO/MhZM52w18XLBx+Fsq80F0VwpzoStKRula8anOhL+Bvj1uAielMOUo3QcpYcV2XfTnM5n0ApwqUhmv/8YoJpHXjGTeKRM1Hem1jFtfWCjSRrlEKEFJALEJImf0iWbZN5Z0TOcfJqzPY09/8h60OOfi0TXcnwVnSX33Zp1oDLDlRnsN7HQg+yIub0N03OwHqmC2AO0ShkO/lBmEMsfqlEoc4o2GJ3YL+JpC5vPsy7fFMad+jNGXlg6jPAshvCJ2DfnmK1jYSSVdVNUUeP1Bk5rhQkFzFH3vgNgX3nFk0gEYrfDn3/Ea6tORybSJzaAkB9bU1n4U2e3OplvWr1Ll8O/t87ws8ctyY/Ah2hRmhSKEG9cdySnm7Uq8H7696MZEEw9aatj+bRJk5CbCVtSX8v49I3C0tERcUBO5M3U+/g0qeBW9hEhxnBeG3y253Bo1FhSxbaZhGwSGJ91htRXLlJlUs2QrOcSYMsCT6p35KdWaqA= + - secure: NMVS9EU+ahQXGiyTCHyZ44rf+8b3me3UXD1DozMm04lCvnWoBqJE4aXBGQsDAWuOL4NTTm0SaVu6sBY6ZTXOYYF59mwEbxt4qpmVjZ+vBrtMbMiqoxv145blquR9JKedkdP6IGSd7VSQwSba71f/RVv5VeGvxUSEhCwA04kKxToOPwmnORmT6qwb7PkPCMNHxz4VpsUIsKx8jRrY6Gmp6FvQJBHfKEHnDQohB1ReIYEYi39ijLvpbCZqrB5u1N9oF6WlpBiNIX3kQizn7ftUyewJgoZMnfpW/Lta6e91yzFInWg75bZdW3faa30Qy0yw0zlQIPLs89c8A/XH1fGVECH9At9VNmdYrb0fD9aWnH7zdX6Im+Bw7Ptph4x6tB7zPeFoZR5cVZT7L06/HbnW7NeQk4tg/N4I1tOaO7AQl+ofhCzesZ56bSxETiNFn9QiNwWFTzjlkG7jxN1iAAkdYsZEQHwtEK63R//NJtXpbbtNA831QqgDqBK+IxyKeLhmxmu17dWcUw9tm4jlZ7d6nPB9bzJcVM6K2uRJyW07SlBqd65WJTXPV1PFww8zh+chAC4ZkLDhupn+7ZSG2ylLYGgepmABoC/CXHkXEsNzdQ8wPX/pDIz2WNmwEXyC/Nv+WNpFS/tWIAryIPOLMuETIgbaOLbD5vZDSKxDZVGDvPE= + - secure: cNr4PiK6ZZADoRMacL4lvdMYWgM9H4lKN3u+nfMA/MrVrnSjeRonkO7RjMJWs9HicPanEks10R1w/T/6nWyFQV2CPkEBSNSLf3DAD+dlRekKfWogGXzYnvqeiki1HzsIPYTImiD5BtPn6SbJmO/ErJt3zuogspyBws/X7XfZ+u8FIpPsYEmvHslT2jARuput0yNfldUgxxyI0IvgkuCEcCTFwePspjbn6zR6Df67e+r5ibFqkdPYdXkQVpvfA90RPpfBUuuaEO7kkFlKbPK+Nl/jbUnjcfbe8zJRpSb8j/S2USAiBUjFsqsdvFqZ9WumjXJLrrNFt/UgIXaMyG3Y8xJl9kzCcx36wcNoaP2mx2qucYTdC0ey49g0uywvOVDdykmctQRF7uYQS+UkMqs5jRLgAjQ1/wJISrvtcpaK/4DyhLBUFrOf9chep2hzWBFaWPto1IUpWu9Sjtz5rtjsAm5LR7zvIxcorvRall5kRokAspHb9+TaQKSDOpYNF+PB77cb9H0OLZBLVPGo0WJHq5dv0NVZSH9JVq4AiJDmvMWI6weBis+tLbECMwbeTezo6hDOuII7VZt/KcHgzlt3KCDwv8krQq71q7ySDt7SxrvLeDjeiEFkjwA0lN7Cin1yqjON83LsIsWkKxbf+xNJRngSE4bPn95j3pHELdo3uqY= + - secure: iDkNjibPknbawHN+jobw1AEqhQDhqFvGPBUX7FkxlrIanNR71Tj8CPAFtDpJbYaBMdPt4IzOmD2JVo9w7E1TfNX4DsOpkb2MbOr55TxfXQ/+y7TBdJ9B/62BvhFWk8Hvq8TWVPTCgNIaVXNfqBAj6WQSX3AbUVHWSOM9Aey5joBZuhdWXSmKo46lLOradacDDPZYjrLEsMTS2CotzduKQ4C8dGCVcMEbBnS3O2WRj3oR0wiiP3X0jbnxJkOV2MCoadZxSu5B+gaaJ+Yv7EKT0vy6ASp6LYrWkjY0eKbTqy8NtCvCFlliND/iaq4LEv838hQfO/o0WeB2b7/2MH2EW1v8XLacV12ak5wJgb7b+L6fG+lMKMta5Re+vjdRYgoU5EVeWQNxrnX1chEdzFXb/q2+5DVp43qH5i1Tu4FR/kSBobQeSAbT7HTkWAVz3kg8HmubYZ3P0eXToZA/DlX0dphWxO9ShR3H+XTJhh3tSqzxMZxxhGqPcN4DPSfOTnJQ0v0NPz016lulCr9SuIOSM3f7HpeGXo5SeQbrY3yCnBG8Qxpx2kYaZZlT4J6fx3iFl77SY/lQu6H/Y8ZtufWEogPSkGEh+NLLWuwwBQFC3vH8l3J26vcqHZR3N9+GyqX13CSqWEUysMF4nBOi52ckhwJRF8hAeX+DIqxoLfjUkDc= + - secure: iZmWFdPBt9JCDJqWGeX0gznM22ulUrz15Z7YnDfWNhniMs759YWgIdt3MGr2blMmo5MpDZiticaBcE9aX3p7aniPZkbXcebswz5DwCKVRUf1dhtrzwbnDebSwKrlYG8SuGgMkKH/7F+sEkHtTy+SFDkFwEAwcGjPdPntEX15U6KPrlpD5AOvq/d83xNV6QYleJmkRjsTiUjgGzbYfm6Lu/bzBsckYjwWjcz5rxkeSIQHV8/nnw7xs0figTVcb0K5p7Rpvhg3TCAlSJcljg9IDOEuvILQGNDpdgzXajTTNOHYm9nOBrNQhTn35c8DRDl0uQFe+c6PBvBNhSvSKZ2t9REjok70gR/IKJNTuecS/rppYG8wfjlGg1axoJMZJG1OwOJxWiMHwa9WM/diJ2e5D/WTbQ1T9DLRp8Aco3LnWSfZ+tZH+4tAS9FE4oXEPLlHtRyRIV+MzPK3HinIXYdwGH7pDaAQDnfH562br3tl3CovjAPSrIlDgJh92qatLk+1fBQT/7paMNdelAWLjcujRRI9MWsKzi/aEUVbTfUOSCJH/vWQRJR9or2saOobHE+G4f2+k77r21YRztrZaJF6aiob/qf3altKW5rQRdj/7PgNtG3N/bwFOgRjpOQ8dgNn0J+xK4PQTtSVH7kD4aIcJma23erS+K29dcY/wk6fuBM= + - secure: IkTwVHWb8yGufpyBaQDL1WpTywnMb5FhhtXgP8ycxK9MIuKSfgxPlQm6Oel/8Xjy2HhfKTUX0TOwQ4yAuZtRuU/T5NdjGgBYdLjGWwd/Wgkxt7JkpkFd8lvMrh2moAgvf/Qfk0P73OdAyn2y2CZhm7F7GI3lD/K4NmbFnR5AkDGDBB7J81zc1eTGjMfYRELHMFLJTg8hKr64SljXuRBmyxAVImaBROXzml2pP3UywWKNsw0eEOaeJaVJFkStBh89IOYaznckdstdbTuMw9+hZywBTcQHqxaaI5stdtyKgJcyEcjdJOqNGwCf1Hqd18SH7aCfXsUUrwx62ZSheiQRapOz+XNMiKC+1nimNvaYIFEuPZUGjYfi2/c27Z6paBI9ylGpoUXSvCH2rmaCYUIJBRXVmqzvhi0am5FKBh9NovkKUE24B8rMhm1xV82HQLTCho6MnemXFfji2AXaJzYAa6lU31b8BAzQfy+Nn9lh8I/MMjd15Y+8nHFjKqeKZIBi5qO4B8AGNYwCBFhrIOF6uHGubOuvyKv8DSBxcaDhCoyOLa5QLT4vQzQCX17xUAFomXkX9mHbJA8kTxMaFOQPeVCHkfjZxuui57vbPt4CafGzbymi/SE03oNUheQbuHdqy+jkjj7XU6d4FdNYHxp4rvhtvjYrPJQ5gr6A3sdurd4= + - secure: WNjlMEt8xGRc6blhZF9HkyWEQukDzEwbk/hIljEz+K00W+ltDxPqFZ4fsgnjCPLS0fTL1zgtg3Rji6Mi8hSQd9Z5Yh98fCHN/gBfGxziKB0BOlVQDAl8yaUchiHZ6biM+HC2AocbZXlaNcSOHJy7vQk981y48PNPFadZgJZWWF4eFMkAn+74sPMW53V9J4J2ot3AZ7+ABg2vHTbqdA1s09/6fjlTcKVSxm7eDkkoOYycxrD5k2lm3N0I3YvkWum3ZMcCi1bWk4WXQSgy8q0dy7QBya7+BnRWh/EO2HNFAO4l/tqdev7GC3/tSgTbepNV5GUHazvsrANzZGnWP5kpjjis+Z4GiB3TMNLN1c+KeH2kbBkxi0vyDP5rzzI/rORRp5a3SBDAPkz0u/CHB9sltriErp86v9w6YxDRx0QcFheEXbcsk+3Vajy3+v1ly2PgMfLXcfojCfc27ymUCCymXMHrTG8DeUMnfIyUoP2xDx59tZH8rRlbUQmtlLhBYxPojsw5I2YhnMv4ThHZ5RhGSbNQuZ2yHsdQeGcTqSZ+bVT6xHbQbP4nk6ZtLHC6eq/pAQjFFUkZhfQLH8fes/2fVbdiRjYeX4P8tGnc2NB/fw3C77klizMYIGNHDAK8pKbfov5Q1zeg5/QB7weOvv9xpizUAjQsW5+jwQ1tkdxC0iU= + - secure: kllkCVsDmsu5Z4ZBMFN98VS5LNoyy0z8mpkreV1sAfPH8mmy/q1ZJAGkXLoXL5vFGOTBumMsDs6a7S74d1dXyBkQGYiegs2G5wqIvWb/hsq4dTudEMPqwWnVPCFs/YmtxtzjX788sS7ivkZsHA2pXh1w6glEZykXHJZgWWGSL6XFofR5Gmvpwc8SfQBP/iBi1xb0EA9YWUqjfW43FAuRMp3QDKQU2gcRoAr1EuSIvmMmarnK9CEMJunK/RY94OW1AwtUcJbUVLnK/PvHBIazY18BTez/o45gATH37eMk0G3JKxMP8yfk+wipAAVyZbloWeTdBW8ik1r/B9NWSI4ZXi/PysXMZzSkrede3Y0TE0TqW3mVkdtdcq4yFXD/B21dMmeNoBLxQGi290p/KNfnJLIcYB04CU+Ll9RoXWnVJH6RhxCK6JGEuiYqJKm/UZkCW5dXMhtFmjK3XH3iBRC9kw0jYI7fcTgstLzYzKzgAXBP7LSiiwxDUFsvq480US+lECGdDIJYL646H1kHogi2Cr3BBgXHvMoBbUFyLVXTHidzi4Vk6PFaT8fMXcJIO/j46WUcP5nyEh+7qiJmgqtZx6AbVIuank5vnDSKg9X5fsiww4cT6Xcf+E9rxcpiLhcfBjsbkUvKiVVcCNQHOIZxYLwEwelghz+VlWrJCFVPwT0= + - secure: g0Gm0G5oj892VmJeX8s2MMNttIR1GW9DX8+DYR2qXs5C8pjT8A30dYxCFiv4a1QHIrPEAhIGIosgtWpHXt8JwCDNLbPaHs6Jp9EkCB6doqLqQ9eOrvdxM67fyp8fJVcd4IH5JenG6FoaE7ofnu4QKoLo5w96Cd2UthwxrSCSQXWniutX8/N7XUt2cE74ISqQbJyslPWJ2UvKYL16HHtB8EzRmxD+EZgSYU/vtlds39Knrk85ogK5h6PKDmne8qoIH1nTPs8vzAu5uzeG6zai5m1qYETYbkiyJPTDs6sdy+DkYLLDaE5nLkNyfC/sHQvbPQ/H/CAW1OTZvSPsjxDN4hksovLaVuiH1DTHTM2+4fZsEab6fhx1itPOVijskoBqMzmksvah0nogIaw/qKmCtXGNBk7I30xeSju0W6c55miYUEEczF3eUn9oxHUSUDhQNs8UuosW/MJph7WTS2ESvmIjU7R6QuQjKmxTK7kZTaLAPpSwj/f77F7RgLUu3v6tO3Db1+i1OxdHvbaIVDiCNPB2J1LNmN4LFQstzJepYRadEUxYiExQzNulmC/FpkyPGwajec06z1r5NvTcWSOgxcTU/5OY6ggRCAsFoZTH4VzAst5nOBLRRGT+WAXpewGLhRlGLhev45eGgL2ctNVZY/6MA0ZBwnRPreczO7f3Nno= + - secure: iJ9fPWg8j+q19cUNqvwAbULZTnmK25wFuF5uBCxaSDuM1qMtbiq3M2l1Gvl0xZWdnq12PLMsUZtfLYNO1r4SRaarMPtnq4pK68DnuH49vkjpwGSy5bLIrcPQBZvzWKqzyG1R8+eOpg43BmXdau8Wo7pOSDw/8Bra7rtvA8Kjua/7MXaiZP08L71DtHaHFOoYlvgGHmuscdvtV3v4rNPntGR54ClzyNgnrRz6DP/NwEUF2a6EF2uhhbbe5XQvCNDTW0sqgxirZ4pDFPgywuWQaPcqqSXcwF78wrA0nLSzmQ1PsW9g/rUkDPbXE3uZuwje8QntNsSa8doTLw6Fizj7ZD8DuyvD8z3ljrLvhym+I00r8k6BpD5vlVhasUEEix/3+7lW4cpbE+ofwVz0Ge96y3Ei/uXkSI4kEhwPVfsU9sCRX/Kupdx9fLCQzm7F/BhkfHUCd403MjpbAtoHN3ACZfzC2QQ5fm14mxtLyMSawsClqwO7vVmc7+5p4f13TTkiUtWllKghMw8zTqxAZLwTiPFRKG6Y7MkX9tQUDBb9auQm/+7+pBFEUHibwoPvsSRwYGqdlZV7OXIFYPgt+WgUt9ZYblzSfZ8YLcGPfVTlGbLHkvW2SgIGL1gvHyXC2xd7kd7yec3M7AutDCiYg2kSzANVuJqGQCZSNr/6/S3QQrY= + - secure: IM9VEzf9MrtQnzK3VSD+YxmZWycxTgB5YbINDwE0LsAwWUWucDq95+2ZZRjKLrip5uYvoMohC5X2lxKuLhW8uDN3M6MxondHcCjgBCh8hWP7JociXWMxSIWA5ctU0oCgobm6rKvbarDuO8Bx+NE4QDDAWgjy9oOywE7mVi1G2//LY+2zC+iwXlkLs7VRdo9wtKgIG+FvswiJHhvFbU6GYxOeh1xclLnjwoWf66zzzbIW9Z4bFY0Zun/3u1ySQJWfF03/6ZzlCi3JPvfXDQ35T6PbD/5BvMDPmiXyWGqKUpZfRNus8VkG1G8TWwvHsVct9M47sZqLDuyzWVcwx613i8uxDsEIp9pVy4IxVl2UxvDJH1CPnNdvEhS3NeD9XfkMKMJtShHu1iVzB+j668W5SXsiv/N+psa2SIs+wuDY7FSirRg2Uwk9mlsXS+lSgV9B8BEcEIPEngA40MZCrEzNUdI0iYxaaqB6GNWeHiszOWz94rMkXk+FF3Ic75O4RyCmfXJmR8wYQs+rH4j7JJGzwTHXpYpV7H5V0OtMVIh6EUEvu3Veu/lkT4rsop55OHmcBmpKv8gGllX1P+g7H0+g35wn6Hz+FaSlkPvAuuSudgHFznOtfCowYhDUVp0TxTkcraLhkiROD4Qidw8r/HWh5CzrkfM9LWEBvziWFM77X/M= + - secure: SfJARcGrIyoemZ1jUENyX272acqmDTqQjk0LBjuzWxR5gqWp0Bn5TJS6x7niD8qzVC/n2FwR7F9FG3ll0cmToBgb8vh9763aL+ciA8cfk8iIUdjjQHNkywdhiLLw1EiR4TuUGOB3pV03WxMXl5JJTopJHScHgvTHfyhHS4vnQdrU3j6PwFDvVLsECyFiTCflDUgZ6zHocbIT4fqVjoBxy+5R0t9vyb4eE3IE3Lqjnac5oYVXm4KCpGUYDdCL0W2OlwPYgaBtWGetoWj/v6Kg4cjUVbKJHGK1q2nKyrCGPRvoKTqw83Pkfw1zkSdC0kCozMKr/gYsYBpt5IFXHaON/OkjvgdLbVKrpAHoauQ5KkWcTg9ntuUNndtg5pMaitBmd/J2AaMIxEde8ZvgfDR3sZdv0umFzLXJeAu/tbLR6ozP/BwOFpj89Lw0jNyNxJjdkLHP/4M2W5Ka0W1s+F6xNTahTuXdV32laPGN8SSmBrjzXwFOLOQTCgwlbD+YbTqAXUs+DLq/zIT6ZTaEfOdas+HTsu6H+cQfsmv9JlMP7PoT/CbL2tYe8kA2tjmxsQwUlVTcE1nP5ni5kHX+yXuClTzLimY+BOqU9SLJBKv6umygFqt/9nXk0YU6+UqfeDFCVzrMN0vnzWkmB0SQ3ElDC6pbb6NE/2RgmIy1pAUMDzY= + - secure: NPgEmEHVxJrLtaQ35IJnmQSbEHMqzSJt1l0OHX+a6VzfBiAP9Z4Qv1Ita/SQZpA4roLK2YkGr6TALndXgiY1EXU3e7ebcPnCgaQ6mCSJpAKtXhSGmArvysEmmxKSJjMxc8aBlJAeeEQlngXb0/pqA//kMw5KC+pPo+8O3UUbBs0qY9Q4FX3/cU+chv04tk5cSVR2P9nhOqY7LYm+FXjwF4vfjGKF7mWEcQUzTS8E2dRsertl/4law5O9/0gEW/9MaqyylPxq+0pS38sXP25zYotgEL3L8t0fupi58loPioDndardxVl35SXaatPuOxEdHr3Q0Q3dKVjbrTEx8bP7NgLY4nh7r5hqcEtvlM0xCAsfiOmGe0Dd0BFqo7kxzK/Fl6oI4MoEfvAYR+1QyBy9zOAJmjoj1hu/NEDAtT9NPxXJKl4A/iJPIYIt3lrMw34ewTSLl16ERojx6CZfSqg58eyUndmmOPBB8b4rIX0fQj5/ShsoLbM/rmOmDpPXN0QGu2EHhtR2eCbpdan84VfPifi5LjPH42PjdUxpJ+rOHZJHLesJ5JwQLFLrnDf7Bg96iucyz1QMpNFoqN+q4YDARot2zdw+1ISjgPx3bhHu8ly8oT1UYNl5mMjv7zfNDvsmBycpQEBQWGvIioRfx6dM/EtZy2E/og5Ci0j9v19YI2E= + - secure: f52LTJZfcwkCnlK0ig1/iskZxeTRyU5dGBjuF2f4CKXdV1ajlfjlNj/oX8V7eVYvSVIz1IkE9h2pi2x+P7Jp63ITdzNPAQlbaiqJqEG15g4JicRvTFgKba+dKfZL8lK+mHd/QZE6dd1QsjPuU2BbHuqKccS3NyrqhFkSFGD2eQtft6fXg7O7uFMEdEss8J2H9rYkmV/jH8H3xkaxH/EBeHa03NCMUBWampQ69SKstg+vSMXQCvrQWLbfpq+/K5SWnAW4UoWrvtlFT6lyFeWUTEXmHP8W8/DgubaMZCbMqrautsw4Qjf5VGucgitMVXEkT14MZIJtvhqjF+KPER8D0oPATDp5lb6k7iI100sZkpESGed2rnTwxP4rOrYpT1K3xju1iaTjUpVOZ55yCHCPyB5G1e5BYCEoXlnO2YzLpuGJJs02Mb/cSLL/JgyjPqI5HguN1mz1SsCG9yxknuEdjwwhyHDYudi3FZ8f8RooOE/SQBA7zycvSd5OqvlzgnN3hzG7cA1Bje3E7v1b4rBjF0sfkPpjadvleq2rex8THhFQeVFlrGvRS777/+xg5IA9C5wLaKqAvcbTMV4hzWO89L7W/+13vfH6JecdQGkbxmdr+Y/bDn1SwflfFcUiju9/p64T2rYFGDbwu5jDKV5KGhbMFbUJxds0ETyUKpGOCRs= + - secure: BDZ2Skv2cJU3mzK9fYISp3qWjJHXKUtQIUw6dYN0MYN78LTApjF1af3NLtjhPP92zcdAefAYNk0Dic7K+nEMS6fxoenVbeH3USUXWgIiu8iRt8wfMOOJiCzmoU8yoM+8HFA0/CgAfgkeqhW+zKTL3auIxwCGaVSElBmWB6tefyXEGF2fci/bXG3bDprQ4kZUAyvON8qABXQ+dsX225YwCP46CMTjRK+C1wj5zb6DDQC04sHRmFOMm3QATLXRXI7Lt5ju+Wkbg9JZqfL2Zo6GvZo0J2mpUijznGaCSSdy+r5edfnMmilPxeKUXODTyabSb33Lbinbp2ITUU3cIFGY76PVPYU7a3JN+vssPgpZm1M3kCDUUV71tipbl32//PjBfN0gYqCEGO6yvzklJIHjdgrd8zX2dDyY+vk7aZPDFySoVd5Dr2jGvCNOyE7bDD/9Ahis7VvkAMdU1S2NrLAUin9ZQaW4LjiiAWj3aXACI1i9FwA5SuGcQoPBnFsK7K3TkpgeSxG1YnaegOjm40QJJ8VqexMcBHDgMM2RECyJfhJbBM4JJpQngL8as0aO8HIbOlNSgyG3VCra4Lsa2GHWoOq1A7JI+HztdhCFICTEAcA6IqsFYsdQR/PC50FJawfqNuyDcXw2OtomYUe77/KsI5oOOL/cDYtNvasWW/brrCg= + - secure: aXiJ7R2Pe086+B5x5cuPmK0kG3f/G0vSryQEgmNvlU+Gpr0KaFsV76xzLyy0b8SOeVxA055O7/X7ZA4Pd/IKc5qz/98RX2gieozPDvwryfZ1weZbEYkjVBnWpzoIadN8jqTzN4LDf5nwTDcdEs45gU4uQgYBMWYvgPUBE20oD9iI7052qo/tknlQusPUisRiISuBpgnB1twnLk81ZZe3lRJdxmAOl6thaQj37UbqfvxCgyZVeVq3Y0tY6xl50/KNMyiecluoZuKu0h4EZGYk1T6UaheQ+MQRWNedJIsXeXhG8WoWrmCy5ZInzXWjsKwU4daLaaSgLzgcoNkYzyjAWwPNsepjiyAdXnVpzyifjdNm+LfB9Al5weAllFfzl580yAWVZlf/g0+X/dDxGxZME1jE53BcvUnvj/9cp0mox8JdjvB+ARHmxbPDUCfC9mcBPg0W24duk3brjT6BuR/xUMKEuydO0WDRAgOKopsAQalf/w+vEXQ+liKFs2CSwuo7CEUbtW+cTKz3XBQq6Ws5PAEeHmE3xy8wE5mafx0n6vv3oPGRQiTiko+CwUPrkqUmHzcHf+0N82RCcnT1CdALO7E+fowab5GVdOffUMKurQs5/pi8ux2YyNbS7qSQopAytV2rjiOei6ZDefExhWOWJWej9mPfDf9MGqlDkzbB8UE= \ No newline at end of file diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/CHANGELOG.md b/vendor/github.com/aliyun/aliyun-oss-go-sdk/CHANGELOG.md new file mode 100644 index 00000000..7f14c0e9 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/CHANGELOG.md @@ -0,0 +1,247 @@ +# ChangeLog - Aliyun OSS SDK for Go +## 版本号:v2.1.6 日期:2021-01-13 +### 变更内容 +- 增加:增加worm接口 + +# ChangeLog - Aliyun OSS SDK for Go +## 版本号:v2.1.5 日期:2020-11-19 +### 变更内容 +- 增加:增加ListObjectsV2接口 +- 增加: 增加RestoreObjectXML接口 + +# ChangeLog - Aliyun OSS SDK for Go +## 版本号:v2.1.4 日期:2020-07-24 +### 变更内容 +- 修复:lifecycle配置支持输入LifecycleVersionTransition数组 + +# ChangeLog - Aliyun OSS SDK for Go +## 版本号:v2.1.3 日期:2020-07-10 +### 变更内容 +- 修复:lifecycle支持冷归档(ColdArchive) + + +# ChangeLog - Aliyun OSS SDK for Go +## 版本号:v2.1.2 日期:2020-06-19 +### 变更内容 +- 增加:支持禁止http跳转功能(go1.7.0版本及以上) + +# ChangeLog - Aliyun OSS SDK for Go +## 版本号:v2.1.1 日期:2020-06-04 +### 变更内容 +- 增加:支持国密byok +- 增加:支持异步任务的设置和读取 + +# ChangeLog - Aliyun OSS SDK for Go +## 版本号:v2.1.0 日期:2020-04-21 +### 变更内容 +- 增加:支持客户端加密、清单、冷归档功能 +- 增加:tcp连接增加keepalive心跳选项 +- 优化: 分块上传事件通知优化 + + +# ChangeLog - Aliyun OSS SDK for Go +## 版本号:v2.0.8 日期:2020-04-09 +### 变更内容 +- 增加:支持用户传入自定义的header和param参数 +- 增加:增加对X-Oss-Range-Behavior支持 + +# ChangeLog - Aliyun OSS SDK for Go +## 版本号:v2.0.7 日期:2020-03-11 +### 变更内容 +- 增加:支持OSS V2 签名 +- 增加:增加SetBucketWebsiteXml接口,支持直接传入xml文件内容 + +# ChangeLog - Aliyun OSS SDK for Go +## 版本号:v2.0.6 日期:2020-02-15 +### 变更内容 +- 修复:CopyFile接口需要支持服务端加密功能 + +# ChangeLog - Aliyun OSS SDK for Go +## 版本号:v2.0.5 日期:2020-01-03 +### 变更内容 +- 增加:增加禁止同名覆盖选项X-Oss-Forbid-Overwrite +- 增加:增加分块上传参数sequential, 支持分块上传返回md5校验值 + +## 版本号:v2.0.4 日期:2019-11-13 +### 变更内容 +- 增加:SSR 对bucket 和 endpoint 做合法性校验,不符合要求要直接提示错误。 +- 增加:select object 功能merge +- 增加:断点续传文件支持多版本 +- 增加:lifecycle 支持多版本 +- 修复:断点续传文件中的时间比较方式优化 +- 修复:修复断点上传不支持服务端加密的bug + + +# ChangeLog - Aliyun OSS SDK for Go +## 版本号:v2.0.3 日期:2019-09-17 +### 变更内容 +- 修复:不支持分块上传归档object +- 增加:增加绑定客户端ip地址 +- 增加: 增加更多的mime type类型 + + +# ChangeLog - Aliyun OSS SDK for Go +## 版本号:v2.0.2 日期:2019-08-06 +### 变更内容 +- 修复:proxy代理不支持https请求 + +# ChangeLog - Aliyun OSS SDK for Go +## 版本号:v2.0.1 日期:2019-07-11 +### 变更内容 +- 增加:增加qos相关api +- 增加:增加payment相关api +- 增加:增加自定义获取AccessKeyID、AccessKeySecret、SecurityToken +- 增加: 增加http请求限速option + + +# ChangeLog - Aliyun OSS SDK for Go +## 版本号:v2.0.0 日期:2019-06-18 +### 变更内容 +- 增加:增加各个接口对versioning的支持 +- 增加:增加设置、查询、删除bucket policy接口 +- 增加: 增加设置website详细配置接口: SetBucketWebsiteDetail +- 增加: 增加Bucket OptionsMethod 接口 + + +# ChangeLog - Aliyun OSS SDK for Go +## 版本号:v1.9.8 日期:2019-05-25 +### 变更内容 +- 增加:增加设置、查询、删除bucket tagging接口 + +# ChangeLog - Aliyun OSS SDK for Go +## 版本号:v1.9.7 日期:2019-05-22 +### 变更内容 +- 增加:增加设置、查询、删除object tagging接口 +- 增加:增加设置、查询、删除bucket encryption接口 +- 增加:增加获取bucket统计信息接口 + +## 版本号:v1.9.6 日期:2019-04-15 +### 变更内容 +- 变更:扩展lifecycle功能,提供设置AbortMutipartUpload和Transitions两种规则的生命周期管理的处理 +- 修复:测试用例BucketName使用固定前缀+随机的字符串 +- 修复:测试用例ObjectName使用固定前缀+随机字符串 +- 修复:测试用例有关bucket相关的异步操作,统一定义sleep时间 +- 修复:测试集结束后,列出bucket内的所有对象并删除所有测试的对象 +- 修复:测试集结束后,列出bucket内的所有未上传完成的分片并删除所有测试过程中产生的为上传完成的分片 +- 修复:支持上传webp类型的对象时从对象的后缀名字自动解析对应的content-type并设置content-type +- 变更:增加在put/copy/append等接口时时设置对象的存储类型的sample +- 修复:sample示例中的配置项的值改为直接从环境变量读取 + +## 版本号:1.9.5 日期:2019-03-08 +### 变更内容 +- 变更:增加了限速上传功能 + +## 版本号:1.9.4 日期:2019-01-25 +### 变更内容 +- 修复:在开启日志后,如果接口返回错误readResponseBody函数被调用两次 +- 变更:增加livechannel功能各个api接口 + +## 版本号:1.9.3 日期:2019-01-10 +### 变更内容 +- 修复:分片上传时传入partSize值不对是的提示信息不准确的问题 +- 修复:仅仅在使用userAgent的时候初始化它的值 +- 变更:添加ContentLanguage选项 +- 变更:支持设置最大的空闲连接个数 +- 变更:当配置的endpoint不对时,输出的错误信息将会打印出正确的endpoint +- 变更:支持ServerSideEncryptionKeyID选项,允许用户传入kms-id +- 变更:添加日志模块,支持设置日志级别 + +## 版本号:1.9.2 日期:2018-11-16 +### 变更内容 +- 变更:添加支持设置request Payer的option +- 变更:添加支持设置checkpoint目录的option +- 变更:getobjectmeta接口增加options参数,可以支持传入option选项 +- 变更:listobjecs接口增加options参数,可以支持传入option选项 +- 变更:listmultipartuploads接口增加options参数, 可以支持传入option选项 +- 修复:解决调用接口返回出错时,且返回的http body为空时,打印错误消息不包含"request_id"的问题 +- 变更:abortmultipartupload接口增加options参数, 可以支持传入option选项 +- 变更:completemultipartupload接口增加options参数, 可以支持传入option选项 + +## 版本号:1.9.1 日期:2018-09-17 +### 变更内容 + - 变更:支持ipv6 + - 变更:支持修改对象的存储类型 + - 修复:修改sample中GetBucketReferer方法名拼写错误 + - 修复:修复NopCloser在close的时候并不释放内存的内存泄漏问题 + - 变更:增加ProcessObject接口 + - 修复:修改图片处理接口参数拼写错误导致无法处理的bug + - 修复:增加ListUploadedParts接口的options选项 + - 修复:增加Callback&CallbackVal选项,支持回调使用 + - 修复:GetObject接口返回Response,支持用户读取crc等返回值 + - 修复:当以压缩格式返回数据时,GetObject接口不校验crc + +## 版本号:1.9.0 日期:2018-06-15 +### 变更内容 + - 变更:国际化 + +## 版本号:1.8.0 日期:2017-12-12 +### 变更内容 + - 变更:空闲链接关闭时间调整为50秒 + - 修复:修复临时账号使用SignURL的问题 + +## 版本号:1.7.0 日期:2017-09-25 +### 变更内容 + - 增加:DownloadFile支持CRC校验 + - 增加:STS测试用例 + +## 版本号:1.6.0 日期:2017-09-01 +### 变更内容 + - 修复:URL中特殊字符的编码问题 + - 变更:不再支持Golang 1.4 + +## 版本号:1.5.1 日期:2017-08-04 +### 变更内容 + - 修复:SignURL中Key编码的问题 + - 修复:DownloadFile下载完成后rename失败的问题 + +## 版本号:1.5.0 日期:2017-07-25 +### 变更内容 + - 增加:支持生成URL签名 + - 增加:GetObject支持ResponseContentType等选项 + - 修复:DownloadFile去除分片小于5GB的限制 + - 修复:AppendObject在appendPosition不正确时发生panic + +## 版本号:1.4.0 日期:2017-05-23 +### 变更内容 + - 增加:支持符号链接symlink + - 增加:支持RestoreObject + - 增加:CreateBucket支持StorageClass + - 增加:支持范围读NormalizedRange + - 修复:IsObjectExist使用GetObjectMeta实现 + +## 版本号:1.3.0 日期:2017-01-13 +### 变更内容 + - 增加:上传下载支持进度条功能 + +## 版本号:1.2.3 日期:2016-12-28 +### 变更内容 + - 修复:每次请求使用一个http.Client修改为共用http.Client + +## 版本号:1.2.2 日期:2016-12-10 +### 变更内容 + - 修复:GetObjectToFile/DownloadFile使用临时文件下载,成功后重命名成下载文件 + - 修复:新建的下载文件权限修改为0664 + +## 版本号:1.2.1 日期:2016-11-11 +### 变更内容 + - 修复:只有当OSS返回x-oss-hash-crc64ecma头部时,才对上传的文件进行CRC64完整性校验 + +## 版本号:1.2.0 日期:2016-10-18 +### 变更内容 + - 增加:支持CRC64校验 + - 增加:支持指定Useragent + - 修复:计算MD5占用内存大的问题 + - 修复:CopyObject时Object名称没有URL编码的问题 + +## 版本号:1.1.0 日期:2016-08-09 +### 变更内容 + - 增加:支持代理服务器 + +## 版本号:1.0.0 日期:2016-06-24 +### 变更内容 + - 增加:断点分片复制接口Bucket.CopyFile + - 增加:Bucket间复制接口Bucket.CopyObjectTo、Bucket.CopyObjectFrom + - 增加:Client.GetBucketInfo接口 + - 增加:Bucket.UploadPartCopy支持Bucket间复制 + - 修复:断点上传、断点下载出错后,协程不退出的Bug + - 删除:接口Bucket.CopyObjectToBucket diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/LICENSE b/vendor/github.com/aliyun/aliyun-oss-go-sdk/LICENSE new file mode 100644 index 00000000..d46e9d12 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/LICENSE @@ -0,0 +1,14 @@ +Copyright (c) 2015 aliyun.com + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/README-CN.md b/vendor/github.com/aliyun/aliyun-oss-go-sdk/README-CN.md new file mode 100644 index 00000000..dd1c63a2 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/README-CN.md @@ -0,0 +1,169 @@ +# Aliyun OSS SDK for Go + +[![GitHub version](https://badge.fury.io/gh/aliyun%2Faliyun-oss-go-sdk.svg)](https://badge.fury.io/gh/aliyun%2Faliyun-oss-go-sdk) +[![Build Status](https://travis-ci.org/aliyun/aliyun-oss-go-sdk.svg?branch=master)](https://travis-ci.org/aliyun/aliyun-oss-go-sdk) +[![Coverage Status](https://coveralls.io/repos/github/aliyun/aliyun-oss-go-sdk/badge.svg?branch=master)](https://coveralls.io/github/aliyun/aliyun-oss-go-sdk?branch=master) + +## [README in English](https://github.com/aliyun/aliyun-oss-go-sdk/blob/master/README.md) + +## 关于 +> - 此Go SDK基于[阿里云对象存储服务](http://www.aliyun.com/product/oss/)官方API构建。 +> - 阿里云对象存储(Object Storage Service,简称OSS),是阿里云对外提供的海量,安全,低成本,高可靠的云存储服务。 +> - OSS适合存放任意文件类型,适合各种网站、开发企业及开发者使用。 +> - 使用此SDK,用户可以方便地在任何应用、任何时间、任何地点上传,下载和管理数据。 + +## 版本 +> - Current version: v2.1.6 + +## 运行环境 +> - Go 1.5及以上。 + +## 安装方法 +### GitHub安装 +> - 执行命令`go get github.com/aliyun/aliyun-oss-go-sdk/oss`获取远程代码包。 +> - 在您的代码中使用`import "github.com/aliyun/aliyun-oss-go-sdk/oss"`引入OSS Go SDK的包。 + +## 快速使用 +#### 获取存储空间列表(List Bucket) +```go + client, err := oss.New("Endpoint", "AccessKeyId", "AccessKeySecret") + if err != nil { + // HandleError(err) + } + + lsRes, err := client.ListBuckets() + if err != nil { + // HandleError(err) + } + + for _, bucket := range lsRes.Buckets { + fmt.Println("Buckets:", bucket.Name) + } +``` + +#### 创建存储空间(Create Bucket) +```go + client, err := oss.New("Endpoint", "AccessKeyId", "AccessKeySecret") + if err != nil { + // HandleError(err) + } + + err = client.CreateBucket("my-bucket") + if err != nil { + // HandleError(err) + } +``` + +#### 删除存储空间(Delete Bucket) +```go + client, err := oss.New("Endpoint", "AccessKeyId", "AccessKeySecret") + if err != nil { + // HandleError(err) + } + + err = client.DeleteBucket("my-bucket") + if err != nil { + // HandleError(err) + } +``` + +#### 上传文件(Put Object) +```go + client, err := oss.New("Endpoint", "AccessKeyId", "AccessKeySecret") + if err != nil { + // HandleError(err) + } + + bucket, err := client.Bucket("my-bucket") + if err != nil { + // HandleError(err) + } + + err = bucket.PutObjectFromFile("my-object", "LocalFile") + if err != nil { + // HandleError(err) + } +``` + +#### 下载文件 (Get Object) +```go + client, err := oss.New("Endpoint", "AccessKeyId", "AccessKeySecret") + if err != nil { + // HandleError(err) + } + + bucket, err := client.Bucket("my-bucket") + if err != nil { + // HandleError(err) + } + + err = bucket.GetObjectToFile("my-object", "LocalFile") + if err != nil { + // HandleError(err) + } +``` + +#### 获取文件列表(List Objects) +```go + client, err := oss.New("Endpoint", "AccessKeyId", "AccessKeySecret") + if err != nil { + // HandleError(err) + } + + bucket, err := client.Bucket("my-bucket") + if err != nil { + // HandleError(err) + } + + lsRes, err := bucket.ListObjects() + if err != nil { + // HandleError(err) + } + + for _, object := range lsRes.Objects { + fmt.Println("Objects:", object.Key) + } +``` + +#### 删除文件(Delete Object) +```go + client, err := oss.New("Endpoint", "AccessKeyId", "AccessKeySecret") + if err != nil { + // HandleError(err) + } + + bucket, err := client.Bucket("my-bucket") + if err != nil { + // HandleError(err) + } + + err = bucket.DeleteObject("my-object") + if err != nil { + // HandleError(err) + } +``` + +#### 其它 +更多的示例程序,请参看OSS Go SDK安装路径(即GOPATH变量中的第一个路径)下的`src\github.com\aliyun\aliyun-oss-go-sdk\sample`,该目录下为示例程序, +或者参看`https://github.com/aliyun/aliyun-oss-go-sdk`下sample目录中的示例文件。 + +## 注意事项 +### 运行sample +> - 拷贝示例文件。到OSS Go SDK的安装路径(即GOPATH变量中的第一个路径),进入OSS Go SDK的代码目录`src\github.com\aliyun\aliyun-oss-go-sdk`, +把其下的sample目录和sample.go复制到您的测试工程src目录下。 +> - 修改sample/config.go里的endpoint、AccessKeyId、AccessKeySecret、BucketName等配置。 +> - 请在您的工程目录下执行`go run src/sample.go`。 + +## 联系我们 +> - [阿里云OSS官方网站](http://oss.aliyun.com) +> - [阿里云OSS官方论坛](http://bbs.aliyun.com) +> - [阿里云OSS官方文档中心](http://www.aliyun.com/product/oss#Docs) +> - 阿里云官方技术支持:[提交工单](https://workorder.console.aliyun.com/#/ticket/createIndex) + +## 作者 +> - [Yubin Bai](https://github.com/baiyubin) +> - [Guozhong Han](https://github.com/hangzws) + +## License +> - MIT License, see [license file](LICENSE) + diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/README.md b/vendor/github.com/aliyun/aliyun-oss-go-sdk/README.md new file mode 100644 index 00000000..5f064960 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/README.md @@ -0,0 +1,168 @@ +# Alibaba Cloud OSS SDK for Go + +[![GitHub Version](https://badge.fury.io/gh/aliyun%2Faliyun-oss-go-sdk.svg)](https://badge.fury.io/gh/aliyun%2Faliyun-oss-go-sdk) +[![Build Status](https://travis-ci.org/aliyun/aliyun-oss-go-sdk.svg?branch=master)](https://travis-ci.org/aliyun/aliyun-oss-go-sdk) +[![Coverage Status](https://coveralls.io/repos/github/aliyun/aliyun-oss-go-sdk/badge.svg?branch=master)](https://coveralls.io/github/aliyun/aliyun-oss-go-sdk?branch=master) + +## [README in Chinese](https://github.com/aliyun/aliyun-oss-go-sdk/blob/master/README-CN.md) + +## About +> - This Go SDK is based on the official APIs of [Alibaba Cloud OSS](http://www.aliyun.com/product/oss/). +> - Alibaba Cloud Object Storage Service (OSS) is a cloud storage service provided by Alibaba Cloud, featuring massive capacity, security, a low cost, and high reliability. +> - The OSS can store any type of files and therefore applies to various websites, development enterprises and developers. +> - With this SDK, you can upload, download and manage data on any app anytime and anywhere conveniently. + +## Version +> - Current version: v2.1.6 + +## Running Environment +> - Go 1.5 or above. + +## Installing +### Install the SDK through GitHub +> - Run the 'go get github.com/aliyun/aliyun-oss-go-sdk/oss' command to get the remote code package. +> - Use 'import "github.com/aliyun/aliyun-oss-go-sdk/oss"' in your code to introduce OSS Go SDK package. + +## Getting Started +### List Bucket +```go + client, err := oss.New("Endpoint", "AccessKeyId", "AccessKeySecret") + if err != nil { + // HandleError(err) + } + + lsRes, err := client.ListBuckets() + if err != nil { + // HandleError(err) + } + + for _, bucket := range lsRes.Buckets { + fmt.Println("Buckets:", bucket.Name) + } +``` + +### Create Bucket +```go + client, err := oss.New("Endpoint", "AccessKeyId", "AccessKeySecret") + if err != nil { + // HandleError(err) + } + + err = client.CreateBucket("my-bucket") + if err != nil { + // HandleError(err) + } +``` + +### Delete Bucket +```go + client, err := oss.New("Endpoint", "AccessKeyId", "AccessKeySecret") + if err != nil { + // HandleError(err) + } + + err = client.DeleteBucket("my-bucket") + if err != nil { + // HandleError(err) + } +``` + +### Put Object +```go + client, err := oss.New("Endpoint", "AccessKeyId", "AccessKeySecret") + if err != nil { + // HandleError(err) + } + + bucket, err := client.Bucket("my-bucket") + if err != nil { + // HandleError(err) + } + + err = bucket.PutObjectFromFile("my-object", "LocalFile") + if err != nil { + // HandleError(err) + } +``` + +### Get Object +```go + client, err := oss.New("Endpoint", "AccessKeyId", "AccessKeySecret") + if err != nil { + // HandleError(err) + } + + bucket, err := client.Bucket("my-bucket") + if err != nil { + // HandleError(err) + } + + err = bucket.GetObjectToFile("my-object", "LocalFile") + if err != nil { + // HandleError(err) + } +``` + +### List Objects +```go + client, err := oss.New("Endpoint", "AccessKeyId", "AccessKeySecret") + if err != nil { + // HandleError(err) + } + + bucket, err := client.Bucket("my-bucket") + if err != nil { + // HandleError(err) + } + + lsRes, err := bucket.ListObjects() + if err != nil { + // HandleError(err) + } + + for _, object := range lsRes.Objects { + fmt.Println("Objects:", object.Key) + } +``` + +### Delete Object +```go + client, err := oss.New("Endpoint", "AccessKeyId", "AccessKeySecret") + if err != nil { + // HandleError(err) + } + + bucket, err := client.Bucket("my-bucket") + if err != nil { + // HandleError(err) + } + + err = bucket.DeleteObject("my-object") + if err != nil { + // HandleError(err) + } +``` + +## Complete Example +More example projects can be found at 'src\github.com\aliyun\aliyun-oss-go-sdk\sample' under the installation path of the OSS Go SDK (the first path of the GOPATH variable). The directory contains example projects. +Or you can refer to the example objects in the sample directory under 'https://github.com/aliyun/aliyun-oss-go-sdk'. + +### Running Example +> - Copy the example file. Go to the installation path of OSS Go SDK (the first path of the GOPATH variable), enter the code directory of the OSS Go SDK, namely 'src\github.com\aliyun\aliyun-oss-go-sdk', +and copy the sample directory and sample.go to the src directory of your test project. +> - Modify the endpoint, AccessKeyId, AccessKeySecret and BucketName configuration settings in sample/config.go. +> - Run 'go run src/sample.go' under your project directory. + +## Contacting us +> - [Alibaba Cloud OSS official website](http://oss.aliyun.com). +> - [Alibaba Cloud OSS official forum](http://bbs.aliyun.com). +> - [Alibaba Cloud OSS official documentation center](http://www.aliyun.com/product/oss#Docs). +> - Alibaba Cloud official technical support: [Submit a ticket](https://workorder.console.aliyun.com/#/ticket/createIndex). + +## Author +> - [Yubin Bai](https://github.com/baiyubin) +> - [Guozhong Han](https://github.com/hangzws) + +## License +> - MIT License, see [license file](LICENSE) + diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/auth.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/auth.go new file mode 100644 index 00000000..bc1e4fa3 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/auth.go @@ -0,0 +1,190 @@ +package oss + +import ( + "bytes" + "crypto/hmac" + "crypto/sha1" + "crypto/sha256" + "encoding/base64" + "fmt" + "hash" + "io" + "net/http" + "sort" + "strconv" + "strings" +) + +// headerSorter defines the key-value structure for storing the sorted data in signHeader. +type headerSorter struct { + Keys []string + Vals []string +} + +// getAdditionalHeaderKeys get exist key in http header +func (conn Conn) getAdditionalHeaderKeys(req *http.Request) ([]string, map[string]string) { + var keysList []string + keysMap := make(map[string]string) + srcKeys := make(map[string]string) + + for k := range req.Header { + srcKeys[strings.ToLower(k)] = "" + } + + for _, v := range conn.config.AdditionalHeaders { + if _, ok := srcKeys[strings.ToLower(v)]; ok { + keysMap[strings.ToLower(v)] = "" + } + } + + for k := range keysMap { + keysList = append(keysList, k) + } + sort.Strings(keysList) + return keysList, keysMap +} + +// signHeader signs the header and sets it as the authorization header. +func (conn Conn) signHeader(req *http.Request, canonicalizedResource string) { + akIf := conn.config.GetCredentials() + authorizationStr := "" + if conn.config.AuthVersion == AuthV2 { + additionalList, _ := conn.getAdditionalHeaderKeys(req) + if len(additionalList) > 0 { + authorizationFmt := "OSS2 AccessKeyId:%v,AdditionalHeaders:%v,Signature:%v" + additionnalHeadersStr := strings.Join(additionalList, ";") + authorizationStr = fmt.Sprintf(authorizationFmt, akIf.GetAccessKeyID(), additionnalHeadersStr, conn.getSignedStr(req, canonicalizedResource, akIf.GetAccessKeySecret())) + } else { + authorizationFmt := "OSS2 AccessKeyId:%v,Signature:%v" + authorizationStr = fmt.Sprintf(authorizationFmt, akIf.GetAccessKeyID(), conn.getSignedStr(req, canonicalizedResource, akIf.GetAccessKeySecret())) + } + } else { + // Get the final authorization string + authorizationStr = "OSS " + akIf.GetAccessKeyID() + ":" + conn.getSignedStr(req, canonicalizedResource, akIf.GetAccessKeySecret()) + } + + // Give the parameter "Authorization" value + req.Header.Set(HTTPHeaderAuthorization, authorizationStr) +} + +func (conn Conn) getSignedStr(req *http.Request, canonicalizedResource string, keySecret string) string { + // Find out the "x-oss-"'s address in header of the request + ossHeadersMap := make(map[string]string) + additionalList, additionalMap := conn.getAdditionalHeaderKeys(req) + for k, v := range req.Header { + if strings.HasPrefix(strings.ToLower(k), "x-oss-") { + ossHeadersMap[strings.ToLower(k)] = v[0] + } else if conn.config.AuthVersion == AuthV2 { + if _, ok := additionalMap[strings.ToLower(k)]; ok { + ossHeadersMap[strings.ToLower(k)] = v[0] + } + } + } + hs := newHeaderSorter(ossHeadersMap) + + // Sort the ossHeadersMap by the ascending order + hs.Sort() + + // Get the canonicalizedOSSHeaders + canonicalizedOSSHeaders := "" + for i := range hs.Keys { + canonicalizedOSSHeaders += hs.Keys[i] + ":" + hs.Vals[i] + "\n" + } + + // Give other parameters values + // when sign URL, date is expires + date := req.Header.Get(HTTPHeaderDate) + contentType := req.Header.Get(HTTPHeaderContentType) + contentMd5 := req.Header.Get(HTTPHeaderContentMD5) + + // default is v1 signature + signStr := req.Method + "\n" + contentMd5 + "\n" + contentType + "\n" + date + "\n" + canonicalizedOSSHeaders + canonicalizedResource + h := hmac.New(func() hash.Hash { return sha1.New() }, []byte(keySecret)) + + // v2 signature + if conn.config.AuthVersion == AuthV2 { + signStr = req.Method + "\n" + contentMd5 + "\n" + contentType + "\n" + date + "\n" + canonicalizedOSSHeaders + strings.Join(additionalList, ";") + "\n" + canonicalizedResource + h = hmac.New(func() hash.Hash { return sha256.New() }, []byte(keySecret)) + } + + // convert sign to log for easy to view + if conn.config.LogLevel >= Debug { + var signBuf bytes.Buffer + for i := 0; i < len(signStr); i++ { + if signStr[i] != '\n' { + signBuf.WriteByte(signStr[i]) + } else { + signBuf.WriteString("\\n") + } + } + conn.config.WriteLog(Debug, "[Req:%p]signStr:%s\n", req, signBuf.String()) + } + + io.WriteString(h, signStr) + signedStr := base64.StdEncoding.EncodeToString(h.Sum(nil)) + + return signedStr +} + +func (conn Conn) getRtmpSignedStr(bucketName, channelName, playlistName string, expiration int64, keySecret string, params map[string]interface{}) string { + if params[HTTPParamAccessKeyID] == nil { + return "" + } + + canonResource := fmt.Sprintf("/%s/%s", bucketName, channelName) + canonParamsKeys := []string{} + for key := range params { + if key != HTTPParamAccessKeyID && key != HTTPParamSignature && key != HTTPParamExpires && key != HTTPParamSecurityToken { + canonParamsKeys = append(canonParamsKeys, key) + } + } + + sort.Strings(canonParamsKeys) + canonParamsStr := "" + for _, key := range canonParamsKeys { + canonParamsStr = fmt.Sprintf("%s%s:%s\n", canonParamsStr, key, params[key].(string)) + } + + expireStr := strconv.FormatInt(expiration, 10) + signStr := expireStr + "\n" + canonParamsStr + canonResource + + h := hmac.New(func() hash.Hash { return sha1.New() }, []byte(keySecret)) + io.WriteString(h, signStr) + signedStr := base64.StdEncoding.EncodeToString(h.Sum(nil)) + return signedStr +} + +// newHeaderSorter is an additional function for function SignHeader. +func newHeaderSorter(m map[string]string) *headerSorter { + hs := &headerSorter{ + Keys: make([]string, 0, len(m)), + Vals: make([]string, 0, len(m)), + } + + for k, v := range m { + hs.Keys = append(hs.Keys, k) + hs.Vals = append(hs.Vals, v) + } + return hs +} + +// Sort is an additional function for function SignHeader. +func (hs *headerSorter) Sort() { + sort.Sort(hs) +} + +// Len is an additional function for function SignHeader. +func (hs *headerSorter) Len() int { + return len(hs.Vals) +} + +// Less is an additional function for function SignHeader. +func (hs *headerSorter) Less(i, j int) bool { + return bytes.Compare([]byte(hs.Keys[i]), []byte(hs.Keys[j])) < 0 +} + +// Swap is an additional function for function SignHeader. +func (hs *headerSorter) Swap(i, j int) { + hs.Vals[i], hs.Vals[j] = hs.Vals[j], hs.Vals[i] + hs.Keys[i], hs.Keys[j] = hs.Keys[j], hs.Keys[i] +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/bucket.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/bucket.go new file mode 100644 index 00000000..430252c0 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/bucket.go @@ -0,0 +1,1289 @@ +package oss + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "encoding/xml" + "fmt" + "hash" + "hash/crc64" + "io" + "net/http" + "net/url" + "os" + "strconv" + "strings" + "time" +) + +// Bucket implements the operations of object. +type Bucket struct { + Client Client + BucketName string +} + +// PutObject creates a new object and it will overwrite the original one if it exists already. +// +// objectKey the object key in UTF-8 encoding. The length must be between 1 and 1023, and cannot start with "/" or "\". +// reader io.Reader instance for reading the data for uploading +// options the options for uploading the object. The valid options here are CacheControl, ContentDisposition, ContentEncoding +// Expires, ServerSideEncryption, ObjectACL and Meta. Refer to the link below for more details. +// https://help.aliyun.com/document_detail/oss/api-reference/object/PutObject.html +// +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) PutObject(objectKey string, reader io.Reader, options ...Option) error { + opts := AddContentType(options, objectKey) + + request := &PutObjectRequest{ + ObjectKey: objectKey, + Reader: reader, + } + resp, err := bucket.DoPutObject(request, opts) + if err != nil { + return err + } + defer resp.Body.Close() + + return err +} + +// PutObjectFromFile creates a new object from the local file. +// +// objectKey object key. +// filePath the local file path to upload. +// options the options for uploading the object. Refer to the parameter options in PutObject for more details. +// +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) PutObjectFromFile(objectKey, filePath string, options ...Option) error { + fd, err := os.Open(filePath) + if err != nil { + return err + } + defer fd.Close() + + opts := AddContentType(options, filePath, objectKey) + + request := &PutObjectRequest{ + ObjectKey: objectKey, + Reader: fd, + } + resp, err := bucket.DoPutObject(request, opts) + if err != nil { + return err + } + defer resp.Body.Close() + + return err +} + +// DoPutObject does the actual upload work. +// +// request the request instance for uploading an object. +// options the options for uploading an object. +// +// Response the response from OSS. +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) DoPutObject(request *PutObjectRequest, options []Option) (*Response, error) { + isOptSet, _, _ := IsOptionSet(options, HTTPHeaderContentType) + if !isOptSet { + options = AddContentType(options, request.ObjectKey) + } + + listener := GetProgressListener(options) + + params := map[string]interface{}{} + resp, err := bucket.do("PUT", request.ObjectKey, params, options, request.Reader, listener) + if err != nil { + return nil, err + } + + if bucket.GetConfig().IsEnableCRC { + err = CheckCRC(resp, "DoPutObject") + if err != nil { + return resp, err + } + } + + err = CheckRespCode(resp.StatusCode, []int{http.StatusOK}) + + return resp, err +} + +// GetObject downloads the object. +// +// objectKey the object key. +// options the options for downloading the object. The valid values are: Range, IfModifiedSince, IfUnmodifiedSince, IfMatch, +// IfNoneMatch, AcceptEncoding. For more details, please check out: +// https://help.aliyun.com/document_detail/oss/api-reference/object/GetObject.html +// +// io.ReadCloser reader instance for reading data from response. It must be called close() after the usage and only valid when error is nil. +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) GetObject(objectKey string, options ...Option) (io.ReadCloser, error) { + result, err := bucket.DoGetObject(&GetObjectRequest{objectKey}, options) + if err != nil { + return nil, err + } + + return result.Response, nil +} + +// GetObjectToFile downloads the data to a local file. +// +// objectKey the object key to download. +// filePath the local file to store the object data. +// options the options for downloading the object. Refer to the parameter options in method GetObject for more details. +// +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) GetObjectToFile(objectKey, filePath string, options ...Option) error { + tempFilePath := filePath + TempFileSuffix + + // Calls the API to actually download the object. Returns the result instance. + result, err := bucket.DoGetObject(&GetObjectRequest{objectKey}, options) + if err != nil { + return err + } + defer result.Response.Close() + + // If the local file does not exist, create a new one. If it exists, overwrite it. + fd, err := os.OpenFile(tempFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, FilePermMode) + if err != nil { + return err + } + + // Copy the data to the local file path. + _, err = io.Copy(fd, result.Response.Body) + fd.Close() + if err != nil { + return err + } + + // Compares the CRC value + hasRange, _, _ := IsOptionSet(options, HTTPHeaderRange) + encodeOpt, _ := FindOption(options, HTTPHeaderAcceptEncoding, nil) + acceptEncoding := "" + if encodeOpt != nil { + acceptEncoding = encodeOpt.(string) + } + if bucket.GetConfig().IsEnableCRC && !hasRange && acceptEncoding != "gzip" { + result.Response.ClientCRC = result.ClientCRC.Sum64() + err = CheckCRC(result.Response, "GetObjectToFile") + if err != nil { + os.Remove(tempFilePath) + return err + } + } + + return os.Rename(tempFilePath, filePath) +} + +// DoGetObject is the actual API that gets the object. It's the internal function called by other public APIs. +// +// request the request to download the object. +// options the options for downloading the file. Checks out the parameter options in method GetObject. +// +// GetObjectResult the result instance of getting the object. +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) DoGetObject(request *GetObjectRequest, options []Option) (*GetObjectResult, error) { + params, _ := GetRawParams(options) + resp, err := bucket.do("GET", request.ObjectKey, params, options, nil, nil) + if err != nil { + return nil, err + } + + result := &GetObjectResult{ + Response: resp, + } + + // CRC + var crcCalc hash.Hash64 + hasRange, _, _ := IsOptionSet(options, HTTPHeaderRange) + if bucket.GetConfig().IsEnableCRC && !hasRange { + crcCalc = crc64.New(CrcTable()) + result.ServerCRC = resp.ServerCRC + result.ClientCRC = crcCalc + } + + // Progress + listener := GetProgressListener(options) + + contentLen, _ := strconv.ParseInt(resp.Headers.Get(HTTPHeaderContentLength), 10, 64) + resp.Body = TeeReader(resp.Body, crcCalc, contentLen, listener, nil) + + return result, nil +} + +// CopyObject copies the object inside the bucket. +// +// srcObjectKey the source object to copy. +// destObjectKey the target object to copy. +// options options for copying an object. You can specify the conditions of copy. The valid conditions are CopySourceIfMatch, +// CopySourceIfNoneMatch, CopySourceIfModifiedSince, CopySourceIfUnmodifiedSince, MetadataDirective. +// Also you can specify the target object's attributes, such as CacheControl, ContentDisposition, ContentEncoding, Expires, +// ServerSideEncryption, ObjectACL, Meta. Refer to the link below for more details : +// https://help.aliyun.com/document_detail/oss/api-reference/object/CopyObject.html +// +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) CopyObject(srcObjectKey, destObjectKey string, options ...Option) (CopyObjectResult, error) { + var out CopyObjectResult + + //first find version id + versionIdKey := "versionId" + versionId, _ := FindOption(options, versionIdKey, nil) + if versionId == nil { + options = append(options, CopySource(bucket.BucketName, url.QueryEscape(srcObjectKey))) + } else { + options = DeleteOption(options, versionIdKey) + options = append(options, CopySourceVersion(bucket.BucketName, url.QueryEscape(srcObjectKey), versionId.(string))) + } + + params := map[string]interface{}{} + resp, err := bucket.do("PUT", destObjectKey, params, options, nil, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// CopyObjectTo copies the object to another bucket. +// +// srcObjectKey source object key. The source bucket is Bucket.BucketName . +// destBucketName target bucket name. +// destObjectKey target object name. +// options copy options, check out parameter options in function CopyObject for more details. +// +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) CopyObjectTo(destBucketName, destObjectKey, srcObjectKey string, options ...Option) (CopyObjectResult, error) { + return bucket.copy(srcObjectKey, destBucketName, destObjectKey, options...) +} + +// +// CopyObjectFrom copies the object to another bucket. +// +// srcBucketName source bucket name. +// srcObjectKey source object name. +// destObjectKey target object name. The target bucket name is Bucket.BucketName. +// options copy options. Check out parameter options in function CopyObject. +// +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) CopyObjectFrom(srcBucketName, srcObjectKey, destObjectKey string, options ...Option) (CopyObjectResult, error) { + destBucketName := bucket.BucketName + var out CopyObjectResult + srcBucket, err := bucket.Client.Bucket(srcBucketName) + if err != nil { + return out, err + } + + return srcBucket.copy(srcObjectKey, destBucketName, destObjectKey, options...) +} + +func (bucket Bucket) copy(srcObjectKey, destBucketName, destObjectKey string, options ...Option) (CopyObjectResult, error) { + var out CopyObjectResult + + //first find version id + versionIdKey := "versionId" + versionId, _ := FindOption(options, versionIdKey, nil) + if versionId == nil { + options = append(options, CopySource(bucket.BucketName, url.QueryEscape(srcObjectKey))) + } else { + options = DeleteOption(options, versionIdKey) + options = append(options, CopySourceVersion(bucket.BucketName, url.QueryEscape(srcObjectKey), versionId.(string))) + } + + headers := make(map[string]string) + err := handleOptions(headers, options) + if err != nil { + return out, err + } + params := map[string]interface{}{} + resp, err := bucket.Client.Conn.Do("PUT", destBucketName, destObjectKey, params, headers, nil, 0, nil) + + // get response header + respHeader, _ := FindOption(options, responseHeader, nil) + if respHeader != nil { + pRespHeader := respHeader.(*http.Header) + *pRespHeader = resp.Headers + } + + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// AppendObject uploads the data in the way of appending an existing or new object. +// +// AppendObject the parameter appendPosition specifies which postion (in the target object) to append. For the first append (to a non-existing file), +// the appendPosition should be 0. The appendPosition in the subsequent calls will be the current object length. +// For example, the first appendObject's appendPosition is 0 and it uploaded 65536 bytes data, then the second call's position is 65536. +// The response header x-oss-next-append-position after each successful request also specifies the next call's append position (so the caller need not to maintain this information). +// +// objectKey the target object to append to. +// reader io.Reader. The read instance for reading the data to append. +// appendPosition the start position to append. +// destObjectProperties the options for the first appending, such as CacheControl, ContentDisposition, ContentEncoding, +// Expires, ServerSideEncryption, ObjectACL. +// +// int64 the next append position, it's valid when error is nil. +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) AppendObject(objectKey string, reader io.Reader, appendPosition int64, options ...Option) (int64, error) { + request := &AppendObjectRequest{ + ObjectKey: objectKey, + Reader: reader, + Position: appendPosition, + } + + result, err := bucket.DoAppendObject(request, options) + if err != nil { + return appendPosition, err + } + + return result.NextPosition, err +} + +// DoAppendObject is the actual API that does the object append. +// +// request the request object for appending object. +// options the options for appending object. +// +// AppendObjectResult the result object for appending object. +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) DoAppendObject(request *AppendObjectRequest, options []Option) (*AppendObjectResult, error) { + params := map[string]interface{}{} + params["append"] = nil + params["position"] = strconv.FormatInt(request.Position, 10) + headers := make(map[string]string) + + opts := AddContentType(options, request.ObjectKey) + handleOptions(headers, opts) + + var initCRC uint64 + isCRCSet, initCRCOpt, _ := IsOptionSet(options, initCRC64) + if isCRCSet { + initCRC = initCRCOpt.(uint64) + } + + listener := GetProgressListener(options) + + handleOptions(headers, opts) + resp, err := bucket.Client.Conn.Do("POST", bucket.BucketName, request.ObjectKey, params, headers, + request.Reader, initCRC, listener) + + // get response header + respHeader, _ := FindOption(options, responseHeader, nil) + if respHeader != nil { + pRespHeader := respHeader.(*http.Header) + *pRespHeader = resp.Headers + } + + if err != nil { + return nil, err + } + defer resp.Body.Close() + + nextPosition, _ := strconv.ParseInt(resp.Headers.Get(HTTPHeaderOssNextAppendPosition), 10, 64) + result := &AppendObjectResult{ + NextPosition: nextPosition, + CRC: resp.ServerCRC, + } + + if bucket.GetConfig().IsEnableCRC && isCRCSet { + err = CheckCRC(resp, "AppendObject") + if err != nil { + return result, err + } + } + + return result, nil +} + +// DeleteObject deletes the object. +// +// objectKey the object key to delete. +// +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) DeleteObject(objectKey string, options ...Option) error { + params, _ := GetRawParams(options) + resp, err := bucket.do("DELETE", objectKey, params, options, nil, nil) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusNoContent}) +} + +// DeleteObjects deletes multiple objects. +// +// objectKeys the object keys to delete. +// options the options for deleting objects. +// Supported option is DeleteObjectsQuiet which means it will not return error even deletion failed (not recommended). By default it's not used. +// +// DeleteObjectsResult the result object. +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) DeleteObjects(objectKeys []string, options ...Option) (DeleteObjectsResult, error) { + out := DeleteObjectsResult{} + dxml := deleteXML{} + for _, key := range objectKeys { + dxml.Objects = append(dxml.Objects, DeleteObject{Key: key}) + } + + isQuiet, _ := FindOption(options, deleteObjectsQuiet, false) + dxml.Quiet = isQuiet.(bool) + + bs, err := xml.Marshal(dxml) + if err != nil { + return out, err + } + buffer := new(bytes.Buffer) + buffer.Write(bs) + + contentType := http.DetectContentType(buffer.Bytes()) + options = append(options, ContentType(contentType)) + sum := md5.Sum(bs) + b64 := base64.StdEncoding.EncodeToString(sum[:]) + options = append(options, ContentMD5(b64)) + + params := map[string]interface{}{} + params["delete"] = nil + params["encoding-type"] = "url" + + resp, err := bucket.do("POST", "", params, options, buffer, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + deletedResult := DeleteObjectVersionsResult{} + if !dxml.Quiet { + if err = xmlUnmarshal(resp.Body, &deletedResult); err == nil { + err = decodeDeleteObjectsResult(&deletedResult) + } + } + + // Keep compatibility:need convert to struct DeleteObjectsResult + out.XMLName = deletedResult.XMLName + for _, v := range deletedResult.DeletedObjectsDetail { + out.DeletedObjects = append(out.DeletedObjects, v.Key) + } + + return out, err +} + +// DeleteObjectVersions deletes multiple object versions. +// +// objectVersions the object keys and versions to delete. +// options the options for deleting objects. +// Supported option is DeleteObjectsQuiet which means it will not return error even deletion failed (not recommended). By default it's not used. +// +// DeleteObjectVersionsResult the result object. +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) DeleteObjectVersions(objectVersions []DeleteObject, options ...Option) (DeleteObjectVersionsResult, error) { + out := DeleteObjectVersionsResult{} + dxml := deleteXML{} + dxml.Objects = objectVersions + + isQuiet, _ := FindOption(options, deleteObjectsQuiet, false) + dxml.Quiet = isQuiet.(bool) + + bs, err := xml.Marshal(dxml) + if err != nil { + return out, err + } + buffer := new(bytes.Buffer) + buffer.Write(bs) + + contentType := http.DetectContentType(buffer.Bytes()) + options = append(options, ContentType(contentType)) + sum := md5.Sum(bs) + b64 := base64.StdEncoding.EncodeToString(sum[:]) + options = append(options, ContentMD5(b64)) + + params := map[string]interface{}{} + params["delete"] = nil + params["encoding-type"] = "url" + + resp, err := bucket.do("POST", "", params, options, buffer, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + if !dxml.Quiet { + if err = xmlUnmarshal(resp.Body, &out); err == nil { + err = decodeDeleteObjectsResult(&out) + } + } + return out, err +} + +// IsObjectExist checks if the object exists. +// +// bool flag of object's existence (true:exists; false:non-exist) when error is nil. +// +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) IsObjectExist(objectKey string, options ...Option) (bool, error) { + _, err := bucket.GetObjectMeta(objectKey, options...) + if err == nil { + return true, nil + } + + switch err.(type) { + case ServiceError: + if err.(ServiceError).StatusCode == 404 { + return false, nil + } + } + + return false, err +} + +// ListObjects lists the objects under the current bucket. +// +// options it contains all the filters for listing objects. +// It could specify a prefix filter on object keys, the max keys count to return and the object key marker and the delimiter for grouping object names. +// The key marker means the returned objects' key must be greater than it in lexicographic order. +// +// For example, if the bucket has 8 objects, my-object-1, my-object-11, my-object-2, my-object-21, +// my-object-22, my-object-3, my-object-31, my-object-32. If the prefix is my-object-2 (no other filters), then it returns +// my-object-2, my-object-21, my-object-22 three objects. If the marker is my-object-22 (no other filters), then it returns +// my-object-3, my-object-31, my-object-32 three objects. If the max keys is 5, then it returns 5 objects. +// The three filters could be used together to achieve filter and paging functionality. +// If the prefix is the folder name, then it could list all files under this folder (including the files under its subfolders). +// But if the delimiter is specified with '/', then it only returns that folder's files (no subfolder's files). The direct subfolders are in the commonPrefixes properties. +// For example, if the bucket has three objects fun/test.jpg, fun/movie/001.avi, fun/movie/007.avi. And if the prefix is "fun/", then it returns all three objects. +// But if the delimiter is '/', then only "fun/test.jpg" is returned as files and fun/movie/ is returned as common prefix. +// +// For common usage scenario, check out sample/list_object.go. +// +// ListObjectsResult the return value after operation succeeds (only valid when error is nil). +// +func (bucket Bucket) ListObjects(options ...Option) (ListObjectsResult, error) { + var out ListObjectsResult + + options = append(options, EncodingType("url")) + params, err := GetRawParams(options) + if err != nil { + return out, err + } + + resp, err := bucket.do("GET", "", params, options, nil, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + if err != nil { + return out, err + } + + err = decodeListObjectsResult(&out) + return out, err +} + +// Recommend to use ListObjectsV2 to replace ListObjects +// ListOListObjectsV2bjects lists the objects under the current bucket. +// ListObjectsResultV2 the return value after operation succeeds (only valid when error is nil). +func (bucket Bucket) ListObjectsV2(options ...Option) (ListObjectsResultV2, error) { + var out ListObjectsResultV2 + + options = append(options, EncodingType("url")) + options = append(options, ListType(2)) + params, err := GetRawParams(options) + if err != nil { + return out, err + } + + resp, err := bucket.do("GET", "", params, options, nil, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + if err != nil { + return out, err + } + + err = decodeListObjectsResultV2(&out) + return out, err +} + +// ListObjectVersions lists objects of all versions under the current bucket. +func (bucket Bucket) ListObjectVersions(options ...Option) (ListObjectVersionsResult, error) { + var out ListObjectVersionsResult + + options = append(options, EncodingType("url")) + params, err := GetRawParams(options) + if err != nil { + return out, err + } + params["versions"] = nil + + resp, err := bucket.do("GET", "", params, options, nil, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + if err != nil { + return out, err + } + + err = decodeListObjectVersionsResult(&out) + return out, err +} + +// SetObjectMeta sets the metadata of the Object. +// +// objectKey object +// options options for setting the metadata. The valid options are CacheControl, ContentDisposition, ContentEncoding, Expires, +// ServerSideEncryption, and custom metadata. +// +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) SetObjectMeta(objectKey string, options ...Option) error { + options = append(options, MetadataDirective(MetaReplace)) + _, err := bucket.CopyObject(objectKey, objectKey, options...) + return err +} + +// GetObjectDetailedMeta gets the object's detailed metadata +// +// objectKey object key. +// options the constraints of the object. Only when the object meets the requirements this method will return the metadata. Otherwise returns error. Valid options are IfModifiedSince, IfUnmodifiedSince, +// IfMatch, IfNoneMatch. For more details check out https://help.aliyun.com/document_detail/oss/api-reference/object/HeadObject.html +// +// http.Header object meta when error is nil. +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) GetObjectDetailedMeta(objectKey string, options ...Option) (http.Header, error) { + params, _ := GetRawParams(options) + resp, err := bucket.do("HEAD", objectKey, params, options, nil, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return resp.Headers, nil +} + +// GetObjectMeta gets object metadata. +// +// GetObjectMeta is more lightweight than GetObjectDetailedMeta as it only returns basic metadata including ETag +// size, LastModified. The size information is in the HTTP header Content-Length. +// +// objectKey object key +// +// http.Header the object's metadata, valid when error is nil. +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) GetObjectMeta(objectKey string, options ...Option) (http.Header, error) { + params, _ := GetRawParams(options) + params["objectMeta"] = nil + //resp, err := bucket.do("GET", objectKey, "?objectMeta", "", nil, nil, nil) + resp, err := bucket.do("HEAD", objectKey, params, options, nil, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return resp.Headers, nil +} + +// SetObjectACL updates the object's ACL. +// +// Only the bucket's owner could update object's ACL which priority is higher than bucket's ACL. +// For example, if the bucket ACL is private and object's ACL is public-read-write. +// Then object's ACL is used and it means all users could read or write that object. +// When the object's ACL is not set, then bucket's ACL is used as the object's ACL. +// +// Object read operations include GetObject, HeadObject, CopyObject and UploadPartCopy on the source object; +// Object write operations include PutObject, PostObject, AppendObject, DeleteObject, DeleteMultipleObjects, +// CompleteMultipartUpload and CopyObject on target object. +// +// objectKey the target object key (to set the ACL on) +// objectAcl object ACL. Valid options are PrivateACL, PublicReadACL, PublicReadWriteACL. +// +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) SetObjectACL(objectKey string, objectACL ACLType, options ...Option) error { + options = append(options, ObjectACL(objectACL)) + params, _ := GetRawParams(options) + params["acl"] = nil + resp, err := bucket.do("PUT", objectKey, params, options, nil, nil) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusOK}) +} + +// GetObjectACL gets object's ACL +// +// objectKey the object to get ACL from. +// +// GetObjectACLResult the result object when error is nil. GetObjectACLResult.Acl is the object ACL. +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) GetObjectACL(objectKey string, options ...Option) (GetObjectACLResult, error) { + var out GetObjectACLResult + params, _ := GetRawParams(options) + params["acl"] = nil + resp, err := bucket.do("GET", objectKey, params, options, nil, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// PutSymlink creates a symlink (to point to an existing object) +// +// Symlink cannot point to another symlink. +// When creating a symlink, it does not check the existence of the target file, and does not check if the target file is symlink. +// Neither it checks the caller's permission on the target file. All these checks are deferred to the actual GetObject call via this symlink. +// If trying to add an existing file, as long as the caller has the write permission, the existing one will be overwritten. +// If the x-oss-meta- is specified, it will be added as the metadata of the symlink file. +// +// symObjectKey the symlink object's key. +// targetObjectKey the target object key to point to. +// +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) PutSymlink(symObjectKey string, targetObjectKey string, options ...Option) error { + options = append(options, symlinkTarget(url.QueryEscape(targetObjectKey))) + params, _ := GetRawParams(options) + params["symlink"] = nil + resp, err := bucket.do("PUT", symObjectKey, params, options, nil, nil) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusOK}) +} + +// GetSymlink gets the symlink object with the specified key. +// If the symlink object does not exist, returns 404. +// +// objectKey the symlink object's key. +// +// error it's nil if no error, otherwise it's an error object. +// When error is nil, the target file key is in the X-Oss-Symlink-Target header of the returned object. +// +func (bucket Bucket) GetSymlink(objectKey string, options ...Option) (http.Header, error) { + params, _ := GetRawParams(options) + params["symlink"] = nil + resp, err := bucket.do("GET", objectKey, params, options, nil, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + targetObjectKey := resp.Headers.Get(HTTPHeaderOssSymlinkTarget) + targetObjectKey, err = url.QueryUnescape(targetObjectKey) + if err != nil { + return resp.Headers, err + } + resp.Headers.Set(HTTPHeaderOssSymlinkTarget, targetObjectKey) + return resp.Headers, err +} + +// RestoreObject restores the object from the archive storage. +// +// An archive object is in cold status by default and it cannot be accessed. +// When restore is called on the cold object, it will become available for access after some time. +// If multiple restores are called on the same file when the object is being restored, server side does nothing for additional calls but returns success. +// By default, the restored object is available for access for one day. After that it will be unavailable again. +// But if another RestoreObject are called after the file is restored, then it will extend one day's access time of that object, up to 7 days. +// +// objectKey object key to restore. +// +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) RestoreObject(objectKey string, options ...Option) error { + params, _ := GetRawParams(options) + params["restore"] = nil + resp, err := bucket.do("POST", objectKey, params, options, nil, nil) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusOK, http.StatusAccepted}) +} + +// RestoreObjectDetail support more features than RestoreObject +func (bucket Bucket) RestoreObjectDetail(objectKey string, restoreConfig RestoreConfiguration, options ...Option) error { + if restoreConfig.Tier == "" { + // Expedited, Standard, Bulk + restoreConfig.Tier = string(RestoreStandard) + } + + if restoreConfig.Days == 0 { + restoreConfig.Days = 1 + } + + bs, err := xml.Marshal(restoreConfig) + if err != nil { + return err + } + + buffer := new(bytes.Buffer) + buffer.Write(bs) + + contentType := http.DetectContentType(buffer.Bytes()) + options = append(options, ContentType(contentType)) + + params, _ := GetRawParams(options) + params["restore"] = nil + + resp, err := bucket.do("POST", objectKey, params, options, buffer, nil) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusOK, http.StatusAccepted}) +} + +// RestoreObjectXML support more features than RestoreObject +func (bucket Bucket) RestoreObjectXML(objectKey, configXML string, options ...Option) error { + buffer := new(bytes.Buffer) + buffer.Write([]byte(configXML)) + + contentType := http.DetectContentType(buffer.Bytes()) + options = append(options, ContentType(contentType)) + + params, _ := GetRawParams(options) + params["restore"] = nil + + resp, err := bucket.do("POST", objectKey, params, options, buffer, nil) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusOK, http.StatusAccepted}) +} + +// SignURL signs the URL. Users could access the object directly with this URL without getting the AK. +// +// objectKey the target object to sign. +// signURLConfig the configuration for the signed URL +// +// string returns the signed URL, when error is nil. +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) SignURL(objectKey string, method HTTPMethod, expiredInSec int64, options ...Option) (string, error) { + if expiredInSec < 0 { + return "", fmt.Errorf("invalid expires: %d, expires must bigger than 0", expiredInSec) + } + expiration := time.Now().Unix() + expiredInSec + + params, err := GetRawParams(options) + if err != nil { + return "", err + } + + headers := make(map[string]string) + err = handleOptions(headers, options) + if err != nil { + return "", err + } + + return bucket.Client.Conn.signURL(method, bucket.BucketName, objectKey, expiration, params, headers), nil +} + +// PutObjectWithURL uploads an object with the URL. If the object exists, it will be overwritten. +// PutObjectWithURL It will not generate minetype according to the key name. +// +// signedURL signed URL. +// reader io.Reader the read instance for reading the data for the upload. +// options the options for uploading the data. The valid options are CacheControl, ContentDisposition, ContentEncoding, +// Expires, ServerSideEncryption, ObjectACL and custom metadata. Check out the following link for details: +// https://help.aliyun.com/document_detail/oss/api-reference/object/PutObject.html +// +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) PutObjectWithURL(signedURL string, reader io.Reader, options ...Option) error { + resp, err := bucket.DoPutObjectWithURL(signedURL, reader, options) + if err != nil { + return err + } + defer resp.Body.Close() + + return err +} + +// PutObjectFromFileWithURL uploads an object from a local file with the signed URL. +// PutObjectFromFileWithURL It does not generate mimetype according to object key's name or the local file name. +// +// signedURL the signed URL. +// filePath local file path, such as dirfile.txt, for uploading. +// options options for uploading, same as the options in PutObject function. +// +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) PutObjectFromFileWithURL(signedURL, filePath string, options ...Option) error { + fd, err := os.Open(filePath) + if err != nil { + return err + } + defer fd.Close() + + resp, err := bucket.DoPutObjectWithURL(signedURL, fd, options) + if err != nil { + return err + } + defer resp.Body.Close() + + return err +} + +// DoPutObjectWithURL is the actual API that does the upload with URL work(internal for SDK) +// +// signedURL the signed URL. +// reader io.Reader the read instance for getting the data to upload. +// options options for uploading. +// +// Response the response object which contains the HTTP response. +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) DoPutObjectWithURL(signedURL string, reader io.Reader, options []Option) (*Response, error) { + listener := GetProgressListener(options) + + params := map[string]interface{}{} + resp, err := bucket.doURL("PUT", signedURL, params, options, reader, listener) + if err != nil { + return nil, err + } + + if bucket.GetConfig().IsEnableCRC { + err = CheckCRC(resp, "DoPutObjectWithURL") + if err != nil { + return resp, err + } + } + + err = CheckRespCode(resp.StatusCode, []int{http.StatusOK}) + + return resp, err +} + +// GetObjectWithURL downloads the object and returns the reader instance, with the signed URL. +// +// signedURL the signed URL. +// options options for downloading the object. Valid options are IfModifiedSince, IfUnmodifiedSince, IfMatch, +// IfNoneMatch, AcceptEncoding. For more information, check out the following link: +// https://help.aliyun.com/document_detail/oss/api-reference/object/GetObject.html +// +// io.ReadCloser the reader object for getting the data from response. It needs be closed after the usage. It's only valid when error is nil. +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) GetObjectWithURL(signedURL string, options ...Option) (io.ReadCloser, error) { + result, err := bucket.DoGetObjectWithURL(signedURL, options) + if err != nil { + return nil, err + } + return result.Response, nil +} + +// GetObjectToFileWithURL downloads the object into a local file with the signed URL. +// +// signedURL the signed URL +// filePath the local file path to download to. +// options the options for downloading object. Check out the parameter options in function GetObject for the reference. +// +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) GetObjectToFileWithURL(signedURL, filePath string, options ...Option) error { + tempFilePath := filePath + TempFileSuffix + + // Get the object's content + result, err := bucket.DoGetObjectWithURL(signedURL, options) + if err != nil { + return err + } + defer result.Response.Close() + + // If the file does not exist, create one. If exists, then overwrite it. + fd, err := os.OpenFile(tempFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, FilePermMode) + if err != nil { + return err + } + + // Save the data to the file. + _, err = io.Copy(fd, result.Response.Body) + fd.Close() + if err != nil { + return err + } + + // Compare the CRC value. If CRC values do not match, return error. + hasRange, _, _ := IsOptionSet(options, HTTPHeaderRange) + encodeOpt, _ := FindOption(options, HTTPHeaderAcceptEncoding, nil) + acceptEncoding := "" + if encodeOpt != nil { + acceptEncoding = encodeOpt.(string) + } + + if bucket.GetConfig().IsEnableCRC && !hasRange && acceptEncoding != "gzip" { + result.Response.ClientCRC = result.ClientCRC.Sum64() + err = CheckCRC(result.Response, "GetObjectToFileWithURL") + if err != nil { + os.Remove(tempFilePath) + return err + } + } + + return os.Rename(tempFilePath, filePath) +} + +// DoGetObjectWithURL is the actual API that downloads the file with the signed URL. +// +// signedURL the signed URL. +// options the options for getting object. Check out parameter options in GetObject for the reference. +// +// GetObjectResult the result object when the error is nil. +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) DoGetObjectWithURL(signedURL string, options []Option) (*GetObjectResult, error) { + params, _ := GetRawParams(options) + resp, err := bucket.doURL("GET", signedURL, params, options, nil, nil) + if err != nil { + return nil, err + } + + result := &GetObjectResult{ + Response: resp, + } + + // CRC + var crcCalc hash.Hash64 + hasRange, _, _ := IsOptionSet(options, HTTPHeaderRange) + if bucket.GetConfig().IsEnableCRC && !hasRange { + crcCalc = crc64.New(CrcTable()) + result.ServerCRC = resp.ServerCRC + result.ClientCRC = crcCalc + } + + // Progress + listener := GetProgressListener(options) + + contentLen, _ := strconv.ParseInt(resp.Headers.Get(HTTPHeaderContentLength), 10, 64) + resp.Body = TeeReader(resp.Body, crcCalc, contentLen, listener, nil) + + return result, nil +} + +// +// ProcessObject apply process on the specified image file. +// +// The supported process includes resize, rotate, crop, watermark, format, +// udf, customized style, etc. +// +// +// objectKey object key to process. +// process process string, such as "image/resize,w_100|sys/saveas,o_dGVzdC5qcGc,b_dGVzdA" +// +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) ProcessObject(objectKey string, process string, options ...Option) (ProcessObjectResult, error) { + var out ProcessObjectResult + params, _ := GetRawParams(options) + params["x-oss-process"] = nil + processData := fmt.Sprintf("%v=%v", "x-oss-process", process) + data := strings.NewReader(processData) + resp, err := bucket.do("POST", objectKey, params, nil, data, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = jsonUnmarshal(resp.Body, &out) + return out, err +} + +// +// PutObjectTagging add tagging to object +// +// objectKey object key to add tagging +// tagging tagging to be added +// +// error nil if success, otherwise error +// +func (bucket Bucket) PutObjectTagging(objectKey string, tagging Tagging, options ...Option) error { + bs, err := xml.Marshal(tagging) + if err != nil { + return err + } + + buffer := new(bytes.Buffer) + buffer.Write(bs) + + params, _ := GetRawParams(options) + params["tagging"] = nil + resp, err := bucket.do("PUT", objectKey, params, options, buffer, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// +// GetObjectTagging get tagging of the object +// +// objectKey object key to get tagging +// +// Tagging +// error nil if success, otherwise error + +func (bucket Bucket) GetObjectTagging(objectKey string, options ...Option) (GetObjectTaggingResult, error) { + var out GetObjectTaggingResult + params, _ := GetRawParams(options) + params["tagging"] = nil + + resp, err := bucket.do("GET", objectKey, params, options, nil, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// +// DeleteObjectTagging delete object taggging +// +// objectKey object key to delete tagging +// +// error nil if success, otherwise error +// +func (bucket Bucket) DeleteObjectTagging(objectKey string, options ...Option) error { + params, _ := GetRawParams(options) + params["tagging"] = nil + + if objectKey == "" { + return fmt.Errorf("invalid argument: object name is empty") + } + + resp, err := bucket.do("DELETE", objectKey, params, options, nil, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return CheckRespCode(resp.StatusCode, []int{http.StatusNoContent}) +} + +func (bucket Bucket) OptionsMethod(objectKey string, options ...Option) (http.Header, error) { + var out http.Header + resp, err := bucket.do("OPTIONS", objectKey, nil, options, nil, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + out = resp.Headers + return out, nil +} + +// public +func (bucket Bucket) Do(method, objectName string, params map[string]interface{}, options []Option, + data io.Reader, listener ProgressListener) (*Response, error) { + return bucket.do(method, objectName, params, options, data, listener) +} + +// Private +func (bucket Bucket) do(method, objectName string, params map[string]interface{}, options []Option, + data io.Reader, listener ProgressListener) (*Response, error) { + headers := make(map[string]string) + err := handleOptions(headers, options) + if err != nil { + return nil, err + } + + err = CheckBucketName(bucket.BucketName) + if len(bucket.BucketName) > 0 && err != nil { + return nil, err + } + + resp, err := bucket.Client.Conn.Do(method, bucket.BucketName, objectName, + params, headers, data, 0, listener) + + // get response header + respHeader, _ := FindOption(options, responseHeader, nil) + if respHeader != nil && resp != nil { + pRespHeader := respHeader.(*http.Header) + *pRespHeader = resp.Headers + } + + return resp, err +} + +func (bucket Bucket) doURL(method HTTPMethod, signedURL string, params map[string]interface{}, options []Option, + data io.Reader, listener ProgressListener) (*Response, error) { + headers := make(map[string]string) + err := handleOptions(headers, options) + if err != nil { + return nil, err + } + + resp, err := bucket.Client.Conn.DoURL(method, signedURL, headers, data, 0, listener) + + // get response header + respHeader, _ := FindOption(options, responseHeader, nil) + if respHeader != nil { + pRespHeader := respHeader.(*http.Header) + *pRespHeader = resp.Headers + } + + return resp, err +} + +func (bucket Bucket) GetConfig() *Config { + return bucket.Client.Config +} + +func AddContentType(options []Option, keys ...string) []Option { + typ := TypeByExtension("") + for _, key := range keys { + typ = TypeByExtension(key) + if typ != "" { + break + } + } + + if typ == "" { + typ = "application/octet-stream" + } + + opts := []Option{ContentType(typ)} + opts = append(opts, options...) + + return opts +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/bucket_credential_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/bucket_credential_test.go new file mode 100644 index 00000000..c4c693cf --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/bucket_credential_test.go @@ -0,0 +1,616 @@ +// Credentials test +package oss + +import ( + "bytes" + "io/ioutil" + "math/rand" + "os" + "strconv" + "strings" + + . "gopkg.in/check.v1" +) + +type OssCredentialBucketSuite struct { + client *Client + creClient *Client + bucket *Bucket + creBucket *Bucket +} + +var _ = Suite(&OssCredentialBucketSuite{}) + +func (cs *OssCredentialBucketSuite) credentialSubUser(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + err = client.CreateBucket(credentialBucketName) + c.Assert(err, IsNil) + cs.client = client + policyInfo := ` + { + "Version":"1", + "Statement":[ + { + "Action":[ + "oss:*" + ], + "Effect":"Allow", + "Principal":["` + credentialUID + `"], + "Resource":["acs:oss:*:*:` + credentialBucketName + `", "acs:oss:*:*:` + credentialBucketName + `/*"] + } + ] + }` + + err = client.SetBucketPolicy(credentialBucketName, policyInfo) + c.Assert(err, IsNil) + + bucket, err := cs.client.Bucket(credentialBucketName) + c.Assert(err, IsNil) + cs.bucket = bucket +} + +// SetUpSuite runs once when the suite starts running. +func (cs *OssCredentialBucketSuite) SetUpSuite(c *C) { + if credentialUID == "" { + testLogger.Println("the cerdential UID is NULL, skip the credential test") + c.Skip("the credential Uid is null") + } + + cs.credentialSubUser(c) + client, err := New(endpoint, credentialAccessID, credentialAccessKey) + c.Assert(err, IsNil) + cs.creClient = client + + bucket, err := cs.creClient.Bucket(credentialBucketName) + c.Assert(err, IsNil) + cs.creBucket = bucket + + testLogger.Println("test credetial bucket started") +} + +func (cs *OssCredentialBucketSuite) TearDownSuite(c *C) { + if credentialUID == "" { + c.Skip("the credential Uid is null") + } + for _, bucket := range []*Bucket{cs.bucket} { + // Delete multipart + keyMarker := KeyMarker("") + uploadIDMarker := UploadIDMarker("") + for { + lmu, err := bucket.ListMultipartUploads(keyMarker, uploadIDMarker) + c.Assert(err, IsNil) + for _, upload := range lmu.Uploads { + imur := InitiateMultipartUploadResult{Bucket: credentialBucketName, Key: upload.Key, UploadID: upload.UploadID} + err = bucket.AbortMultipartUpload(imur) + c.Assert(err, IsNil) + } + keyMarker = KeyMarker(lmu.NextKeyMarker) + uploadIDMarker = UploadIDMarker(lmu.NextUploadIDMarker) + if !lmu.IsTruncated { + break + } + } + // Delete objects + marker := Marker("") + for { + lor, err := bucket.ListObjects(marker) + c.Assert(err, IsNil) + for _, object := range lor.Objects { + err = bucket.DeleteObject(object.Key) + c.Assert(err, IsNil) + } + marker = Marker(lor.NextMarker) + if !lor.IsTruncated { + break + } + } + } + err := cs.client.DeleteBucket(credentialBucketName) + c.Assert(err, IsNil) + testLogger.Println("test credential bucket completed") +} + +// Test put/get/list/delte object +func (cs *OssCredentialBucketSuite) TestReqerPaymentNoRequester(c *C) { + // Set bucket is requester who send the request + reqPayConf := RequestPaymentConfiguration{ + Payer: string(Requester), + } + err := cs.client.SetBucketRequestPayment(credentialBucketName, reqPayConf) + c.Assert(err, IsNil) + + key := objectNamePrefix + RandStr(8) + objectValue := RandStr(18) + + // Put object + err = cs.creBucket.PutObject(key, strings.NewReader(objectValue)) + c.Assert(err, NotNil) + + // Get object + _, err = cs.creBucket.GetObject(key) + c.Assert(err, NotNil) + + // List object + _, err = cs.creBucket.ListObjects() + c.Assert(err, NotNil) + + err = cs.creBucket.DeleteObject(key) + c.Assert(err, NotNil) + + // Set bucket is BucketOwner + reqPayConf.Payer = string(BucketOwner) + err = cs.client.SetBucketRequestPayment(credentialBucketName, reqPayConf) + c.Assert(err, IsNil) +} + +// Test put/get/list/delte object +func (cs *OssCredentialBucketSuite) TestReqerPaymentWithRequester(c *C) { + // Set bucket is requester who send the request + reqPayConf := RequestPaymentConfiguration{ + Payer: string(Requester), + } + err := cs.client.SetBucketRequestPayment(credentialBucketName, reqPayConf) + c.Assert(err, IsNil) + + key := objectNamePrefix + RandStr(8) + objectValue := RandStr(18) + + // Put object with a bucketowner + err = cs.creBucket.PutObject(key, strings.NewReader(objectValue), RequestPayer(BucketOwner)) + c.Assert(err, NotNil) + + // Put object + err = cs.creBucket.PutObject(key, strings.NewReader(objectValue), RequestPayer(Requester)) + c.Assert(err, IsNil) + + // Get object + body, err := cs.creBucket.GetObject(key, RequestPayer(Requester)) + c.Assert(err, IsNil) + defer body.Close() + + data, err := ioutil.ReadAll(body) + c.Assert(err, IsNil) + c.Assert(string(data), Equals, objectValue) + + // List object + lor, err := cs.creBucket.ListObjects(RequestPayer(Requester)) + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, 1) + + err = cs.creBucket.DeleteObject(key, RequestPayer(Requester)) + c.Assert(err, IsNil) + + // Set bucket is BucketOwner + reqPayConf.Payer = string(BucketOwner) + err = cs.client.SetBucketRequestPayment(credentialBucketName, reqPayConf) + c.Assert(err, IsNil) +} + +// Test put/get/list/delte object +func (cs *OssCredentialBucketSuite) TestOwnerPaymentNoRequester(c *C) { + // Set bucket is requester who send the request + reqPayConf := RequestPaymentConfiguration{ + Payer: string(BucketOwner), + } + err := cs.client.SetBucketRequestPayment(credentialBucketName, reqPayConf) + c.Assert(err, IsNil) + + key := objectNamePrefix + RandStr(8) + objectValue := RandStr(18) + + // Put object + err = cs.creBucket.PutObject(key, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Get object + body, err := cs.creBucket.GetObject(key) + c.Assert(err, IsNil) + defer body.Close() + + data, err := ioutil.ReadAll(body) + c.Assert(err, IsNil) + c.Assert(string(data), Equals, objectValue) + + // List object + lor, err := cs.creBucket.ListObjects() + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, 1) + + err = cs.creBucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +// Test put/get/list/delte object +func (cs *OssCredentialBucketSuite) TestOwnerPaymentWithRequester(c *C) { + // Set bucket is BucketOwner payer + reqPayConf := RequestPaymentConfiguration{ + Payer: string(BucketOwner), + } + + err := cs.client.SetBucketRequestPayment(credentialBucketName, reqPayConf) + c.Assert(err, IsNil) + + key := objectNamePrefix + RandStr(8) + objectValue := RandStr(18) + + // Put object + err = cs.creBucket.PutObject(key, strings.NewReader(objectValue), RequestPayer(BucketOwner)) + c.Assert(err, IsNil) + + // Put object + err = cs.creBucket.PutObject(key, strings.NewReader(objectValue), RequestPayer(Requester)) + c.Assert(err, IsNil) + + // Get object + body, err := cs.creBucket.GetObject(key, RequestPayer(Requester)) + c.Assert(err, IsNil) + defer body.Close() + + data, err := ioutil.ReadAll(body) + c.Assert(err, IsNil) + c.Assert(string(data), Equals, objectValue) + + // List object + lor, err := cs.creBucket.ListObjects(RequestPayer(Requester)) + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, 1) + + err = cs.creBucket.DeleteObject(key, RequestPayer(Requester)) + c.Assert(err, IsNil) +} + +// TestPutObjectFromFile +func (cs *OssCredentialBucketSuite) TestPutObjectFromFile(c *C) { + objectName := objectNamePrefix + RandStr(8) + localFile := "../sample/BingWallpaper-2015-11-07.jpg" + newFile := RandStr(8) + ".jpg" + + // Put + err := cs.creBucket.PutObjectFromFile(objectName, localFile, RequestPayer(Requester)) + c.Assert(err, IsNil) + + // Check + err = cs.creBucket.GetObjectToFile(objectName, newFile, RequestPayer(Requester)) + c.Assert(err, IsNil) + eq, err := compareFiles(localFile, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + meta, err := cs.creBucket.GetObjectDetailedMeta(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + c.Assert(meta.Get("Content-Type"), Equals, "image/jpeg") + + acl, err := cs.creBucket.GetObjectACL(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + testLogger.Println("aclRes:", acl) + c.Assert(acl.ACL, Equals, "default") + + err = cs.creBucket.DeleteObject(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + + // Put with properties + options := []Option{ + Expires(futureDate), + ObjectACL(ACLPublicRead), + Meta("myprop", "mypropval"), + RequestPayer(Requester), + } + err = cs.creBucket.PutObjectFromFile(objectName, localFile, options...) + c.Assert(err, IsNil) + + // Check + err = cs.creBucket.GetObjectToFile(objectName, newFile, RequestPayer(Requester)) + c.Assert(err, IsNil) + eq, err = compareFiles(localFile, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + acl, err = cs.creBucket.GetObjectACL(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + testLogger.Println("GetObjectACL:", acl) + c.Assert(acl.ACL, Equals, string(ACLPublicRead)) + + meta, err = cs.creBucket.GetObjectDetailedMeta(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + testLogger.Println("GetObjectDetailedMeta:", meta) + c.Assert(meta.Get("X-Oss-Meta-Myprop"), Equals, "mypropval") + + err = cs.creBucket.DeleteObject(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + os.Remove(newFile) +} + +// TestCopyObject +func (cs *OssCredentialBucketSuite) TestCopyObject(c *C) { + objectName := objectNamePrefix + RandStr(8) + objectValue := RandStr(18) + + err := cs.creBucket.PutObject(objectName, strings.NewReader(objectValue), + ACL(ACLPublicRead), Meta("my", "myprop"), RequestPayer(Requester)) + c.Assert(err, IsNil) + + // Copy + var objectNameDest = objectName + "dest" + _, err = cs.creBucket.CopyObject(objectName, objectNameDest, RequestPayer(Requester)) + c.Assert(err, IsNil) + + // Check + lor, err := cs.creBucket.ListObjects(Prefix(objectName), RequestPayer(Requester)) + c.Assert(err, IsNil) + testLogger.Println("objects:", lor.Objects) + c.Assert(len(lor.Objects), Equals, 2) + + body, err := cs.creBucket.GetObject(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + str, err := readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + err = cs.creBucket.DeleteObject(objectNameDest, RequestPayer(Requester)) + c.Assert(err, IsNil) + + // Copy with constraints x-oss-copy-source-if-modified-since + _, err = cs.creBucket.CopyObject(objectName, objectNameDest, CopySourceIfModifiedSince(futureDate), RequestPayer(Requester)) + c.Assert(err, NotNil) + testLogger.Println("CopyObject:", err) + + // Copy with constraints x-oss-copy-source-if-unmodified-since + _, err = cs.creBucket.CopyObject(objectName, objectNameDest, CopySourceIfUnmodifiedSince(futureDate), RequestPayer(Requester)) + c.Assert(err, IsNil) + + // Check + lor, err = cs.creBucket.ListObjects(Prefix(objectName), RequestPayer(Requester)) + c.Assert(err, IsNil) + testLogger.Println("objects:", lor.Objects) + c.Assert(len(lor.Objects), Equals, 2) + + body, err = cs.creBucket.GetObject(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + err = cs.creBucket.DeleteObject(objectNameDest, RequestPayer(Requester)) + c.Assert(err, IsNil) + + // Copy with constraints x-oss-copy-source-if-match + meta, err := cs.creBucket.GetObjectDetailedMeta(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + testLogger.Println("GetObjectDetailedMeta:", meta) + + _, err = cs.creBucket.CopyObject(objectName, objectNameDest, CopySourceIfMatch(meta.Get("Etag")), RequestPayer(Requester)) + c.Assert(err, IsNil) + + // Check + body, err = cs.creBucket.GetObject(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + err = cs.creBucket.DeleteObject(objectNameDest, RequestPayer(Requester)) + c.Assert(err, IsNil) + + // Copy with constraints x-oss-copy-source-if-none-match + _, err = cs.creBucket.CopyObject(objectName, objectNameDest, CopySourceIfNoneMatch(meta.Get("Etag")), RequestPayer(Requester)) + c.Assert(err, NotNil) + + // Copy with constraints x-oss-metadata-directive + _, err = cs.creBucket.CopyObject(objectName, objectNameDest, Meta("my", "mydestprop"), + MetadataDirective(MetaCopy), RequestPayer(Requester)) + c.Assert(err, IsNil) + + // Check + body, err = cs.creBucket.GetObject(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + destMeta, err := cs.creBucket.GetObjectDetailedMeta(objectNameDest, RequestPayer(Requester)) + c.Assert(err, IsNil) + c.Assert(meta.Get("X-Oss-Meta-My"), Equals, "myprop") + + acl, err := cs.creBucket.GetObjectACL(objectNameDest, RequestPayer(Requester)) + c.Assert(err, IsNil) + c.Assert(acl.ACL, Equals, "default") + + err = cs.creBucket.DeleteObject(objectNameDest, RequestPayer(Requester)) + c.Assert(err, IsNil) + + // Copy with constraints x-oss-metadata-directive and self defined dest object meta + options := []Option{ + ObjectACL(ACLPublicReadWrite), + Meta("my", "mydestprop"), + MetadataDirective(MetaReplace), + RequestPayer(Requester), + } + _, err = cs.creBucket.CopyObject(objectName, objectNameDest, options...) + c.Assert(err, IsNil) + + // Check + body, err = cs.creBucket.GetObject(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + destMeta, err = cs.creBucket.GetObjectDetailedMeta(objectNameDest, RequestPayer(Requester)) + c.Assert(err, IsNil) + c.Assert(destMeta.Get("X-Oss-Meta-My"), Equals, "mydestprop") + + acl, err = cs.creBucket.GetObjectACL(objectNameDest, RequestPayer(Requester)) + c.Assert(err, IsNil) + c.Assert(acl.ACL, Equals, string(ACLPublicReadWrite)) + + err = cs.creBucket.DeleteObject(objectNameDest, RequestPayer(Requester)) + c.Assert(err, IsNil) + + err = cs.creBucket.DeleteObject(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) +} + +// TestCopyObjectToOrFrom +func (cs *OssCredentialBucketSuite) TestCopyObjectToOrFrom(c *C) { + objectName := objectNamePrefix + RandStr(8) + objectValue := RandStr(18) + sorBucketName := credentialBucketName + "-sor" + objectNameDest := objectName + "-Dest" + + err := cs.client.CreateBucket(sorBucketName) + c.Assert(err, IsNil) + // Set ACL_PUBLIC_R + err = cs.client.SetBucketACL(sorBucketName, ACLPublicRead) + c.Assert(err, IsNil) + + sorBucket, err := cs.client.Bucket(sorBucketName) + c.Assert(err, IsNil) + + err = sorBucket.PutObject(objectName, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Copy from + _, err = cs.creBucket.CopyObjectFrom(sorBucketName, objectName, objectNameDest, RequestPayer(Requester)) + c.Assert(err, IsNil) + + // Check + body, err := cs.creBucket.GetObject(objectNameDest, RequestPayer(Requester)) + c.Assert(err, IsNil) + str, err := readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + err = cs.creBucket.DeleteObject(objectNameDest, RequestPayer(Requester)) + c.Assert(err, IsNil) + + // Copy to + _, err = sorBucket.CopyObjectTo(credentialBucketName, objectName, objectName) + c.Assert(err, IsNil) + + // Check + body, err = cs.creBucket.GetObject(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + // Clean + err = sorBucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + err = cs.creBucket.DeleteObject(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + + err = cs.client.DeleteBucket(sorBucketName) + c.Assert(err, IsNil) +} + +// TestAppendObject +func (cs *OssCredentialBucketSuite) TestAppendObject(c *C) { + objectName := objectNamePrefix + RandStr(8) + objectValue1 := RandStr(18) + objectValue2 := RandStr(18) + objectValue := objectValue1 + objectValue2 + var val = []byte(objectValue) + var localFile = RandStr(8) + ".txt" + var nextPos int64 + var midPos = 1 + rand.Intn(len(val)-1) + + var err = CreateFileAndWrite(localFile+"1", val[0:midPos]) + c.Assert(err, IsNil) + err = CreateFileAndWrite(localFile+"2", val[midPos:]) + c.Assert(err, IsNil) + + // String append + nextPos, err = cs.creBucket.AppendObject(objectName, strings.NewReader(objectValue1), nextPos, RequestPayer(Requester)) + c.Assert(err, IsNil) + nextPos, err = cs.creBucket.AppendObject(objectName, strings.NewReader(objectValue2), nextPos, RequestPayer(Requester)) + c.Assert(err, IsNil) + + body, err := cs.creBucket.GetObject(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + str, err := readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + err = cs.creBucket.DeleteObject(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + + // Byte append + nextPos = 0 + nextPos, err = cs.creBucket.AppendObject(objectName, bytes.NewReader(val[0:midPos]), nextPos, RequestPayer(Requester)) + c.Assert(err, IsNil) + nextPos, err = cs.creBucket.AppendObject(objectName, bytes.NewReader(val[midPos:]), nextPos, RequestPayer(Requester)) + c.Assert(err, IsNil) + + body, err = cs.creBucket.GetObject(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + err = cs.creBucket.DeleteObject(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + + // File append + options := []Option{ + ObjectACL(ACLPublicReadWrite), + Meta("my", "myprop"), + RequestPayer(Requester), + } + + fd, err := os.Open(localFile + "1") + c.Assert(err, IsNil) + defer fd.Close() + nextPos = 0 + nextPos, err = cs.creBucket.AppendObject(objectName, fd, nextPos, options...) + c.Assert(err, IsNil) + + meta, err := cs.creBucket.GetObjectDetailedMeta(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + testLogger.Println("GetObjectDetailedMeta:", meta, ",", nextPos) + c.Assert(meta.Get("X-Oss-Object-Type"), Equals, "Appendable") + c.Assert(meta.Get("X-Oss-Meta-My"), Equals, "myprop") + c.Assert(meta.Get("x-oss-Meta-Mine"), Equals, "") + c.Assert(meta.Get("X-Oss-Next-Append-Position"), Equals, strconv.FormatInt(nextPos, 10)) + + acl, err := cs.creBucket.GetObjectACL(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + testLogger.Println("GetObjectACL:", acl) + c.Assert(acl.ACL, Equals, string(ACLPublicReadWrite)) + + // Second append + options = []Option{ + ObjectACL(ACLPublicRead), + Meta("my", "myproptwo"), + Meta("mine", "mypropmine"), + RequestPayer(Requester), + } + fd, err = os.Open(localFile + "2") + c.Assert(err, IsNil) + defer fd.Close() + nextPos, err = cs.creBucket.AppendObject(objectName, fd, nextPos, options...) + c.Assert(err, IsNil) + + body, err = cs.creBucket.GetObject(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + meta, err = cs.creBucket.GetObjectDetailedMeta(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + testLogger.Println("GetObjectDetailedMeta xxx:", meta) + c.Assert(meta.Get("X-Oss-Object-Type"), Equals, "Appendable") + c.Assert(meta.Get("X-Oss-Meta-My"), Equals, "myprop") + c.Assert(meta.Get("x-Oss-Meta-Mine"), Equals, "") + c.Assert(meta.Get("X-Oss-Next-Append-Position"), Equals, strconv.FormatInt(nextPos, 10)) + + acl, err = cs.creBucket.GetObjectACL(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) + c.Assert(acl.ACL, Equals, string(ACLPublicRead)) + + err = cs.creBucket.DeleteObject(objectName, RequestPayer(Requester)) + c.Assert(err, IsNil) +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/bucket_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/bucket_test.go new file mode 100644 index 00000000..d7b2a762 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/bucket_test.go @@ -0,0 +1,5313 @@ +package oss + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "io" + "io/ioutil" + "math/rand" + "net/http" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/baiyubin/aliyun-sts-go-sdk/sts" + + . "gopkg.in/check.v1" +) + +type OssBucketSuite struct { + client *Client + bucket *Bucket + archiveBucket *Bucket +} + +var _ = Suite(&OssBucketSuite{}) + +var ( + pastDate = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) + futureDate = time.Date(2049, time.January, 10, 23, 0, 0, 0, time.UTC) +) + +// SetUpSuite runs once when the suite starts running. +func (s *OssBucketSuite) SetUpSuite(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + s.client = client + + s.client.CreateBucket(bucketName) + + err = s.client.CreateBucket(archiveBucketName, StorageClass(StorageArchive)) + c.Assert(err, IsNil) + + bucket, err := s.client.Bucket(bucketName) + c.Assert(err, IsNil) + s.bucket = bucket + + archiveBucket, err := s.client.Bucket(archiveBucketName) + c.Assert(err, IsNil) + s.archiveBucket = archiveBucket + + testLogger.Println("test bucket started") +} + +// TearDownSuite runs before each test or benchmark starts running. +func (s *OssBucketSuite) TearDownSuite(c *C) { + for _, bucket := range []*Bucket{s.bucket, s.archiveBucket} { + // Delete multipart + keyMarker := KeyMarker("") + uploadIDMarker := UploadIDMarker("") + for { + lmu, err := bucket.ListMultipartUploads(keyMarker, uploadIDMarker) + c.Assert(err, IsNil) + for _, upload := range lmu.Uploads { + imur := InitiateMultipartUploadResult{Bucket: bucketName, Key: upload.Key, UploadID: upload.UploadID} + err = bucket.AbortMultipartUpload(imur) + c.Assert(err, IsNil) + } + keyMarker = KeyMarker(lmu.NextKeyMarker) + uploadIDMarker = UploadIDMarker(lmu.NextUploadIDMarker) + if !lmu.IsTruncated { + break + } + } + + // Delete objects + marker := Marker("") + for { + lor, err := bucket.ListObjects(marker) + c.Assert(err, IsNil) + for _, object := range lor.Objects { + err = bucket.DeleteObject(object.Key) + c.Assert(err, IsNil) + } + marker = Marker(lor.NextMarker) + if !lor.IsTruncated { + break + } + } + + // Delete bucket + err := s.client.DeleteBucket(bucket.BucketName) + c.Assert(err, IsNil) + } + + testLogger.Println("test bucket completed") +} + +// SetUpTest runs after each test or benchmark runs. +func (s *OssBucketSuite) SetUpTest(c *C) { + err := removeTempFiles("../oss", ".jpg") + c.Assert(err, IsNil) +} + +// TearDownTest runs once after all tests or benchmarks have finished running. +func (s *OssBucketSuite) TearDownTest(c *C) { + err := removeTempFiles("../oss", ".jpg") + c.Assert(err, IsNil) + + err = removeTempFiles("../oss", ".txt") + c.Assert(err, IsNil) + + err = removeTempFiles("../oss", ".temp") + c.Assert(err, IsNil) + + err = removeTempFiles("../oss", ".txt1") + c.Assert(err, IsNil) + + err = removeTempFiles("../oss", ".txt2") + c.Assert(err, IsNil) +} + +// TestPutObject +func (s *OssBucketSuite) TestPutObjectOnly(c *C) { + objectName := objectNamePrefix + RandStr(8) + objectValue := "大江东去,浪淘尽,千古风流人物。 故垒西边,人道是、三国周郎赤壁。 乱石穿空,惊涛拍岸,卷起千堆雪。 江山如画,一时多少豪杰。" + + "遥想公谨当年,小乔初嫁了,雄姿英发。 羽扇纶巾,谈笑间、樯橹灰飞烟灭。故国神游,多情应笑我,早生华发,人生如梦,一尊还酹江月。" + + // Put string + var respHeader http.Header + err := s.bucket.PutObject(objectName, strings.NewReader(objectValue), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + + // Check + body, err := s.bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err := readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + acl, err := s.bucket.GetObjectACL(objectName) + c.Assert(err, IsNil) + testLogger.Println("aclRes:", acl) + c.Assert(acl.ACL, Equals, "default") + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Put bytes + err = s.bucket.PutObject(objectName, bytes.NewReader([]byte(objectValue))) + c.Assert(err, IsNil) + + // Check + body, err = s.bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Put file + err = CreateFileAndWrite(objectName+".txt", []byte(objectValue)) + c.Assert(err, IsNil) + fd, err := os.Open(objectName + ".txt") + c.Assert(err, IsNil) + + err = s.bucket.PutObject(objectName, fd) + c.Assert(err, IsNil) + os.Remove(objectName + ".txt") + + // Check + body, err = s.bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Put with properties + objectName = objectNamePrefix + RandStr(8) + options := []Option{ + Expires(futureDate), + ObjectACL(ACLPublicRead), + Meta("myprop", "mypropval"), + } + err = s.bucket.PutObject(objectName, strings.NewReader(objectValue), options...) + c.Assert(err, IsNil) + + // Check + body, err = s.bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + acl, err = s.bucket.GetObjectACL(objectName) + c.Assert(err, IsNil) + testLogger.Println("GetObjectACL:", acl) + c.Assert(acl.ACL, Equals, string(ACLPublicRead)) + + meta, err := s.bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + testLogger.Println("GetObjectDetailedMeta:", meta) + c.Assert(meta.Get("X-Oss-Meta-Myprop"), Equals, "mypropval") + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +func (s *OssBucketSuite) SignURLTestFunc(c *C, authVersion AuthVersionType, extraHeaders []string) { + objectName := objectNamePrefix + RandStr(8) + objectValue := RandStr(20) + + filePath := RandLowStr(10) + content := "复写object" + CreateFile(filePath, content, c) + + notExistfilePath := RandLowStr(10) + os.Remove(notExistfilePath) + + oldType := s.bucket.Client.Config.AuthVersion + oldHeaders := s.bucket.Client.Config.AdditionalHeaders + + s.bucket.Client.Config.AuthVersion = authVersion + s.bucket.Client.Config.AdditionalHeaders = extraHeaders + + // Sign URL for put + str, err := s.bucket.SignURL(objectName, HTTPPut, 60) + c.Assert(err, IsNil) + + if s.bucket.Client.Config.AuthVersion == AuthV1 { + c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true) + } else { + c.Assert(strings.Contains(str, HTTPParamSignatureVersion+"=OSS2"), Equals, true) + c.Assert(strings.Contains(str, HTTPParamExpiresV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyIDV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignatureV2+"="), Equals, true) + } + + // Error put object with URL + err = s.bucket.PutObjectWithURL(str, strings.NewReader(objectValue), ContentType("image/tiff")) + c.Assert(err, NotNil) + c.Assert(err.(ServiceError).Code, Equals, "SignatureDoesNotMatch") + + err = s.bucket.PutObjectFromFileWithURL(str, filePath, ContentType("image/tiff")) + c.Assert(err, NotNil) + c.Assert(err.(ServiceError).Code, Equals, "SignatureDoesNotMatch") + + // Put object with URL + err = s.bucket.PutObjectWithURL(str, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + acl, err := s.bucket.GetObjectACL(objectName) + c.Assert(err, IsNil) + c.Assert(acl.ACL, Equals, "default") + + // Get object meta + meta, err := s.bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + c.Assert(meta.Get(HTTPHeaderContentType), Equals, "application/octet-stream") + c.Assert(meta.Get("X-Oss-Meta-Myprop"), Equals, "") + + // Sign URL for function GetObjectWithURL + str, err = s.bucket.SignURL(objectName, HTTPGet, 60) + c.Assert(err, IsNil) + if s.bucket.Client.Config.AuthVersion == AuthV1 { + c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true) + } else { + c.Assert(strings.Contains(str, HTTPParamSignatureVersion+"=OSS2"), Equals, true) + c.Assert(strings.Contains(str, HTTPParamExpiresV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyIDV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignatureV2+"="), Equals, true) + } + + // Get object with URL + body, err := s.bucket.GetObjectWithURL(str) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + // Sign URL for function PutObjectWithURL + options := []Option{ + ObjectACL(ACLPublicRead), + Meta("myprop", "mypropval"), + ContentType("image/tiff"), + ResponseContentEncoding("deflate"), + } + str, err = s.bucket.SignURL(objectName, HTTPPut, 60, options...) + c.Assert(err, IsNil) + if s.bucket.Client.Config.AuthVersion == AuthV1 { + c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true) + } else { + c.Assert(strings.Contains(str, HTTPParamSignatureVersion+"=OSS2"), Equals, true) + c.Assert(strings.Contains(str, HTTPParamExpiresV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyIDV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignatureV2+"="), Equals, true) + } + + // Put object with URL from file + // Without option, error + err = s.bucket.PutObjectWithURL(str, strings.NewReader(objectValue)) + c.Assert(err, NotNil) + c.Assert(err.(ServiceError).Code, Equals, "SignatureDoesNotMatch") + + err = s.bucket.PutObjectFromFileWithURL(str, filePath) + c.Assert(err, NotNil) + c.Assert(err.(ServiceError).Code, Equals, "SignatureDoesNotMatch") + + // With option, error file + err = s.bucket.PutObjectFromFileWithURL(str, notExistfilePath, options...) + c.Assert(err, NotNil) + + // With option + err = s.bucket.PutObjectFromFileWithURL(str, filePath, options...) + c.Assert(err, IsNil) + + // Get object meta + meta, err = s.bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + c.Assert(meta.Get("X-Oss-Meta-Myprop"), Equals, "mypropval") + c.Assert(meta.Get(HTTPHeaderContentType), Equals, "image/tiff") + + acl, err = s.bucket.GetObjectACL(objectName) + c.Assert(err, IsNil) + c.Assert(acl.ACL, Equals, string(ACLPublicRead)) + + // Sign URL for function GetObjectToFileWithURL + str, err = s.bucket.SignURL(objectName, HTTPGet, 60) + c.Assert(err, IsNil) + + // Get object to file with URL + newFile := RandStr(10) + err = s.bucket.GetObjectToFileWithURL(str, newFile) + c.Assert(err, IsNil) + eq, err := compareFiles(filePath, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + os.Remove(newFile) + + // Get object to file error + err = s.bucket.GetObjectToFileWithURL(str, newFile, options...) + c.Assert(err, NotNil) + c.Assert(err.(ServiceError).Code, Equals, "SignatureDoesNotMatch") + _, err = os.Stat(newFile) + c.Assert(err, NotNil) + + // Get object error + body, err = s.bucket.GetObjectWithURL(str, options...) + c.Assert(err, NotNil) + c.Assert(err.(ServiceError).Code, Equals, "SignatureDoesNotMatch") + c.Assert(body, IsNil) + + // Sign URL for function GetObjectToFileWithURL + options = []Option{ + Expires(futureDate), + ObjectACL(ACLPublicRead), + Meta("myprop", "mypropval"), + ContentType("image/tiff"), + ResponseContentEncoding("deflate"), + } + str, err = s.bucket.SignURL(objectName, HTTPGet, 60, options...) + c.Assert(err, IsNil) + + // Get object to file with URL and options + err = s.bucket.GetObjectToFileWithURL(str, newFile, options...) + c.Assert(err, IsNil) + eq, err = compareFiles(filePath, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + os.Remove(newFile) + + // Get object to file error + err = s.bucket.GetObjectToFileWithURL(str, newFile) + c.Assert(err, NotNil) + c.Assert(err.(ServiceError).Code, Equals, "SignatureDoesNotMatch") + _, err = os.Stat(newFile) + c.Assert(err, NotNil) + + // Get object error + body, err = s.bucket.GetObjectWithURL(str) + c.Assert(err, NotNil) + c.Assert(err.(ServiceError).Code, Equals, "SignatureDoesNotMatch") + c.Assert(body, IsNil) + + err = s.bucket.PutObjectFromFile(objectName, "../sample/The Go Programming Language.html") + c.Assert(err, IsNil) + str, err = s.bucket.SignURL(objectName, HTTPGet, 3600, AcceptEncoding("gzip")) + c.Assert(err, IsNil) + s.bucket.GetObjectToFileWithURL(str, newFile) + c.Assert(err, IsNil) + + os.Remove(filePath) + os.Remove(newFile) + + // Sign URL error + str, err = s.bucket.SignURL(objectName, HTTPGet, -1) + c.Assert(err, NotNil) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Invalid URL parse + str = RandStr(20) + + err = s.bucket.PutObjectWithURL(str, strings.NewReader(objectValue)) + c.Assert(err, NotNil) + + err = s.bucket.GetObjectToFileWithURL(str, newFile) + c.Assert(err, NotNil) + + s.bucket.Client.Config.AuthVersion = oldType + s.bucket.Client.Config.AdditionalHeaders = oldHeaders +} + +func (s *OssBucketSuite) TestSignURL(c *C) { + s.SignURLTestFunc(c, AuthV1, []string{}) + s.SignURLTestFunc(c, AuthV2, []string{}) + s.SignURLTestFunc(c, AuthV2, []string{"host", "range", "user-agent"}) +} + +func (s *OssBucketSuite) SignURLWithEscapedKeyTestFunc(c *C, authVersion AuthVersionType, extraHeaders []string) { + // Key with '/' + objectName := "zyimg/86/e8/653b5dc97bb0022051a84c632bc4" + objectValue := "弃我去者,昨日之日不可留;乱我心者,今日之日多烦忧。长风万里送秋雁,对此可以酣高楼。蓬莱文章建安骨,中间小谢又清发。" + + "俱怀逸兴壮思飞,欲上青天揽明月。抽刀断水水更流,举杯销愁愁更愁。人生在世不称意,明朝散发弄扁舟。" + + oldType := s.bucket.Client.Config.AuthVersion + oldHeaders := s.bucket.Client.Config.AdditionalHeaders + + s.bucket.Client.Config.AuthVersion = authVersion + s.bucket.Client.Config.AdditionalHeaders = extraHeaders + + // Sign URL for function PutObjectWithURL + str, err := s.bucket.SignURL(objectName, HTTPPut, 60) + c.Assert(err, IsNil) + if s.bucket.Client.Config.AuthVersion == AuthV1 { + c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true) + } else { + c.Assert(strings.Contains(str, HTTPParamSignatureVersion+"=OSS2"), Equals, true) + c.Assert(strings.Contains(str, HTTPParamExpiresV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyIDV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignatureV2+"="), Equals, true) + } + + // Put object with URL + err = s.bucket.PutObjectWithURL(str, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Sign URL for function GetObjectWithURL + str, err = s.bucket.SignURL(objectName, HTTPGet, 60) + c.Assert(err, IsNil) + if s.bucket.Client.Config.AuthVersion == AuthV1 { + c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true) + } else { + c.Assert(strings.Contains(str, HTTPParamSignatureVersion+"=OSS2"), Equals, true) + c.Assert(strings.Contains(str, HTTPParamExpiresV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyIDV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignatureV2+"="), Equals, true) + } + + // Get object with URL + body, err := s.bucket.GetObjectWithURL(str) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + // Key with escaped chars + objectName = "<>[]()`?.,!@#$%^&'/*-_=+~:;" + + // Sign URL for funciton PutObjectWithURL + str, err = s.bucket.SignURL(objectName, HTTPPut, 60) + c.Assert(err, IsNil) + if s.bucket.Client.Config.AuthVersion == AuthV1 { + c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true) + } else { + c.Assert(strings.Contains(str, HTTPParamSignatureVersion+"=OSS2"), Equals, true) + c.Assert(strings.Contains(str, HTTPParamExpiresV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyIDV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignatureV2+"="), Equals, true) + } + + // Put object with URL + err = s.bucket.PutObjectWithURL(str, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Sign URL for function GetObjectWithURL + str, err = s.bucket.SignURL(objectName, HTTPGet, 60) + c.Assert(err, IsNil) + if s.bucket.Client.Config.AuthVersion == AuthV1 { + c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true) + } else { + c.Assert(strings.Contains(str, HTTPParamSignatureVersion+"=OSS2"), Equals, true) + c.Assert(strings.Contains(str, HTTPParamExpiresV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyIDV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignatureV2+"="), Equals, true) + } + + // Get object with URL + body, err = s.bucket.GetObjectWithURL(str) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + // Key with Chinese chars + objectName = "风吹柳花满店香,吴姬压酒劝客尝。金陵子弟来相送,欲行不行各尽觞。请君试问东流水,别意与之谁短长。" + + // Sign URL for function PutObjectWithURL + str, err = s.bucket.SignURL(objectName, HTTPPut, 60) + c.Assert(err, IsNil) + if s.bucket.Client.Config.AuthVersion == AuthV1 { + c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true) + } else { + c.Assert(strings.Contains(str, HTTPParamSignatureVersion+"=OSS2"), Equals, true) + c.Assert(strings.Contains(str, HTTPParamExpiresV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyIDV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignatureV2+"="), Equals, true) + } + + // Put object with URL + err = s.bucket.PutObjectWithURL(str, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Sign URL for get function GetObjectWithURL + str, err = s.bucket.SignURL(objectName, HTTPGet, 60) + c.Assert(err, IsNil) + if s.bucket.Client.Config.AuthVersion == AuthV1 { + c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true) + } else { + c.Assert(strings.Contains(str, HTTPParamSignatureVersion+"=OSS2"), Equals, true) + c.Assert(strings.Contains(str, HTTPParamExpiresV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyIDV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignatureV2+"="), Equals, true) + } + + // Get object with URL + body, err = s.bucket.GetObjectWithURL(str) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + // Key + objectName = "test/此情无计可消除/才下眉头/却上 心头/。,;:‘’“”?()『』【】《》!@#¥%……&×/test+ =-_*&^%$#@!`~[]{}()<>|\\/?.,;.txt" + + // Sign URL for function PutObjectWithURL + str, err = s.bucket.SignURL(objectName, HTTPPut, 60) + c.Assert(err, IsNil) + + // Put object with URL + err = s.bucket.PutObjectWithURL(str, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Sign URL for function GetObjectWithURL + str, err = s.bucket.SignURL(objectName, HTTPGet, 60) + c.Assert(err, IsNil) + + // Get object with URL + body, err = s.bucket.GetObjectWithURL(str) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + // Put object + err = s.bucket.PutObject(objectName, bytes.NewReader([]byte(objectValue))) + c.Assert(err, IsNil) + + // Get object + body, err = s.bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + // Delete object + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + s.bucket.Client.Config.AuthVersion = oldType + s.bucket.Client.Config.AdditionalHeaders = oldHeaders +} + +func (s *OssBucketSuite) TestSignURLWithEscapedKey(c *C) { + s.SignURLWithEscapedKeyTestFunc(c, AuthV1, []string{}) + s.SignURLWithEscapedKeyTestFunc(c, AuthV2, []string{}) + s.SignURLWithEscapedKeyTestFunc(c, AuthV2, []string{"host", "range", "user-agent"}) +} + +func (s *OssBucketSuite) SignURLWithEscapedKeyAndPorxyTestFunc(c *C, authVersion AuthVersionType, extraHeaders []string) { + // Key with '/' + objectName := "zyimg/86/e8/653b5dc97bb0022051a84c632bc4" + objectValue := "弃我去者,昨日之日不可留;乱我心者,今日之日多烦忧。长风万里送秋雁,对此可以酣高楼。蓬莱文章建安骨,中间小谢又清发。" + + "俱怀逸兴壮思飞,欲上青天揽明月。抽刀断水水更流,举杯销愁愁更愁。人生在世不称意,明朝散发弄扁舟。" + + options := []ClientOption{ + AuthProxy(proxyHost, proxyUser, proxyPasswd), + AuthVersion(authVersion), + AdditionalHeaders(extraHeaders), + } + + client, err := New(endpoint, accessID, accessKey, options...) + bucket, err := client.Bucket(bucketName) + + // Sign URL for put + str, err := bucket.SignURL(objectName, HTTPPut, 60) + c.Assert(err, IsNil) + if bucket.Client.Config.AuthVersion == AuthV1 { + c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true) + } else { + c.Assert(strings.Contains(str, HTTPParamSignatureVersion+"=OSS2"), Equals, true) + c.Assert(strings.Contains(str, HTTPParamExpiresV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyIDV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignatureV2+"="), Equals, true) + } + + // Put object with URL + err = bucket.PutObjectWithURL(str, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Sign URL for function GetObjectWithURL + str, err = bucket.SignURL(objectName, HTTPGet, 60) + c.Assert(err, IsNil) + if bucket.Client.Config.AuthVersion == AuthV1 { + c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true) + } else { + c.Assert(strings.Contains(str, HTTPParamSignatureVersion+"=OSS2"), Equals, true) + c.Assert(strings.Contains(str, HTTPParamExpiresV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyIDV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignatureV2+"="), Equals, true) + } + + // Get object with URL + body, err := bucket.GetObjectWithURL(str) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + // Key with Chinese chars + objectName = "test/此情无计可消除/才下眉头/却上 心头/。,;:‘’“”?()『』【】《》!@#¥%……&×/test+ =-_*&^%$#@!`~[]{}()<>|\\/?.,;.txt" + + // Sign URL for function PutObjectWithURL + str, err = bucket.SignURL(objectName, HTTPPut, 60) + c.Assert(err, IsNil) + + // Put object with URL + err = bucket.PutObjectWithURL(str, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Sign URL for function GetObjectWithURL + str, err = bucket.SignURL(objectName, HTTPGet, 60) + c.Assert(err, IsNil) + + // Get object with URL + body, err = bucket.GetObjectWithURL(str) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + // Put object + err = bucket.PutObject(objectName, bytes.NewReader([]byte(objectValue))) + c.Assert(err, IsNil) + + // Get object + body, err = bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + // Delete object + err = bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +func (s *OssBucketSuite) TestSignURLWithEscapedKeyAndPorxy(c *C) { + s.SignURLWithEscapedKeyAndPorxyTestFunc(c, AuthV1, []string{}) + s.SignURLWithEscapedKeyAndPorxyTestFunc(c, AuthV2, []string{}) + s.SignURLWithEscapedKeyAndPorxyTestFunc(c, AuthV2, []string{"host", "range", "user-agent"}) +} + +func (s *OssBucketSuite) TestQueryStringAuthV2(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + // set oss v2 signatrue + client.Config.AuthVersion = AuthV2 + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // build QueryString + QueryKey1 := "abc" + QueryKey2 := "|abc" + c.Assert(strings.Compare(QueryKey1, QueryKey2) < 0, Equals, true) + c.Assert(strings.Compare(url.QueryEscape(QueryKey1), url.QueryEscape(QueryKey2)) > 0, Equals, true) + + options := []Option{} + params := map[string]interface{}{} + params[QueryKey1] = "queryValue1" + params[QueryKey2] = "queryValue2" + objectKey := objectNamePrefix + RandStr(8) + resp, _ := bucket.do("HEAD", objectKey, params, options, nil, nil) + + // object not exist,no signature error + c.Assert(resp.StatusCode, Equals, 404) + ForceDeleteBucket(client, bucketName, c) +} + +// TestPutObjectType +func (s *OssBucketSuite) TestPutObjectType(c *C) { + objectName := objectNamePrefix + RandStr(8) + objectValue := "乱石穿空,惊涛拍岸,卷起千堆雪。 江山如画,一时多少豪杰。" + + // Put + err := s.bucket.PutObject(objectName, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Check + body, err := s.bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err := readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + meta, err := s.bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + c.Assert(meta.Get("Content-Type"), Equals, "application/octet-stream") + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Put + err = s.bucket.PutObject(objectName+".txt", strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + meta, err = s.bucket.GetObjectDetailedMeta(objectName + ".txt") + c.Assert(err, IsNil) + c.Assert(strings.Contains(meta.Get("Content-Type"), "text/plain"), Equals, true) + + err = s.bucket.DeleteObject(objectName + ".txt") + c.Assert(err, IsNil) + + // Put + err = s.bucket.PutObject(objectName+".apk", strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + meta, err = s.bucket.GetObjectDetailedMeta(objectName + ".apk") + c.Assert(err, IsNil) + c.Assert(meta.Get("Content-Type"), Equals, "application/vnd.android.package-archive") + + err = s.bucket.DeleteObject(objectName + ".txt") + c.Assert(err, IsNil) +} + +// TestPutObject +func (s *OssBucketSuite) TestPutObjectKeyChars(c *C) { + objectName := objectNamePrefix + RandStr(8) + objectValue := "白日依山尽,黄河入海流。欲穷千里目,更上一层楼。" + + // Put + objectKey := objectName + "十步杀一人,千里不留行。事了拂衣去,深藏身与名" + err := s.bucket.PutObject(objectKey, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Check + body, err := s.bucket.GetObject(objectKey) + c.Assert(err, IsNil) + str, err := readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + err = s.bucket.DeleteObject(objectKey) + c.Assert(err, IsNil) + + // Put + objectKey = objectName + "ごきげん如何ですかおれの顔をよく拝んでおけ" + err = s.bucket.PutObject(objectKey, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Check + body, err = s.bucket.GetObject(objectKey) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + err = s.bucket.DeleteObject(objectKey) + c.Assert(err, IsNil) + + // Put + objectKey = objectName + "~!@#$%^&*()_-+=|\\[]{}<>,./?" + err = s.bucket.PutObject(objectKey, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Check + body, err = s.bucket.GetObject(objectKey) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + err = s.bucket.DeleteObject(objectKey) + c.Assert(err, IsNil) + + // Put + objectKey = "go/中国 日本 +-#&=*" + err = s.bucket.PutObject(objectKey, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Check + body, err = s.bucket.GetObject(objectKey) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + err = s.bucket.DeleteObject(objectKey) + c.Assert(err, IsNil) +} + +// TestPutObjectNegative +func (s *OssBucketSuite) TestPutObjectNegative(c *C) { + objectName := objectNamePrefix + RandStr(8) + objectValue := "大江东去,浪淘尽,千古风流人物。 " + + // Put + objectName = objectNamePrefix + RandStr(8) + err := s.bucket.PutObject(objectName, strings.NewReader(objectValue), + Meta("meta-my", "myprop")) + c.Assert(err, IsNil) + + // Check meta + body, err := s.bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err := readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + meta, err := s.bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + c.Assert(meta.Get("X-Oss-Meta-My"), Not(Equals), "myprop") + c.Assert(meta.Get("X-Oss-Meta-My"), Equals, "") + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Invalid option + err = s.bucket.PutObject(objectName, strings.NewReader(objectValue), + IfModifiedSince(pastDate)) + c.Assert(err, NotNil) + + err = s.bucket.PutObjectFromFile(objectName, "bucket.go", IfModifiedSince(pastDate)) + c.Assert(err, NotNil) + + err = s.bucket.PutObjectFromFile(objectName, "/tmp/xxx") + c.Assert(err, NotNil) +} + +// TestPutObjectFromFile +func (s *OssBucketSuite) TestPutObjectFromFile(c *C) { + objectName := objectNamePrefix + RandStr(8) + localFile := "../sample/BingWallpaper-2015-11-07.jpg" + newFile := "newpic11.jpg" + + // Put + err := s.bucket.PutObjectFromFile(objectName, localFile) + c.Assert(err, IsNil) + + // Check + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + eq, err := compareFiles(localFile, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + acl, err := s.bucket.GetObjectACL(objectName) + c.Assert(err, IsNil) + testLogger.Println("aclRes:", acl) + c.Assert(acl.ACL, Equals, "default") + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Put with properties + options := []Option{ + Expires(futureDate), + ObjectACL(ACLPublicRead), + Meta("myprop", "mypropval"), + } + os.Remove(newFile) + err = s.bucket.PutObjectFromFile(objectName, localFile, options...) + c.Assert(err, IsNil) + + // Check + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + eq, err = compareFiles(localFile, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + acl, err = s.bucket.GetObjectACL(objectName) + c.Assert(err, IsNil) + testLogger.Println("GetObjectACL:", acl) + c.Assert(acl.ACL, Equals, string(ACLPublicRead)) + + meta, err := s.bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + testLogger.Println("GetObjectDetailedMeta:", meta) + c.Assert(meta.Get("X-Oss-Meta-Myprop"), Equals, "mypropval") + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + os.Remove(newFile) +} + +// TestPutObjectFromFile +func (s *OssBucketSuite) TestPutObjectFromFileType(c *C) { + objectName := objectNamePrefix + RandStr(8) + localFile := "../sample/BingWallpaper-2015-11-07.jpg" + newFile := RandStr(8) + ".jpg" + + // Put + err := s.bucket.PutObjectFromFile(objectName, localFile) + c.Assert(err, IsNil) + + // Check + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + eq, err := compareFiles(localFile, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + meta, err := s.bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + c.Assert(meta.Get("Content-Type"), Equals, "image/jpeg") + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + os.Remove(newFile) +} + +// TestGetObject +func (s *OssBucketSuite) TestGetObjectNormal(c *C) { + objectName := objectNamePrefix + RandStr(8) + objectValue := "长忆观潮,满郭人争江上望。来疑沧海尽成空,万面鼓声中。弄潮儿向涛头立,手把红旗旗不湿。别来几向梦中看,梦觉尚心寒。" + + // Put + err := s.bucket.PutObject(objectName, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Check + body, err := s.bucket.GetObject(objectName) + c.Assert(err, IsNil) + data, err := ioutil.ReadAll(body) + body.Close() + str := string(data) + c.Assert(str, Equals, objectValue) + testLogger.Println("GetObjec:", str) + + // Range + var subObjectValue = string(([]byte(objectValue))[15:36]) + body, err = s.bucket.GetObject(objectName, Range(15, 35)) + c.Assert(err, IsNil) + data, err = ioutil.ReadAll(body) + body.Close() + str = string(data) + c.Assert(str, Equals, subObjectValue) + testLogger.Println("GetObject:", str, ",", subObjectValue) + + // If-Modified-Since + _, err = s.bucket.GetObject(objectName, IfModifiedSince(futureDate)) + c.Assert(err, NotNil) + + // If-Unmodified-Since + body, err = s.bucket.GetObject(objectName, IfUnmodifiedSince(futureDate)) + c.Assert(err, IsNil) + data, err = ioutil.ReadAll(body) + body.Close() + c.Assert(string(data), Equals, objectValue) + + meta, err := s.bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + + // If-Match + body, err = s.bucket.GetObject(objectName, IfMatch(meta.Get("Etag"))) + c.Assert(err, IsNil) + data, err = ioutil.ReadAll(body) + body.Close() + c.Assert(string(data), Equals, objectValue) + + // If-None-Match + _, err = s.bucket.GetObject(objectName, IfNoneMatch(meta.Get("Etag"))) + c.Assert(err, NotNil) + + // process + err = s.bucket.PutObjectFromFile(objectName, "../sample/BingWallpaper-2015-11-07.jpg") + c.Assert(err, IsNil) + _, err = s.bucket.GetObject(objectName, Process("image/format,png")) + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +// TestGetObjectNegative +func (s *OssBucketSuite) TestGetObjectToWriterNegative(c *C) { + objectName := objectNamePrefix + RandStr(8) + objectValue := "长忆观潮,满郭人争江上望。" + + // Object not exist + _, err := s.bucket.GetObject("NotExist") + c.Assert(err, NotNil) + + // Constraint invalid + err = s.bucket.PutObject(objectName, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Out of range + _, err = s.bucket.GetObject(objectName, Range(15, 1000)) + c.Assert(err, IsNil) + + // Not exist + err = s.bucket.GetObjectToFile(objectName, "/root1/123abc9874") + c.Assert(err, NotNil) + + // Invalid option + _, err = s.bucket.GetObject(objectName, ACL(ACLPublicRead)) + c.Assert(err, IsNil) + + err = s.bucket.GetObjectToFile(objectName, "newpic15.jpg", ACL(ACLPublicRead)) + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +// TestGetObjectToFile +func (s *OssBucketSuite) TestGetObjectToFile(c *C) { + objectName := objectNamePrefix + RandStr(8) + objectValue := "江南好,风景旧曾谙;日出江花红胜火,春来江水绿如蓝。能不忆江南?江南忆,最忆是杭州;山寺月中寻桂子,郡亭枕上看潮头。何日更重游!" + newFile := RandStr(8) + ".jpg" + + // Put + var val = []byte(objectValue) + err := s.bucket.PutObject(objectName, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Check + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + eq, err := compareFileData(newFile, val) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + os.Remove(newFile) + + // Range + err = s.bucket.GetObjectToFile(objectName, newFile, Range(15, 35)) + c.Assert(err, IsNil) + eq, err = compareFileData(newFile, val[15:36]) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + os.Remove(newFile) + + err = s.bucket.GetObjectToFile(objectName, newFile, NormalizedRange("15-35")) + c.Assert(err, IsNil) + eq, err = compareFileData(newFile, val[15:36]) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + os.Remove(newFile) + + err = s.bucket.GetObjectToFile(objectName, newFile, NormalizedRange("15-")) + c.Assert(err, IsNil) + eq, err = compareFileData(newFile, val[15:]) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + os.Remove(newFile) + + err = s.bucket.GetObjectToFile(objectName, newFile, NormalizedRange("-10")) + c.Assert(err, IsNil) + eq, err = compareFileData(newFile, val[(len(val)-10):len(val)]) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + os.Remove(newFile) + + // If-Modified-Since + err = s.bucket.GetObjectToFile(objectName, newFile, IfModifiedSince(futureDate)) + c.Assert(err, NotNil) + + // If-Unmodified-Since + err = s.bucket.GetObjectToFile(objectName, newFile, IfUnmodifiedSince(futureDate)) + c.Assert(err, IsNil) + eq, err = compareFileData(newFile, val) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + os.Remove(newFile) + + meta, err := s.bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + testLogger.Println("GetObjectDetailedMeta:", meta) + + // If-Match + err = s.bucket.GetObjectToFile(objectName, newFile, IfMatch(meta.Get("Etag"))) + c.Assert(err, IsNil) + eq, err = compareFileData(newFile, val) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // If-None-Match + err = s.bucket.GetObjectToFile(objectName, newFile, IfNoneMatch(meta.Get("Etag"))) + c.Assert(err, NotNil) + + // Accept-Encoding:gzip + err = s.bucket.PutObjectFromFile(objectName, "../sample/The Go Programming Language.html") + c.Assert(err, IsNil) + err = s.bucket.GetObjectToFile(objectName, newFile, AcceptEncoding("gzip")) + c.Assert(err, IsNil) + + os.Remove(newFile) + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +// TestListObjects +func (s *OssBucketSuite) TestListObjects(c *C) { + objectName := objectNamePrefix + RandStr(8) + + // List empty bucket + lor, err := s.bucket.ListObjects() + c.Assert(err, IsNil) + left := len(lor.Objects) + + // Put three objects + err = s.bucket.PutObject(objectName+"1", strings.NewReader("")) + c.Assert(err, IsNil) + err = s.bucket.PutObject(objectName+"2", strings.NewReader("")) + c.Assert(err, IsNil) + err = s.bucket.PutObject(objectName+"3", strings.NewReader("")) + c.Assert(err, IsNil) + + // List + lor, err = s.bucket.ListObjects() + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, left+3) + + // List with prefix + lor, err = s.bucket.ListObjects(Prefix(objectName + "2")) + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, 1) + + lor, err = s.bucket.ListObjects(Prefix(objectName + "22")) + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, 0) + + // List with max keys + lor, err = s.bucket.ListObjects(Prefix(objectName), MaxKeys(2)) + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, 2) + + // List with marker + lor, err = s.bucket.ListObjects(Marker(objectName+"1"), MaxKeys(1)) + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, 1) + + err = s.bucket.DeleteObject(objectName + "1") + c.Assert(err, IsNil) + err = s.bucket.DeleteObject(objectName + "2") + c.Assert(err, IsNil) + err = s.bucket.DeleteObject(objectName + "3") + c.Assert(err, IsNil) +} + +// TestListObjects +func (s *OssBucketSuite) TestListObjectsV2NotBatch(c *C) { + objectName := objectNamePrefix + RandStr(8) + + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // List empty bucket + lor, err := bucket.ListObjectsV2(StartAfter("")) + c.Assert(err, IsNil) + left := len(lor.Objects) + + // Put three objects + err = bucket.PutObject(objectName+"1", strings.NewReader("")) + c.Assert(err, IsNil) + err = bucket.PutObject(objectName+"2", strings.NewReader("")) + c.Assert(err, IsNil) + err = bucket.PutObject(objectName+"3", strings.NewReader("")) + c.Assert(err, IsNil) + + // List + lor, err = bucket.ListObjectsV2(FetchOwner(true)) + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, left+3) + c.Assert(len(lor.Objects[0].Owner.ID) > 0, Equals, true) + c.Assert(len(lor.Objects[0].Owner.DisplayName) > 0, Equals, true) + + // List with prefix + lor, err = bucket.ListObjectsV2(Prefix(objectName + "2")) + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, 1) + c.Assert(lor.Objects[0].Key, Equals, objectName+"2") + + lor, err = bucket.ListObjectsV2(Prefix(objectName + "22")) + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, 0) + + // List with max keys + lor, err = bucket.ListObjectsV2(Prefix(objectName), MaxKeys(2)) + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, 2) + + // List with marker + lor, err = bucket.ListObjectsV2(StartAfter(objectName+"1"), MaxKeys(1)) + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, 1) + c.Assert(lor.IsTruncated, Equals, true) + c.Assert(len(lor.NextContinuationToken) > 0, Equals, true) + c.Assert(lor.Objects[0].Key, Equals, objectName+"2") + + lor, err = bucket.ListObjectsV2(Prefix(objectName), StartAfter(objectName+"1"), MaxKeys(2)) + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, 2) + c.Assert(lor.IsTruncated, Equals, false) + c.Assert(lor.NextContinuationToken, Equals, "") + ForceDeleteBucket(client, bucketName, c) + c.Assert(lor.Objects[0].Key, Equals, objectName+"2") + c.Assert(lor.Objects[1].Key, Equals, objectName+"3") +} + +// TestListObjects +func (s *OssBucketSuite) TestListObjectsV2BatchList(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // Put three objects + count := 17 + objectName := "testobject-" + RandLowStr(6) + for i := 0; i < count; i++ { + err = bucket.PutObject(objectName+strconv.Itoa(i), strings.NewReader("")) + c.Assert(err, IsNil) + } + + Objects := []ObjectProperties{} + + // List Object + continuationToken := "" + prefix := "" + for { + lor, err := bucket.ListObjectsV2(Prefix(prefix), ContinuationToken(continuationToken), MaxKeys(3)) + c.Assert(err, IsNil) + Objects = append(Objects, lor.Objects...) + continuationToken = lor.NextContinuationToken + if !lor.IsTruncated { + break + } + } + c.Assert(len(Objects), Equals, count) + ForceDeleteBucket(client, bucketName, c) +} + +// TestListObjects +func (s *OssBucketSuite) TestListObjectsEncodingType(c *C) { + prefix := objectNamePrefix + "床前明月光,疑是地上霜。举头望明月,低头思故乡。" + + for i := 0; i < 10; i++ { + err := s.bucket.PutObject(prefix+strconv.Itoa(i), strings.NewReader("")) + c.Assert(err, IsNil) + } + + lor, err := s.bucket.ListObjects(Prefix(objectNamePrefix + "床前明月光,")) + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, 10) + + lor, err = s.bucket.ListObjects(Marker(objectNamePrefix + "床前明月光,疑是地上霜。举头望明月,低头思故乡。")) + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, 10) + + lor, err = s.bucket.ListObjects(Prefix(objectNamePrefix + "床前明月光")) + c.Assert(err, IsNil) + for i, obj := range lor.Objects { + c.Assert(obj.Key, Equals, prefix+strconv.Itoa(i)) + } + + for i := 0; i < 10; i++ { + err = s.bucket.DeleteObject(prefix + strconv.Itoa(i)) + c.Assert(err, IsNil) + } + + // Special characters + objectName := objectNamePrefix + "` ~ ! @ # $ % ^ & * () - _ + =[] {} \\ | < > , . ? / 0" + err = s.bucket.PutObject(objectName, strings.NewReader("明月几时有,把酒问青天")) + c.Assert(err, IsNil) + + lor, err = s.bucket.ListObjects(Prefix(objectName)) + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, 1) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + objectName = objectNamePrefix + "中国 日本 +-#&=*" + err = s.bucket.PutObject(objectName, strings.NewReader("明月几时有,把酒问青天")) + c.Assert(err, IsNil) + + lor, err = s.bucket.ListObjects(Prefix(objectName)) + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, 1) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +// TestIsBucketExist +func (s *OssBucketSuite) TestIsObjectExist(c *C) { + objectName := objectNamePrefix + RandStr(8) + + // Put three objects + err := s.bucket.PutObject(objectName+"1", strings.NewReader("")) + c.Assert(err, IsNil) + err = s.bucket.PutObject(objectName+"11", strings.NewReader("")) + c.Assert(err, IsNil) + err = s.bucket.PutObject(objectName+"111", strings.NewReader("")) + c.Assert(err, IsNil) + + // Exist + exist, err := s.bucket.IsObjectExist(objectName + "11") + c.Assert(err, IsNil) + c.Assert(exist, Equals, true) + + exist, err = s.bucket.IsObjectExist(objectName + "1") + c.Assert(err, IsNil) + c.Assert(exist, Equals, true) + + exist, err = s.bucket.IsObjectExist(objectName + "111") + c.Assert(err, IsNil) + c.Assert(exist, Equals, true) + + // Not exist + exist, err = s.bucket.IsObjectExist(objectName + "1111") + c.Assert(err, IsNil) + c.Assert(exist, Equals, false) + + exist, err = s.bucket.IsObjectExist(objectName) + c.Assert(err, IsNil) + c.Assert(exist, Equals, false) + + err = s.bucket.DeleteObject(objectName + "1") + c.Assert(err, IsNil) + err = s.bucket.DeleteObject(objectName + "11") + c.Assert(err, IsNil) + err = s.bucket.DeleteObject(objectName + "111") + c.Assert(err, IsNil) +} + +// TestDeleteObject +func (s *OssBucketSuite) TestDeleteObject(c *C) { + objectName := objectNamePrefix + RandStr(8) + + err := s.bucket.PutObject(objectName, strings.NewReader("")) + c.Assert(err, IsNil) + + lor, err := s.bucket.ListObjects(Prefix(objectName)) + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, 1) + + // Delete + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Duplicate delete + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + lor, err = s.bucket.ListObjects(Prefix(objectName)) + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, 0) +} + +// TestDeleteObjects +func (s *OssBucketSuite) TestDeleteObjectsNormal(c *C) { + objectName := objectNamePrefix + RandStr(8) + + // Delete objects + err := s.bucket.PutObject(objectName, strings.NewReader("")) + c.Assert(err, IsNil) + + res, err := s.bucket.DeleteObjects([]string{objectName}) + c.Assert(err, IsNil) + c.Assert(len(res.DeletedObjects), Equals, 1) + + lor, err := s.bucket.ListObjects(Prefix(objectName)) + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, 0) + + // Delete objects + err = s.bucket.PutObject(objectName+"1", strings.NewReader("")) + c.Assert(err, IsNil) + + err = s.bucket.PutObject(objectName+"2", strings.NewReader("")) + c.Assert(err, IsNil) + + res, err = s.bucket.DeleteObjects([]string{objectName + "1", objectName + "2"}) + c.Assert(err, IsNil) + c.Assert(len(res.DeletedObjects), Equals, 2) + + lor, err = s.bucket.ListObjects(Prefix(objectName)) + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, 0) + + // Delete 0 + _, err = s.bucket.DeleteObjects([]string{}) + c.Assert(err, NotNil) + + // DeleteObjectsQuiet + err = s.bucket.PutObject(objectName+"1", strings.NewReader("")) + c.Assert(err, IsNil) + + err = s.bucket.PutObject(objectName+"2", strings.NewReader("")) + c.Assert(err, IsNil) + + res, err = s.bucket.DeleteObjects([]string{objectName + "1", objectName + "2"}, + DeleteObjectsQuiet(false)) + c.Assert(err, IsNil) + c.Assert(len(res.DeletedObjects), Equals, 2) + + lor, err = s.bucket.ListObjects(Prefix(objectName)) + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, 0) + + // DeleteObjectsQuiet + err = s.bucket.PutObject(objectName+"1", strings.NewReader("")) + c.Assert(err, IsNil) + + err = s.bucket.PutObject(objectName+"2", strings.NewReader("")) + c.Assert(err, IsNil) + + res, err = s.bucket.DeleteObjects([]string{objectName + "1", objectName + "2"}, + DeleteObjectsQuiet(true)) + c.Assert(err, IsNil) + c.Assert(len(res.DeletedObjects), Equals, 0) + + lor, err = s.bucket.ListObjects(Prefix(objectName)) + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, 0) + + // EncodingType + err = s.bucket.PutObject("中国人", strings.NewReader("")) + c.Assert(err, IsNil) + + res, err = s.bucket.DeleteObjects([]string{"中国人"}) + c.Assert(err, IsNil) + c.Assert(len(res.DeletedObjects), Equals, 1) + c.Assert(res.DeletedObjects[0], Equals, "中国人") + + // EncodingType + err = s.bucket.PutObject("中国人", strings.NewReader("")) + c.Assert(err, IsNil) + + res, err = s.bucket.DeleteObjects([]string{"中国人"}, DeleteObjectsQuiet(false)) + c.Assert(err, IsNil) + c.Assert(len(res.DeletedObjects), Equals, 1) + c.Assert(res.DeletedObjects[0], Equals, "中国人") + + // EncodingType + err = s.bucket.PutObject("中国人", strings.NewReader("")) + c.Assert(err, IsNil) + + res, err = s.bucket.DeleteObjects([]string{"中国人"}, DeleteObjectsQuiet(true)) + c.Assert(err, IsNil) + c.Assert(len(res.DeletedObjects), Equals, 0) + + // Special characters + key := "A ' < > \" & ~ ` ! @ # $ % ^ & * ( ) [] {} - _ + = / | \\ ? . , : ; A" + err = s.bucket.PutObject(key, strings.NewReader("value")) + c.Assert(err, IsNil) + + _, err = s.bucket.DeleteObjects([]string{key}) + c.Assert(err, IsNil) + + ress, err := s.bucket.ListObjects(Prefix(key)) + c.Assert(err, IsNil) + c.Assert(len(ress.Objects), Equals, 0) + + // Not exist + _, err = s.bucket.DeleteObjects([]string{"NotExistObject"}) + c.Assert(err, IsNil) +} + +// TestSetObjectMeta +func (s *OssBucketSuite) TestSetObjectMeta(c *C) { + objectName := objectNamePrefix + RandStr(8) + + err := s.bucket.PutObject(objectName, strings.NewReader("")) + c.Assert(err, IsNil) + + err = s.bucket.SetObjectMeta(objectName, + Expires(futureDate), + Meta("myprop", "mypropval")) + c.Assert(err, IsNil) + + meta, err := s.bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + testLogger.Println("Meta:", meta) + c.Assert(meta.Get("Expires"), Equals, futureDate.Format(http.TimeFormat)) + c.Assert(meta.Get("X-Oss-Meta-Myprop"), Equals, "mypropval") + + acl, err := s.bucket.GetObjectACL(objectName) + c.Assert(err, IsNil) + c.Assert(acl.ACL, Equals, "default") + + // Invalid option + err = s.bucket.SetObjectMeta(objectName, AcceptEncoding("url")) + c.Assert(err, IsNil) + + // Invalid option value + err = s.bucket.SetObjectMeta(objectName, ServerSideEncryption("invalid")) + c.Assert(err, NotNil) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Not exist + err = s.bucket.SetObjectMeta(objectName, Expires(futureDate)) + c.Assert(err, NotNil) +} + +// TestGetObjectMeta +func (s *OssBucketSuite) TestGetObjectMeta(c *C) { + objectName := objectNamePrefix + RandStr(8) + + // Put + err := s.bucket.PutObject(objectName, strings.NewReader("")) + c.Assert(err, IsNil) + + meta, err := s.bucket.GetObjectMeta(objectName) + c.Assert(err, IsNil) + c.Assert(len(meta) > 0, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + _, err = s.bucket.GetObjectMeta("NotExistObject") + c.Assert(err, NotNil) +} + +// TestGetObjectDetailedMeta +func (s *OssBucketSuite) TestGetObjectDetailedMeta(c *C) { + objectName := objectNamePrefix + RandStr(8) + + // Put + err := s.bucket.PutObject(objectName, strings.NewReader(""), + Expires(futureDate), Meta("myprop", "mypropval")) + c.Assert(err, IsNil) + + // Check + meta, err := s.bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + testLogger.Println("GetObjectDetailedMeta:", meta) + c.Assert(meta.Get("Expires"), Equals, futureDate.Format(http.TimeFormat)) + c.Assert(meta.Get("X-Oss-Meta-Myprop"), Equals, "mypropval") + c.Assert(meta.Get("Content-Length"), Equals, "0") + c.Assert(len(meta.Get("Date")) > 0, Equals, true) + c.Assert(len(meta.Get("X-Oss-Request-Id")) > 0, Equals, true) + c.Assert(len(meta.Get("Last-Modified")) > 0, Equals, true) + + // IfModifiedSince/IfModifiedSince + _, err = s.bucket.GetObjectDetailedMeta(objectName, IfModifiedSince(futureDate)) + c.Assert(err, NotNil) + + meta, err = s.bucket.GetObjectDetailedMeta(objectName, IfUnmodifiedSince(futureDate)) + c.Assert(err, IsNil) + c.Assert(meta.Get("Expires"), Equals, futureDate.Format(http.TimeFormat)) + c.Assert(meta.Get("X-Oss-Meta-Myprop"), Equals, "mypropval") + + // IfMatch/IfNoneMatch + _, err = s.bucket.GetObjectDetailedMeta(objectName, IfNoneMatch(meta.Get("Etag"))) + c.Assert(err, NotNil) + + meta, err = s.bucket.GetObjectDetailedMeta(objectName, IfMatch(meta.Get("Etag"))) + c.Assert(err, IsNil) + c.Assert(meta.Get("Expires"), Equals, futureDate.Format(http.TimeFormat)) + c.Assert(meta.Get("X-Oss-Meta-Myprop"), Equals, "mypropval") + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + _, err = s.bucket.GetObjectDetailedMeta("NotExistObject") + c.Assert(err, NotNil) +} + +// TestSetAndGetObjectAcl +func (s *OssBucketSuite) TestSetAndGetObjectAcl(c *C) { + objectName := objectNamePrefix + RandStr(8) + + err := s.bucket.PutObject(objectName, strings.NewReader("")) + c.Assert(err, IsNil) + + // Default + acl, err := s.bucket.GetObjectACL(objectName) + c.Assert(err, IsNil) + c.Assert(acl.ACL, Equals, "default") + + // Set ACL_PUBLIC_RW + err = s.bucket.SetObjectACL(objectName, ACLPublicReadWrite) + c.Assert(err, IsNil) + + acl, err = s.bucket.GetObjectACL(objectName) + c.Assert(err, IsNil) + c.Assert(acl.ACL, Equals, string(ACLPublicReadWrite)) + + // Set ACL_PRIVATE + err = s.bucket.SetObjectACL(objectName, ACLPrivate) + c.Assert(err, IsNil) + + acl, err = s.bucket.GetObjectACL(objectName) + c.Assert(err, IsNil) + c.Assert(acl.ACL, Equals, string(ACLPrivate)) + + // Set ACL_PUBLIC_R + err = s.bucket.SetObjectACL(objectName, ACLPublicRead) + c.Assert(err, IsNil) + + acl, err = s.bucket.GetObjectACL(objectName) + c.Assert(err, IsNil) + c.Assert(acl.ACL, Equals, string(ACLPublicRead)) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +// TestSetAndGetObjectAclNegative +func (s *OssBucketSuite) TestSetAndGetObjectAclNegative(c *C) { + objectName := objectNamePrefix + RandStr(8) + + // Object not exist + err := s.bucket.SetObjectACL(objectName, ACLPublicRead) + c.Assert(err, NotNil) +} + +// TestCopyObject +func (s *OssBucketSuite) TestCopyObject(c *C) { + objectName := objectNamePrefix + RandStr(8) + objectValue := "男儿何不带吴钩,收取关山五十州。请君暂上凌烟阁,若个书生万户侯?" + + err := s.bucket.PutObject(objectName, strings.NewReader(objectValue), + ACL(ACLPublicRead), Meta("my", "myprop")) + c.Assert(err, IsNil) + + // Copy + var objectNameDest = objectName + "dest" + _, err = s.bucket.CopyObject(objectName, objectNameDest) + c.Assert(err, IsNil) + + // Check + lor, err := s.bucket.ListObjects(Prefix(objectName)) + c.Assert(err, IsNil) + testLogger.Println("objects:", lor.Objects) + c.Assert(len(lor.Objects), Equals, 2) + + body, err := s.bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err := readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + err = s.bucket.DeleteObject(objectNameDest) + c.Assert(err, IsNil) + + // Copy with constraints x-oss-copy-source-if-modified-since + _, err = s.bucket.CopyObject(objectName, objectNameDest, CopySourceIfModifiedSince(futureDate)) + c.Assert(err, NotNil) + testLogger.Println("CopyObject:", err) + + // Copy with constraints x-oss-copy-source-if-unmodified-since + _, err = s.bucket.CopyObject(objectName, objectNameDest, CopySourceIfUnmodifiedSince(futureDate)) + c.Assert(err, IsNil) + + // Check + lor, err = s.bucket.ListObjects(Prefix(objectName)) + c.Assert(err, IsNil) + testLogger.Println("objects:", lor.Objects) + c.Assert(len(lor.Objects), Equals, 2) + + body, err = s.bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + err = s.bucket.DeleteObject(objectNameDest) + c.Assert(err, IsNil) + + // Copy with constraints x-oss-copy-source-if-match + meta, err := s.bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + testLogger.Println("GetObjectDetailedMeta:", meta) + + _, err = s.bucket.CopyObject(objectName, objectNameDest, CopySourceIfMatch(meta.Get("Etag"))) + c.Assert(err, IsNil) + + // Check + body, err = s.bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + err = s.bucket.DeleteObject(objectNameDest) + c.Assert(err, IsNil) + + // Copy with constraints x-oss-copy-source-if-none-match + _, err = s.bucket.CopyObject(objectName, objectNameDest, CopySourceIfNoneMatch(meta.Get("Etag"))) + c.Assert(err, NotNil) + + // Copy with constraints x-oss-metadata-directive + _, err = s.bucket.CopyObject(objectName, objectNameDest, Meta("my", "mydestprop"), + MetadataDirective(MetaCopy)) + c.Assert(err, IsNil) + + // Check + body, err = s.bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + destMeta, err := s.bucket.GetObjectDetailedMeta(objectNameDest) + c.Assert(err, IsNil) + c.Assert(meta.Get("X-Oss-Meta-My"), Equals, "myprop") + + acl, err := s.bucket.GetObjectACL(objectNameDest) + c.Assert(err, IsNil) + c.Assert(acl.ACL, Equals, "default") + + err = s.bucket.DeleteObject(objectNameDest) + c.Assert(err, IsNil) + + // Copy with constraints x-oss-metadata-directive and self defined dest object meta + options := []Option{ + ObjectACL(ACLPublicReadWrite), + Meta("my", "mydestprop"), + MetadataDirective(MetaReplace), + } + _, err = s.bucket.CopyObject(objectName, objectNameDest, options...) + c.Assert(err, IsNil) + + // Check + body, err = s.bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + destMeta, err = s.bucket.GetObjectDetailedMeta(objectNameDest) + c.Assert(err, IsNil) + c.Assert(destMeta.Get("X-Oss-Meta-My"), Equals, "mydestprop") + + acl, err = s.bucket.GetObjectACL(objectNameDest) + c.Assert(err, IsNil) + c.Assert(acl.ACL, Equals, string(ACLPublicReadWrite)) + + err = s.bucket.DeleteObject(objectNameDest) + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +// TestCopyObjectToOrFrom +func (s *OssBucketSuite) TestCopyObjectToOrFrom(c *C) { + objectName := objectNamePrefix + RandStr(8) + objectValue := "男儿何不带吴钩,收取关山五十州。请君暂上凌烟阁,若个书生万户侯?" + destBucketName := bucketName + "-dest" + objectNameDest := objectName + "-dest" + + err := s.client.CreateBucket(destBucketName) + c.Assert(err, IsNil) + + destBucket, err := s.client.Bucket(destBucketName) + c.Assert(err, IsNil) + + err = s.bucket.PutObject(objectName, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Copy from + _, err = destBucket.CopyObjectFrom(bucketName, objectName, objectNameDest) + c.Assert(err, IsNil) + + // Check + body, err := destBucket.GetObject(objectNameDest) + c.Assert(err, IsNil) + str, err := readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Copy to + _, err = destBucket.CopyObjectTo(bucketName, objectName, objectNameDest) + c.Assert(err, IsNil) + + // Check + body, err = s.bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + // Clean + err = destBucket.DeleteObject(objectNameDest) + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + err = s.client.DeleteBucket(destBucketName) + c.Assert(err, IsNil) +} + +// TestCopyObjectToOrFromNegative +func (s *OssBucketSuite) TestCopyObjectToOrFromNegative(c *C) { + objectName := objectNamePrefix + RandStr(8) + destBucket := bucketName + "-dest" + objectNameDest := objectName + "-dest" + + // Object not exist + _, err := s.bucket.CopyObjectTo(bucketName, objectName, objectNameDest) + c.Assert(err, NotNil) + + // Bucket not exist + _, err = s.bucket.CopyObjectFrom(destBucket, objectNameDest, objectName) + c.Assert(err, NotNil) +} + +// TestAppendObject +func (s *OssBucketSuite) TestAppendObject(c *C) { + objectName := objectNamePrefix + RandStr(8) + objectValue := "昨夜雨疏风骤,浓睡不消残酒。试问卷帘人,却道海棠依旧。知否?知否?应是绿肥红瘦。" + var val = []byte(objectValue) + var localFile = RandStr(8) + ".txt" + var nextPos int64 + var midPos = 1 + rand.Intn(len(val)-1) + + var err = CreateFileAndWrite(localFile+"1", val[0:midPos]) + c.Assert(err, IsNil) + err = CreateFileAndWrite(localFile+"2", val[midPos:]) + c.Assert(err, IsNil) + + // String append + nextPos, err = s.bucket.AppendObject(objectName, strings.NewReader("昨夜雨疏风骤,浓睡不消残酒。试问卷帘人,"), nextPos) + c.Assert(err, IsNil) + nextPos, err = s.bucket.AppendObject(objectName, strings.NewReader("却道海棠依旧。知否?知否?应是绿肥红瘦。"), nextPos) + c.Assert(err, IsNil) + + body, err := s.bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err := readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Byte append + nextPos = 0 + nextPos, err = s.bucket.AppendObject(objectName, bytes.NewReader(val[0:midPos]), nextPos) + c.Assert(err, IsNil) + nextPos, err = s.bucket.AppendObject(objectName, bytes.NewReader(val[midPos:]), nextPos) + c.Assert(err, IsNil) + + body, err = s.bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // File append + options := []Option{ + ObjectACL(ACLPublicReadWrite), + Meta("my", "myprop"), + } + + fd, err := os.Open(localFile + "1") + c.Assert(err, IsNil) + defer fd.Close() + nextPos = 0 + nextPos, err = s.bucket.AppendObject(objectName, fd, nextPos, options...) + c.Assert(err, IsNil) + + meta, err := s.bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + testLogger.Println("GetObjectDetailedMeta:", meta, ",", nextPos) + c.Assert(meta.Get("X-Oss-Object-Type"), Equals, "Appendable") + c.Assert(meta.Get("X-Oss-Meta-My"), Equals, "myprop") + c.Assert(meta.Get("x-oss-Meta-Mine"), Equals, "") + c.Assert(meta.Get("X-Oss-Next-Append-Position"), Equals, strconv.FormatInt(nextPos, 10)) + + acl, err := s.bucket.GetObjectACL(objectName) + c.Assert(err, IsNil) + testLogger.Println("GetObjectACL:", acl) + c.Assert(acl.ACL, Equals, string(ACLPublicReadWrite)) + + // Second append + options = []Option{ + ObjectACL(ACLPublicRead), + Meta("my", "myproptwo"), + Meta("mine", "mypropmine"), + } + fd, err = os.Open(localFile + "2") + c.Assert(err, IsNil) + defer fd.Close() + nextPos, err = s.bucket.AppendObject(objectName, fd, nextPos, options...) + c.Assert(err, IsNil) + + body, err = s.bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + meta, err = s.bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + testLogger.Println("GetObjectDetailedMeta xxx:", meta) + c.Assert(meta.Get("X-Oss-Object-Type"), Equals, "Appendable") + c.Assert(meta.Get("X-Oss-Meta-My"), Equals, "myprop") + c.Assert(meta.Get("x-Oss-Meta-Mine"), Equals, "") + c.Assert(meta.Get("X-Oss-Next-Append-Position"), Equals, strconv.FormatInt(nextPos, 10)) + + acl, err = s.bucket.GetObjectACL(objectName) + c.Assert(err, IsNil) + c.Assert(acl.ACL, Equals, string(ACLPublicRead)) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +// TestAppendObjectNegative +func (s *OssBucketSuite) TestAppendObjectNegative(c *C) { + objectName := objectNamePrefix + RandStr(8) + nextPos := int64(0) + + nextPos, err := s.bucket.AppendObject(objectName, strings.NewReader("ObjectValue"), nextPos) + c.Assert(err, IsNil) + + nextPos, err = s.bucket.AppendObject(objectName, strings.NewReader("ObjectValue"), 0) + c.Assert(err, NotNil) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +// TestContentType +func (s *OssBucketSuite) TestAddContentType(c *C) { + opts := AddContentType(nil, "abc.txt") + typ, err := FindOption(opts, HTTPHeaderContentType, "") + c.Assert(err, IsNil) + c.Assert(strings.Contains(typ.(string), "text/plain"), Equals, true) + + opts = AddContentType(nil) + typ, err = FindOption(opts, HTTPHeaderContentType, "") + c.Assert(err, IsNil) + c.Assert(len(opts), Equals, 1) + c.Assert(strings.Contains(typ.(string), "application/octet-stream"), Equals, true) + + opts = AddContentType(nil, "abc.txt", "abc.pdf") + typ, err = FindOption(opts, HTTPHeaderContentType, "") + c.Assert(err, IsNil) + c.Assert(strings.Contains(typ.(string), "text/plain"), Equals, true) + + opts = AddContentType(nil, "abc", "abc.txt", "abc.pdf") + typ, err = FindOption(opts, HTTPHeaderContentType, "") + c.Assert(err, IsNil) + c.Assert(strings.Contains(typ.(string), "text/plain"), Equals, true) + + opts = AddContentType(nil, "abc", "abc", "edf") + typ, err = FindOption(opts, HTTPHeaderContentType, "") + c.Assert(err, IsNil) + c.Assert(strings.Contains(typ.(string), "application/octet-stream"), Equals, true) + + opts = AddContentType([]Option{Meta("meta", "my")}, "abc", "abc.txt", "abc.pdf") + typ, err = FindOption(opts, HTTPHeaderContentType, "") + c.Assert(err, IsNil) + c.Assert(len(opts), Equals, 2) + c.Assert(strings.Contains(typ.(string), "text/plain"), Equals, true) +} + +func (s *OssBucketSuite) TestGetConfig(c *C) { + client, err := New(endpoint, accessID, accessKey, UseCname(true), + Timeout(11, 12), SecurityToken("token"), EnableMD5(false)) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + + c.Assert(bucket.GetConfig().HTTPTimeout.ConnectTimeout, Equals, time.Second*11) + c.Assert(bucket.GetConfig().HTTPTimeout.ReadWriteTimeout, Equals, time.Second*12) + c.Assert(bucket.GetConfig().HTTPTimeout.HeaderTimeout, Equals, time.Second*12) + c.Assert(bucket.GetConfig().HTTPTimeout.IdleConnTimeout, Equals, time.Second*12) + c.Assert(bucket.GetConfig().HTTPTimeout.LongTimeout, Equals, time.Second*12*10) + + c.Assert(bucket.GetConfig().SecurityToken, Equals, "token") + c.Assert(bucket.GetConfig().IsCname, Equals, true) + c.Assert(bucket.GetConfig().IsEnableMD5, Equals, false) +} + +func (s *OssBucketSuite) TestSTSToken(c *C) { + objectName := objectNamePrefix + RandStr(8) + objectValue := "红藕香残玉簟秋。轻解罗裳,独上兰舟。云中谁寄锦书来?雁字回时,月满西楼。" + + stsClient := sts.NewClient(stsaccessID, stsaccessKey, stsARN, "oss_test_sess") + + resp, err := stsClient.AssumeRole(1800) + c.Assert(err, IsNil) + + client, err := New(endpoint, resp.Credentials.AccessKeyId, resp.Credentials.AccessKeySecret, + SecurityToken(resp.Credentials.SecurityToken)) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + + // Put + err = bucket.PutObject(objectName, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Get + body, err := bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err := readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + // List + lor, err := bucket.ListObjects() + c.Assert(err, IsNil) + testLogger.Println("Objects:", lor.Objects) + + // Put with URL + signedURL, err := bucket.SignURL(objectName, HTTPPut, 3600) + c.Assert(err, IsNil) + + err = bucket.PutObjectWithURL(signedURL, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Get with URL + signedURL, err = bucket.SignURL(objectName, HTTPGet, 3600) + c.Assert(err, IsNil) + + body, err = bucket.GetObjectWithURL(signedURL) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + // Delete + err = bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +func (s *OssBucketSuite) TestSTSTonekNegative(c *C) { + objectName := objectNamePrefix + RandStr(8) + localFile := objectName + ".jpg" + + client, err := New(endpoint, accessID, accessKey, SecurityToken("Invalid")) + c.Assert(err, IsNil) + + _, err = client.ListBuckets() + c.Assert(err, NotNil) + + bucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + + err = bucket.PutObject(objectName, strings.NewReader("")) + c.Assert(err, NotNil) + + err = bucket.PutObjectFromFile(objectName, "") + c.Assert(err, NotNil) + + _, err = bucket.GetObject(objectName) + c.Assert(err, NotNil) + + err = bucket.GetObjectToFile(objectName, "") + c.Assert(err, NotNil) + + _, err = bucket.ListObjects() + c.Assert(err, NotNil) + + err = bucket.SetObjectACL(objectName, ACLPublicRead) + c.Assert(err, NotNil) + + _, err = bucket.GetObjectACL(objectName) + c.Assert(err, NotNil) + + err = bucket.UploadFile(objectName, localFile, MinPartSize) + c.Assert(err, NotNil) + + err = bucket.DownloadFile(objectName, localFile, MinPartSize) + c.Assert(err, NotNil) + + _, err = bucket.IsObjectExist(objectName) + c.Assert(err, NotNil) + + _, err = bucket.ListMultipartUploads() + c.Assert(err, NotNil) + + err = bucket.DeleteObject(objectName) + c.Assert(err, NotNil) + + _, err = bucket.DeleteObjects([]string{objectName}) + c.Assert(err, NotNil) + + err = client.DeleteBucket(bucketName) + c.Assert(err, NotNil) +} + +func (s *OssBucketSuite) TestUploadBigFile(c *C) { + objectName := objectNamePrefix + RandStr(8) + bigFile := "D:\\tmp\\bigfile.zip" + newFile := "D:\\tmp\\newbigfile.zip" + + exist, err := isFileExist(bigFile) + c.Assert(err, IsNil) + if !exist { + return + } + + // Put + start := GetNowSec() + err = s.bucket.PutObjectFromFile(objectName, bigFile) + c.Assert(err, IsNil) + end := GetNowSec() + testLogger.Println("Put big file:", bigFile, "use sec:", end-start) + + // Check + start = GetNowSec() + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + end = GetNowSec() + testLogger.Println("Get big file:", bigFile, "use sec:", end-start) + + start = GetNowSec() + eq, err := compareFiles(bigFile, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + end = GetNowSec() + testLogger.Println("Compare big file:", bigFile, "use sec:", end-start) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +func (s *OssBucketSuite) TestSymlink(c *C) { + objectName := objectNamePrefix + RandStr(8) + targetObjectName := objectName + "target" + + err := s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(targetObjectName) + c.Assert(err, IsNil) + + meta, err := s.bucket.GetSymlink(objectName) + c.Assert(err, NotNil) + + // Put symlink + err = s.bucket.PutSymlink(objectName, targetObjectName) + c.Assert(err, IsNil) + + err = s.bucket.PutObject(targetObjectName, strings.NewReader("target")) + c.Assert(err, IsNil) + + err = s.bucket.PutSymlink(objectName, targetObjectName) + c.Assert(err, IsNil) + + meta, err = s.bucket.GetSymlink(objectName) + c.Assert(err, IsNil) + c.Assert(meta.Get(HTTPHeaderOssSymlinkTarget), Equals, targetObjectName) + + // List object + lor, err := s.bucket.ListObjects() + c.Assert(err, IsNil) + exist, v := s.getObject(lor.Objects, objectName) + c.Assert(exist, Equals, true) + c.Assert(v.Type, Equals, "Symlink") + + body, err := s.bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err := readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, "target") + + meta, err = s.bucket.GetSymlink(targetObjectName) + c.Assert(err, NotNil) + + err = s.bucket.PutObject(objectName, strings.NewReader("src")) + c.Assert(err, IsNil) + + body, err = s.bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, "src") + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(targetObjectName) + c.Assert(err, IsNil) + + // Put symlink again + objectName = objectNamePrefix + RandStr(8) + targetObjectName = objectName + "-target" + + err = s.bucket.PutSymlink(objectName, targetObjectName) + c.Assert(err, IsNil) + + err = s.bucket.PutObject(targetObjectName, strings.NewReader("target1")) + c.Assert(err, IsNil) + + meta, err = s.bucket.GetSymlink(objectName) + c.Assert(err, IsNil) + c.Assert(meta.Get(HTTPHeaderOssSymlinkTarget), Equals, targetObjectName) + + body, err = s.bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, "target1") + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(targetObjectName) + c.Assert(err, IsNil) +} + +// TestRestoreObject +func (s *OssBucketSuite) TestRestoreObject(c *C) { + objectName := objectNamePrefix + RandStr(8) + + // List objects + lor, err := s.archiveBucket.ListObjects() + c.Assert(err, IsNil) + left := len(lor.Objects) + + // Put object + err = s.archiveBucket.PutObject(objectName, strings.NewReader("")) + c.Assert(err, IsNil) + + // List + lor, err = s.archiveBucket.ListObjects() + c.Assert(err, IsNil) + c.Assert(len(lor.Objects), Equals, left+1) + for _, object := range lor.Objects { + c.Assert(object.StorageClass, Equals, string(StorageArchive)) + c.Assert(object.Type, Equals, "Normal") + } + + // Head object + meta, err := s.archiveBucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + _, ok := meta["X-Oss-Restore"] + c.Assert(ok, Equals, false) + c.Assert(meta.Get("X-Oss-Storage-Class"), Equals, "Archive") + + // Error restore object + err = s.archiveBucket.RestoreObject("notexistobject") + c.Assert(err, NotNil) + + // Restore object + err = s.archiveBucket.RestoreObject(objectName) + c.Assert(err, IsNil) + + // Head object + meta, err = s.archiveBucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + c.Assert(meta.Get("X-Oss-Restore"), Equals, "ongoing-request=\"true\"") + c.Assert(meta.Get("X-Oss-Storage-Class"), Equals, "Archive") +} + +// TestRestoreObjectWithXml +func (s *OssBucketSuite) TestRestoreObjectWithConfig(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName, StorageClass(StorageColdArchive)) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + objectName := objectNamePrefix + RandStr(8) + + // Put object + err = bucket.PutObject(objectName, strings.NewReader("123456789"), ObjectStorageClass(StorageColdArchive)) + c.Assert(err, IsNil) + + var restoreConfig RestoreConfiguration + restoreConfig.Days = 2 + + err = bucket.RestoreObjectDetail(objectName, restoreConfig) + c.Assert(err, IsNil) + + objectName = objectNamePrefix + RandStr(8) + err = bucket.PutObject(objectName, strings.NewReader("123456789"), ObjectStorageClass(StorageColdArchive)) + c.Assert(err, IsNil) + restoreConfig.Tier = string(RestoreBulk) + err = bucket.RestoreObjectDetail(objectName, restoreConfig) + c.Assert(err, IsNil) + + objectName = objectNamePrefix + RandStr(8) + err = bucket.PutObject(objectName, strings.NewReader("123456789"), ObjectStorageClass(StorageColdArchive)) + c.Assert(err, IsNil) + restoreConfig.Days = 0 + err = bucket.RestoreObjectDetail(objectName, restoreConfig) + c.Assert(err, IsNil) + + ForceDeleteBucket(client, bucketName, c) +} + +// TestRestoreObjectWithXml +func (s *OssBucketSuite) TestRestoreObjectWithXml(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName, StorageClass(StorageArchive)) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + objectName := objectNamePrefix + RandStr(8) + + // Put object + err = bucket.PutObject(objectName, strings.NewReader("123456789"), ObjectStorageClass(StorageArchive)) + c.Assert(err, IsNil) + + xmlConfig := `7` + + err = bucket.RestoreObjectXML(objectName, xmlConfig) + c.Assert(err, IsNil) + ForceDeleteBucket(client, bucketName, c) +} + +// TestProcessObject +func (s *OssBucketSuite) TestProcessObject(c *C) { + objectName := objectNamePrefix + RandStr(8) + ".jpg" + err := s.bucket.PutObjectFromFile(objectName, "../sample/BingWallpaper-2015-11-07.jpg") + c.Assert(err, IsNil) + + // If bucket-name not specified, it is saved to the current bucket by default. + destObjName := objectNamePrefix + RandStr(8) + "-dest.jpg" + process := fmt.Sprintf("image/resize,w_100|sys/saveas,o_%v", base64.URLEncoding.EncodeToString([]byte(destObjName))) + result, err := s.bucket.ProcessObject(objectName, process) + c.Assert(err, IsNil) + exist, _ := s.bucket.IsObjectExist(destObjName) + c.Assert(exist, Equals, true) + c.Assert(result.Bucket, Equals, "") + c.Assert(result.Object, Equals, destObjName) + + destObjName = objectNamePrefix + RandStr(8) + "-dest.jpg" + process = fmt.Sprintf("image/resize,w_100|sys/saveas,o_%v,b_%v", base64.URLEncoding.EncodeToString([]byte(destObjName)), base64.URLEncoding.EncodeToString([]byte(s.bucket.BucketName))) + result, err = s.bucket.ProcessObject(objectName, process) + c.Assert(err, IsNil) + exist, _ = s.bucket.IsObjectExist(destObjName) + c.Assert(exist, Equals, true) + c.Assert(result.Bucket, Equals, s.bucket.BucketName) + c.Assert(result.Object, Equals, destObjName) + + //no support process + process = fmt.Sprintf("image/resize,w_100|saveas,o_%v,b_%v", base64.URLEncoding.EncodeToString([]byte(destObjName)), base64.URLEncoding.EncodeToString([]byte(s.bucket.BucketName))) + result, err = s.bucket.ProcessObject(objectName, process) + c.Assert(err, NotNil) +} + +// Private +func CreateFileAndWrite(fileName string, data []byte) error { + os.Remove(fileName) + + fo, err := os.Create(fileName) + if err != nil { + return err + } + defer fo.Close() + + bytes, err := fo.Write(data) + if err != nil { + return err + } + + if bytes != len(data) { + return fmt.Errorf(fmt.Sprintf("write %d bytes not equal data length %d", bytes, len(data))) + } + + return nil +} + +// Compare the content between fileL and fileR +func compareFiles(fileL string, fileR string) (bool, error) { + finL, err := os.Open(fileL) + if err != nil { + return false, err + } + defer finL.Close() + + finR, err := os.Open(fileR) + if err != nil { + return false, err + } + defer finR.Close() + + statL, err := finL.Stat() + if err != nil { + return false, err + } + + statR, err := finR.Stat() + if err != nil { + return false, err + } + + if statL.Size() != statR.Size() { + return false, nil + } + + size := statL.Size() + if size > 102400 { + size = 102400 + } + + bufL := make([]byte, size) + bufR := make([]byte, size) + for { + n, _ := finL.Read(bufL) + if 0 == n { + break + } + + n, _ = finR.Read(bufR) + if 0 == n { + break + } + + if !bytes.Equal(bufL, bufR) { + return false, nil + } + } + + return true, nil +} + +// Compare the content of file and data +func compareFileData(file string, data []byte) (bool, error) { + fin, err := os.Open(file) + if err != nil { + return false, err + } + defer fin.Close() + + stat, err := fin.Stat() + if err != nil { + return false, err + } + + if stat.Size() != (int64)(len(data)) { + return false, nil + } + + buf := make([]byte, stat.Size()) + n, err := fin.Read(buf) + if err != nil { + return false, err + } + if stat.Size() != (int64)(n) { + return false, errors.New("read error") + } + + if !bytes.Equal(buf, data) { + return false, nil + } + + return true, nil +} + +func walkDir(dirPth, suffix string) ([]string, error) { + var files = []string{} + suffix = strings.ToUpper(suffix) + err := filepath.Walk(dirPth, + func(filename string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + if fi.IsDir() { + return nil + } + if strings.HasSuffix(strings.ToUpper(fi.Name()), suffix) { + files = append(files, filename) + } + return nil + }) + return files, err +} + +func removeTempFiles(path string, prefix string) error { + files, err := walkDir(path, prefix) + if err != nil { + return nil + } + + for _, file := range files { + os.Remove(file) + } + + return nil +} + +func isFileExist(filename string) (bool, error) { + _, err := os.Stat(filename) + if err != nil && os.IsNotExist(err) { + return false, nil + } else if err != nil { + return false, err + } else { + return true, nil + } +} + +func readBody(body io.ReadCloser) (string, error) { + data, err := ioutil.ReadAll(body) + body.Close() + if err != nil { + return "", err + } + return string(data), nil +} + +func (s *OssBucketSuite) getObject(objects []ObjectProperties, object string) (bool, ObjectProperties) { + for _, v := range objects { + if v.Key == object { + return true, v + } + } + return false, ObjectProperties{} +} + +func (s *OssBucketSuite) detectUploadSpeed(bucket *Bucket, c *C) (upSpeed int) { + objectName := objectNamePrefix + RandStr(8) + + // 1M byte + textBuffer := RandStr(1024 * 1024) + + // Put string + startT := time.Now() + err := bucket.PutObject(objectName, strings.NewReader(textBuffer)) + endT := time.Now() + + c.Assert(err, IsNil) + err = bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // byte/s + upSpeed = len(textBuffer) * 1000 / int(endT.UnixNano()/1000/1000-startT.UnixNano()/1000/1000) + return upSpeed +} + +func (s *OssBucketSuite) TestPutSingleObjectLimitSpeed(c *C) { + + // create client and bucket + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.LimitUploadSpeed(1) + if err != nil { + // go version is less than go1.7,not support limit upload speed + // doesn't run this test + return + } + // set unlimited again + client.LimitUploadSpeed(0) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + + //detect speed:byte/s + detectSpeed := s.detectUploadSpeed(bucket, c) + + var limitSpeed = 0 + if detectSpeed <= perTokenBandwidthSize*2 { + limitSpeed = perTokenBandwidthSize + } else { + //this situation, the test works better + limitSpeed = detectSpeed / 2 + } + + // KB/s + err = client.LimitUploadSpeed(limitSpeed / perTokenBandwidthSize) + c.Assert(err, IsNil) + + objectName := objectNamePrefix + RandStr(8) + + // 1M byte + textBuffer := RandStr(1024 * 1024) + + // Put body + startT := time.Now() + err = bucket.PutObject(objectName, strings.NewReader(textBuffer)) + endT := time.Now() + + realSpeed := int64(len(textBuffer)) * 1000 / (endT.UnixNano()/1000/1000 - startT.UnixNano()/1000/1000) + + fmt.Printf("detect speed:%d,limit speed:%d,real speed:%d.\n", detectSpeed, limitSpeed, realSpeed) + + c.Assert(float64(realSpeed) < float64(limitSpeed)*1.1, Equals, true) + + if detectSpeed > perTokenBandwidthSize { + // the minimum uploas limit speed is perTokenBandwidthSize(1024 byte/s) + c.Assert(float64(realSpeed) > float64(limitSpeed)*0.9, Equals, true) + } + + // Get object and compare content + body, err := bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err := readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, textBuffer) + + bucket.DeleteObject(objectName) + client.DeleteBucket(bucketName) + c.Assert(err, IsNil) + + return +} + +func putObjectRoutin(bucket *Bucket, object string, textBuffer *string, notifyChan chan int) error { + err := bucket.PutObject(object, strings.NewReader(*textBuffer)) + if err == nil { + notifyChan <- 1 + } else { + notifyChan <- 0 + } + return err +} + +func (s *OssBucketSuite) TestPutManyObjectLimitSpeed(c *C) { + // create client and bucket + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.LimitUploadSpeed(1) + if err != nil { + // go version is less than go1.7,not support limit upload speed + // doesn't run this test + return + } + // set unlimited + client.LimitUploadSpeed(0) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + + //detect speed:byte/s + detectSpeed := s.detectUploadSpeed(bucket, c) + var limitSpeed = 0 + if detectSpeed <= perTokenBandwidthSize*2 { + limitSpeed = perTokenBandwidthSize + } else { + limitSpeed = detectSpeed / 2 + } + + // KB/s + err = client.LimitUploadSpeed(limitSpeed / perTokenBandwidthSize) + c.Assert(err, IsNil) + + // object1 + objectNameFirst := objectNamePrefix + RandStr(8) + objectNameSecond := objectNamePrefix + RandStr(8) + + // 1M byte + textBuffer := RandStr(1024 * 1024) + + objectCount := 2 + notifyChan := make(chan int, objectCount) + + //start routin + startT := time.Now() + go putObjectRoutin(bucket, objectNameFirst, &textBuffer, notifyChan) + go putObjectRoutin(bucket, objectNameSecond, &textBuffer, notifyChan) + + // wait routin end + sum := int(0) + for j := 0; j < objectCount; j++ { + result := <-notifyChan + sum += result + } + endT := time.Now() + + realSpeed := len(textBuffer) * 2 * 1000 / int(endT.UnixNano()/1000/1000-startT.UnixNano()/1000/1000) + c.Assert(float64(realSpeed) < float64(limitSpeed)*1.1, Equals, true) + + if detectSpeed > perTokenBandwidthSize { + // the minimum uploas limit speed is perTokenBandwidthSize(1024 byte/s) + c.Assert(float64(realSpeed) > float64(limitSpeed)*0.9, Equals, true) + } + c.Assert(sum, Equals, 2) + + // Get object and compare content + body, err := bucket.GetObject(objectNameFirst) + c.Assert(err, IsNil) + str, err := readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, textBuffer) + + body, err = bucket.GetObject(objectNameSecond) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, textBuffer) + + // clear bucket and object + bucket.DeleteObject(objectNameFirst) + bucket.DeleteObject(objectNameSecond) + client.DeleteBucket(bucketName) + + fmt.Printf("detect speed:%d,limit speed:%d,real speed:%d.\n", detectSpeed, limitSpeed, realSpeed) + + return +} + +func (s *OssBucketSuite) TestPutMultipartObjectLimitSpeed(c *C) { + + // create client and bucket + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.LimitUploadSpeed(1) + if err != nil { + // go version is less than go1.7,not support limit upload speed + // doesn't run this test + return + } + // set unlimited + client.LimitUploadSpeed(0) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + + //detect speed:byte/s + detectSpeed := s.detectUploadSpeed(bucket, c) + + var limitSpeed = 0 + if detectSpeed <= perTokenBandwidthSize*2 { + limitSpeed = perTokenBandwidthSize + } else { + //this situation, the test works better + limitSpeed = detectSpeed / 2 + } + + // KB/s + err = client.LimitUploadSpeed(limitSpeed / perTokenBandwidthSize) + c.Assert(err, IsNil) + + objectName := objectNamePrefix + RandStr(8) + fileName := "." + string(os.PathSeparator) + objectName + + // 1M byte + fileSize := 0 + textBuffer := RandStr(1024 * 1024) + if detectSpeed < perTokenBandwidthSize { + ioutil.WriteFile(fileName, []byte(textBuffer), 0644) + f, err := os.Stat(fileName) + c.Assert(err, IsNil) + + fileSize = int(f.Size()) + c.Assert(fileSize, Equals, len(textBuffer)) + + } else { + loopCount := 5 + f, err := os.OpenFile(fileName, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0660) + c.Assert(err, IsNil) + + for i := 0; i < loopCount; i++ { + f.Write([]byte(textBuffer)) + } + + fileInfo, err := f.Stat() + c.Assert(err, IsNil) + + fileSize = int(fileInfo.Size()) + c.Assert(fileSize, Equals, len(textBuffer)*loopCount) + + f.Close() + } + + // Put body + startT := time.Now() + err = bucket.UploadFile(objectName, fileName, 100*1024, Routines(3), Checkpoint(true, "")) + endT := time.Now() + + c.Assert(err, IsNil) + realSpeed := fileSize * 1000 / int(endT.UnixNano()/1000/1000-startT.UnixNano()/1000/1000) + c.Assert(float64(realSpeed) < float64(limitSpeed)*1.1, Equals, true) + + if detectSpeed > perTokenBandwidthSize { + // the minimum uploas limit speed is perTokenBandwidthSize(1024 byte/s) + c.Assert(float64(realSpeed) > float64(limitSpeed)*0.9, Equals, true) + } + + // Get object and compare content + body, err := bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err := readBody(body) + c.Assert(err, IsNil) + + fileBody, err := ioutil.ReadFile(fileName) + c.Assert(err, IsNil) + c.Assert(str, Equals, string(fileBody)) + + // delete bucket、object、file + bucket.DeleteObject(objectName) + client.DeleteBucket(bucketName) + os.Remove(fileName) + + fmt.Printf("detect speed:%d,limit speed:%d,real speed:%d.\n", detectSpeed, limitSpeed, realSpeed) + + return +} + +func (s *OssBucketSuite) TestPutObjectFromFileLimitSpeed(c *C) { + // create client and bucket + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.LimitUploadSpeed(1) + if err != nil { + // go version is less than go1.7,not support limit upload speed + // doesn't run this test + return + } + // set unlimited + client.LimitUploadSpeed(0) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + + //detect speed:byte/s + detectSpeed := s.detectUploadSpeed(bucket, c) + + var limitSpeed = 0 + if detectSpeed <= perTokenBandwidthSize*2 { + limitSpeed = perTokenBandwidthSize + } else { + //this situation, the test works better + limitSpeed = detectSpeed / 2 + } + + // KB/s + err = client.LimitUploadSpeed(limitSpeed / perTokenBandwidthSize) + c.Assert(err, IsNil) + + objectName := objectNamePrefix + RandStr(8) + fileName := "." + string(os.PathSeparator) + objectName + + // 1M byte + fileSize := 0 + textBuffer := RandStr(1024 * 1024) + if detectSpeed < perTokenBandwidthSize { + ioutil.WriteFile(fileName, []byte(textBuffer), 0644) + f, err := os.Stat(fileName) + c.Assert(err, IsNil) + + fileSize = int(f.Size()) + c.Assert(fileSize, Equals, len(textBuffer)) + + } else { + loopCount := 2 + f, err := os.OpenFile(fileName, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0660) + c.Assert(err, IsNil) + + for i := 0; i < loopCount; i++ { + f.Write([]byte(textBuffer)) + } + + fileInfo, err := f.Stat() + c.Assert(err, IsNil) + + fileSize = int(fileInfo.Size()) + c.Assert(fileSize, Equals, len(textBuffer)*loopCount) + + f.Close() + } + + // Put body + startT := time.Now() + err = bucket.PutObjectFromFile(objectName, fileName) + endT := time.Now() + + c.Assert(err, IsNil) + realSpeed := fileSize * 1000 / int(endT.UnixNano()/1000/1000-startT.UnixNano()/1000/1000) + c.Assert(float64(realSpeed) < float64(limitSpeed)*1.1, Equals, true) + + if detectSpeed > perTokenBandwidthSize { + // the minimum uploas limit speed is perTokenBandwidthSize(1024 byte/s) + c.Assert(float64(realSpeed) > float64(limitSpeed)*0.9, Equals, true) + } + + // Get object and compare content + body, err := bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err := readBody(body) + c.Assert(err, IsNil) + + fileBody, err := ioutil.ReadFile(fileName) + c.Assert(err, IsNil) + c.Assert(str, Equals, string(fileBody)) + + // delete bucket、file、object + bucket.DeleteObject(objectName) + client.DeleteBucket(bucketName) + os.Remove(fileName) + + fmt.Printf("detect speed:%d,limit speed:%d,real speed:%d.\n", detectSpeed, limitSpeed, realSpeed) + + return +} + +// upload speed limit parameters will not affect download speed +func (s *OssBucketSuite) TestUploadObjectLimitSpeed(c *C) { + // create limit client and bucket + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + tokenCount := 1 + err = client.LimitUploadSpeed(tokenCount) + if err != nil { + // go version is less than go1.7,not support limit upload speed + // doesn't run this test + return + } + // set unlimited + client.LimitUploadSpeed(0) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + + //first:upload a object + textBuffer := RandStr(1024 * 100) + objectName := objectNamePrefix + RandStr(8) + err = bucket.PutObject(objectName, strings.NewReader(textBuffer)) + c.Assert(err, IsNil) + + // limit upload speed + err = client.LimitUploadSpeed(tokenCount) + c.Assert(err, IsNil) + + // then download the object + startT := time.Now() + body, err := bucket.GetObject(objectName) + c.Assert(err, IsNil) + + str, err := readBody(body) + c.Assert(err, IsNil) + endT := time.Now() + + c.Assert(str, Equals, textBuffer) + + // byte/s + downloadSpeed := len(textBuffer) * 1000 / int(endT.UnixNano()/1000/1000-startT.UnixNano()/1000/1000) + + // upload speed limit parameters will not affect download speed + c.Assert(downloadSpeed > 2*tokenCount*perTokenBandwidthSize, Equals, true) + + bucket.DeleteObject(objectName) + client.DeleteBucket(bucketName) +} + +// test LimitUploadSpeed failure +func (s *OssBucketSuite) TestLimitUploadSpeedFail(c *C) { + // create limit client and bucket + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.LimitUploadSpeed(-1) + c.Assert(err, NotNil) + + client.Config = nil + err = client.LimitUploadSpeed(100) + c.Assert(err, NotNil) +} + +// upload webp object +func (s *OssBucketSuite) TestUploadObjectWithWebpFormat(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + + // create webp file + textBuffer := RandStr(1024) + objectName := objectNamePrefix + RandStr(8) + fileName := "." + string(os.PathSeparator) + objectName + ".webp" + ioutil.WriteFile(fileName, []byte(textBuffer), 0644) + _, err = os.Stat(fileName) + c.Assert(err, IsNil) + + err = bucket.PutObjectFromFile(objectName, fileName) + c.Assert(err, IsNil) + + // check object content-type + props, err := bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + c.Assert(props["Content-Type"][0], Equals, "image/webp") + + os.Remove(fileName) + bucket.DeleteObject(objectName) + client.DeleteBucket(bucketName) +} + +func (s *OssBucketSuite) TestPutObjectTagging(c *C) { + // put object with tagging + objectName := objectNamePrefix + RandStr(8) + tag1 := Tag{ + Key: RandStr(8), + Value: RandStr(9), + } + tag2 := Tag{ + Key: RandStr(10), + Value: RandStr(11), + } + tagging := Tagging{ + Tags: []Tag{tag1, tag2}, + } + err := s.bucket.PutObject(objectName, strings.NewReader(RandStr(1024)), SetTagging(tagging)) + c.Assert(err, IsNil) + + headers, err := s.bucket.GetObjectDetailedMeta(objectName) + taggingCount, err := strconv.Atoi(headers["X-Oss-Tagging-Count"][0]) + c.Assert(err, IsNil) + c.Assert(taggingCount, Equals, 2) + + // copy object with default option + destObjectName := objectNamePrefix + RandStr(8) + _, err = s.bucket.CopyObject(objectName, destObjectName) + c.Assert(err, IsNil) + headers, err = s.bucket.GetObjectDetailedMeta(destObjectName) + taggingCount, err = strconv.Atoi(headers["X-Oss-Tagging-Count"][0]) + c.Assert(err, IsNil) + c.Assert(taggingCount, Equals, 2) + + // delete object tagging + err = s.bucket.DeleteObjectTagging(objectName) + c.Assert(err, IsNil) + + // get object tagging again + taggingResult, err := s.bucket.GetObjectTagging(objectName) + c.Assert(err, IsNil) + c.Assert(len(taggingResult.Tags), Equals, 0) + + // put tagging + tag := Tag{ + Key: RandStr(8), + Value: RandStr(16), + } + tagging.Tags = []Tag{tag} + err = s.bucket.PutObjectTagging(objectName, tagging) + c.Assert(err, IsNil) + + taggingResult, err = s.bucket.GetObjectTagging(objectName) + c.Assert(len(taggingResult.Tags), Equals, 1) + c.Assert(taggingResult.Tags[0].Key, Equals, tag.Key) + c.Assert(taggingResult.Tags[0].Value, Equals, tag.Value) + + //put tagging, the length of the key exceeds 128 + tag = Tag{ + Key: RandStr(129), + Value: RandStr(16), + } + tagging.Tags = []Tag{tag} + err = s.bucket.PutObjectTagging(objectName, tagging) + c.Assert(err, NotNil) + + //put tagging, the length of the value exceeds 256 + tag = Tag{ + Key: RandStr(8), + Value: RandStr(257), + } + tagging.Tags = []Tag{tag} + err = s.bucket.PutObjectTagging(objectName, tagging) + c.Assert(err, NotNil) + + //put tagging, the lens of tags exceed 10 + tagging.Tags = []Tag{} + for i := 0; i < 11; i++ { + tag = Tag{ + Key: RandStr(8), + Value: RandStr(16), + } + tagging.Tags = append(tagging.Tags, tag) + } + err = s.bucket.PutObjectTagging(objectName, tagging) + c.Assert(err, NotNil) + + //put tagging, invalid value of tag key + tag = Tag{ + Key: RandStr(8) + "&", + Value: RandStr(16), + } + tagging.Tags = []Tag{tag} + err = s.bucket.PutObjectTagging(objectName, tagging) + c.Assert(err, NotNil) + + //put tagging, invalid value of tag value + tag = Tag{ + Key: RandStr(8), + Value: RandStr(16) + "&", + } + tagging.Tags = []Tag{tag} + err = s.bucket.PutObjectTagging(objectName, tagging) + c.Assert(err, NotNil) + + //put tagging, repeated tag keys + tag1 = Tag{ + Key: RandStr(8), + Value: RandStr(16), + } + tag2 = Tag{ + Key: tag1.Key, + Value: RandStr(16), + } + tagging.Tags = []Tag{tag1, tag2} + err = s.bucket.PutObjectTagging(objectName, tagging) + c.Assert(err, NotNil) + + s.bucket.DeleteObject(destObjectName) + s.bucket.DeleteObject(objectName) +} + +func (s *OssBucketSuite) TestGetObjectTagging(c *C) { + // get object which has 2 tags + objectName := objectNamePrefix + RandStr(8) + tag1 := Tag{ + Key: RandStr(8), + Value: RandStr(9), + } + tag2 := Tag{ + Key: RandStr(10), + Value: RandStr(11), + } + + taggingInfo := Tagging{ + Tags: []Tag{tag1, tag2}, + } + + err := s.bucket.PutObject(objectName, strings.NewReader(RandStr(1024)), SetTagging(taggingInfo)) + c.Assert(err, IsNil) + + tagging, err := s.bucket.GetObjectTagging(objectName) + c.Assert(len(tagging.Tags), Equals, 2) + if tagging.Tags[0].Key == tag1.Key { + c.Assert(tagging.Tags[0].Value, Equals, tag1.Value) + c.Assert(tagging.Tags[1].Key, Equals, tag2.Key) + c.Assert(tagging.Tags[1].Value, Equals, tag2.Value) + } else { + c.Assert(tagging.Tags[0].Key, Equals, tag2.Key) + c.Assert(tagging.Tags[0].Value, Equals, tag2.Value) + c.Assert(tagging.Tags[1].Key, Equals, tag1.Key) + c.Assert(tagging.Tags[1].Value, Equals, tag1.Value) + } + + // get tagging of an object that is not exist + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + tagging, err = s.bucket.GetObjectTagging(objectName) + c.Assert(err, NotNil) + c.Assert(len(tagging.Tags), Equals, 0) + + // get object which has no tag + objectName = objectNamePrefix + RandStr(8) + err = s.bucket.PutObject(objectName, strings.NewReader(RandStr(1024))) + c.Assert(err, IsNil) + tagging, err = s.bucket.GetObjectTagging(objectName) + c.Assert(err, IsNil) + c.Assert(len(tagging.Tags), Equals, 0) + + // copy object, with tagging option + destObjectName := objectName + "-dest" + tagging.Tags = []Tag{tag1, tag2} + _, err = s.bucket.CopyObject(objectName, destObjectName, SetTagging(taggingInfo)) + c.Assert(err, IsNil) + tagging, err = s.bucket.GetObjectTagging(objectName) + c.Assert(err, IsNil) + c.Assert(len(tagging.Tags), Equals, 0) + + // copy object, with tagging option, the value of tagging directive is "REPLACE" + tagging.Tags = []Tag{tag1, tag2} + _, err = s.bucket.CopyObject(objectName, destObjectName, SetTagging(taggingInfo), TaggingDirective(TaggingReplace)) + c.Assert(err, IsNil) + tagging, err = s.bucket.GetObjectTagging(destObjectName) + c.Assert(err, IsNil) + c.Assert(len(tagging.Tags), Equals, 2) + if tagging.Tags[0].Key == tag1.Key { + c.Assert(tagging.Tags[0].Value, Equals, tag1.Value) + c.Assert(tagging.Tags[1].Key, Equals, tag2.Key) + c.Assert(tagging.Tags[1].Value, Equals, tag2.Value) + } else { + c.Assert(tagging.Tags[0].Key, Equals, tag2.Key) + c.Assert(tagging.Tags[0].Value, Equals, tag2.Value) + c.Assert(tagging.Tags[1].Key, Equals, tag1.Key) + c.Assert(tagging.Tags[1].Value, Equals, tag1.Value) + } + + s.bucket.DeleteObject(objectName) + s.bucket.DeleteObject(destObjectName) +} + +func (s *OssBucketSuite) TestDeleteObjectTagging(c *C) { + // delete object tagging, the object is not exist + objectName := objectNamePrefix + RandStr(8) + err := s.bucket.DeleteObjectTagging(objectName) + c.Assert(err, NotNil) + + // delete object tagging + tag := Tag{ + Key: RandStr(8), + Value: RandStr(16), + } + tagging := Tagging{ + Tags: []Tag{tag}, + } + err = s.bucket.PutObject(objectName, strings.NewReader(RandStr(1024)), SetTagging(tagging)) + c.Assert(err, IsNil) + err = s.bucket.DeleteObjectTagging(objectName) + c.Assert(err, IsNil) + taggingResult, err := s.bucket.GetObjectTagging(objectName) + c.Assert(err, IsNil) + c.Assert(len(taggingResult.Tags), Equals, 0) + + //delete object tagging again + err = s.bucket.DeleteObjectTagging(objectName) + c.Assert(err, IsNil) + + s.bucket.DeleteObject(objectName) +} + +func (s *OssBucketSuite) TestUploadFileMimeShtml(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + objectName := objectNamePrefix + RandStr(8) + fileName := "oss-sdk-test-file-" + RandLowStr(5) + ".shtml" + CreateFile(fileName, "123", c) + + err = bucket.PutObjectFromFile(objectName, fileName) + c.Assert(err, IsNil) + + headResult, err := bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + strContentType := headResult.Get("Content-Type") + c.Assert(strings.Contains(strContentType, "text/html"), Equals, true) + os.Remove(fileName) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestVersioningBucketVerison(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // Get default bucket info + bucketResult, err := client.GetBucketInfo(bucketName) + c.Assert(err, IsNil) + + c.Assert(bucketResult.BucketInfo.SseRule.KMSMasterKeyID, Equals, "") + c.Assert(bucketResult.BucketInfo.SseRule.SSEAlgorithm, Equals, "") + c.Assert(bucketResult.BucketInfo.Versioning, Equals, "") + + // put bucket version:enabled + var respHeader http.Header + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig, GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + c.Assert(GetRequestId(respHeader) != "", Equals, true) + + bucketResult, err = client.GetBucketInfo(bucketName) + c.Assert(err, IsNil) + c.Assert(bucketResult.BucketInfo.Versioning, Equals, string(VersionEnabled)) + + // put bucket version:Suspended + versioningConfig.Status = string(VersionSuspended) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + bucketResult, err = client.GetBucketInfo(bucketName) + c.Assert(err, IsNil) + c.Assert(bucketResult.BucketInfo.Versioning, Equals, string(VersionSuspended)) + + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestVersioningPutAndGetObject(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + bucketResult, err := client.GetBucketInfo(bucketName) + c.Assert(err, IsNil) + c.Assert(bucketResult.BucketInfo.Versioning, Equals, string(VersionEnabled)) + + // put object v1 + objectName := objectNamePrefix + RandStr(8) + contextV1 := RandStr(100) + versionIdV1 := "" + + var respHeader http.Header + err = bucket.PutObject(objectName, strings.NewReader(contextV1), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV1 = GetVersionId(respHeader) + c.Assert(len(versionIdV1) > 0, Equals, true) + + // put object v2 + contextV2 := RandStr(200) + versionIdV2 := "" + err = bucket.PutObject(objectName, strings.NewReader(contextV2), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV2 = GetVersionId(respHeader) + c.Assert(len(versionIdV2) > 0, Equals, true) + + // check v1 and v2 + c.Assert(versionIdV1 != versionIdV2, Equals, true) + + // get object v1 + body, err := bucket.GetObject(objectName, VersionId(versionIdV1)) + c.Assert(err, IsNil) + str, err := readBody(body) + c.Assert(err, IsNil) + body.Close() + c.Assert(str, Equals, contextV1) + + // get object v2 + body, err = bucket.GetObject(objectName, VersionId(versionIdV2)) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + body.Close() + c.Assert(str, Equals, contextV2) + + // get object without version + body, err = bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + body.Close() + c.Assert(str, Equals, contextV2) + + err = bucket.DeleteObject(objectName, VersionId(versionIdV1)) + err = bucket.DeleteObject(objectName, VersionId(versionIdV2)) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestVersioningHeadObject(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + bucketResult, err := client.GetBucketInfo(bucketName) + c.Assert(err, IsNil) + c.Assert(bucketResult.BucketInfo.Versioning, Equals, string(VersionEnabled)) + + // put object v1 + objectName := objectNamePrefix + RandStr(8) + contextV1 := RandStr(100) + versionIdV1 := "" + + var respHeader http.Header + err = bucket.PutObject(objectName, strings.NewReader(contextV1), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV1 = GetVersionId(respHeader) + c.Assert(len(versionIdV1) > 0, Equals, true) + + // put object v2 + contextV2 := RandStr(200) + versionIdV2 := "" + err = bucket.PutObject(objectName, strings.NewReader(contextV2), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV2 = GetVersionId(respHeader) + c.Assert(len(versionIdV2) > 0, Equals, true) + + // check v1 and v2 + c.Assert(versionIdV1 != versionIdV2, Equals, true) + + // head object v1 + headResultV1, err := bucket.GetObjectMeta(objectName, VersionId(versionIdV1)) + objLen, err := strconv.Atoi(headResultV1.Get("Content-Length")) + c.Assert(objLen, Equals, len(contextV1)) + + headResultV1, err = bucket.GetObjectDetailedMeta(objectName, VersionId(versionIdV1)) + objLen, err = strconv.Atoi(headResultV1.Get("Content-Length")) + c.Assert(objLen, Equals, len(contextV1)) + + // head object v2 + headResultV2, err := bucket.GetObjectMeta(objectName, VersionId(versionIdV2)) + objLen, err = strconv.Atoi(headResultV2.Get("Content-Length")) + c.Assert(objLen, Equals, len(contextV2)) + + headResultV2, err = bucket.GetObjectDetailedMeta(objectName, VersionId(versionIdV2)) + objLen, err = strconv.Atoi(headResultV2.Get("Content-Length")) + c.Assert(objLen, Equals, len(contextV2)) + + // head object without version + // head object v2 + headResult, err := bucket.GetObjectMeta(objectName) + objLen, err = strconv.Atoi(headResult.Get("Content-Length")) + c.Assert(objLen, Equals, len(contextV2)) + + headResult, err = bucket.GetObjectDetailedMeta(objectName) + objLen, err = strconv.Atoi(headResultV2.Get("Content-Length")) + c.Assert(objLen, Equals, len(contextV2)) + + err = bucket.DeleteObject(objectName, VersionId(versionIdV1)) + err = bucket.DeleteObject(objectName, VersionId(versionIdV2)) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestVersioningDeleteLatestVersionObject(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + bucketResult, err := client.GetBucketInfo(bucketName) + c.Assert(err, IsNil) + c.Assert(bucketResult.BucketInfo.Versioning, Equals, string(VersionEnabled)) + + // put object v1 + objectName := objectNamePrefix + RandStr(8) + contextV1 := RandStr(100) + versionIdV1 := "" + + var respHeader http.Header + err = bucket.PutObject(objectName, strings.NewReader(contextV1), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV1 = GetVersionId(respHeader) + c.Assert(len(versionIdV1) > 0, Equals, true) + + // put object v2 + contextV2 := RandStr(200) + versionIdV2 := "" + err = bucket.PutObject(objectName, strings.NewReader(contextV2), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV2 = GetVersionId(respHeader) + c.Assert(len(versionIdV2) > 0, Equals, true) + + // check v1 and v2 + c.Assert(versionIdV1 != versionIdV2, Equals, true) + + // delete v2 object:permently delete + options := []Option{VersionId(versionIdV2), GetResponseHeader(&respHeader)} + err = bucket.DeleteObject(objectName, options...) + c.Assert(err, IsNil) + c.Assert(GetVersionId(respHeader), Equals, versionIdV2) + + // get v2 object failure + body, err := bucket.GetObject(objectName, VersionId(versionIdV2)) + c.Assert(err, NotNil) + c.Assert(err.(ServiceError).Code, Equals, "NoSuchVersion") + + // get v1 object success + body, err = bucket.GetObject(objectName, VersionId(versionIdV1)) + c.Assert(err, IsNil) + str, err := readBody(body) + body.Close() + c.Assert(err, IsNil) + c.Assert(str, Equals, contextV1) + + // get default object success:v1 + body, err = bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err = readBody(body) + body.Close() + c.Assert(err, IsNil) + c.Assert(str, Equals, contextV1) + + err = bucket.DeleteObject(objectName, VersionId(versionIdV1)) + err = bucket.DeleteObject(objectName, VersionId(versionIdV2)) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestVersioningDeleteOldVersionObject(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + bucketResult, err := client.GetBucketInfo(bucketName) + c.Assert(err, IsNil) + c.Assert(bucketResult.BucketInfo.Versioning, Equals, string(VersionEnabled)) + + // put object v1 + objectName := objectNamePrefix + RandStr(8) + contextV1 := RandStr(100) + versionIdV1 := "" + + var respHeader http.Header + err = bucket.PutObject(objectName, strings.NewReader(contextV1), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV1 = GetVersionId(respHeader) + c.Assert(len(versionIdV1) > 0, Equals, true) + + // put object v2 + contextV2 := RandStr(200) + versionIdV2 := "" + err = bucket.PutObject(objectName, strings.NewReader(contextV2), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV2 = GetVersionId(respHeader) + c.Assert(len(versionIdV2) > 0, Equals, true) + + // check v1 and v2 + c.Assert(versionIdV1 != versionIdV2, Equals, true) + + // delete v1 object:permently delete + options := []Option{VersionId(versionIdV1), GetResponseHeader(&respHeader)} + err = bucket.DeleteObject(objectName, options...) + c.Assert(err, IsNil) + c.Assert(GetVersionId(respHeader), Equals, versionIdV1) + + // get v2 object success + body, err := bucket.GetObject(objectName, VersionId(versionIdV2)) + c.Assert(err, IsNil) + str, err := readBody(body) + body.Close() + c.Assert(err, IsNil) + c.Assert(str, Equals, contextV2) + + // get v1 object faliure + body, err = bucket.GetObject(objectName, VersionId(versionIdV1)) + c.Assert(err, NotNil) + c.Assert(err.(ServiceError).Code, Equals, "NoSuchVersion") + + // get default object success:v2 + body, err = bucket.GetObject(objectName) + c.Assert(err, IsNil) + str, err = readBody(body) + body.Close() + c.Assert(err, IsNil) + c.Assert(str, Equals, contextV2) + + err = bucket.DeleteObject(objectName, VersionId(versionIdV1)) + err = bucket.DeleteObject(objectName, VersionId(versionIdV2)) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestVersioningDeleteDefaultVersionObject(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + bucketResult, err := client.GetBucketInfo(bucketName) + c.Assert(err, IsNil) + c.Assert(bucketResult.BucketInfo.Versioning, Equals, string(VersionEnabled)) + + // put object v1 + objectName := objectNamePrefix + RandStr(8) + contextV1 := RandStr(100) + versionIdV1 := "" + + var respHeader http.Header + err = bucket.PutObject(objectName, strings.NewReader(contextV1), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV1 = GetVersionId(respHeader) + c.Assert(len(versionIdV1) > 0, Equals, true) + + // put object v2 + contextV2 := RandStr(200) + versionIdV2 := "" + err = bucket.PutObject(objectName, strings.NewReader(contextV2), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV2 = GetVersionId(respHeader) + c.Assert(len(versionIdV2) > 0, Equals, true) + + // check v1 and v2 + c.Assert(versionIdV1 != versionIdV2, Equals, true) + + // delete default object:mark delete v2 + options := []Option{GetResponseHeader(&respHeader)} + err = bucket.DeleteObject(objectName, options...) + c.Assert(err, IsNil) + + markVersionId := GetVersionId(respHeader) + c.Assert(len(markVersionId) > 0, Equals, true) + c.Assert(respHeader.Get("x-oss-delete-marker"), Equals, "true") + + // get v2 object success + body, err := bucket.GetObject(objectName, VersionId(versionIdV2)) + c.Assert(err, IsNil) + str, err := readBody(body) + body.Close() + c.Assert(err, IsNil) + c.Assert(str, Equals, contextV2) + + // get v1 object success + body, err = bucket.GetObject(objectName, VersionId(versionIdV1)) + c.Assert(err, IsNil) + str, err = readBody(body) + body.Close() + c.Assert(err, IsNil) + c.Assert(str, Equals, contextV1) + + // get default object failure:marker v2 + body, err = bucket.GetObject(objectName, GetResponseHeader(&respHeader)) + c.Assert(err, NotNil) + c.Assert(err.(ServiceError).Code, Equals, "NoSuchKey") + c.Assert(respHeader.Get("x-oss-delete-marker"), Equals, "true") + + // delete mark v2 + options = []Option{VersionId(markVersionId), GetResponseHeader(&respHeader)} + err = bucket.DeleteObject(objectName, options...) + c.Assert(err, IsNil) + c.Assert(GetVersionId(respHeader), Equals, markVersionId) + + // get default object success:v2 + body, err = bucket.GetObject(objectName, VersionId(versionIdV2)) + c.Assert(err, IsNil) + str, err = readBody(body) + body.Close() + c.Assert(err, IsNil) + c.Assert(str, Equals, contextV2) + + err = bucket.DeleteObject(objectName, VersionId(versionIdV1)) + err = bucket.DeleteObject(objectName, VersionId(versionIdV2)) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestVersioningListObjectVersions(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + bucketResult, err := client.GetBucketInfo(bucketName) + c.Assert(err, IsNil) + c.Assert(bucketResult.BucketInfo.Versioning, Equals, string(VersionEnabled)) + + // put object v1 + objectName := objectNamePrefix + RandStr(8) + contextV1 := RandStr(100) + versionIdV1 := "" + + var respHeader http.Header + err = bucket.PutObject(objectName, strings.NewReader(contextV1), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV1 = GetVersionId(respHeader) + c.Assert(len(versionIdV1) > 0, Equals, true) + + // put object v2 + contextV2 := RandStr(200) + versionIdV2 := "" + err = bucket.PutObject(objectName, strings.NewReader(contextV2), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV2 = GetVersionId(respHeader) + c.Assert(len(versionIdV2) > 0, Equals, true) + + // check v1 and v2 + c.Assert(versionIdV1 != versionIdV2, Equals, true) + + // delete default object:mark delete v2 + options := []Option{GetResponseHeader(&respHeader)} + err = bucket.DeleteObject(objectName, options...) + c.Assert(err, IsNil) + + markVersionId := GetVersionId(respHeader) + c.Assert(len(markVersionId) > 0, Equals, true) + c.Assert(respHeader.Get("x-oss-delete-marker"), Equals, "true") + + // delete default object again:mark delete v2 + err = bucket.DeleteObject(objectName, options...) + c.Assert(err, IsNil) + markVersionIdAgain := GetVersionId(respHeader) + c.Assert(len(markVersionIdAgain) > 0, Equals, true) + c.Assert(respHeader.Get("x-oss-delete-marker"), Equals, "true") + c.Assert(markVersionId != markVersionIdAgain, Equals, true) + + // list bucket versions + listResult, err := bucket.ListObjectVersions() + c.Assert(err, IsNil) + c.Assert(len(listResult.ObjectDeleteMarkers), Equals, 2) + c.Assert(len(listResult.ObjectVersions), Equals, 2) + mapMarkVersion := map[string]string{} + mapMarkVersion[listResult.ObjectDeleteMarkers[0].VersionId] = listResult.ObjectDeleteMarkers[0].VersionId + mapMarkVersion[listResult.ObjectDeleteMarkers[1].VersionId] = listResult.ObjectDeleteMarkers[1].VersionId + + // check delete mark + _, ok := mapMarkVersion[markVersionId] + c.Assert(ok == true, Equals, true) + _, ok = mapMarkVersion[markVersionIdAgain] + c.Assert(ok == true, Equals, true) + + // check versionId + mapVersion := map[string]string{} + mapVersion[listResult.ObjectVersions[0].VersionId] = listResult.ObjectVersions[0].VersionId + mapVersion[listResult.ObjectVersions[1].VersionId] = listResult.ObjectVersions[1].VersionId + _, ok = mapVersion[versionIdV1] + c.Assert(ok == true, Equals, true) + _, ok = mapVersion[versionIdV2] + c.Assert(ok == true, Equals, true) + + // delete deleteMark v2 + options = []Option{VersionId(markVersionId), GetResponseHeader(&respHeader)} + err = bucket.DeleteObject(objectName, options...) + c.Assert(err, IsNil) + c.Assert(GetVersionId(respHeader), Equals, markVersionId) + + // delete deleteMark v2 again + options = []Option{VersionId(markVersionIdAgain), GetResponseHeader(&respHeader)} + err = bucket.DeleteObject(objectName, options...) + c.Assert(err, IsNil) + c.Assert(GetVersionId(respHeader), Equals, markVersionIdAgain) + + // delete versionId + bucket.DeleteObject(objectName, VersionId(versionIdV1)) + bucket.DeleteObject(objectName, VersionId(versionIdV2)) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestVersioningBatchDeleteVersionObjects(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + bucketResult, err := client.GetBucketInfo(bucketName) + c.Assert(err, IsNil) + c.Assert(bucketResult.BucketInfo.Versioning, Equals, string(VersionEnabled)) + + // put object v1 + objectName1 := objectNamePrefix + RandStr(8) + contextV1 := RandStr(100) + versionIdV1 := "" + + var respHeader http.Header + err = bucket.PutObject(objectName1, strings.NewReader(contextV1), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV1 = GetVersionId(respHeader) + c.Assert(len(versionIdV1) > 0, Equals, true) + + // put object v2 + objectName2 := objectNamePrefix + RandStr(8) + contextV2 := RandStr(200) + versionIdV2 := "" + err = bucket.PutObject(objectName2, strings.NewReader(contextV2), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV2 = GetVersionId(respHeader) + c.Assert(len(versionIdV2) > 0, Equals, true) + + // check v1 and v2 + c.Assert(versionIdV1 != versionIdV2, Equals, true) + + //batch delete objects + versionIds := []DeleteObject{DeleteObject{Key: objectName1, VersionId: versionIdV1}, + DeleteObject{Key: objectName2, VersionId: versionIdV2}} + deleteResult, err := bucket.DeleteObjectVersions(versionIds) + c.Assert(err, IsNil) + c.Assert(len(deleteResult.DeletedObjectsDetail), Equals, 2) + + // check delete detail info:key + deleteMap := map[string]string{} + deleteMap[deleteResult.DeletedObjectsDetail[0].Key] = deleteResult.DeletedObjectsDetail[0].VersionId + deleteMap[deleteResult.DeletedObjectsDetail[1].Key] = deleteResult.DeletedObjectsDetail[1].VersionId + id1, ok := deleteMap[objectName1] + c.Assert(ok, Equals, true) + c.Assert(id1, Equals, versionIdV1) + + id2, ok := deleteMap[objectName2] + c.Assert(ok, Equals, true) + c.Assert(id2, Equals, versionIdV2) + + // list bucket versions + listResult, err := bucket.ListObjectVersions() + c.Assert(err, IsNil) + c.Assert(len(listResult.ObjectDeleteMarkers), Equals, 0) + c.Assert(len(listResult.ObjectVersions), Equals, 0) + + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestVersioningBatchDeleteDefaultVersionObjects(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + bucketResult, err := client.GetBucketInfo(bucketName) + c.Assert(err, IsNil) + c.Assert(bucketResult.BucketInfo.Versioning, Equals, string(VersionEnabled)) + + // put object v1 + objectName1 := objectNamePrefix + RandStr(8) + contextV1 := RandStr(100) + versionIdV1 := "" + + var respHeader http.Header + err = bucket.PutObject(objectName1, strings.NewReader(contextV1), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV1 = GetVersionId(respHeader) + c.Assert(len(versionIdV1) > 0, Equals, true) + + // put object v2 + objectName2 := objectNamePrefix + RandStr(8) + contextV2 := RandStr(200) + versionIdV2 := "" + err = bucket.PutObject(objectName2, strings.NewReader(contextV2), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV2 = GetVersionId(respHeader) + c.Assert(len(versionIdV2) > 0, Equals, true) + + // check v1 and v2 + c.Assert(versionIdV1 != versionIdV2, Equals, true) + + //batch delete objects + versionIds := []DeleteObject{DeleteObject{Key: objectName1, VersionId: ""}, + DeleteObject{Key: objectName2, VersionId: ""}} + deleteResult, err := bucket.DeleteObjectVersions(versionIds) + c.Assert(err, IsNil) + + // check delete detail info:key + deleteDetailMap := map[string]DeletedKeyInfo{} + deleteDetailMap[deleteResult.DeletedObjectsDetail[0].Key] = deleteResult.DeletedObjectsDetail[0] + deleteDetailMap[deleteResult.DeletedObjectsDetail[1].Key] = deleteResult.DeletedObjectsDetail[1] + keyInfo1, ok := deleteDetailMap[objectName1] + c.Assert(ok, Equals, true) + c.Assert(keyInfo1.Key, Equals, objectName1) + c.Assert(keyInfo1.VersionId, Equals, "") + c.Assert(keyInfo1.DeleteMarker, Equals, true) + c.Assert(keyInfo1.DeleteMarkerVersionId != versionIdV1, Equals, true) + + keyInfo2, ok := deleteDetailMap[objectName2] + c.Assert(ok, Equals, true) + c.Assert(keyInfo2.Key, Equals, objectName2) + c.Assert(keyInfo2.VersionId, Equals, "") + c.Assert(keyInfo2.DeleteMarker, Equals, true) + c.Assert(keyInfo2.DeleteMarkerVersionId != versionIdV2, Equals, true) + + // list bucket versions + listResult, err := bucket.ListObjectVersions() + c.Assert(err, IsNil) + c.Assert(len(listResult.ObjectDeleteMarkers), Equals, 2) + c.Assert(len(listResult.ObjectVersions), Equals, 2) + + // delete version object + versionIds = []DeleteObject{DeleteObject{Key: objectName1, VersionId: versionIdV1}, + DeleteObject{Key: objectName2, VersionId: versionIdV2}} + deleteResult, err = bucket.DeleteObjectVersions(versionIds) + c.Assert(err, IsNil) + + // delete deleteMark object + versionIds = []DeleteObject{DeleteObject{Key: objectName1, VersionId: keyInfo1.DeleteMarkerVersionId}, + DeleteObject{Key: objectName2, VersionId: keyInfo2.DeleteMarkerVersionId}} + deleteResult, err = bucket.DeleteObjectVersions(versionIds) + c.Assert(err, IsNil) + + ForceDeleteBucket(client, bucketName, c) +} + +// bucket has no versioning flag +func (s *OssBucketSuite) TestVersioningBatchDeleteNormalObjects(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // not put bucket versioning + + bucket, err := client.Bucket(bucketName) + + // put object v1 + objectName1 := objectNamePrefix + RandStr(8) + contextV1 := RandStr(100) + versionIdV1 := "" + + var respHeader http.Header + err = bucket.PutObject(objectName1, strings.NewReader(contextV1), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV1 = GetVersionId(respHeader) + c.Assert(len(versionIdV1), Equals, 0) + + // put object v2 + objectName2 := objectNamePrefix + RandStr(8) + contextV2 := RandStr(200) + versionIdV2 := "" + err = bucket.PutObject(objectName2, strings.NewReader(contextV2), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV2 = GetVersionId(respHeader) + c.Assert(len(versionIdV2), Equals, 0) + + //batch delete objects + keys := []string{objectName1, objectName2} + deleteResult, err := bucket.DeleteObjects(keys) + c.Assert(len(deleteResult.DeletedObjects), Equals, 2) + + // check delete info + deleteMap := map[string]string{} + deleteMap[deleteResult.DeletedObjects[0]] = deleteResult.DeletedObjects[0] + deleteMap[deleteResult.DeletedObjects[1]] = deleteResult.DeletedObjects[1] + _, ok := deleteMap[objectName1] + c.Assert(ok, Equals, true) + _, ok = deleteMap[objectName2] + c.Assert(ok, Equals, true) + + ForceDeleteBucket(client, bucketName, c) + c.Assert(err, IsNil) +} + +func (s *OssBucketSuite) TestVersioningSymlink(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + // put object 1 + objectName1 := objectNamePrefix + RandStr(8) + contextV1 := RandStr(100) + versionIdV1 := "" + + var respHeader http.Header + err = bucket.PutObject(objectName1, strings.NewReader(contextV1), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV1 = GetVersionId(respHeader) + c.Assert(len(versionIdV1) > 0, Equals, true) + + // put object 2 + objectName2 := objectNamePrefix + RandStr(8) + contextV2 := RandStr(200) + versionIdV2 := "" + err = bucket.PutObject(objectName2, strings.NewReader(contextV2), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV2 = GetVersionId(respHeader) + c.Assert(len(versionIdV2) > 0, Equals, true) + + // check v1 and v2 + c.Assert(versionIdV1 != versionIdV2, Equals, true) + + // put symlink for object 1 + linkName := objectNamePrefix + RandStr(8) + err = bucket.PutSymlink(linkName, objectName1, GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + linkVersionIdV1 := GetVersionId(respHeader) + + // PutSymlink for object 2 + err = bucket.PutSymlink(linkName, objectName2, GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + linkVersionIdV2 := GetVersionId(respHeader) + + // check v1 and v2 + c.Assert(linkVersionIdV1 != linkVersionIdV2, Equals, true) + + // GetSymlink for object1 + getResult, err := bucket.GetSymlink(linkName, VersionId(linkVersionIdV1)) + c.Assert(err, IsNil) + c.Assert(getResult.Get("x-oss-symlink-target"), Equals, objectName1) + + // GetSymlink for object2 + getResult, err = bucket.GetSymlink(linkName, VersionId(linkVersionIdV2)) + c.Assert(err, IsNil) + c.Assert(getResult.Get("x-oss-symlink-target"), Equals, objectName2) + + bucket.DeleteObject(linkName) + bucket.DeleteObject(objectName1) + bucket.DeleteObject(objectName2) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestVersioningObjectAcl(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + // put object v1 + objectName := objectNamePrefix + RandStr(8) + contextV1 := RandStr(100) + versionIdV1 := "" + + var respHeader http.Header + err = bucket.PutObject(objectName, strings.NewReader(contextV1), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV1 = GetVersionId(respHeader) + c.Assert(len(versionIdV1) > 0, Equals, true) + + // put object v2 + contextV2 := RandStr(200) + versionIdV2 := "" + err = bucket.PutObject(objectName, strings.NewReader(contextV2), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV2 = GetVersionId(respHeader) + c.Assert(len(versionIdV2) > 0, Equals, true) + + // check v1 and v2 + c.Assert(versionIdV1 != versionIdV2, Equals, true) + + // put Acl for v1 + err = bucket.SetObjectACL(objectName, ACLPublicRead, VersionId(versionIdV1)) + c.Assert(err, IsNil) + + // put Acl for v2 + err = bucket.SetObjectACL(objectName, ACLPublicReadWrite, VersionId(versionIdV2)) + c.Assert(err, IsNil) + + // GetAcl for v1 + getResult, err := bucket.GetObjectACL(objectName, VersionId(versionIdV1)) + c.Assert(err, IsNil) + c.Assert(getResult.ACL, Equals, string(ACLPublicRead)) + + // GetAcl for v2 + getResult, err = bucket.GetObjectACL(objectName, VersionId(versionIdV2)) + c.Assert(err, IsNil) + c.Assert(getResult.ACL, Equals, string(ACLPublicReadWrite)) + + // delete default version + err = bucket.DeleteObject(objectName, GetResponseHeader(&respHeader)) + c.Assert(len(GetVersionId(respHeader)) > 0, Equals, true) + c.Assert(respHeader.Get("x-oss-delete-marker"), Equals, "true") + + // GetAcl for v1 agagin + getResult, err = bucket.GetObjectACL(objectName, VersionId(versionIdV1)) + c.Assert(err, IsNil) + c.Assert(getResult.ACL, Equals, string(ACLPublicRead)) + + // GetAcl for v2 again + getResult, err = bucket.GetObjectACL(objectName, VersionId(versionIdV2)) + c.Assert(err, IsNil) + c.Assert(getResult.ACL, Equals, string(ACLPublicReadWrite)) + + // GetAcl for default failure + getResult, err = bucket.GetObjectACL(objectName) + c.Assert(err, NotNil) + + bucket.DeleteObject(objectName) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestVersioningAppendObject(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + // append object + var nextPos int64 = 0 + var respHeader http.Header + objectName := objectNamePrefix + RandStr(8) + nextPos, err = bucket.AppendObject(objectName, strings.NewReader("123"), nextPos, GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + c.Assert(GetVersionId(respHeader), Equals, NullVersion) + + nextPos, err = bucket.AppendObject(objectName, strings.NewReader("456"), nextPos, GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + c.Assert(GetVersionId(respHeader), Equals, NullVersion) + + // delete object + err = bucket.DeleteObject(objectName, GetResponseHeader(&respHeader)) + markVersionId := GetVersionId(respHeader) + + // get default object failure + _, err = bucket.GetObject(objectName) + c.Assert(err, NotNil) + + // get null version success + body, err := bucket.GetObject(objectName, VersionId(NullVersion)) + c.Assert(err, IsNil) + str, err := readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, "123456") + + // append object again:failure + nextPos, err = bucket.AppendObject(objectName, strings.NewReader("789"), nextPos, GetResponseHeader(&respHeader)) + c.Assert(err, NotNil) + + // delete deletemark + options := []Option{VersionId(markVersionId), GetResponseHeader(&respHeader)} + err = bucket.DeleteObject(objectName, options...) + c.Assert(markVersionId, Equals, GetVersionId(respHeader)) + + // append object again:success + nextPos, err = bucket.AppendObject(objectName, strings.NewReader("789"), nextPos, GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + c.Assert(int(nextPos), Equals, 9) + + bucket.DeleteObject(objectName) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestVersioningCopyObject(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + // put object v1 + objectName := objectNamePrefix + RandStr(8) + contextV1 := RandStr(100) + versionIdV1 := "" + + var respHeader http.Header + err = bucket.PutObject(objectName, strings.NewReader(contextV1), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV1 = GetVersionId(respHeader) + c.Assert(len(versionIdV1) > 0, Equals, true) + + // put object v2 + contextV2 := RandStr(200) + versionIdV2 := "" + err = bucket.PutObject(objectName, strings.NewReader(contextV2), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV2 = GetVersionId(respHeader) + c.Assert(len(versionIdV2) > 0, Equals, true) + + // check v1 and v2 + c.Assert(versionIdV1 != versionIdV2, Equals, true) + + destObjectKey := objectNamePrefix + RandStr(8) + + // copyobject default + _, err = bucket.CopyObject(objectName, destObjectKey, GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + srcVersionId := GetCopySrcVersionId(respHeader) + c.Assert(srcVersionId, Equals, versionIdV2) + + body, err := bucket.GetObject(destObjectKey) + c.Assert(err, IsNil) + str, err := readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, contextV2) + + // copyobject v1 + options := []Option{VersionId(versionIdV1), GetResponseHeader(&respHeader)} + _, err = bucket.CopyObject(objectName, destObjectKey, options...) + c.Assert(err, IsNil) + srcVersionId = GetCopySrcVersionId(respHeader) + c.Assert(srcVersionId, Equals, versionIdV1) + + body, err = bucket.GetObject(destObjectKey) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, contextV1) + + // delete object + err = bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // default copyobject again,failuer + _, err = bucket.CopyObject(objectName, destObjectKey, GetResponseHeader(&respHeader)) + c.Assert(err, NotNil) + + bucket.DeleteObject(objectName) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestVersioningCompleteMultipartUpload(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + objectName := objectNamePrefix + RandStr(8) + var fileName = "test-file-" + RandStr(8) + content := RandStr(500 * 1024) + CreateFile(fileName, content, c) + + chunks, err := SplitFileByPartNum(fileName, 3) + c.Assert(err, IsNil) + + options := []Option{ + Expires(futureDate), Meta("my", "myprop"), + } + + fd, err := os.Open(fileName) + c.Assert(err, IsNil) + defer fd.Close() + + imur, err := bucket.InitiateMultipartUpload(objectName, options...) + c.Assert(err, IsNil) + var parts []UploadPart + for _, chunk := range chunks { + fd.Seek(chunk.Offset, os.SEEK_SET) + part, err := bucket.UploadPart(imur, fd, chunk.Size, chunk.Number) + c.Assert(err, IsNil) + parts = append(parts, part) + } + + var respHeader http.Header + _, err = bucket.CompleteMultipartUpload(imur, parts, GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + + //get versionId + versionIdV1 := GetVersionId(respHeader) + c.Assert(len(versionIdV1) > 0, Equals, true) + + meta, err := bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + c.Assert(meta.Get("X-Oss-Meta-My"), Equals, "myprop") + c.Assert(meta.Get("Expires"), Equals, futureDate.Format(http.TimeFormat)) + c.Assert(meta.Get("X-Oss-Object-Type"), Equals, "Multipart") + + // put object agagin + err = bucket.PutObject(objectName, strings.NewReader(""), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV2 := GetVersionId(respHeader) + c.Assert(versionIdV1 == versionIdV2, Equals, false) + + // get meta v1 + meta, err = bucket.GetObjectDetailedMeta(objectName, VersionId(versionIdV1)) + c.Assert(err, IsNil) + c.Assert(meta.Get("content-length"), Equals, strconv.Itoa(len(content))) + + // get meta v2 + meta, err = bucket.GetObjectDetailedMeta(objectName, VersionId(versionIdV2)) + c.Assert(err, IsNil) + c.Assert(meta.Get("content-length"), Equals, strconv.Itoa(0)) + + os.Remove(fileName) + bucket.DeleteObject(objectName) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestVersioningUploadPartCopy(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + // put object v1 + objectName := objectNamePrefix + RandStr(8) + contextV1 := RandStr(100) + versionIdV1 := "" + + var respHeader http.Header + err = bucket.PutObject(objectName, strings.NewReader(contextV1), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV1 = GetVersionId(respHeader) + c.Assert(len(versionIdV1) > 0, Equals, true) + + // put object v2 + contextV2 := RandStr(200) + versionIdV2 := "" + err = bucket.PutObject(objectName, strings.NewReader(contextV2), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV2 = GetVersionId(respHeader) + c.Assert(len(versionIdV2) > 0, Equals, true) + + // upload mutlipart object with v1 + multiName := objectNamePrefix + RandStr(8) + var parts []UploadPart + imur, err := bucket.InitiateMultipartUpload(multiName) + c.Assert(err, IsNil) + + part, err := bucket.UploadPartCopy(imur, bucketName, objectName, 0, int64(len(contextV1)), 1, VersionId(versionIdV1)) + parts = []UploadPart{part} + c.Assert(err, IsNil) + + _, err = bucket.CompleteMultipartUpload(imur, parts, GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + + //get versionId + partVersionIdV1 := GetVersionId(respHeader) + c.Assert(len(partVersionIdV1) > 0, Equals, true) + + // get meta v1 + meta, err := bucket.GetObjectDetailedMeta(multiName, VersionId(partVersionIdV1)) + c.Assert(err, IsNil) + c.Assert(meta.Get("content-length"), Equals, strconv.Itoa(len(contextV1))) + + // upload mutlipart object with v2 + imur, err = bucket.InitiateMultipartUpload(multiName) + part, err = bucket.UploadPartCopy(imur, bucketName, objectName, 0, int64(len(contextV2)), 1, VersionId(versionIdV2)) + parts = []UploadPart{part} + + _, err = bucket.CompleteMultipartUpload(imur, parts, GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + + //get versionId + partVersionIdV2 := GetVersionId(respHeader) + c.Assert(len(partVersionIdV2) > 0, Equals, true) + + // get meta v2 + meta, err = bucket.GetObjectDetailedMeta(multiName, VersionId(partVersionIdV2)) + c.Assert(err, IsNil) + c.Assert(meta.Get("content-length"), Equals, strconv.Itoa(len(contextV2))) + + bucket.DeleteObject(objectName) + bucket.DeleteObject(multiName) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestVersioningRestoreObject(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName, StorageClass(StorageArchive)) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + // put object v1 + objectName := objectNamePrefix + RandStr(8) + contextV1 := RandStr(100) + versionIdV1 := "" + + var respHeader http.Header + err = bucket.PutObject(objectName, strings.NewReader(contextV1), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV1 = GetVersionId(respHeader) + c.Assert(len(versionIdV1) > 0, Equals, true) + + // put object v2 + contextV2 := RandStr(200) + versionIdV2 := "" + err = bucket.PutObject(objectName, strings.NewReader(contextV2), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV2 = GetVersionId(respHeader) + c.Assert(len(versionIdV2) > 0, Equals, true) + + // RestoreObject v1 + options := []Option{GetResponseHeader(&respHeader), VersionId(versionIdV1)} + err = bucket.RestoreObject(objectName, options...) + c.Assert(err, IsNil) + c.Assert(GetVersionId(respHeader), Equals, versionIdV1) + + // RestoreObject v2 + options = []Option{GetResponseHeader(&respHeader), VersionId(versionIdV2)} + err = bucket.RestoreObject(objectName, options...) + c.Assert(err, IsNil) + c.Assert(GetVersionId(respHeader), Equals, versionIdV2) + + bucket.DeleteObject(objectName) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestVersioningObjectTagging(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName, StorageClass(StorageArchive)) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + // put object v1 + objectName := objectNamePrefix + RandStr(8) + contextV1 := RandStr(100) + versionIdV1 := "" + + var respHeader http.Header + err = bucket.PutObject(objectName, strings.NewReader(contextV1), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV1 = GetVersionId(respHeader) + c.Assert(len(versionIdV1) > 0, Equals, true) + + // put object v2 + contextV2 := RandStr(200) + versionIdV2 := "" + err = bucket.PutObject(objectName, strings.NewReader(contextV2), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV2 = GetVersionId(respHeader) + c.Assert(len(versionIdV2) > 0, Equals, true) + + // ObjectTagging v1 + var tagging1 Tagging + tagging1.Tags = []Tag{Tag{Key: "testkey1", Value: "testvalue1"}} + err = bucket.PutObjectTagging(objectName, tagging1, VersionId(versionIdV1)) + c.Assert(err, IsNil) + getResult, err := bucket.GetObjectTagging(objectName, VersionId(versionIdV1)) + c.Assert(err, IsNil) + c.Assert(getResult.Tags[0].Key, Equals, tagging1.Tags[0].Key) + c.Assert(getResult.Tags[0].Value, Equals, tagging1.Tags[0].Value) + + // ObjectTagging v2 + var tagging2 Tagging + tagging2.Tags = []Tag{Tag{Key: "testkey2", Value: "testvalue2"}} + err = bucket.PutObjectTagging(objectName, tagging2, VersionId(versionIdV2)) + c.Assert(err, IsNil) + getResult, err = bucket.GetObjectTagging(objectName, VersionId(versionIdV2)) + c.Assert(err, IsNil) + c.Assert(getResult.Tags[0].Key, Equals, tagging2.Tags[0].Key) + c.Assert(getResult.Tags[0].Value, Equals, tagging2.Tags[0].Value) + + // delete ObjectTagging v2 + err = bucket.DeleteObjectTagging(objectName, VersionId(versionIdV2)) + c.Assert(err, IsNil) + + getResult, err = bucket.GetObjectTagging(objectName, VersionId(versionIdV2)) + c.Assert(err, IsNil) + c.Assert(len(getResult.Tags), Equals, 0) + + bucket.DeleteObject(objectName) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestVersioningIsObjectExist(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + bucket, err := client.Bucket(bucketName) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + // put object v1 + objectName := objectNamePrefix + RandStr(8) + contextV1 := RandStr(100) + versionIdV1 := "" + + var respHeader http.Header + err = bucket.PutObject(objectName, strings.NewReader(contextV1), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV1 = GetVersionId(respHeader) + c.Assert(len(versionIdV1) > 0, Equals, true) + + // put object v2 + contextV2 := RandStr(200) + versionIdV2 := "" + err = bucket.PutObject(objectName, strings.NewReader(contextV2), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV2 = GetVersionId(respHeader) + c.Assert(len(versionIdV2) > 0, Equals, true) + + // check v1 and v2 + c.Assert(versionIdV1 != versionIdV2, Equals, true) + + // check default exist + exist, err := bucket.IsObjectExist(objectName) + c.Assert(err, IsNil) + c.Assert(exist, Equals, true) + + // check object v1 exist + exist, err = bucket.IsObjectExist(objectName, VersionId(versionIdV1)) + c.Assert(err, IsNil) + c.Assert(exist, Equals, true) + + // check object v2 exist + exist, err = bucket.IsObjectExist(objectName, VersionId(versionIdV2)) + c.Assert(err, IsNil) + c.Assert(exist, Equals, true) + + // rm v2 + err = bucket.DeleteObject(objectName, VersionId(versionIdV2)) + c.Assert(err, IsNil) + + // check object v2 not exist + exist, err = bucket.IsObjectExist(objectName, VersionId(versionIdV2)) + c.Assert(err, IsNil) + c.Assert(exist, Equals, false) + + // rm default + err = bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // check default exist + exist, err = bucket.IsObjectExist(objectName) + c.Assert(err, IsNil) + c.Assert(exist, Equals, false) + + // check v1 exist + exist, err = bucket.IsObjectExist(objectName, VersionId(versionIdV1)) + c.Assert(err, IsNil) + c.Assert(exist, Equals, true) + + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestOptionsMethod(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // put bucket cors + var rule = CORSRule{ + AllowedOrigin: []string{"www.aliyun.com"}, + AllowedMethod: []string{"PUT", "GET", "POST"}, + AllowedHeader: []string{"x-oss-meta-author"}, + ExposeHeader: []string{"x-oss-meta-name"}, + MaxAgeSeconds: 100, + } + + // set cors + err = client.SetBucketCORS(bucketName, []CORSRule{rule}) + c.Assert(err, IsNil) + + // bucket options success + options := []Option{} + originOption := Origin("www.aliyun.com") + acMethodOption := ACReqMethod("PUT") + acHeadersOption := ACReqHeaders("x-oss-meta-author") + options = append(options, originOption) + options = append(options, acMethodOption) + options = append(options, acHeadersOption) + _, err = bucket.OptionsMethod("", options...) + c.Assert(err, IsNil) + + // options failure + options = []Option{} + originOption = Origin("www.aliyun.com") + acMethodOption = ACReqMethod("PUT") + acHeadersOption = ACReqHeaders("x-oss-meta-author-1") + options = append(options, originOption) + options = append(options, acMethodOption) + options = append(options, acHeadersOption) + _, err = bucket.OptionsMethod("", options...) + c.Assert(err, NotNil) + + // put object + objectName := objectNamePrefix + RandStr(8) + context := RandStr(100) + err = bucket.PutObject(objectName, strings.NewReader(context)) + c.Assert(err, IsNil) + + // object options success + options = []Option{} + originOption = Origin("www.aliyun.com") + acMethodOption = ACReqMethod("PUT") + acHeadersOption = ACReqHeaders("x-oss-meta-author") + options = append(options, originOption) + options = append(options, acMethodOption) + options = append(options, acHeadersOption) + _, err = bucket.OptionsMethod("", options...) + c.Assert(err, IsNil) + + // options failure + options = []Option{} + originOption = Origin("www.aliyun.com") + acMethodOption = ACReqMethod("PUT") + acHeadersOption = ACReqHeaders("x-oss-meta-author-1") + options = append(options, originOption) + options = append(options, acMethodOption) + options = append(options, acHeadersOption) + _, err = bucket.OptionsMethod("", options...) + c.Assert(err, NotNil) + + bucket.DeleteObject(objectName) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestBucketTrafficLimitObject1(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + var respHeader http.Header + var qosDelayTime string + var traffic int64 = 819220 // 100KB + maxTraffic := traffic * 120 / 100 + + objectName := objectNamePrefix + RandStr(8) + localFile := "../sample/BingWallpaper-2015-11-07.jpg" + fd, err := os.Open(localFile) + c.Assert(err, IsNil) + defer fd.Close() + + tryGetFileSize := func(f *os.File) int64 { + fInfo, _ := f.Stat() + return fInfo.Size() + } + contentLength := tryGetFileSize(fd) * 8 + + // put object + start := time.Now().UnixNano() / 1000 / 1000 + err = bucket.PutObject(objectName, fd, TrafficLimitHeader(traffic), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + endingTime := time.Now().UnixNano() / 1000 / 1000 + costT := endingTime - start + costV := contentLength * 1000 / costT // bit * 1000 / Millisecond = bit/s + c.Assert((costV < maxTraffic), Equals, true) + qosDelayTime = GetQosDelayTime(respHeader) + c.Assert(len(qosDelayTime) > 0, Equals, true) + + // putobject without TrafficLimit + // + // fd, err = os.Open(localFile) + // c.Assert(err, IsNil) + // defer fd.Close() + // start = time.Now().UnixNano() / 1000 / 1000 + // err = bucket.PutObject(objectName, fd) + // c.Assert(err, IsNil) + // endingTime = time.Now().UnixNano() / 1000 / 1000 + // costT = endingTime - start + // costV = contentLength * 1000 / costT // bit * 1000 / Millisecond = bit/s + // testLogger.Println(traffic, maxTraffic, contentLength, costT, costV) + // c.Assert((costV < maxTraffic), Equals, true) + + // get object to file + newFile := "test-file-" + RandStr(10) + start = time.Now().UnixNano() / 1000 / 1000 + err = bucket.GetObjectToFile(objectName, newFile, TrafficLimitHeader(traffic)) + c.Assert(err, IsNil) + endingTime = time.Now().UnixNano() / 1000 / 1000 + costT = endingTime - start + costV = contentLength * 1000 / costT // bit * 1000 / Millisecond = bit/s + c.Assert((costV < maxTraffic), Equals, true) + os.Remove(newFile) + + // append object + newFile = "test-file-" + RandStr(10) + objectKey := objectNamePrefix + RandStr(8) + var nextPos int64 + fd, err = os.Open(localFile) + c.Assert(err, IsNil) + defer fd.Close() + start = time.Now().UnixNano() / 1000 / 1000 + nextPos, err = bucket.AppendObject(objectKey, strings.NewReader(RandStr(18)), nextPos) + c.Assert(err, IsNil) + + var respAppendHeader http.Header + nextPos, err = bucket.AppendObject(objectKey, fd, nextPos, TrafficLimitHeader(traffic), GetResponseHeader(&respAppendHeader)) + c.Assert(err, IsNil) + endingTime = time.Now().UnixNano() / 1000 / 1000 + costT = endingTime - start + costV = contentLength * 1000 / costT // bit * 1000 / Millisecond = bit/s + c.Assert((costV < maxTraffic), Equals, true) + qosDelayTime = GetQosDelayTime(respAppendHeader) + c.Assert(len(qosDelayTime) > 0, Equals, true) + + err = bucket.GetObjectToFile(objectKey, newFile, TrafficLimitHeader(traffic)) + c.Assert(err, IsNil) + err = bucket.DeleteObject(objectKey) + c.Assert(err, IsNil) + os.Remove(newFile) + + // put object with url + fd, err = os.Open(localFile) + c.Assert(err, IsNil) + defer fd.Close() + strURL, err := bucket.SignURL(objectName, HTTPPut, 60, TrafficLimitParam(traffic)) + start = time.Now().UnixNano() / 1000 / 1000 + err = bucket.PutObjectWithURL(strURL, fd) + c.Assert(err, IsNil) + endingTime = time.Now().UnixNano() / 1000 / 1000 + costT = endingTime - start + costV = contentLength * 1000 / costT // bit * 1000 / Millisecond = bit/s + c.Assert((costV < maxTraffic), Equals, true) + + // get object with url + newFile = "test-file-" + RandStr(10) + strURL, err = bucket.SignURL(objectName, HTTPGet, 60, TrafficLimitParam(traffic)) + c.Assert(err, IsNil) + start = time.Now().UnixNano() / 1000 / 1000 + err = bucket.GetObjectToFileWithURL(strURL, newFile) + c.Assert(err, IsNil) + endingTime = time.Now().UnixNano() / 1000 / 1000 + costT = endingTime - start + costV = contentLength * 1000 / costT // bit * 1000 / Millisecond = bit/s + c.Assert((costV < maxTraffic), Equals, true) + os.Remove(newFile) + + // copy object + destObjectName := objectNamePrefix + RandStr(8) + _, err = bucket.CopyObject(objectName, destObjectName, TrafficLimitHeader(traffic)) + c.Assert(err, IsNil) + err = bucket.DeleteObject(destObjectName) + c.Assert(err, IsNil) + + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestBucketTrafficLimitUpload(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + var traffic int64 = 819220 // 100KB + maxTraffic := traffic * 120 / 100 + contentLength := 500 * 1024 + + var fileName = "test-file-" + RandStr(8) + objectName := objectNamePrefix + RandStr(8) + content := RandStr(contentLength) + CreateFile(fileName, content, c) + + chunks, err := SplitFileByPartNum(fileName, 3) + c.Assert(err, IsNil) + + options := []Option{ + Expires(futureDate), Meta("my", "myprop"), + } + + fd, err := os.Open(fileName) + c.Assert(err, IsNil) + defer fd.Close() + + imur, err := bucket.InitiateMultipartUpload(objectName, options...) + c.Assert(err, IsNil) + var parts []UploadPart + start := time.Now().UnixNano() / 1000 / 1000 + for _, chunk := range chunks { + fd.Seek(chunk.Offset, os.SEEK_SET) + part, err := bucket.UploadPart(imur, fd, chunk.Size, chunk.Number, TrafficLimitHeader(traffic)) + c.Assert(err, IsNil) + parts = append(parts, part) + } + _, err = bucket.CompleteMultipartUpload(imur, parts) + c.Assert(err, IsNil) + endingTime := time.Now().UnixNano() / 1000 / 1000 + costT := endingTime - start + costV := int64(contentLength) * 8 * 1000 / costT // B * 8 * 1000 / Millisecond = bit/s + c.Assert((costV < maxTraffic), Equals, true) + os.Remove(fileName) + + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestPutObjectWithForbidOverWrite(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + bucket, err := client.Bucket(bucketName) + + contentLength := 1024 + objectName := objectNamePrefix + RandStr(8) + content := RandStr(contentLength) + + // first put success + err = bucket.PutObject(objectName, strings.NewReader(content), ForbidOverWrite(true)) + c.Assert(err, IsNil) + + // second put failure with ForbidOverWrite true + var respHeader http.Header + err = bucket.PutObject(objectName, strings.NewReader(content), ForbidOverWrite(true), GetResponseHeader(&respHeader)) + c.Assert(err, NotNil) + + // third put success with ForbidOverWrite false + err = bucket.PutObject(objectName, strings.NewReader(content), ForbidOverWrite(false)) + c.Assert(err, IsNil) + + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestCopyObjectWithForbidOverWrite(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + bucket, err := client.Bucket(bucketName) + + contentLength := 1024 + objectName := objectNamePrefix + RandStr(8) + content := RandStr(contentLength) + + err = bucket.PutObject(objectName, strings.NewReader(content)) + c.Assert(err, IsNil) + + // first copy success + copyObjectName := objectName + "-copy" + _, err = bucket.CopyObject(objectName, copyObjectName, ForbidOverWrite(true)) + c.Assert(err, IsNil) + + // second copy failure with ForbidOverWrite true + var respHeader http.Header + _, err = bucket.CopyObject(objectName, copyObjectName, ForbidOverWrite(true), GetResponseHeader(&respHeader)) + c.Assert(err, NotNil) + + // third copy success with ForbidOverWrite false + _, err = bucket.CopyObject(objectName, copyObjectName, ForbidOverWrite(false)) + c.Assert(err, IsNil) + + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestDeleteObjectsWithSpecialCharacter(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + bucket, err := client.Bucket(bucketName) + + contentLength := 100 + objectName1 := objectNamePrefix + RandStr(8) + "<-->+&*\r%%" + objectName2 := objectNamePrefix + RandStr(8) + "\r&*\r%%" + //objectName2 := objectNamePrefix + RandStr(8) + "%C0%AE%C0%AE%2F%C0%AE%C0%AE%2F%C0%AE%C0%AE%2F%C0%AE%C0%AE%2F%C0%AE%C0%AE%2F%C0%AE%C0%AE%2F%C0%AE%C0%AE%2F%C0%AE%C0%AE%2F%C0%AE%C0%AE%2F%C0%AE%C0%AE%2Fetc%2Fprofile" + //objectName2, err = url.QueryUnescape(objectName2) + + c.Assert(err, IsNil) + content := RandStr(contentLength) + + err = bucket.PutObject(objectName1, strings.NewReader(content)) + c.Assert(err, IsNil) + + err = bucket.PutObject(objectName2, strings.NewReader(content)) + c.Assert(err, IsNil) + + // delete objectName1 objectName2 + objectKeys := []string{objectName1, objectName2} + _, err = bucket.DeleteObjects(objectKeys) + c.Assert(err, IsNil) + + // objectName1 is not exist + exist, err := bucket.IsObjectExist(objectName1) + c.Assert(err, IsNil) + c.Assert(exist, Equals, false) + + // objectName2 is not exist + exist, err = bucket.IsObjectExist(objectName2) + c.Assert(err, IsNil) + c.Assert(exist, Equals, false) + + ForceDeleteBucket(client, bucketName, c) +} + +// TestGetObjectRangeBehavior +func (s *OssBucketSuite) TestGetObjectRangeBehavior(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + bucket, err := client.Bucket(bucketName) + + objectName := objectNamePrefix + RandStr(8) + objectLen := 1000 + objectValue := RandStr(objectLen) + + // Put + err = bucket.PutObject(objectName, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Range 1 + options := []Option{ + RangeBehavior("standard"), + Range(1000, 2000), + } + resp, err := bucket.GetObject(objectName, options...) + c.Assert(resp, IsNil) + c.Assert(err.(ServiceError).StatusCode, Equals, 416) + + // Range 2 + options = []Option{ + RangeBehavior("standard"), + Range(0, 2000), + } + resp, err = bucket.GetObject(objectName, options...) + c.Assert(err, IsNil) + data, err := ioutil.ReadAll(resp) + resp.Close() + str := string(data) + c.Assert(len(str), Equals, 1000) + c.Assert(resp.(*Response).StatusCode, Equals, 206) + + // Range 3 + options = []Option{ + RangeBehavior("standard"), + Range(500, 2000), + } + resp, err = bucket.GetObject(objectName, options...) + c.Assert(err, IsNil) + data, err = ioutil.ReadAll(resp) + resp.Close() + str = string(data) + c.Assert(len(str), Equals, 500) + c.Assert(resp.(*Response).StatusCode, Equals, 206) + + ForceDeleteBucket(client, bucketName, c) +} + +// RangeBehavior is an option to set Range value, such as "standard" +func MyRangeBehavior(value string) Option { + return SetHeader(HTTPHeaderOssRangeBehavior, value) +} + +// TestUserSetHeader +func (s *OssBucketSuite) TestSupportUserSetHeader(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + bucket, err := client.Bucket(bucketName) + + objectName := objectNamePrefix + RandStr(8) + objectLen := 1000 + objectValue := RandStr(objectLen) + + // Put + err = bucket.PutObject(objectName, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Range 1 + options := []Option{ + MyRangeBehavior("standard"), + Range(1000, 2000), + } + resp, err := bucket.GetObject(objectName, options...) + c.Assert(resp, IsNil) + c.Assert(err.(ServiceError).StatusCode, Equals, 416) + + // Range 2 + options = []Option{ + MyRangeBehavior("standard"), + Range(0, 2000), + } + resp, err = bucket.GetObject(objectName, options...) + c.Assert(err, IsNil) + data, err := ioutil.ReadAll(resp) + resp.Close() + str := string(data) + c.Assert(len(str), Equals, 1000) + c.Assert(resp.(*Response).StatusCode, Equals, 206) + + // Range 3 + options = []Option{ + MyRangeBehavior("standard"), + Range(500, 2000), + } + resp, err = bucket.GetObject(objectName, options...) + c.Assert(err, IsNil) + data, err = ioutil.ReadAll(resp) + resp.Close() + str = string(data) + c.Assert(len(str), Equals, 500) + c.Assert(resp.(*Response).StatusCode, Equals, 206) + + ForceDeleteBucket(client, bucketName, c) +} + +// user can set param +func MyVersionId(value string) Option { + return AddParam("versionId", value) +} + +func (s *OssBucketSuite) TestSupportUserSetParam(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + bucketResult, err := client.GetBucketInfo(bucketName) + c.Assert(err, IsNil) + c.Assert(bucketResult.BucketInfo.Versioning, Equals, string(VersionEnabled)) + + // put object v1 + objectName := objectNamePrefix + RandStr(8) + contextV1 := RandStr(100) + versionIdV1 := "" + + var respHeader http.Header + err = bucket.PutObject(objectName, strings.NewReader(contextV1), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV1 = GetVersionId(respHeader) + c.Assert(len(versionIdV1) > 0, Equals, true) + + // put object v2 + contextV2 := RandStr(200) + versionIdV2 := "" + err = bucket.PutObject(objectName, strings.NewReader(contextV2), GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + versionIdV2 = GetVersionId(respHeader) + c.Assert(len(versionIdV2) > 0, Equals, true) + + // check v1 and v2 + c.Assert(versionIdV1 != versionIdV2, Equals, true) + + // get object v1 + body, err := bucket.GetObject(objectName, MyVersionId(versionIdV1)) + c.Assert(err, IsNil) + str, err := readBody(body) + c.Assert(err, IsNil) + body.Close() + c.Assert(str, Equals, contextV1) + + // get object v2 + body, err = bucket.GetObject(objectName, MyVersionId(versionIdV2)) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + body.Close() + c.Assert(str, Equals, contextV2) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssBucketSuite) TestPutObjectWithKmsSm4(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + objectName := objectNamePrefix + RandStr(8) + objectValue := RandStr(1024) + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + bucket, err := client.Bucket(bucketName) + + err = bucket.PutObject(objectName, strings.NewReader(objectValue), ServerSideEncryption("KMS"), ServerSideDataEncryption("SM4")) + headers, err := bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + c.Assert(headers.Get(HTTPHeaderOssServerSideEncryption), Equals, "KMS") + c.Assert(headers.Get(HTTPHeaderOssServerSideDataEncryption), Equals, "SM4") + c.Assert(err, IsNil) + ForceDeleteBucket(client, bucketName, c) +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/client.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/client.go new file mode 100644 index 00000000..fd13e4f8 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/client.go @@ -0,0 +1,1722 @@ +// Package oss implements functions for access oss service. +// It has two main struct Client and Bucket. +package oss + +import ( + "bytes" + "encoding/xml" + "fmt" + "io" + "io/ioutil" + "log" + "net" + "net/http" + "strings" + "time" +) + +// Client SDK's entry point. It's for bucket related options such as create/delete/set bucket (such as set/get ACL/lifecycle/referer/logging/website). +// Object related operations are done by Bucket class. +// Users use oss.New to create Client instance. +// +type ( + // Client OSS client + Client struct { + Config *Config // OSS client configuration + Conn *Conn // Send HTTP request + HTTPClient *http.Client //http.Client to use - if nil will make its own + } + + // ClientOption client option such as UseCname, Timeout, SecurityToken. + ClientOption func(*Client) +) + +// New creates a new client. +// +// endpoint the OSS datacenter endpoint such as http://oss-cn-hangzhou.aliyuncs.com . +// accessKeyId access key Id. +// accessKeySecret access key secret. +// +// Client creates the new client instance, the returned value is valid when error is nil. +// error it's nil if no error, otherwise it's an error object. +// +func New(endpoint, accessKeyID, accessKeySecret string, options ...ClientOption) (*Client, error) { + // Configuration + config := getDefaultOssConfig() + config.Endpoint = endpoint + config.AccessKeyID = accessKeyID + config.AccessKeySecret = accessKeySecret + + // URL parse + url := &urlMaker{} + err := url.Init(config.Endpoint, config.IsCname, config.IsUseProxy) + if err != nil { + return nil, err + } + + // HTTP connect + conn := &Conn{config: config, url: url} + + // OSS client + client := &Client{ + Config: config, + Conn: conn, + } + + // Client options parse + for _, option := range options { + option(client) + } + + if config.AuthVersion != AuthV1 && config.AuthVersion != AuthV2 { + return nil, fmt.Errorf("Init client Error, invalid Auth version: %v", config.AuthVersion) + } + + // Create HTTP connection + err = conn.init(config, url, client.HTTPClient) + + return client, err +} + +// Bucket gets the bucket instance. +// +// bucketName the bucket name. +// Bucket the bucket object, when error is nil. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) Bucket(bucketName string) (*Bucket, error) { + err := CheckBucketName(bucketName) + if err != nil { + return nil, err + } + + return &Bucket{ + client, + bucketName, + }, nil +} + +// CreateBucket creates a bucket. +// +// bucketName the bucket name, it's globably unique and immutable. The bucket name can only consist of lowercase letters, numbers and dash ('-'). +// It must start with lowercase letter or number and the length can only be between 3 and 255. +// options options for creating the bucket, with optional ACL. The ACL could be ACLPrivate, ACLPublicRead, and ACLPublicReadWrite. By default it's ACLPrivate. +// It could also be specified with StorageClass option, which supports StorageStandard, StorageIA(infrequent access), StorageArchive. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) CreateBucket(bucketName string, options ...Option) error { + headers := make(map[string]string) + handleOptions(headers, options) + + buffer := new(bytes.Buffer) + + var cbConfig createBucketConfiguration + cbConfig.StorageClass = StorageStandard + + isStorageSet, valStroage, _ := IsOptionSet(options, storageClass) + isRedundancySet, valRedundancy, _ := IsOptionSet(options, redundancyType) + if isStorageSet { + cbConfig.StorageClass = valStroage.(StorageClassType) + } + + if isRedundancySet { + cbConfig.DataRedundancyType = valRedundancy.(DataRedundancyType) + } + + bs, err := xml.Marshal(cbConfig) + if err != nil { + return err + } + buffer.Write(bs) + contentType := http.DetectContentType(buffer.Bytes()) + headers[HTTPHeaderContentType] = contentType + + params := map[string]interface{}{} + resp, err := client.do("PUT", bucketName, params, headers, buffer, options...) + if err != nil { + return err + } + + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusOK}) +} + +// ListBuckets lists buckets of the current account under the given endpoint, with optional filters. +// +// options specifies the filters such as Prefix, Marker and MaxKeys. Prefix is the bucket name's prefix filter. +// And marker makes sure the returned buckets' name are greater than it in lexicographic order. +// Maxkeys limits the max keys to return, and by default it's 100 and up to 1000. +// For the common usage scenario, please check out list_bucket.go in the sample. +// ListBucketsResponse the response object if error is nil. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) ListBuckets(options ...Option) (ListBucketsResult, error) { + var out ListBucketsResult + + params, err := GetRawParams(options) + if err != nil { + return out, err + } + + resp, err := client.do("GET", "", params, nil, nil, options...) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// IsBucketExist checks if the bucket exists +// +// bucketName the bucket name. +// +// bool true if it exists, and it's only valid when error is nil. +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) IsBucketExist(bucketName string) (bool, error) { + listRes, err := client.ListBuckets(Prefix(bucketName), MaxKeys(1)) + if err != nil { + return false, err + } + + if len(listRes.Buckets) == 1 && listRes.Buckets[0].Name == bucketName { + return true, nil + } + return false, nil +} + +// DeleteBucket deletes the bucket. Only empty bucket can be deleted (no object and parts). +// +// bucketName the bucket name. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) DeleteBucket(bucketName string, options ...Option) error { + params := map[string]interface{}{} + resp, err := client.do("DELETE", bucketName, params, nil, nil, options...) + if err != nil { + return err + } + + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusNoContent}) +} + +// GetBucketLocation gets the bucket location. +// +// Checks out the following link for more information : +// https://help.aliyun.com/document_detail/oss/user_guide/oss_concept/endpoint.html +// +// bucketName the bucket name +// +// string bucket's datacenter location +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) GetBucketLocation(bucketName string) (string, error) { + params := map[string]interface{}{} + params["location"] = nil + resp, err := client.do("GET", bucketName, params, nil, nil) + if err != nil { + return "", err + } + defer resp.Body.Close() + + var LocationConstraint string + err = xmlUnmarshal(resp.Body, &LocationConstraint) + return LocationConstraint, err +} + +// SetBucketACL sets bucket's ACL. +// +// bucketName the bucket name +// bucketAcl the bucket ACL: ACLPrivate, ACLPublicRead and ACLPublicReadWrite. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) SetBucketACL(bucketName string, bucketACL ACLType) error { + headers := map[string]string{HTTPHeaderOssACL: string(bucketACL)} + params := map[string]interface{}{} + params["acl"] = nil + resp, err := client.do("PUT", bucketName, params, headers, nil) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusOK}) +} + +// GetBucketACL gets the bucket ACL. +// +// bucketName the bucket name. +// +// GetBucketAclResponse the result object, and it's only valid when error is nil. +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) GetBucketACL(bucketName string) (GetBucketACLResult, error) { + var out GetBucketACLResult + params := map[string]interface{}{} + params["acl"] = nil + resp, err := client.do("GET", bucketName, params, nil, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// SetBucketLifecycle sets the bucket's lifecycle. +// +// For more information, checks out following link: +// https://help.aliyun.com/document_detail/oss/user_guide/manage_object/object_lifecycle.html +// +// bucketName the bucket name. +// rules the lifecycle rules. There're two kind of rules: absolute time expiration and relative time expiration in days and day/month/year respectively. +// Check out sample/bucket_lifecycle.go for more details. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) SetBucketLifecycle(bucketName string, rules []LifecycleRule) error { + err := verifyLifecycleRules(rules) + if err != nil { + return err + } + lifecycleCfg := LifecycleConfiguration{Rules: rules} + bs, err := xml.Marshal(lifecycleCfg) + if err != nil { + return err + } + buffer := new(bytes.Buffer) + buffer.Write(bs) + + contentType := http.DetectContentType(buffer.Bytes()) + headers := map[string]string{} + headers[HTTPHeaderContentType] = contentType + + params := map[string]interface{}{} + params["lifecycle"] = nil + resp, err := client.do("PUT", bucketName, params, headers, buffer) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusOK}) +} + +// DeleteBucketLifecycle deletes the bucket's lifecycle. +// +// +// bucketName the bucket name. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) DeleteBucketLifecycle(bucketName string) error { + params := map[string]interface{}{} + params["lifecycle"] = nil + resp, err := client.do("DELETE", bucketName, params, nil, nil) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusNoContent}) +} + +// GetBucketLifecycle gets the bucket's lifecycle settings. +// +// bucketName the bucket name. +// +// GetBucketLifecycleResponse the result object upon successful request. It's only valid when error is nil. +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) GetBucketLifecycle(bucketName string) (GetBucketLifecycleResult, error) { + var out GetBucketLifecycleResult + params := map[string]interface{}{} + params["lifecycle"] = nil + resp, err := client.do("GET", bucketName, params, nil, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + + // NonVersionTransition is not suggested to use + // to keep compatible + for k, rule := range out.Rules { + if len(rule.NonVersionTransitions) > 0 { + out.Rules[k].NonVersionTransition = &(out.Rules[k].NonVersionTransitions[0]) + } + } + return out, err +} + +// SetBucketReferer sets the bucket's referer whitelist and the flag if allowing empty referrer. +// +// To avoid stealing link on OSS data, OSS supports the HTTP referrer header. A whitelist referrer could be set either by API or web console, as well as +// the allowing empty referrer flag. Note that this applies to requests from webbrowser only. +// For example, for a bucket os-example and its referrer http://www.aliyun.com, all requests from this URL could access the bucket. +// For more information, please check out this link : +// https://help.aliyun.com/document_detail/oss/user_guide/security_management/referer.html +// +// bucketName the bucket name. +// referers the referrer white list. A bucket could have a referrer list and each referrer supports one '*' and multiple '?' as wildcards. +// The sample could be found in sample/bucket_referer.go +// allowEmptyReferer the flag of allowing empty referrer. By default it's true. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) SetBucketReferer(bucketName string, referers []string, allowEmptyReferer bool) error { + rxml := RefererXML{} + rxml.AllowEmptyReferer = allowEmptyReferer + if referers == nil { + rxml.RefererList = append(rxml.RefererList, "") + } else { + for _, referer := range referers { + rxml.RefererList = append(rxml.RefererList, referer) + } + } + + bs, err := xml.Marshal(rxml) + if err != nil { + return err + } + buffer := new(bytes.Buffer) + buffer.Write(bs) + + contentType := http.DetectContentType(buffer.Bytes()) + headers := map[string]string{} + headers[HTTPHeaderContentType] = contentType + + params := map[string]interface{}{} + params["referer"] = nil + resp, err := client.do("PUT", bucketName, params, headers, buffer) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusOK}) +} + +// GetBucketReferer gets the bucket's referrer white list. +// +// bucketName the bucket name. +// +// GetBucketRefererResponse the result object upon successful request. It's only valid when error is nil. +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) GetBucketReferer(bucketName string) (GetBucketRefererResult, error) { + var out GetBucketRefererResult + params := map[string]interface{}{} + params["referer"] = nil + resp, err := client.do("GET", bucketName, params, nil, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// SetBucketLogging sets the bucket logging settings. +// +// OSS could automatically store the access log. Only the bucket owner could enable the logging. +// Once enabled, OSS would save all the access log into hourly log files in a specified bucket. +// For more information, please check out https://help.aliyun.com/document_detail/oss/user_guide/security_management/logging.html +// +// bucketName bucket name to enable the log. +// targetBucket the target bucket name to store the log files. +// targetPrefix the log files' prefix. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) SetBucketLogging(bucketName, targetBucket, targetPrefix string, + isEnable bool) error { + var err error + var bs []byte + if isEnable { + lxml := LoggingXML{} + lxml.LoggingEnabled.TargetBucket = targetBucket + lxml.LoggingEnabled.TargetPrefix = targetPrefix + bs, err = xml.Marshal(lxml) + } else { + lxml := loggingXMLEmpty{} + bs, err = xml.Marshal(lxml) + } + + if err != nil { + return err + } + + buffer := new(bytes.Buffer) + buffer.Write(bs) + + contentType := http.DetectContentType(buffer.Bytes()) + headers := map[string]string{} + headers[HTTPHeaderContentType] = contentType + + params := map[string]interface{}{} + params["logging"] = nil + resp, err := client.do("PUT", bucketName, params, headers, buffer) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusOK}) +} + +// DeleteBucketLogging deletes the logging configuration to disable the logging on the bucket. +// +// bucketName the bucket name to disable the logging. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) DeleteBucketLogging(bucketName string) error { + params := map[string]interface{}{} + params["logging"] = nil + resp, err := client.do("DELETE", bucketName, params, nil, nil) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusNoContent}) +} + +// GetBucketLogging gets the bucket's logging settings +// +// bucketName the bucket name +// GetBucketLoggingResponse the result object upon successful request. It's only valid when error is nil. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) GetBucketLogging(bucketName string) (GetBucketLoggingResult, error) { + var out GetBucketLoggingResult + params := map[string]interface{}{} + params["logging"] = nil + resp, err := client.do("GET", bucketName, params, nil, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// SetBucketWebsite sets the bucket's static website's index and error page. +// +// OSS supports static web site hosting for the bucket data. When the bucket is enabled with that, you can access the file in the bucket like the way to access a static website. +// For more information, please check out: https://help.aliyun.com/document_detail/oss/user_guide/static_host_website.html +// +// bucketName the bucket name to enable static web site. +// indexDocument index page. +// errorDocument error page. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) SetBucketWebsite(bucketName, indexDocument, errorDocument string) error { + wxml := WebsiteXML{} + wxml.IndexDocument.Suffix = indexDocument + wxml.ErrorDocument.Key = errorDocument + + bs, err := xml.Marshal(wxml) + if err != nil { + return err + } + buffer := new(bytes.Buffer) + buffer.Write(bs) + + contentType := http.DetectContentType(buffer.Bytes()) + headers := make(map[string]string) + headers[HTTPHeaderContentType] = contentType + + params := map[string]interface{}{} + params["website"] = nil + resp, err := client.do("PUT", bucketName, params, headers, buffer) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusOK}) +} + +// SetBucketWebsiteDetail sets the bucket's static website's detail +// +// OSS supports static web site hosting for the bucket data. When the bucket is enabled with that, you can access the file in the bucket like the way to access a static website. +// For more information, please check out: https://help.aliyun.com/document_detail/oss/user_guide/static_host_website.html +// +// bucketName the bucket name to enable static web site. +// +// wxml the website's detail +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) SetBucketWebsiteDetail(bucketName string, wxml WebsiteXML, options ...Option) error { + bs, err := xml.Marshal(wxml) + if err != nil { + return err + } + buffer := new(bytes.Buffer) + buffer.Write(bs) + + contentType := http.DetectContentType(buffer.Bytes()) + headers := make(map[string]string) + headers[HTTPHeaderContentType] = contentType + + params := map[string]interface{}{} + params["website"] = nil + resp, err := client.do("PUT", bucketName, params, headers, buffer, options...) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusOK}) +} + +// SetBucketWebsiteXml sets the bucket's static website's rule +// +// OSS supports static web site hosting for the bucket data. When the bucket is enabled with that, you can access the file in the bucket like the way to access a static website. +// For more information, please check out: https://help.aliyun.com/document_detail/oss/user_guide/static_host_website.html +// +// bucketName the bucket name to enable static web site. +// +// wxml the website's detail +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) SetBucketWebsiteXml(bucketName string, webXml string, options ...Option) error { + buffer := new(bytes.Buffer) + buffer.Write([]byte(webXml)) + + contentType := http.DetectContentType(buffer.Bytes()) + headers := make(map[string]string) + headers[HTTPHeaderContentType] = contentType + + params := map[string]interface{}{} + params["website"] = nil + resp, err := client.do("PUT", bucketName, params, headers, buffer, options...) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusOK}) +} + +// DeleteBucketWebsite deletes the bucket's static web site settings. +// +// bucketName the bucket name. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) DeleteBucketWebsite(bucketName string) error { + params := map[string]interface{}{} + params["website"] = nil + resp, err := client.do("DELETE", bucketName, params, nil, nil) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusNoContent}) +} + +// GetBucketWebsite gets the bucket's default page (index page) and the error page. +// +// bucketName the bucket name +// +// GetBucketWebsiteResponse the result object upon successful request. It's only valid when error is nil. +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) GetBucketWebsite(bucketName string) (GetBucketWebsiteResult, error) { + var out GetBucketWebsiteResult + params := map[string]interface{}{} + params["website"] = nil + resp, err := client.do("GET", bucketName, params, nil, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// SetBucketCORS sets the bucket's CORS rules +// +// For more information, please check out https://help.aliyun.com/document_detail/oss/user_guide/security_management/cors.html +// +// bucketName the bucket name +// corsRules the CORS rules to set. The related sample code is in sample/bucket_cors.go. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) SetBucketCORS(bucketName string, corsRules []CORSRule) error { + corsxml := CORSXML{} + for _, v := range corsRules { + cr := CORSRule{} + cr.AllowedMethod = v.AllowedMethod + cr.AllowedOrigin = v.AllowedOrigin + cr.AllowedHeader = v.AllowedHeader + cr.ExposeHeader = v.ExposeHeader + cr.MaxAgeSeconds = v.MaxAgeSeconds + corsxml.CORSRules = append(corsxml.CORSRules, cr) + } + + bs, err := xml.Marshal(corsxml) + if err != nil { + return err + } + buffer := new(bytes.Buffer) + buffer.Write(bs) + + contentType := http.DetectContentType(buffer.Bytes()) + headers := map[string]string{} + headers[HTTPHeaderContentType] = contentType + + params := map[string]interface{}{} + params["cors"] = nil + resp, err := client.do("PUT", bucketName, params, headers, buffer) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusOK}) +} + +// DeleteBucketCORS deletes the bucket's static website settings. +// +// bucketName the bucket name. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) DeleteBucketCORS(bucketName string) error { + params := map[string]interface{}{} + params["cors"] = nil + resp, err := client.do("DELETE", bucketName, params, nil, nil) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusNoContent}) +} + +// GetBucketCORS gets the bucket's CORS settings. +// +// bucketName the bucket name. +// GetBucketCORSResult the result object upon successful request. It's only valid when error is nil. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) GetBucketCORS(bucketName string) (GetBucketCORSResult, error) { + var out GetBucketCORSResult + params := map[string]interface{}{} + params["cors"] = nil + resp, err := client.do("GET", bucketName, params, nil, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// GetBucketInfo gets the bucket information. +// +// bucketName the bucket name. +// GetBucketInfoResult the result object upon successful request. It's only valid when error is nil. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) GetBucketInfo(bucketName string, options ...Option) (GetBucketInfoResult, error) { + var out GetBucketInfoResult + params := map[string]interface{}{} + params["bucketInfo"] = nil + resp, err := client.do("GET", bucketName, params, nil, nil, options...) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + + // convert None to "" + if err == nil { + if out.BucketInfo.SseRule.KMSMasterKeyID == "None" { + out.BucketInfo.SseRule.KMSMasterKeyID = "" + } + + if out.BucketInfo.SseRule.SSEAlgorithm == "None" { + out.BucketInfo.SseRule.SSEAlgorithm = "" + } + + if out.BucketInfo.SseRule.KMSDataEncryption == "None" { + out.BucketInfo.SseRule.KMSDataEncryption = "" + } + } + return out, err +} + +// SetBucketVersioning set bucket versioning:Enabled、Suspended +// bucketName the bucket name. +// error it's nil if no error, otherwise it's an error object. +func (client Client) SetBucketVersioning(bucketName string, versioningConfig VersioningConfig, options ...Option) error { + var err error + var bs []byte + bs, err = xml.Marshal(versioningConfig) + + if err != nil { + return err + } + + buffer := new(bytes.Buffer) + buffer.Write(bs) + + contentType := http.DetectContentType(buffer.Bytes()) + headers := map[string]string{} + headers[HTTPHeaderContentType] = contentType + + params := map[string]interface{}{} + params["versioning"] = nil + resp, err := client.do("PUT", bucketName, params, headers, buffer, options...) + + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusOK}) +} + +// GetBucketVersioning get bucket versioning status:Enabled、Suspended +// bucketName the bucket name. +// error it's nil if no error, otherwise it's an error object. +func (client Client) GetBucketVersioning(bucketName string, options ...Option) (GetBucketVersioningResult, error) { + var out GetBucketVersioningResult + params := map[string]interface{}{} + params["versioning"] = nil + resp, err := client.do("GET", bucketName, params, nil, nil, options...) + + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// SetBucketEncryption set bucket encryption config +// bucketName the bucket name. +// error it's nil if no error, otherwise it's an error object. +func (client Client) SetBucketEncryption(bucketName string, encryptionRule ServerEncryptionRule, options ...Option) error { + var err error + var bs []byte + bs, err = xml.Marshal(encryptionRule) + + if err != nil { + return err + } + + buffer := new(bytes.Buffer) + buffer.Write(bs) + + contentType := http.DetectContentType(buffer.Bytes()) + headers := map[string]string{} + headers[HTTPHeaderContentType] = contentType + + params := map[string]interface{}{} + params["encryption"] = nil + resp, err := client.do("PUT", bucketName, params, headers, buffer, options...) + + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusOK}) +} + +// GetBucketEncryption get bucket encryption +// bucketName the bucket name. +// error it's nil if no error, otherwise it's an error object. +func (client Client) GetBucketEncryption(bucketName string, options ...Option) (GetBucketEncryptionResult, error) { + var out GetBucketEncryptionResult + params := map[string]interface{}{} + params["encryption"] = nil + resp, err := client.do("GET", bucketName, params, nil, nil, options...) + + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// DeleteBucketEncryption delete bucket encryption config +// bucketName the bucket name. +// error it's nil if no error, otherwise it's an error bucket +func (client Client) DeleteBucketEncryption(bucketName string, options ...Option) error { + params := map[string]interface{}{} + params["encryption"] = nil + resp, err := client.do("DELETE", bucketName, params, nil, nil, options...) + + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusNoContent}) +} + +// +// SetBucketTagging add tagging to bucket +// bucketName name of bucket +// tagging tagging to be added +// error nil if success, otherwise error +func (client Client) SetBucketTagging(bucketName string, tagging Tagging, options ...Option) error { + var err error + var bs []byte + bs, err = xml.Marshal(tagging) + + if err != nil { + return err + } + + buffer := new(bytes.Buffer) + buffer.Write(bs) + + contentType := http.DetectContentType(buffer.Bytes()) + headers := map[string]string{} + headers[HTTPHeaderContentType] = contentType + + params := map[string]interface{}{} + params["tagging"] = nil + resp, err := client.do("PUT", bucketName, params, headers, buffer, options...) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusOK}) +} + +// GetBucketTagging get tagging of the bucket +// bucketName name of bucket +// error nil if success, otherwise error +func (client Client) GetBucketTagging(bucketName string, options ...Option) (GetBucketTaggingResult, error) { + var out GetBucketTaggingResult + params := map[string]interface{}{} + params["tagging"] = nil + resp, err := client.do("GET", bucketName, params, nil, nil, options...) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// +// DeleteBucketTagging delete bucket tagging +// bucketName name of bucket +// error nil if success, otherwise error +// +func (client Client) DeleteBucketTagging(bucketName string, options ...Option) error { + params := map[string]interface{}{} + params["tagging"] = nil + resp, err := client.do("DELETE", bucketName, params, nil, nil, options...) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusNoContent}) +} + +// GetBucketStat get bucket stat +// bucketName the bucket name. +// error it's nil if no error, otherwise it's an error object. +func (client Client) GetBucketStat(bucketName string) (GetBucketStatResult, error) { + var out GetBucketStatResult + params := map[string]interface{}{} + params["stat"] = nil + resp, err := client.do("GET", bucketName, params, nil, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// GetBucketPolicy API operation for Object Storage Service. +// +// Get the policy from the bucket. +// +// bucketName the bucket name. +// +// string return the bucket's policy, and it's only valid when error is nil. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) GetBucketPolicy(bucketName string, options ...Option) (string, error) { + params := map[string]interface{}{} + params["policy"] = nil + + resp, err := client.do("GET", bucketName, params, nil, nil, options...) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + + out := string(body) + return out, err +} + +// SetBucketPolicy API operation for Object Storage Service. +// +// Set the policy from the bucket. +// +// bucketName the bucket name. +// +// policy the bucket policy. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) SetBucketPolicy(bucketName string, policy string, options ...Option) error { + params := map[string]interface{}{} + params["policy"] = nil + + buffer := strings.NewReader(policy) + + resp, err := client.do("PUT", bucketName, params, nil, buffer, options...) + if err != nil { + return err + } + defer resp.Body.Close() + + return CheckRespCode(resp.StatusCode, []int{http.StatusOK}) +} + +// DeleteBucketPolicy API operation for Object Storage Service. +// +// Deletes the policy from the bucket. +// +// bucketName the bucket name. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) DeleteBucketPolicy(bucketName string, options ...Option) error { + params := map[string]interface{}{} + params["policy"] = nil + resp, err := client.do("DELETE", bucketName, params, nil, nil, options...) + if err != nil { + return err + } + + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusNoContent}) +} + +// SetBucketRequestPayment API operation for Object Storage Service. +// +// Set the requestPayment of bucket +// +// bucketName the bucket name. +// +// paymentConfig the payment configuration +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) SetBucketRequestPayment(bucketName string, paymentConfig RequestPaymentConfiguration, options ...Option) error { + params := map[string]interface{}{} + params["requestPayment"] = nil + + var bs []byte + bs, err := xml.Marshal(paymentConfig) + + if err != nil { + return err + } + + buffer := new(bytes.Buffer) + buffer.Write(bs) + + contentType := http.DetectContentType(buffer.Bytes()) + headers := map[string]string{} + headers[HTTPHeaderContentType] = contentType + + resp, err := client.do("PUT", bucketName, params, headers, buffer, options...) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusOK}) +} + +// GetBucketRequestPayment API operation for Object Storage Service. +// +// Get bucket requestPayment +// +// bucketName the bucket name. +// +// RequestPaymentConfiguration the payment configuration +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) GetBucketRequestPayment(bucketName string, options ...Option) (RequestPaymentConfiguration, error) { + var out RequestPaymentConfiguration + params := map[string]interface{}{} + params["requestPayment"] = nil + + resp, err := client.do("GET", bucketName, params, nil, nil, options...) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// GetUserQoSInfo API operation for Object Storage Service. +// +// Get user qos. +// +// UserQoSConfiguration the User Qos and range Information. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) GetUserQoSInfo(options ...Option) (UserQoSConfiguration, error) { + var out UserQoSConfiguration + params := map[string]interface{}{} + params["qosInfo"] = nil + + resp, err := client.do("GET", "", params, nil, nil, options...) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// SetBucketQoSInfo API operation for Object Storage Service. +// +// Set Bucket Qos information. +// +// bucketName the bucket name. +// +// qosConf the qos configuration. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) SetBucketQoSInfo(bucketName string, qosConf BucketQoSConfiguration, options ...Option) error { + params := map[string]interface{}{} + params["qosInfo"] = nil + + var bs []byte + bs, err := xml.Marshal(qosConf) + if err != nil { + return err + } + buffer := new(bytes.Buffer) + buffer.Write(bs) + + contentTpye := http.DetectContentType(buffer.Bytes()) + headers := map[string]string{} + headers[HTTPHeaderContentType] = contentTpye + + resp, err := client.do("PUT", bucketName, params, headers, buffer, options...) + if err != nil { + return err + } + + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusOK}) +} + +// GetBucketQosInfo API operation for Object Storage Service. +// +// Get Bucket Qos information. +// +// bucketName the bucket name. +// +// BucketQoSConfiguration the return qos configuration. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) GetBucketQosInfo(bucketName string, options ...Option) (BucketQoSConfiguration, error) { + var out BucketQoSConfiguration + params := map[string]interface{}{} + params["qosInfo"] = nil + + resp, err := client.do("GET", bucketName, params, nil, nil, options...) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// DeleteBucketQosInfo API operation for Object Storage Service. +// +// Delete Bucket QoS information. +// +// bucketName the bucket name. +// +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) DeleteBucketQosInfo(bucketName string, options ...Option) error { + params := map[string]interface{}{} + params["qosInfo"] = nil + + resp, err := client.do("DELETE", bucketName, params, nil, nil, options...) + if err != nil { + return err + } + defer resp.Body.Close() + + return CheckRespCode(resp.StatusCode, []int{http.StatusNoContent}) +} + +// SetBucketInventory API operation for Object Storage Service +// +// Set the Bucket inventory. +// +// bucketName tht bucket name. +// +// inventoryConfig the inventory configuration. +// +// error it's nil if no error, otherwise it's an error. +// +func (client Client) SetBucketInventory(bucketName string, inventoryConfig InventoryConfiguration, options ...Option) error { + params := map[string]interface{}{} + params["inventoryId"] = inventoryConfig.Id + params["inventory"] = nil + + var bs []byte + bs, err := xml.Marshal(inventoryConfig) + + if err != nil { + return err + } + + buffer := new(bytes.Buffer) + buffer.Write(bs) + + contentType := http.DetectContentType(buffer.Bytes()) + headers := make(map[string]string) + headers[HTTPHeaderContentType] = contentType + + resp, err := client.do("PUT", bucketName, params, headers, buffer, options...) + + if err != nil { + return err + } + + defer resp.Body.Close() + + return CheckRespCode(resp.StatusCode, []int{http.StatusOK}) +} + +// GetBucketInventory API operation for Object Storage Service +// +// Get the Bucket inventory. +// +// bucketName tht bucket name. +// +// strInventoryId the inventory id. +// +// InventoryConfiguration the inventory configuration. +// +// error it's nil if no error, otherwise it's an error. +// +func (client Client) GetBucketInventory(bucketName string, strInventoryId string, options ...Option) (InventoryConfiguration, error) { + var out InventoryConfiguration + params := map[string]interface{}{} + params["inventory"] = nil + params["inventoryId"] = strInventoryId + + resp, err := client.do("GET", bucketName, params, nil, nil, options...) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// ListBucketInventory API operation for Object Storage Service +// +// List the Bucket inventory. +// +// bucketName tht bucket name. +// +// continuationToken the users token. +// +// ListInventoryConfigurationsResult list all inventory configuration by . +// +// error it's nil if no error, otherwise it's an error. +// +func (client Client) ListBucketInventory(bucketName, continuationToken string, options ...Option) (ListInventoryConfigurationsResult, error) { + var out ListInventoryConfigurationsResult + params := map[string]interface{}{} + params["inventory"] = nil + if continuationToken == "" { + params["continuation-token"] = nil + } else { + params["continuation-token"] = continuationToken + } + + resp, err := client.do("GET", bucketName, params, nil, nil, options...) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// DeleteBucketInventory API operation for Object Storage Service. +// +// Delete Bucket inventory information. +// +// bucketName tht bucket name. +// +// strInventoryId the inventory id. +// +// error it's nil if no error, otherwise it's an error. +// +func (client Client) DeleteBucketInventory(bucketName, strInventoryId string, options ...Option) error { + params := map[string]interface{}{} + params["inventory"] = nil + params["inventoryId"] = strInventoryId + + resp, err := client.do("DELETE", bucketName, params, nil, nil, options...) + if err != nil { + return err + } + defer resp.Body.Close() + + return CheckRespCode(resp.StatusCode, []int{http.StatusNoContent}) +} + +// SetBucketAsyncTask API operation for set async fetch task +// +// bucketName tht bucket name. +// +// asynConf configruation +// +// error it's nil if success, otherwise it's an error. +func (client Client) SetBucketAsyncTask(bucketName string, asynConf AsyncFetchTaskConfiguration, options ...Option) (AsyncFetchTaskResult, error) { + var out AsyncFetchTaskResult + params := map[string]interface{}{} + params["asyncFetch"] = nil + + var bs []byte + bs, err := xml.Marshal(asynConf) + + if err != nil { + return out, err + } + + buffer := new(bytes.Buffer) + buffer.Write(bs) + + contentType := http.DetectContentType(buffer.Bytes()) + headers := make(map[string]string) + headers[HTTPHeaderContentType] = contentType + + resp, err := client.do("POST", bucketName, params, headers, buffer, options...) + + if err != nil { + return out, err + } + + defer resp.Body.Close() + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// GetBucketAsyncTask API operation for set async fetch task +// +// bucketName tht bucket name. +// +// taskid returned by SetBucketAsyncTask +// +// error it's nil if success, otherwise it's an error. +func (client Client) GetBucketAsyncTask(bucketName string, taskID string, options ...Option) (AsynFetchTaskInfo, error) { + var out AsynFetchTaskInfo + params := map[string]interface{}{} + params["asyncFetch"] = nil + + headers := make(map[string]string) + headers[HTTPHeaderOssTaskID] = taskID + resp, err := client.do("GET", bucketName, params, headers, nil, options...) + if err != nil { + return out, err + } + defer resp.Body.Close() + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// InitiateBucketWorm creates bucket worm Configuration +// bucketName the bucket name. +// retentionDays the retention period in days +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) InitiateBucketWorm(bucketName string, retentionDays int, options ...Option) (string, error) { + var initiateWormConf InitiateWormConfiguration + initiateWormConf.RetentionPeriodInDays = retentionDays + + var respHeader http.Header + isOptSet, _, _ := IsOptionSet(options, responseHeader) + if !isOptSet { + options = append(options, GetResponseHeader(&respHeader)) + } + + bs, err := xml.Marshal(initiateWormConf) + if err != nil { + return "", err + } + buffer := new(bytes.Buffer) + buffer.Write(bs) + + contentType := http.DetectContentType(buffer.Bytes()) + headers := make(map[string]string) + headers[HTTPHeaderContentType] = contentType + + params := map[string]interface{}{} + params["worm"] = nil + + resp, err := client.do("POST", bucketName, params, headers, buffer, options...) + if err != nil { + return "", err + } + defer resp.Body.Close() + + respOpt, _ := FindOption(options, responseHeader, nil) + wormID := "" + err = CheckRespCode(resp.StatusCode, []int{http.StatusOK}) + if err == nil && respOpt != nil { + wormID = (respOpt.(*http.Header)).Get("x-oss-worm-id") + } + return wormID, err +} + +// AbortBucketWorm delete bucket worm Configuration +// bucketName the bucket name. +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) AbortBucketWorm(bucketName string, options ...Option) error { + params := map[string]interface{}{} + params["worm"] = nil + resp, err := client.do("DELETE", bucketName, params, nil, nil, options...) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusNoContent}) +} + +// CompleteBucketWorm complete bucket worm Configuration +// bucketName the bucket name. +// wormID the worm id +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) CompleteBucketWorm(bucketName string, wormID string, options ...Option) error { + params := map[string]interface{}{} + params["wormId"] = wormID + resp, err := client.do("POST", bucketName, params, nil, nil, options...) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusOK}) +} + +// ExtendBucketWorm exetend bucket worm Configuration +// bucketName the bucket name. +// retentionDays the retention period in days +// wormID the worm id +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) ExtendBucketWorm(bucketName string, retentionDays int, wormID string, options ...Option) error { + var extendWormConf ExtendWormConfiguration + extendWormConf.RetentionPeriodInDays = retentionDays + + bs, err := xml.Marshal(extendWormConf) + if err != nil { + return err + } + buffer := new(bytes.Buffer) + buffer.Write(bs) + + contentType := http.DetectContentType(buffer.Bytes()) + headers := make(map[string]string) + headers[HTTPHeaderContentType] = contentType + + params := map[string]interface{}{} + params["wormId"] = wormID + params["wormExtend"] = nil + + resp, err := client.do("POST", bucketName, params, headers, buffer, options...) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusOK}) +} + +// GetBucketWorm get bucket worm Configuration +// bucketName the bucket name. +// error it's nil if no error, otherwise it's an error object. +// +func (client Client) GetBucketWorm(bucketName string, options ...Option) (WormConfiguration, error) { + var out WormConfiguration + params := map[string]interface{}{} + params["worm"] = nil + + resp, err := client.do("GET", bucketName, params, nil, nil, options...) + if err != nil { + return out, err + } + defer resp.Body.Close() + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// LimitUploadSpeed set upload bandwidth limit speed,default is 0,unlimited +// upSpeed KB/s, 0 is unlimited,default is 0 +// error it's nil if success, otherwise failure +func (client Client) LimitUploadSpeed(upSpeed int) error { + if client.Config == nil { + return fmt.Errorf("client config is nil") + } + return client.Config.LimitUploadSpeed(upSpeed) +} + +// UseCname sets the flag of using CName. By default it's false. +// +// isUseCname true: the endpoint has the CName, false: the endpoint does not have cname. Default is false. +// +func UseCname(isUseCname bool) ClientOption { + return func(client *Client) { + client.Config.IsCname = isUseCname + client.Conn.url.Init(client.Config.Endpoint, client.Config.IsCname, client.Config.IsUseProxy) + } +} + +// Timeout sets the HTTP timeout in seconds. +// +// connectTimeoutSec HTTP timeout in seconds. Default is 10 seconds. 0 means infinite (not recommended) +// readWriteTimeout HTTP read or write's timeout in seconds. Default is 20 seconds. 0 means infinite. +// +func Timeout(connectTimeoutSec, readWriteTimeout int64) ClientOption { + return func(client *Client) { + client.Config.HTTPTimeout.ConnectTimeout = + time.Second * time.Duration(connectTimeoutSec) + client.Config.HTTPTimeout.ReadWriteTimeout = + time.Second * time.Duration(readWriteTimeout) + client.Config.HTTPTimeout.HeaderTimeout = + time.Second * time.Duration(readWriteTimeout) + client.Config.HTTPTimeout.IdleConnTimeout = + time.Second * time.Duration(readWriteTimeout) + client.Config.HTTPTimeout.LongTimeout = + time.Second * time.Duration(readWriteTimeout*10) + } +} + +// SecurityToken sets the temporary user's SecurityToken. +// +// token STS token +// +func SecurityToken(token string) ClientOption { + return func(client *Client) { + client.Config.SecurityToken = strings.TrimSpace(token) + } +} + +// EnableMD5 enables MD5 validation. +// +// isEnableMD5 true: enable MD5 validation; false: disable MD5 validation. +// +func EnableMD5(isEnableMD5 bool) ClientOption { + return func(client *Client) { + client.Config.IsEnableMD5 = isEnableMD5 + } +} + +// MD5ThresholdCalcInMemory sets the memory usage threshold for computing the MD5, default is 16MB. +// +// threshold the memory threshold in bytes. When the uploaded content is more than 16MB, the temp file is used for computing the MD5. +// +func MD5ThresholdCalcInMemory(threshold int64) ClientOption { + return func(client *Client) { + client.Config.MD5Threshold = threshold + } +} + +// EnableCRC enables the CRC checksum. Default is true. +// +// isEnableCRC true: enable CRC checksum; false: disable the CRC checksum. +// +func EnableCRC(isEnableCRC bool) ClientOption { + return func(client *Client) { + client.Config.IsEnableCRC = isEnableCRC + } +} + +// UserAgent specifies UserAgent. The default is aliyun-sdk-go/1.2.0 (windows/-/amd64;go1.5.2). +// +// userAgent the user agent string. +// +func UserAgent(userAgent string) ClientOption { + return func(client *Client) { + client.Config.UserAgent = userAgent + client.Config.UserSetUa = true + } +} + +// Proxy sets the proxy (optional). The default is not using proxy. +// +// proxyHost the proxy host in the format "host:port". For example, proxy.com:80 . +// +func Proxy(proxyHost string) ClientOption { + return func(client *Client) { + client.Config.IsUseProxy = true + client.Config.ProxyHost = proxyHost + client.Conn.url.Init(client.Config.Endpoint, client.Config.IsCname, client.Config.IsUseProxy) + } +} + +// AuthProxy sets the proxy information with user name and password. +// +// proxyHost the proxy host in the format "host:port". For example, proxy.com:80 . +// proxyUser the proxy user name. +// proxyPassword the proxy password. +// +func AuthProxy(proxyHost, proxyUser, proxyPassword string) ClientOption { + return func(client *Client) { + client.Config.IsUseProxy = true + client.Config.ProxyHost = proxyHost + client.Config.IsAuthProxy = true + client.Config.ProxyUser = proxyUser + client.Config.ProxyPassword = proxyPassword + client.Conn.url.Init(client.Config.Endpoint, client.Config.IsCname, client.Config.IsUseProxy) + } +} + +// +// HTTPClient sets the http.Client in use to the one passed in +// +func HTTPClient(HTTPClient *http.Client) ClientOption { + return func(client *Client) { + client.HTTPClient = HTTPClient + } +} + +// +// SetLogLevel sets the oss sdk log level +// +func SetLogLevel(LogLevel int) ClientOption { + return func(client *Client) { + client.Config.LogLevel = LogLevel + } +} + +// +// SetLogger sets the oss sdk logger +// +func SetLogger(Logger *log.Logger) ClientOption { + return func(client *Client) { + client.Config.Logger = Logger + } +} + +// SetCredentialsProvider sets funciton for get the user's ak +func SetCredentialsProvider(provider CredentialsProvider) ClientOption { + return func(client *Client) { + client.Config.CredentialsProvider = provider + } +} + +// SetLocalAddr sets funciton for local addr +func SetLocalAddr(localAddr net.Addr) ClientOption { + return func(client *Client) { + client.Config.LocalAddr = localAddr + } +} + +// AuthVersion sets auth version: v1 or v2 signature which oss_server needed +func AuthVersion(authVersion AuthVersionType) ClientOption { + return func(client *Client) { + client.Config.AuthVersion = authVersion + } +} + +// AdditionalHeaders sets special http headers needed to be signed +func AdditionalHeaders(headers []string) ClientOption { + return func(client *Client) { + client.Config.AdditionalHeaders = headers + } +} + +// only effective from go1.7 onward,RedirectEnabled set http redirect enabled or not +func RedirectEnabled(enabled bool) ClientOption { + return func(client *Client) { + client.Config.RedirectEnabled = enabled + } +} + +// Private +func (client Client) do(method, bucketName string, params map[string]interface{}, + headers map[string]string, data io.Reader, options ...Option) (*Response, error) { + err := CheckBucketName(bucketName) + if len(bucketName) > 0 && err != nil { + return nil, err + } + + // option headers + addHeaders := make(map[string]string) + err = handleOptions(addHeaders, options) + if err != nil { + return nil, err + } + + // merge header + if headers == nil { + headers = make(map[string]string) + } + + for k, v := range addHeaders { + if _, ok := headers[k]; !ok { + headers[k] = v + } + } + + resp, err := client.Conn.Do(method, bucketName, "", params, headers, data, 0, nil) + + // get response header + respHeader, _ := FindOption(options, responseHeader, nil) + if respHeader != nil { + pRespHeader := respHeader.(*http.Header) + *pRespHeader = resp.Headers + } + + return resp, err +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/client_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/client_test.go new file mode 100644 index 00000000..c2f4f7b3 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/client_test.go @@ -0,0 +1,3788 @@ +// client test +// use gocheck, install gocheck to execute "go get gopkg.in/check.v1", +// see https://labix.org/gocheck + +package oss + +import ( + "encoding/json" + "encoding/xml" + "fmt" + "io/ioutil" + "log" + "math/rand" + "net" + "net/http" + "os" + "reflect" + "runtime" + "strconv" + "strings" + "testing" + "time" + + . "gopkg.in/check.v1" +) + +// Test hooks up gocheck into the "go test" runner. +func Test(t *testing.T) { + TestingT(t) +} + +type OssClientSuite struct{} + +var _ = Suite(&OssClientSuite{}) + +var ( + // Endpoint/ID/Key + endpoint = os.Getenv("OSS_TEST_ENDPOINT") + accessID = os.Getenv("OSS_TEST_ACCESS_KEY_ID") + accessKey = os.Getenv("OSS_TEST_ACCESS_KEY_SECRET") + accountID = os.Getenv("OSS_TEST_ACCOUNT_ID") + + // Proxy + proxyHost = os.Getenv("OSS_TEST_PROXY_HOST") + proxyUser = os.Getenv("OSS_TEST_PROXY_USER") + proxyPasswd = os.Getenv("OSS_TEST_PROXY_PASSWORD") + + // STS + stsaccessID = os.Getenv("OSS_TEST_STS_ID") + stsaccessKey = os.Getenv("OSS_TEST_STS_KEY") + stsARN = os.Getenv("OSS_TEST_STS_ARN") + + // Credential + credentialAccessID = os.Getenv("OSS_CREDENTIAL_KEY_ID") + credentialAccessKey = os.Getenv("OSS_CREDENTIAL_KEY_SECRET") + credentialUID = os.Getenv("OSS_CREDENTIAL_UID") +) + +var ( + // prefix of bucket name for bucket ops test + bucketNamePrefix = "go-sdk-test-bucket-" + // bucket name for object ops test + bucketName = bucketNamePrefix + RandLowStr(6) + archiveBucketName = bucketNamePrefix + "arch-" + RandLowStr(6) + // object name for object ops test + objectNamePrefix = "go-sdk-test-object-" + // sts region is one and only hangzhou + stsRegion = "cn-hangzhou" + // Credentials + credentialBucketName = bucketNamePrefix + RandLowStr(6) +) + +var ( + logPath = "go_sdk_test_" + time.Now().Format("20060102_150405") + ".log" + testLogFile, _ = os.OpenFile(logPath, os.O_RDWR|os.O_CREATE, 0664) + testLogger = log.New(testLogFile, "", log.Ldate|log.Ltime|log.Lshortfile) + letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + timeoutInOperation = 3 * time.Second +) + +func RandStr(n int) string { + b := make([]rune, n) + randMarker := rand.New(rand.NewSource(time.Now().UnixNano())) + for i := range b { + b[i] = letters[randMarker.Intn(len(letters))] + } + return string(b) +} + +func CreateFile(fileName, content string, c *C) { + fout, err := os.Create(fileName) + defer fout.Close() + c.Assert(err, IsNil) + _, err = fout.WriteString(content) + c.Assert(err, IsNil) +} + +func RandLowStr(n int) string { + return strings.ToLower(RandStr(n)) +} + +func ForceDeleteBucket(client *Client, bucketName string, c *C) { + bucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + + // Delete Object + marker := Marker("") + for { + lor, err := bucket.ListObjects(marker) + c.Assert(err, IsNil) + for _, object := range lor.Objects { + err = bucket.DeleteObject(object.Key) + c.Assert(err, IsNil) + } + marker = Marker(lor.NextMarker) + if !lor.IsTruncated { + break + } + } + + // Delete Object Versions and DeleteMarks + keyMarker := KeyMarker("") + versionIdMarker := VersionIdMarker("") + options := []Option{keyMarker, versionIdMarker} + for { + lor, err := bucket.ListObjectVersions(options...) + if err != nil { + break + } + + for _, object := range lor.ObjectDeleteMarkers { + err = bucket.DeleteObject(object.Key, VersionId(object.VersionId)) + c.Assert(err, IsNil) + } + + for _, object := range lor.ObjectVersions { + err = bucket.DeleteObject(object.Key, VersionId(object.VersionId)) + c.Assert(err, IsNil) + } + + keyMarker = KeyMarker(lor.NextKeyMarker) + versionIdMarker := VersionIdMarker(lor.NextVersionIdMarker) + options = []Option{keyMarker, versionIdMarker} + + if !lor.IsTruncated { + break + } + } + + // Delete Part + keyMarker = KeyMarker("") + uploadIDMarker := UploadIDMarker("") + for { + lmur, err := bucket.ListMultipartUploads(keyMarker, uploadIDMarker) + c.Assert(err, IsNil) + for _, upload := range lmur.Uploads { + var imur = InitiateMultipartUploadResult{Bucket: bucketName, + Key: upload.Key, UploadID: upload.UploadID} + err = bucket.AbortMultipartUpload(imur) + c.Assert(err, IsNil) + } + keyMarker = KeyMarker(lmur.NextKeyMarker) + uploadIDMarker = UploadIDMarker(lmur.NextUploadIDMarker) + if !lmur.IsTruncated { + break + } + } + + // delete live channel + strMarker := "" + for { + result, err := bucket.ListLiveChannel(Marker(strMarker)) + c.Assert(err, IsNil) + + for _, channel := range result.LiveChannel { + err := bucket.DeleteLiveChannel(channel.Name) + c.Assert(err, IsNil) + } + + if result.IsTruncated { + strMarker = result.NextMarker + } else { + break + } + } + + // Delete Bucket + err = client.DeleteBucket(bucketName) + c.Assert(err, IsNil) +} + +// SetUpSuite runs once when the suite starts running +func (s *OssClientSuite) SetUpSuite(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + lbr, err := client.ListBuckets(Prefix(bucketNamePrefix), MaxKeys(1000)) + c.Assert(err, IsNil) + + for _, bucket := range lbr.Buckets { + ForceDeleteBucket(client, bucket.Name, c) + } + + testLogger.Println("test client started") +} + +// TearDownSuite runs before each test or benchmark starts running +func (s *OssClientSuite) TearDownSuite(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + lbr, err := client.ListBuckets(Prefix(bucketNamePrefix), MaxKeys(1000)) + c.Assert(err, IsNil) + + for _, bucket := range lbr.Buckets { + s.deleteBucket(client, bucket.Name, c) + } + + testLogger.Println("test client completed") +} + +func (s *OssClientSuite) deleteBucket(client *Client, bucketName string, c *C) { + ForceDeleteBucket(client, bucketName, c) +} + +// SetUpTest runs after each test or benchmark runs +func (s *OssClientSuite) SetUpTest(c *C) { +} + +// TearDownTest runs once after all tests or benchmarks have finished running +func (s *OssClientSuite) TearDownTest(c *C) { +} + +// TestCreateBucket +func (s *OssClientSuite) TestCreateBucket(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + // Create + client.DeleteBucket(bucketNameTest) + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + //sleep 3 seconds after create bucket + time.Sleep(timeoutInOperation) + + // verify bucket is exist + found, err := client.IsBucketExist(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(found, Equals, true) + + res, err := client.GetBucketACL(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.ACL, Equals, string(ACLPrivate)) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + // CreateBucket creates with ACLPublicRead + err = client.CreateBucket(bucketNameTest, ACL(ACLPublicRead)) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + res, err = client.GetBucketACL(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.ACL, Equals, string(ACLPublicRead)) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + // ACLPublicReadWrite + err = client.CreateBucket(bucketNameTest, ACL(ACLPublicReadWrite)) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + res, err = client.GetBucketACL(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.ACL, Equals, string(ACLPublicReadWrite)) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + // ACLPrivate + err = client.CreateBucket(bucketNameTest, ACL(ACLPrivate)) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + res, err = client.GetBucketACL(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.ACL, Equals, string(ACLPrivate)) + + // Delete + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) + + // Create bucket with configuration and test GetBucketInfo + for _, storage := range []StorageClassType{StorageStandard, StorageIA, StorageArchive, StorageColdArchive} { + bucketNameTest := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketNameTest, StorageClass(storage), ACL(ACLPublicRead)) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + res, err := client.GetBucketInfo(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.BucketInfo.Name, Equals, bucketNameTest) + c.Assert(res.BucketInfo.StorageClass, Equals, string(storage)) + c.Assert(res.BucketInfo.ACL, Equals, string(ACLPublicRead)) + + // Delete + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) + } + + // Error put bucket with configuration + err = client.CreateBucket("ERRORBUCKETNAME", StorageClass(StorageArchive)) + c.Assert(err, NotNil) + + // Create bucket with configuration and test ListBuckets + for _, storage := range []StorageClassType{StorageStandard, StorageIA, StorageArchive, StorageColdArchive} { + bucketNameTest := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketNameTest, StorageClass(storage)) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + res, err := client.GetBucketInfo(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.BucketInfo.Name, Equals, bucketNameTest) + c.Assert(res.BucketInfo.StorageClass, Equals, string(storage)) + + // Delete + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) + } +} + +func (s *OssClientSuite) TestCreateBucketRedundancyType(c *C) { + bucketNameTest := bucketNamePrefix + RandLowStr(6) + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + // CreateBucket creates without property + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + client.DeleteBucket(bucketNameTest) + time.Sleep(timeoutInOperation) + + // CreateBucket creates with RedundancyZRS + err = client.CreateBucket(bucketNameTest, RedundancyType(RedundancyZRS)) + c.Assert(err, IsNil) + + res, err := client.GetBucketInfo(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.BucketInfo.RedundancyType, Equals, string(RedundancyZRS)) + client.DeleteBucket(bucketNameTest) + time.Sleep(timeoutInOperation) + + // CreateBucket creates with RedundancyLRS + err = client.CreateBucket(bucketNameTest, RedundancyType(RedundancyLRS)) + c.Assert(err, IsNil) + + res, err = client.GetBucketInfo(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.BucketInfo.RedundancyType, Equals, string(RedundancyLRS)) + c.Assert(res.BucketInfo.StorageClass, Equals, string(StorageStandard)) + client.DeleteBucket(bucketNameTest) + time.Sleep(timeoutInOperation) + + // CreateBucket creates with ACLPublicRead RedundancyZRS + err = client.CreateBucket(bucketNameTest, ACL(ACLPublicRead), RedundancyType(RedundancyZRS)) + c.Assert(err, IsNil) + + res, err = client.GetBucketInfo(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.BucketInfo.RedundancyType, Equals, string(RedundancyZRS)) + c.Assert(res.BucketInfo.ACL, Equals, string(ACLPublicRead)) + client.DeleteBucket(bucketNameTest) +} + +// TestCreateBucketNegative +func (s *OssClientSuite) TestCreateBucketNegative(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + // Bucket name invalid + err = client.CreateBucket("xx") + c.Assert(err, NotNil) + + err = client.CreateBucket("XXXX") + c.Assert(err, NotNil) + testLogger.Println(err) + + err = client.CreateBucket("_bucket") + c.Assert(err, NotNil) + testLogger.Println(err) + + // ACL invalid + err = client.CreateBucket(bucketNamePrefix+RandLowStr(6), ACL("InvaldAcl")) + c.Assert(err, NotNil) + testLogger.Println(err) +} + +// TestDeleteBucket +func (s *OssClientSuite) TestDeleteBucket(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + // Create + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + // Check + found, err := client.IsBucketExist(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(found, Equals, true) + + // Delete + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + // Check + found, err = client.IsBucketExist(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(found, Equals, false) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, NotNil) +} + +// TestDeleteBucketNegative +func (s *OssClientSuite) TestDeleteBucketNegative(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + // Bucket name invalid + err = client.DeleteBucket("xx") + c.Assert(err, NotNil) + + err = client.DeleteBucket("XXXX") + c.Assert(err, NotNil) + + err = client.DeleteBucket("_bucket") + c.Assert(err, NotNil) + + // Delete no exist bucket + err = client.DeleteBucket("notexist") + c.Assert(err, NotNil) + + // No permission to delete, this ak/sk for js sdk + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + accessID := "" + accessKey := "" + clientOtherUser, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = clientOtherUser.DeleteBucket(bucketNameTest) + c.Assert(err, NotNil) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestListBucket +func (s *OssClientSuite) TestListBucket(c *C) { + var prefix = bucketNamePrefix + RandLowStr(6) + var bucketNameLbOne = prefix + "tlb1" + var bucketNameLbTwo = prefix + "tlb2" + var bucketNameLbThree = prefix + "tlb3" + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + // CreateBucket + err = client.CreateBucket(bucketNameLbOne) + c.Assert(err, IsNil) + err = client.CreateBucket(bucketNameLbTwo) + c.Assert(err, IsNil) + err = client.CreateBucket(bucketNameLbThree) + c.Assert(err, IsNil) + + // ListBuckets, specified prefix + var respHeader http.Header + lbr, err := client.ListBuckets(Prefix(prefix), MaxKeys(2), GetResponseHeader(&respHeader)) + c.Assert(GetRequestId(respHeader) != "", Equals, true) + c.Assert(err, IsNil) + c.Assert(len(lbr.Buckets), Equals, 2) + + // ListBuckets, specified max keys + lbr, err = client.ListBuckets(MaxKeys(2)) + c.Assert(err, IsNil) + c.Assert(len(lbr.Buckets), Equals, 2) + + // ListBuckets, specified max keys + lbr, err = client.ListBuckets(Marker(bucketNameLbOne), MaxKeys(1)) + c.Assert(err, IsNil) + c.Assert(len(lbr.Buckets), Equals, 1) + + // ListBuckets, specified max keys + lbr, err = client.ListBuckets(Marker(bucketNameLbOne)) + c.Assert(err, IsNil) + c.Assert(len(lbr.Buckets) >= 2, Equals, true) + + // DeleteBucket + err = client.DeleteBucket(bucketNameLbOne) + c.Assert(err, IsNil) + err = client.DeleteBucket(bucketNameLbTwo) + c.Assert(err, IsNil) + err = client.DeleteBucket(bucketNameLbThree) + c.Assert(err, IsNil) +} + +// TestListBucket +func (s *OssClientSuite) TestIsBucketExist(c *C) { + var prefix = bucketNamePrefix + RandLowStr(6) + var bucketNameLbOne = prefix + "tibe1" + var bucketNameLbTwo = prefix + "tibe11" + var bucketNameLbThree = prefix + "tibe111" + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + // CreateBucket + err = client.CreateBucket(bucketNameLbOne) + c.Assert(err, IsNil) + err = client.CreateBucket(bucketNameLbTwo) + c.Assert(err, IsNil) + err = client.CreateBucket(bucketNameLbThree) + c.Assert(err, IsNil) + + // Exist + exist, err := client.IsBucketExist(bucketNameLbTwo) + c.Assert(err, IsNil) + c.Assert(exist, Equals, true) + + exist, err = client.IsBucketExist(bucketNameLbThree) + c.Assert(err, IsNil) + c.Assert(exist, Equals, true) + + exist, err = client.IsBucketExist(bucketNameLbOne) + c.Assert(err, IsNil) + c.Assert(exist, Equals, true) + + // Not exist + exist, err = client.IsBucketExist(prefix + "tibe") + c.Assert(err, IsNil) + c.Assert(exist, Equals, false) + + exist, err = client.IsBucketExist(prefix + "tibe1111") + c.Assert(err, IsNil) + c.Assert(exist, Equals, false) + + // Negative + exist, err = client.IsBucketExist("BucketNameInvalid") + c.Assert(err, NotNil) + + // DeleteBucket + err = client.DeleteBucket(bucketNameLbOne) + c.Assert(err, IsNil) + err = client.DeleteBucket(bucketNameLbTwo) + c.Assert(err, IsNil) + err = client.DeleteBucket(bucketNameLbThree) + c.Assert(err, IsNil) +} + +// TestSetBucketAcl +func (s *OssClientSuite) TestSetBucketAcl(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + // Private + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + res, err := client.GetBucketACL(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.ACL, Equals, string(ACLPrivate)) + + // Set ACL_PUBLIC_R + err = client.SetBucketACL(bucketNameTest, ACLPublicRead) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + res, err = client.GetBucketACL(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.ACL, Equals, string(ACLPublicRead)) + + // Set ACL_PUBLIC_RW + err = client.SetBucketACL(bucketNameTest, ACLPublicReadWrite) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + res, err = client.GetBucketACL(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.ACL, Equals, string(ACLPublicReadWrite)) + + // Set ACL_PUBLIC_RW + err = client.SetBucketACL(bucketNameTest, ACLPrivate) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + res, err = client.GetBucketACL(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.ACL, Equals, string(ACLPrivate)) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestSetBucketAclNegative +func (s *OssClientSuite) TestBucketAclNegative(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + err = client.SetBucketACL(bucketNameTest, "InvalidACL") + c.Assert(err, NotNil) + testLogger.Println(err) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestGetBucketAcl +func (s *OssClientSuite) TestGetBucketAcl(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + // Private + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + res, err := client.GetBucketACL(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.ACL, Equals, string(ACLPrivate)) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + // PublicRead + err = client.CreateBucket(bucketNameTest, ACL(ACLPublicRead)) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + res, err = client.GetBucketACL(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.ACL, Equals, string(ACLPublicRead)) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + // PublicReadWrite + err = client.CreateBucket(bucketNameTest, ACL(ACLPublicReadWrite)) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + res, err = client.GetBucketACL(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.ACL, Equals, string(ACLPublicReadWrite)) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestGetBucketAcl +func (s *OssClientSuite) TestGetBucketLocation(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + // Private + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + loc, err := client.GetBucketLocation(bucketNameTest) + c.Assert(strings.HasPrefix(loc, "oss-"), Equals, true) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestGetBucketLocationNegative +func (s *OssClientSuite) TestGetBucketLocationNegative(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + // Not exist + _, err = client.GetBucketLocation(bucketNameTest) + c.Assert(err, NotNil) + + // Not exist + _, err = client.GetBucketLocation("InvalidBucketName_") + c.Assert(err, NotNil) +} + +// TestSetBucketLifecycle +func (s *OssClientSuite) TestSetBucketLifecycle(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + var rule1 = BuildLifecycleRuleByDate("rule1", "one", true, 2015, 11, 11) + var rule2 = BuildLifecycleRuleByDays("rule2", "two", true, 3) + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + // Set single rule + var rules = []LifecycleRule{rule1} + err = client.SetBucketLifecycle(bucketNameTest, rules) + c.Assert(err, IsNil) + // Double set rule + err = client.SetBucketLifecycle(bucketNameTest, rules) + c.Assert(err, IsNil) + + res, err := client.GetBucketLifecycle(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(len(res.Rules), Equals, 1) + c.Assert(res.Rules[0].ID, Equals, "rule1") + + err = client.DeleteBucketLifecycle(bucketNameTest) + c.Assert(err, IsNil) + + // Set two rules + rules = []LifecycleRule{rule1, rule2} + err = client.SetBucketLifecycle(bucketNameTest, rules) + c.Assert(err, IsNil) + + // Eliminate effect of cache + time.Sleep(timeoutInOperation) + + res, err = client.GetBucketLifecycle(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(len(res.Rules), Equals, 2) + c.Assert(res.Rules[0].ID, Equals, "rule1") + c.Assert(res.Rules[1].ID, Equals, "rule2") + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestSetBucketLifecycleNew +func (s *OssClientSuite) TestSetBucketLifecycleNew(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + //invalid status of lifecyclerule + expiration := LifecycleExpiration{ + Days: 30, + } + rule := LifecycleRule{ + ID: "rule1", + Prefix: "one", + Status: "Invalid", + Expiration: &expiration, + } + rules := []LifecycleRule{rule} + err = client.SetBucketLifecycle(bucketNameTest, rules) + c.Assert(err, NotNil) + + //invalid value of CreatedBeforeDate + expiration = LifecycleExpiration{ + CreatedBeforeDate: RandStr(10), + } + rule = LifecycleRule{ + ID: "rule1", + Prefix: "one", + Status: "Enabled", + Expiration: &expiration, + } + rules = []LifecycleRule{rule} + err = client.SetBucketLifecycle(bucketNameTest, rules) + c.Assert(err, NotNil) + + //invalid value of Days + abortMPU := LifecycleAbortMultipartUpload{ + Days: -30, + } + rule = LifecycleRule{ + ID: "rule1", + Prefix: "one", + Status: "Enabled", + AbortMultipartUpload: &abortMPU, + } + rules = []LifecycleRule{rule} + err = client.SetBucketLifecycle(bucketNameTest, rules) + c.Assert(err, NotNil) + + expiration = LifecycleExpiration{ + CreatedBeforeDate: "2015-11-11T00:00:00.000Z", + } + rule1 := LifecycleRule{ + ID: "rule1", + Prefix: "one", + Status: "Enabled", + Expiration: &expiration, + } + + abortMPU = LifecycleAbortMultipartUpload{ + Days: 30, + } + rule2 := LifecycleRule{ + ID: "rule2", + Prefix: "two", + Status: "Enabled", + Expiration: &expiration, + AbortMultipartUpload: &abortMPU, + } + + transition1 := LifecycleTransition{ + Days: 3, + StorageClass: StorageIA, + } + transition2 := LifecycleTransition{ + Days: 30, + StorageClass: StorageArchive, + } + transitions := []LifecycleTransition{transition1, transition2} + rule3 := LifecycleRule{ + ID: "rule3", + Prefix: "three", + Status: "Enabled", + AbortMultipartUpload: &abortMPU, + Transitions: transitions, + } + + // Set single rule + rules = []LifecycleRule{rule1} + err = client.SetBucketLifecycle(bucketNameTest, rules) + c.Assert(err, IsNil) + + res, err := client.GetBucketLifecycle(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(len(res.Rules), Equals, 1) + c.Assert(res.Rules[0].ID, Equals, "rule1") + c.Assert(res.Rules[0].Expiration, NotNil) + c.Assert(res.Rules[0].Expiration.CreatedBeforeDate, Equals, "2015-11-11T00:00:00.000Z") + + err = client.DeleteBucketLifecycle(bucketNameTest) + c.Assert(err, IsNil) + + // Set two rule: rule1 and rule2 + rules = []LifecycleRule{rule1, rule2} + err = client.SetBucketLifecycle(bucketNameTest, rules) + c.Assert(err, IsNil) + + res, err = client.GetBucketLifecycle(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(len(res.Rules), Equals, 2) + c.Assert(res.Rules[0].ID, Equals, "rule1") + c.Assert(res.Rules[0].Expiration, NotNil) + c.Assert(res.Rules[0].Expiration.CreatedBeforeDate, Equals, "2015-11-11T00:00:00.000Z") + c.Assert(res.Rules[1].ID, Equals, "rule2") + c.Assert(res.Rules[1].Expiration, NotNil) + c.Assert(res.Rules[1].Expiration.CreatedBeforeDate, Equals, "2015-11-11T00:00:00.000Z") + c.Assert(res.Rules[1].AbortMultipartUpload, NotNil) + c.Assert(res.Rules[1].AbortMultipartUpload.Days, Equals, 30) + + err = client.DeleteBucketLifecycle(bucketNameTest) + c.Assert(err, IsNil) + + // Set two rule: rule2 and rule3 + rules = []LifecycleRule{rule2, rule3} + err = client.SetBucketLifecycle(bucketNameTest, rules) + c.Assert(err, IsNil) + + res, err = client.GetBucketLifecycle(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(len(res.Rules), Equals, 2) + c.Assert(res.Rules[0].ID, Equals, "rule2") + c.Assert(res.Rules[0].Expiration, NotNil) + c.Assert(res.Rules[0].Expiration.CreatedBeforeDate, Equals, "2015-11-11T00:00:00.000Z") + c.Assert(res.Rules[0].AbortMultipartUpload, NotNil) + c.Assert(res.Rules[0].AbortMultipartUpload.Days, Equals, 30) + c.Assert(res.Rules[1].ID, Equals, "rule3") + c.Assert(res.Rules[1].AbortMultipartUpload, NotNil) + c.Assert(res.Rules[1].AbortMultipartUpload.Days, Equals, 30) + c.Assert(len(res.Rules[1].Transitions), Equals, 2) + c.Assert(res.Rules[1].Transitions[0].StorageClass, Equals, StorageIA) + c.Assert(res.Rules[1].Transitions[0].Days, Equals, 3) + c.Assert(res.Rules[1].Transitions[1].StorageClass, Equals, StorageArchive) + c.Assert(res.Rules[1].Transitions[1].Days, Equals, 30) + + err = client.DeleteBucketLifecycle(bucketNameTest) + c.Assert(err, IsNil) + + // Set two rule: rule1 and rule3 + rules = []LifecycleRule{rule1, rule3} + err = client.SetBucketLifecycle(bucketNameTest, rules) + c.Assert(err, IsNil) + + res, err = client.GetBucketLifecycle(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(len(res.Rules), Equals, 2) + c.Assert(res.Rules[0].ID, Equals, "rule1") + c.Assert(res.Rules[0].Expiration, NotNil) + c.Assert(res.Rules[0].Expiration.CreatedBeforeDate, Equals, "2015-11-11T00:00:00.000Z") + c.Assert(res.Rules[1].ID, Equals, "rule3") + c.Assert(res.Rules[1].AbortMultipartUpload, NotNil) + c.Assert(res.Rules[1].AbortMultipartUpload.Days, Equals, 30) + c.Assert(len(res.Rules[1].Transitions), Equals, 2) + c.Assert(res.Rules[1].Transitions[0].StorageClass, Equals, StorageIA) + c.Assert(res.Rules[1].Transitions[0].Days, Equals, 3) + c.Assert(res.Rules[1].Transitions[1].StorageClass, Equals, StorageArchive) + c.Assert(res.Rules[1].Transitions[1].Days, Equals, 30) + + err = client.DeleteBucketLifecycle(bucketNameTest) + c.Assert(err, IsNil) + + // Set three rules + rules = []LifecycleRule{rule1, rule2, rule3} + err = client.SetBucketLifecycle(bucketNameTest, rules) + c.Assert(err, IsNil) + + res, err = client.GetBucketLifecycle(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(len(res.Rules), Equals, 3) + c.Assert(res.Rules[0].ID, Equals, "rule1") + c.Assert(res.Rules[0].Expiration, NotNil) + c.Assert(res.Rules[0].Expiration.CreatedBeforeDate, Equals, "2015-11-11T00:00:00.000Z") + c.Assert(res.Rules[1].ID, Equals, "rule2") + c.Assert(res.Rules[1].Expiration, NotNil) + c.Assert(res.Rules[1].Expiration.CreatedBeforeDate, Equals, "2015-11-11T00:00:00.000Z") + c.Assert(res.Rules[1].AbortMultipartUpload, NotNil) + c.Assert(res.Rules[1].AbortMultipartUpload.Days, Equals, 30) + c.Assert(res.Rules[2].ID, Equals, "rule3") + c.Assert(res.Rules[2].AbortMultipartUpload, NotNil) + c.Assert(res.Rules[2].AbortMultipartUpload.Days, Equals, 30) + c.Assert(len(res.Rules[2].Transitions), Equals, 2) + c.Assert(res.Rules[2].Transitions[0].StorageClass, Equals, StorageIA) + c.Assert(res.Rules[2].Transitions[0].Days, Equals, 3) + c.Assert(res.Rules[2].Transitions[1].StorageClass, Equals, StorageArchive) + c.Assert(res.Rules[2].Transitions[1].Days, Equals, 30) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestSetBucketLifecycleAboutVersionObject +func (s *OssClientSuite) TestSetBucketLifecycleAboutVersionObject(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + deleteMark := true + expiration := LifecycleExpiration{ + ExpiredObjectDeleteMarker: &deleteMark, + } + + versionExpiration := LifecycleVersionExpiration{ + NoncurrentDays: 20, + } + + versionTransition := LifecycleVersionTransition{ + NoncurrentDays: 10, + StorageClass: "IA", + } + + rule := LifecycleRule{ + Status: "Enabled", + Expiration: &expiration, + NonVersionExpiration: &versionExpiration, + NonVersionTransition: &versionTransition, + } + rules := []LifecycleRule{rule} + + err = client.SetBucketLifecycle(bucketNameTest, rules) + c.Assert(err, IsNil) + + res, err := client.GetBucketLifecycle(bucketNameTest) + c.Assert(err, IsNil) + + c.Assert(res.Rules[0].Expiration, NotNil) + c.Assert(res.Rules[0].Expiration.Days, Equals, 0) + c.Assert(res.Rules[0].Expiration.Date, Equals, "") + c.Assert(*(res.Rules[0].Expiration.ExpiredObjectDeleteMarker), Equals, true) + + c.Assert(res.Rules[0].NonVersionExpiration.NoncurrentDays, Equals, 20) + c.Assert(res.Rules[0].NonVersionTransition.NoncurrentDays, Equals, 10) + c.Assert(res.Rules[0].NonVersionTransition.StorageClass, Equals, StorageClassType("IA")) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestSetBucketLifecycleAboutVersionObject +func (s *OssClientSuite) TestSetBucketLifecycleAboutVersionObjectError(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + deleteMark := true + expiration := LifecycleExpiration{ + ExpiredObjectDeleteMarker: &deleteMark, + } + + versionExpiration := LifecycleVersionExpiration{ + NoncurrentDays: 20, + } + + versionTransition := LifecycleVersionTransition{ + NoncurrentDays: 10, + StorageClass: "IA", + } + + // NonVersionTransition and NonVersionTransitions can not both have value + rule := LifecycleRule{ + Status: "Enabled", + Expiration: &expiration, + NonVersionExpiration: &versionExpiration, + NonVersionTransition: &versionTransition, + NonVersionTransitions: []LifecycleVersionTransition{versionTransition}, + } + rules := []LifecycleRule{rule} + + err = client.SetBucketLifecycle(bucketNameTest, rules) + c.Assert(err, NotNil) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestSetBucketLifecycleAboutVersionObject +func (s *OssClientSuite) TestSetBucketLifecycleAboutVersionObjectNew(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + deleteMark := true + expiration := LifecycleExpiration{ + ExpiredObjectDeleteMarker: &deleteMark, + } + + versionExpiration := LifecycleVersionExpiration{ + NoncurrentDays: 40, + } + + versionTransition1 := LifecycleVersionTransition{ + NoncurrentDays: 25, + StorageClass: "IA", + } + + versionTransition2 := LifecycleVersionTransition{ + NoncurrentDays: 30, + StorageClass: "ColdArchive", + } + + rule := LifecycleRule{ + Status: "Enabled", + Expiration: &expiration, + NonVersionExpiration: &versionExpiration, + NonVersionTransitions: []LifecycleVersionTransition{versionTransition1, versionTransition2}, + } + rules := []LifecycleRule{rule} + + err = client.SetBucketLifecycle(bucketNameTest, rules) + c.Assert(err, IsNil) + + res, err := client.GetBucketLifecycle(bucketNameTest) + c.Assert(err, IsNil) + + c.Assert(res.Rules[0].Expiration, NotNil) + c.Assert(res.Rules[0].Expiration.Days, Equals, 0) + c.Assert(res.Rules[0].Expiration.Date, Equals, "") + c.Assert(*(res.Rules[0].Expiration.ExpiredObjectDeleteMarker), Equals, true) + + c.Assert(res.Rules[0].NonVersionExpiration.NoncurrentDays, Equals, 40) + c.Assert(res.Rules[0].NonVersionTransition.NoncurrentDays, Equals, 25) + c.Assert(res.Rules[0].NonVersionTransition.StorageClass, Equals, StorageClassType("IA")) + c.Assert(len(res.Rules[0].NonVersionTransitions), Equals, 2) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestDeleteBucketLifecycle +func (s *OssClientSuite) TestDeleteBucketLifecycle(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + + var rule1 = BuildLifecycleRuleByDate("rule1", "one", true, 2015, 11, 11) + var rule2 = BuildLifecycleRuleByDays("rule2", "two", true, 3) + var rules = []LifecycleRule{rule1, rule2} + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + //time.Sleep(timeoutInOperation) + + err = client.DeleteBucketLifecycle(bucketNameTest) + c.Assert(err, IsNil) + + err = client.SetBucketLifecycle(bucketNameTest, rules) + c.Assert(err, IsNil) + //time.Sleep(timeoutInOperation) + + res, err := client.GetBucketLifecycle(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(len(res.Rules), Equals, 2) + + // Delete + err = client.DeleteBucketLifecycle(bucketNameTest) + c.Assert(err, IsNil) + + //time.Sleep(timeoutInOperation) + res, err = client.GetBucketLifecycle(bucketNameTest) + c.Assert(err, NotNil) + + // Eliminate effect of cache + //time.Sleep(timeoutInOperation) + + // Delete when not set + err = client.DeleteBucketLifecycle(bucketNameTest) + c.Assert(err, IsNil) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestSetBucketLifecycleNegative +func (s *OssClientSuite) TestBucketLifecycleNegative(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + var rules = []LifecycleRule{} + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + // Set with no rule + err = client.SetBucketLifecycle(bucketNameTest, rules) + c.Assert(err, NotNil) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) + + // Not exist + err = client.SetBucketLifecycle(bucketNameTest, rules) + c.Assert(err, NotNil) + + // Not exist + _, err = client.GetBucketLifecycle(bucketNameTest) + c.Assert(err, NotNil) + + // Not exist + err = client.DeleteBucketLifecycle(bucketNameTest) + c.Assert(err, NotNil) +} + +// TestSetBucketReferer +func (s *OssClientSuite) TestSetBucketReferer(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + var referers = []string{"http://www.aliyun.com", "https://www.aliyun.com"} + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + res, err := client.GetBucketReferer(bucketNameTest) + c.Assert(res.AllowEmptyReferer, Equals, true) + c.Assert(len(res.RefererList), Equals, 0) + + // Set referers + err = client.SetBucketReferer(bucketNameTest, referers, false) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + res, err = client.GetBucketReferer(bucketNameTest) + c.Assert(res.AllowEmptyReferer, Equals, false) + c.Assert(len(res.RefererList), Equals, 2) + c.Assert(res.RefererList[0], Equals, "http://www.aliyun.com") + c.Assert(res.RefererList[1], Equals, "https://www.aliyun.com") + + // Reset referer, referers empty + referers = []string{""} + err = client.SetBucketReferer(bucketNameTest, referers, true) + c.Assert(err, IsNil) + + referers = []string{} + err = client.SetBucketReferer(bucketNameTest, referers, true) + c.Assert(err, IsNil) + + res, err = client.GetBucketReferer(bucketNameTest) + c.Assert(res.AllowEmptyReferer, Equals, true) + c.Assert(len(res.RefererList), Equals, 0) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestSetBucketRefererNegative +func (s *OssClientSuite) TestBucketRefererNegative(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + var referers = []string{""} + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + // Not exist + _, err = client.GetBucketReferer(bucketNameTest) + c.Assert(err, NotNil) + testLogger.Println(err) + + // Not exist + err = client.SetBucketReferer(bucketNameTest, referers, true) + c.Assert(err, NotNil) + testLogger.Println(err) +} + +// TestSetBucketLogging +func (s *OssClientSuite) TestSetBucketLogging(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + var bucketNameTarget = bucketNameTest + "-target" + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + err = client.CreateBucket(bucketNameTarget) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + // Set logging + err = client.SetBucketLogging(bucketNameTest, bucketNameTarget, "prefix", true) + c.Assert(err, IsNil) + // Reset + err = client.SetBucketLogging(bucketNameTest, bucketNameTarget, "prefix", false) + c.Assert(err, IsNil) + + time.Sleep(timeoutInOperation) + res, err := client.GetBucketLogging(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.LoggingEnabled.TargetBucket, Equals, "") + c.Assert(res.LoggingEnabled.TargetPrefix, Equals, "") + + err = client.DeleteBucketLogging(bucketNameTest) + c.Assert(err, IsNil) + + // Set to self + err = client.SetBucketLogging(bucketNameTest, bucketNameTest, "prefix", true) + c.Assert(err, IsNil) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) + err = client.DeleteBucket(bucketNameTarget) + c.Assert(err, IsNil) +} + +// TestDeleteBucketLogging +func (s *OssClientSuite) TestDeleteBucketLogging(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + var bucketNameTarget = bucketNameTest + "-target" + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + err = client.CreateBucket(bucketNameTarget) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + // Get when not set + res, err := client.GetBucketLogging(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.LoggingEnabled.TargetBucket, Equals, "") + c.Assert(res.LoggingEnabled.TargetPrefix, Equals, "") + + // Set + err = client.SetBucketLogging(bucketNameTest, bucketNameTarget, "prefix", true) + c.Assert(err, IsNil) + + // Get + time.Sleep(timeoutInOperation) + res, err = client.GetBucketLogging(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.LoggingEnabled.TargetBucket, Equals, bucketNameTarget) + c.Assert(res.LoggingEnabled.TargetPrefix, Equals, "prefix") + + // Set + err = client.SetBucketLogging(bucketNameTest, bucketNameTarget, "prefix", false) + c.Assert(err, IsNil) + + // Get + time.Sleep(timeoutInOperation) + res, err = client.GetBucketLogging(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.LoggingEnabled.TargetBucket, Equals, "") + c.Assert(res.LoggingEnabled.TargetPrefix, Equals, "") + + // Delete + err = client.DeleteBucketLogging(bucketNameTest) + c.Assert(err, IsNil) + + // Get after delete + time.Sleep(timeoutInOperation) + res, err = client.GetBucketLogging(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.LoggingEnabled.TargetBucket, Equals, "") + c.Assert(res.LoggingEnabled.TargetPrefix, Equals, "") + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) + err = client.DeleteBucket(bucketNameTarget) + c.Assert(err, IsNil) +} + +// TestSetBucketLoggingNegative +func (s *OssClientSuite) TestSetBucketLoggingNegative(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + var bucketNameTarget = bucketNameTest + "-target" + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + // Not exist + _, err = client.GetBucketLogging(bucketNameTest) + c.Assert(err, NotNil) + + // Not exist + err = client.SetBucketLogging(bucketNameTest, "targetbucket", "prefix", true) + c.Assert(err, NotNil) + + // Not exist + err = client.DeleteBucketLogging(bucketNameTest) + c.Assert(err, NotNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + // Target bucket not exist + err = client.SetBucketLogging(bucketNameTest, bucketNameTarget, "prefix", true) + c.Assert(err, NotNil) + + // Parameter invalid + err = client.SetBucketLogging(bucketNameTest, "XXXX", "prefix", true) + c.Assert(err, NotNil) + + err = client.SetBucketLogging(bucketNameTest, "xx", "prefix", true) + c.Assert(err, NotNil) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestSetBucketWebsite +func (s *OssClientSuite) TestSetBucketWebsite(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + var indexWebsite = "myindex.html" + var errorWebsite = "myerror.html" + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + // Set + err = client.SetBucketWebsite(bucketNameTest, indexWebsite, errorWebsite) + c.Assert(err, IsNil) + + // Double set + err = client.SetBucketWebsite(bucketNameTest, indexWebsite, errorWebsite) + c.Assert(err, IsNil) + + res, err := client.GetBucketWebsite(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.IndexDocument.Suffix, Equals, indexWebsite) + c.Assert(res.ErrorDocument.Key, Equals, errorWebsite) + + // Reset + err = client.SetBucketWebsite(bucketNameTest, "your"+indexWebsite, "your"+errorWebsite) + c.Assert(err, IsNil) + + time.Sleep(timeoutInOperation) + res, err = client.GetBucketWebsite(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.IndexDocument.Suffix, Equals, "your"+indexWebsite) + c.Assert(res.ErrorDocument.Key, Equals, "your"+errorWebsite) + + err = client.DeleteBucketWebsite(bucketNameTest) + c.Assert(err, IsNil) + + // Set after delete + err = client.SetBucketWebsite(bucketNameTest, indexWebsite, errorWebsite) + c.Assert(err, IsNil) + + // Eliminate effect of cache + time.Sleep(timeoutInOperation) + + res, err = client.GetBucketWebsite(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.IndexDocument.Suffix, Equals, indexWebsite) + c.Assert(res.ErrorDocument.Key, Equals, errorWebsite) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestDeleteBucketWebsite +func (s *OssClientSuite) TestDeleteBucketWebsite(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + var indexWebsite = "myindex.html" + var errorWebsite = "myerror.html" + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + // Get + res, err := client.GetBucketWebsite(bucketNameTest) + c.Assert(err, NotNil) + + // Detele without set + err = client.DeleteBucketWebsite(bucketNameTest) + c.Assert(err, IsNil) + + // Set + err = client.SetBucketWebsite(bucketNameTest, indexWebsite, errorWebsite) + c.Assert(err, IsNil) + + time.Sleep(timeoutInOperation) + res, err = client.GetBucketWebsite(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.IndexDocument.Suffix, Equals, indexWebsite) + c.Assert(res.ErrorDocument.Key, Equals, errorWebsite) + + // Detele + time.Sleep(timeoutInOperation) + err = client.DeleteBucketWebsite(bucketNameTest) + c.Assert(err, IsNil) + + time.Sleep(timeoutInOperation) + res, err = client.GetBucketWebsite(bucketNameTest) + c.Assert(err, NotNil) + + // Detele after delete + err = client.DeleteBucketWebsite(bucketNameTest) + c.Assert(err, IsNil) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestSetBucketWebsiteNegative +func (s *OssClientSuite) TestSetBucketWebsiteNegative(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + var indexWebsite = "myindex.html" + var errorWebsite = "myerror.html" + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.DeleteBucket(bucketNameTest) + + // Not exist + _, err = client.GetBucketWebsite(bucketNameTest) + c.Assert(err, NotNil) + + err = client.DeleteBucketWebsite(bucketNameTest) + c.Assert(err, NotNil) + + err = client.SetBucketWebsite(bucketNameTest, indexWebsite, errorWebsite) + c.Assert(err, NotNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + // Set + time.Sleep(timeoutInOperation) + err = client.SetBucketWebsite(bucketNameTest, "myindex", "myerror") + c.Assert(err, IsNil) + + res, err := client.GetBucketWebsite(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.IndexDocument.Suffix, Equals, "myindex") + c.Assert(res.ErrorDocument.Key, Equals, "myerror") + + // Detele + err = client.DeleteBucketWebsite(bucketNameTest) + c.Assert(err, IsNil) + + time.Sleep(timeoutInOperation) + _, err = client.GetBucketWebsite(bucketNameTest) + c.Assert(err, NotNil) + + // Detele after delete + err = client.DeleteBucketWebsite(bucketNameTest) + c.Assert(err, IsNil) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestSetBucketWebsiteDetail +func (s *OssClientSuite) TestSetBucketWebsiteDetail(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + var indexWebsite = "myindex.html" + var errorWebsite = "myerror.html" + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + btrue := true + bfalse := false + // Define one routing rule + ruleOk := RoutingRule{ + RuleNumber: 1, + Condition: Condition{ + KeyPrefixEquals: "", + HTTPErrorCodeReturnedEquals: 404, + }, + Redirect: Redirect{ + RedirectType: "Mirror", + // PassQueryString: &btrue, // set default value + MirrorURL: "http://www.test.com/", + // MirrorPassQueryString:&btrue, // set default value + // MirrorFollowRedirect:&bfalse, // set default value + // MirrorCheckMd5:&bfalse, // set default value + MirrorHeaders: MirrorHeaders{ + // PassAll:&bfalse, // set default value + Pass: []string{"myheader-key1", "myheader-key2"}, + Remove: []string{"myheader-key3", "myheader-key4"}, + Set: []MirrorHeaderSet{ + MirrorHeaderSet{ + Key: "myheader-key5", + Value: "myheader-value5", + }, + }, + }, + }, + } + + // Define array routing rule + ruleArrOk := []RoutingRule{ + RoutingRule{ + RuleNumber: 2, + Condition: Condition{ + KeyPrefixEquals: "abc/", + HTTPErrorCodeReturnedEquals: 404, + IncludeHeader: []IncludeHeader{ + IncludeHeader{ + Key: "host", + Equals: "test.oss-cn-beijing-internal.aliyuncs.com", + }, + }, + }, + Redirect: Redirect{ + RedirectType: "AliCDN", + Protocol: "http", + HostName: "www.test.com", + PassQueryString: &bfalse, + ReplaceKeyWith: "prefix/${key}.suffix", + HttpRedirectCode: 301, + }, + }, + RoutingRule{ + RuleNumber: 3, + Condition: Condition{ + KeyPrefixEquals: "", + HTTPErrorCodeReturnedEquals: 404, + }, + Redirect: Redirect{ + RedirectType: "Mirror", + PassQueryString: &btrue, + MirrorURL: "http://www.test.com/", + MirrorPassQueryString: &btrue, + MirrorFollowRedirect: &bfalse, + MirrorCheckMd5: &bfalse, + MirrorHeaders: MirrorHeaders{ + PassAll: &btrue, + Pass: []string{"myheader-key1", "myheader-key2"}, + Remove: []string{"myheader-key3", "myheader-key4"}, + Set: []MirrorHeaderSet{ + MirrorHeaderSet{ + Key: "myheader-key5", + Value: "myheader-value5", + }, + }, + }, + }, + }, + } + + // Set one routing rule + wxmlOne := WebsiteXML{} + wxmlOne.RoutingRules = append(wxmlOne.RoutingRules, ruleOk) + var responseHeader http.Header + err = client.SetBucketWebsiteDetail(bucketNameTest, wxmlOne, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + requestId := GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + res, err := client.GetBucketWebsite(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.RoutingRules[0].Redirect.RedirectType, Equals, "Mirror") + c.Assert(*res.RoutingRules[0].Redirect.PassQueryString, Equals, false) + c.Assert(*res.RoutingRules[0].Redirect.MirrorPassQueryString, Equals, false) + c.Assert(*res.RoutingRules[0].Redirect.MirrorFollowRedirect, Equals, true) + c.Assert(*res.RoutingRules[0].Redirect.MirrorCheckMd5, Equals, false) + c.Assert(*res.RoutingRules[0].Redirect.MirrorHeaders.PassAll, Equals, false) + + // Set one routing rule and IndexDocument, IndexDocument + wxml := WebsiteXML{ + IndexDocument: IndexDocument{Suffix: indexWebsite}, + ErrorDocument: ErrorDocument{Key: errorWebsite}, + } + wxml.RoutingRules = append(wxml.RoutingRules, ruleOk) + err = client.SetBucketWebsiteDetail(bucketNameTest, wxml, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + requestId = GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + res, err = client.GetBucketWebsite(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.IndexDocument.Suffix, Equals, indexWebsite) + c.Assert(res.ErrorDocument.Key, Equals, errorWebsite) + c.Assert(res.RoutingRules[0].Redirect.RedirectType, Equals, "Mirror") + + // Set array routing rule + wxml.RoutingRules = append(wxml.RoutingRules, ruleArrOk...) + err = client.SetBucketWebsiteDetail(bucketNameTest, wxml, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + requestId = GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + res, err = client.GetBucketWebsite(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.IndexDocument.Suffix, Equals, indexWebsite) + c.Assert(res.ErrorDocument.Key, Equals, errorWebsite) + c.Assert(len(res.RoutingRules), Equals, 3) + c.Assert(res.RoutingRules[1].Redirect.RedirectType, Equals, "AliCDN") + c.Assert(*res.RoutingRules[2].Redirect.MirrorPassQueryString, Equals, true) + c.Assert(*res.RoutingRules[2].Redirect.MirrorFollowRedirect, Equals, false) + + // Define one error routing rule + ruleErr := RoutingRule{ + RuleNumber: 1, + Redirect: Redirect{ + RedirectType: "Mirror", + PassQueryString: &btrue, + }, + } + // Define array error routing rule + rulesErrArr := []RoutingRule{ + RoutingRule{ + RuleNumber: 1, + Redirect: Redirect{ + RedirectType: "Mirror", + PassQueryString: &btrue, + }, + }, + RoutingRule{ + RuleNumber: 2, + Redirect: Redirect{ + RedirectType: "Mirror", + PassQueryString: &btrue, + }, + }, + } + + ruleIntErr := RoutingRule{ + // RuleNumber:0, // set NULL value + Condition: Condition{ + KeyPrefixEquals: "", + HTTPErrorCodeReturnedEquals: 404, + }, + Redirect: Redirect{ + RedirectType: "Mirror", + // PassQueryString: &btrue, // set default value + MirrorURL: "http://www.test.com/", + // MirrorPassQueryString:&btrue, // set default value + // MirrorFollowRedirect:&bfalse, // set default value + // MirrorCheckMd5:&bfalse, // set default value + MirrorHeaders: MirrorHeaders{ + // PassAll:&bfalse, // set default value + Pass: []string{"myheader-key1", "myheader-key2"}, + Remove: []string{"myheader-key3", "myheader-key4"}, + Set: []MirrorHeaderSet{ + MirrorHeaderSet{ + Key: "myheader-key5", + Value: "myheader-value5", + }, + }, + }, + }, + } + + // Set one int type error rule + wxmlIntErr := WebsiteXML{} + wxmlIntErr.RoutingRules = append(wxmlIntErr.RoutingRules, ruleIntErr) + err = client.SetBucketWebsiteDetail(bucketNameTest, wxmlIntErr) + c.Assert(err, NotNil) + + // Set one error rule + wxmlErr := WebsiteXML{} + wxmlErr.RoutingRules = append(wxmlErr.RoutingRules, ruleErr) + err = client.SetBucketWebsiteDetail(bucketNameTest, wxmlErr) + c.Assert(err, NotNil) + + // Set one error rule and one correct rule + wxmlErr.RoutingRules = append(wxmlErr.RoutingRules, ruleOk) + err = client.SetBucketWebsiteDetail(bucketNameTest, wxmlErr) + c.Assert(err, NotNil) + + wxmlErrRuleArr := WebsiteXML{} + wxmlErrRuleArr.RoutingRules = append(wxmlErrRuleArr.RoutingRules, rulesErrArr...) + // Set array error routing rule + err = client.SetBucketWebsiteDetail(bucketNameTest, wxmlErrRuleArr) + c.Assert(err, NotNil) + + err = client.DeleteBucketWebsite(bucketNameTest) + c.Assert(err, IsNil) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestSetBucketWebsiteXml +func (s *OssClientSuite) TestSetBucketWebsiteXml(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + // Define one routing rule + ruleOk := RoutingRule{ + RuleNumber: 1, + Condition: Condition{ + KeyPrefixEquals: "", + HTTPErrorCodeReturnedEquals: 404, + }, + Redirect: Redirect{ + RedirectType: "Mirror", + // PassQueryString: &btrue, // set default value + MirrorURL: "http://www.test.com/", + // MirrorPassQueryString:&btrue, // set default value + // MirrorFollowRedirect:&bfalse, // set default value + // MirrorCheckMd5:&bfalse, // set default value + MirrorHeaders: MirrorHeaders{ + // PassAll:&bfalse, // set default value + Pass: []string{"myheader-key1", "myheader-key2"}, + Remove: []string{"myheader-key3", "myheader-key4"}, + Set: []MirrorHeaderSet{ + MirrorHeaderSet{ + Key: "myheader-key5", + Value: "myheader-value5", + }, + }, + }, + }, + } + + // Set one routing rule + wxmlOne := WebsiteXML{} + wxmlOne.RoutingRules = append(wxmlOne.RoutingRules, ruleOk) + bs, err := xml.Marshal(wxmlOne) + c.Assert(err, IsNil) + + var responseHeader http.Header + err = client.SetBucketWebsiteXml(bucketNameTest, string(bs), GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + + requestId := GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + res, err := client.GetBucketWebsite(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.RoutingRules[0].Redirect.RedirectType, Equals, "Mirror") + c.Assert(*res.RoutingRules[0].Redirect.PassQueryString, Equals, false) + c.Assert(*res.RoutingRules[0].Redirect.MirrorPassQueryString, Equals, false) + c.Assert(*res.RoutingRules[0].Redirect.MirrorFollowRedirect, Equals, true) + c.Assert(*res.RoutingRules[0].Redirect.MirrorCheckMd5, Equals, false) + c.Assert(*res.RoutingRules[0].Redirect.MirrorHeaders.PassAll, Equals, false) +} + +// TestSetBucketCORS +func (s *OssClientSuite) TestSetBucketCORS(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + var rule1 = CORSRule{ + AllowedOrigin: []string{"*"}, + AllowedMethod: []string{"PUT", "GET", "POST"}, + AllowedHeader: []string{}, + ExposeHeader: []string{}, + MaxAgeSeconds: 100, + } + + var rule2 = CORSRule{ + AllowedOrigin: []string{"http://www.a.com", "http://www.b.com"}, + AllowedMethod: []string{"GET"}, + AllowedHeader: []string{"Authorization"}, + ExposeHeader: []string{"x-oss-test", "x-oss-test1"}, + MaxAgeSeconds: 200, + } + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + // Set + err = client.SetBucketCORS(bucketNameTest, []CORSRule{rule1}) + c.Assert(err, IsNil) + + gbcr, err := client.GetBucketCORS(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(len(gbcr.CORSRules), Equals, 1) + c.Assert(len(gbcr.CORSRules[0].AllowedOrigin), Equals, 1) + c.Assert(len(gbcr.CORSRules[0].AllowedMethod), Equals, 3) + c.Assert(len(gbcr.CORSRules[0].AllowedHeader), Equals, 0) + c.Assert(len(gbcr.CORSRules[0].ExposeHeader), Equals, 0) + c.Assert(gbcr.CORSRules[0].MaxAgeSeconds, Equals, 100) + + // Double set + err = client.SetBucketCORS(bucketNameTest, []CORSRule{rule1}) + c.Assert(err, IsNil) + + gbcr, err = client.GetBucketCORS(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(len(gbcr.CORSRules), Equals, 1) + c.Assert(len(gbcr.CORSRules[0].AllowedOrigin), Equals, 1) + c.Assert(len(gbcr.CORSRules[0].AllowedMethod), Equals, 3) + c.Assert(len(gbcr.CORSRules[0].AllowedHeader), Equals, 0) + c.Assert(len(gbcr.CORSRules[0].ExposeHeader), Equals, 0) + c.Assert(gbcr.CORSRules[0].MaxAgeSeconds, Equals, 100) + + // Set rule2 + err = client.SetBucketCORS(bucketNameTest, []CORSRule{rule2}) + c.Assert(err, IsNil) + + time.Sleep(timeoutInOperation) + gbcr, err = client.GetBucketCORS(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(len(gbcr.CORSRules), Equals, 1) + c.Assert(len(gbcr.CORSRules[0].AllowedOrigin), Equals, 2) + c.Assert(len(gbcr.CORSRules[0].AllowedMethod), Equals, 1) + c.Assert(len(gbcr.CORSRules[0].AllowedHeader), Equals, 1) + c.Assert(len(gbcr.CORSRules[0].ExposeHeader), Equals, 2) + c.Assert(gbcr.CORSRules[0].MaxAgeSeconds, Equals, 200) + + // Reset + err = client.SetBucketCORS(bucketNameTest, []CORSRule{rule1, rule2}) + c.Assert(err, IsNil) + + time.Sleep(timeoutInOperation) + gbcr, err = client.GetBucketCORS(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(len(gbcr.CORSRules), Equals, 2) + + // Set after delete + err = client.DeleteBucketCORS(bucketNameTest) + c.Assert(err, IsNil) + + err = client.SetBucketCORS(bucketNameTest, []CORSRule{rule1, rule2}) + c.Assert(err, IsNil) + + time.Sleep(timeoutInOperation) + gbcr, err = client.GetBucketCORS(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(len(gbcr.CORSRules), Equals, 2) + + err = client.DeleteBucketCORS(bucketNameTest) + c.Assert(err, IsNil) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestDeleteBucketCORS +func (s *OssClientSuite) TestDeleteBucketCORS(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + var rule = CORSRule{ + AllowedOrigin: []string{"*"}, + AllowedMethod: []string{"PUT", "GET", "POST"}, + AllowedHeader: []string{}, + ExposeHeader: []string{}, + MaxAgeSeconds: 100, + } + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + // Delete not set + err = client.DeleteBucketCORS(bucketNameTest) + c.Assert(err, IsNil) + + // Set + err = client.SetBucketCORS(bucketNameTest, []CORSRule{rule}) + c.Assert(err, IsNil) + + time.Sleep(timeoutInOperation) + _, err = client.GetBucketCORS(bucketNameTest) + c.Assert(err, IsNil) + + // Detele + err = client.DeleteBucketCORS(bucketNameTest) + c.Assert(err, IsNil) + + time.Sleep(timeoutInOperation) + _, err = client.GetBucketCORS(bucketNameTest) + c.Assert(err, NotNil) + + // Detele after deleting + err = client.DeleteBucketCORS(bucketNameTest) + c.Assert(err, IsNil) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestSetBucketCORSNegative +func (s *OssClientSuite) TestSetBucketCORSNegative(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + var rule = CORSRule{ + AllowedOrigin: []string{"*"}, + AllowedMethod: []string{"PUT", "GET", "POST"}, + AllowedHeader: []string{}, + ExposeHeader: []string{}, + MaxAgeSeconds: 100, + } + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.DeleteBucket(bucketNameTest) + + // Not exist + _, err = client.GetBucketCORS(bucketNameTest) + c.Assert(err, NotNil) + + err = client.DeleteBucketCORS(bucketNameTest) + c.Assert(err, NotNil) + + err = client.SetBucketCORS(bucketNameTest, []CORSRule{rule}) + c.Assert(err, NotNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + _, err = client.GetBucketCORS(bucketNameTest) + c.Assert(err, NotNil) + + // Set + err = client.SetBucketCORS(bucketNameTest, []CORSRule{rule}) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + _, err = client.GetBucketCORS(bucketNameTest) + c.Assert(err, IsNil) + + // Delete + err = client.DeleteBucketCORS(bucketNameTest) + c.Assert(err, IsNil) + + time.Sleep(timeoutInOperation) + _, err = client.GetBucketCORS(bucketNameTest) + c.Assert(err, NotNil) + + // Delete after deleting + err = client.DeleteBucketCORS(bucketNameTest) + c.Assert(err, IsNil) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestGetBucketInfo +func (s *OssClientSuite) TestGetBucketInfo(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + res, err := client.GetBucketInfo(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.BucketInfo.Name, Equals, bucketNameTest) + c.Assert(strings.HasPrefix(res.BucketInfo.Location, "oss-"), Equals, true) + c.Assert(res.BucketInfo.ACL, Equals, "private") + c.Assert(strings.HasSuffix(res.BucketInfo.ExtranetEndpoint, ".com"), Equals, true) + c.Assert(strings.HasSuffix(res.BucketInfo.IntranetEndpoint, ".com"), Equals, true) + c.Assert(res.BucketInfo.CreationDate, NotNil) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestGetBucketInfoNegative +func (s *OssClientSuite) TestGetBucketInfoNegative(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + // Not exist + _, err = client.GetBucketInfo(bucketNameTest) + c.Assert(err, NotNil) + + // Bucket name invalid + _, err = client.GetBucketInfo("InvalidBucketName_") + c.Assert(err, NotNil) +} + +// TestEndpointFormat +func (s *OssClientSuite) TestEndpointFormat(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + + // http://host + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + res, err := client.GetBucketACL(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.ACL, Equals, string(ACLPrivate)) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) + time.Sleep(timeoutInOperation) + + // http://host:port + client, err = New(endpoint+":80", accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + time.Sleep(timeoutInOperation) + res, err = client.GetBucketACL(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.ACL, Equals, string(ACLPrivate)) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestCname +func (s *OssClientSuite) _TestCname(c *C) { + var bucketNameTest = "" + + client, err := New("", "", "", UseCname(true)) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + _, err = client.ListBuckets() + c.Assert(err, NotNil) + + res, err := client.GetBucketACL(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.ACL, Equals, string(ACLPrivate)) +} + +// TestCnameNegative +func (s *OssClientSuite) _TestCnameNegative(c *C) { + var bucketNameTest = "" + + client, err := New("", "", "", UseCname(true)) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, NotNil) + + _, err = client.ListBuckets() + c.Assert(err, NotNil) + + _, err = client.GetBucketACL(bucketNameTest) + c.Assert(err, NotNil) +} + +// _TestHTTPS +func (s *OssClientSuite) _TestHTTPS(c *C) { + var bucketNameTest = "" + + client, err := New("", "", "") + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + res, err := client.GetBucketACL(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(res.ACL, Equals, string(ACLPrivate)) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestClientOption +func (s *OssClientSuite) TestClientOption(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + + client, err := New(endpoint, accessID, accessKey, UseCname(true), + Timeout(11, 12), SecurityToken("token"), Proxy(proxyHost)) + c.Assert(err, IsNil) + + // CreateBucket timeout + err = client.CreateBucket(bucketNameTest) + c.Assert(err, NotNil) + + c.Assert(client.Conn.config.HTTPTimeout.ConnectTimeout, Equals, time.Second*11) + c.Assert(client.Conn.config.HTTPTimeout.ReadWriteTimeout, Equals, time.Second*12) + c.Assert(client.Conn.config.HTTPTimeout.HeaderTimeout, Equals, time.Second*12) + c.Assert(client.Conn.config.HTTPTimeout.IdleConnTimeout, Equals, time.Second*12) + c.Assert(client.Conn.config.HTTPTimeout.LongTimeout, Equals, time.Second*12*10) + + c.Assert(client.Conn.config.SecurityToken, Equals, "token") + c.Assert(client.Conn.config.IsCname, Equals, true) + + c.Assert(client.Conn.config.IsUseProxy, Equals, true) + c.Assert(client.Config.ProxyHost, Equals, proxyHost) + + client, err = New(endpoint, accessID, accessKey, AuthProxy(proxyHost, proxyUser, proxyPasswd)) + + c.Assert(client.Conn.config.IsUseProxy, Equals, true) + c.Assert(client.Config.ProxyHost, Equals, proxyHost) + c.Assert(client.Conn.config.IsAuthProxy, Equals, true) + c.Assert(client.Conn.config.ProxyUser, Equals, proxyUser) + c.Assert(client.Conn.config.ProxyPassword, Equals, proxyPasswd) + + client, err = New(endpoint, accessID, accessKey, UserAgent("go sdk user agent")) + c.Assert(client.Conn.config.UserAgent, Equals, "go sdk user agent") + + // Check we can overide the http.Client + httpClient := new(http.Client) + client, err = New(endpoint, accessID, accessKey, HTTPClient(httpClient)) + c.Assert(client.HTTPClient, Equals, httpClient) + c.Assert(client.Conn.client, Equals, httpClient) + client, err = New(endpoint, accessID, accessKey) + c.Assert(client.HTTPClient, IsNil) +} + +// TestProxy +func (s *OssClientSuite) ProxyTestFunc(c *C, authVersion AuthVersionType, extraHeaders []string) { + bucketNameTest := bucketNamePrefix + RandLowStr(6) + objectName := "体育/奥运/首金" + objectValue := "大江东去,浪淘尽,千古风流人物。 故垒西边,人道是、三国周郎赤壁。" + + client, err := New(endpoint, accessID, accessKey, AuthProxy(proxyHost, proxyUser, proxyPasswd)) + + oldType := client.Config.AuthVersion + oldHeaders := client.Config.AdditionalHeaders + client.Config.AuthVersion = authVersion + client.Config.AdditionalHeaders = extraHeaders + + // Create bucket + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + // Get bucket info + _, err = client.GetBucketInfo(bucketNameTest) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketNameTest) + + // Sign URL + str, err := bucket.SignURL(objectName, HTTPPut, 60) + c.Assert(err, IsNil) + if bucket.Client.Config.AuthVersion == AuthV1 { + c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true) + } else { + c.Assert(strings.Contains(str, HTTPParamSignatureVersion+"=OSS2"), Equals, true) + c.Assert(strings.Contains(str, HTTPParamExpiresV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyIDV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignatureV2+"="), Equals, true) + } + + // Put object with URL + err = bucket.PutObjectWithURL(str, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Sign URL for get object + str, err = bucket.SignURL(objectName, HTTPGet, 60) + c.Assert(err, IsNil) + if bucket.Client.Config.AuthVersion == AuthV1 { + c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true) + } else { + c.Assert(strings.Contains(str, HTTPParamSignatureVersion+"=OSS2"), Equals, true) + c.Assert(strings.Contains(str, HTTPParamExpiresV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyIDV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignatureV2+"="), Equals, true) + } + + // Get object with URL + body, err := bucket.GetObjectWithURL(str) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + // Put object + err = bucket.PutObject(objectName, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Get object + _, err = bucket.GetObject(objectName) + c.Assert(err, IsNil) + + // List objects + _, err = bucket.ListObjects() + c.Assert(err, IsNil) + + // Delete object + err = bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Delete bucket + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) + + client.Config.AuthVersion = oldType + client.Config.AdditionalHeaders = oldHeaders +} + +func (s *OssClientSuite) TestProxy(c *C) { + s.ProxyTestFunc(c, AuthV1, []string{}) + s.ProxyTestFunc(c, AuthV2, []string{}) + s.ProxyTestFunc(c, AuthV2, []string{"host", "range", "user-agent"}) +} + +// TestProxy for https endpoint +func (s *OssClientSuite) TestHttpsEndpointProxy(c *C) { + bucketNameTest := bucketNamePrefix + RandLowStr(6) + objectName := objectNamePrefix + RandLowStr(6) + objectValue := RandLowStr(100) + + httpsEndPoint := "" + if strings.HasPrefix(endpoint, "http://") { + httpsEndPoint = strings.Replace(endpoint, "http://", "https://", 1) + } else if !strings.HasPrefix(endpoint, "https://") { + httpsEndPoint = "https://" + endpoint + } else { + httpsEndPoint = endpoint + } + + client, err := New(httpsEndPoint, accessID, accessKey, AuthProxy(proxyHost, proxyUser, proxyPasswd)) + + // Create bucket + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketNameTest) + + // Put object + err = bucket.PutObject(objectName, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Get object + _, err = bucket.GetObject(objectName) + c.Assert(err, IsNil) + + // List objects + _, err = bucket.ListObjects() + c.Assert(err, IsNil) + + // Delete object + err = bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Delete bucket + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// Private +func (s *OssClientSuite) checkBucket(buckets []BucketProperties, bucket string) bool { + for _, v := range buckets { + if v.Name == bucket { + return true + } + } + return false +} + +func (s *OssClientSuite) getBucket(buckets []BucketProperties, bucket string) (bool, BucketProperties) { + for _, v := range buckets { + if v.Name == bucket { + return true, v + } + } + return false, BucketProperties{} +} + +func (s *OssClientSuite) TestHttpLogNotSignUrl(c *C) { + logName := "." + string(os.PathSeparator) + "test-go-sdk-httpdebug.log" + RandStr(5) + f, err := os.OpenFile(logName, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0660) + c.Assert(err, IsNil) + + client, err := New(endpoint, accessID, accessKey) + client.Config.LogLevel = Debug + + client.Config.Logger = log.New(f, "", log.LstdFlags) + + var testBucketName = bucketNamePrefix + RandLowStr(6) + + // CreateBucket + err = client.CreateBucket(testBucketName) + f.Close() + + // read log file,get http info + contents, err := ioutil.ReadFile(logName) + c.Assert(err, IsNil) + + httpContent := string(contents) + //fmt.Println(httpContent) + + c.Assert(strings.Contains(httpContent, "signStr"), Equals, true) + c.Assert(strings.Contains(httpContent, "Method:"), Equals, true) + + // delete test bucket and log + os.Remove(logName) + client.DeleteBucket(testBucketName) +} + +func (s *OssClientSuite) HttpLogSignUrlTestFunc(c *C, authVersion AuthVersionType, extraHeaders []string) { + logName := "." + string(os.PathSeparator) + "test-go-sdk-httpdebug-signurl.log" + RandStr(5) + f, err := os.OpenFile(logName, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0660) + c.Assert(err, IsNil) + + client, err := New(endpoint, accessID, accessKey) + client.Config.LogLevel = Debug + client.Config.Logger = log.New(f, "", log.LstdFlags) + + oldType := client.Config.AuthVersion + oldHeaders := client.Config.AdditionalHeaders + client.Config.AuthVersion = authVersion + client.Config.AdditionalHeaders = extraHeaders + + var testBucketName = bucketNamePrefix + RandLowStr(6) + + // CreateBucket + err = client.CreateBucket(testBucketName) + f.Close() + + // clear log + f, err = os.OpenFile(logName, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0660) + client.Config.Logger = log.New(f, "", log.LstdFlags) + + bucket, _ := client.Bucket(testBucketName) + objectName := objectNamePrefix + RandStr(8) + objectValue := RandStr(20) + + // Sign URL for put + str, err := bucket.SignURL(objectName, HTTPPut, 60) + c.Assert(err, IsNil) + if bucket.Client.Config.AuthVersion == AuthV1 { + c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true) + } else { + c.Assert(strings.Contains(str, HTTPParamSignatureVersion+"=OSS2"), Equals, true) + c.Assert(strings.Contains(str, HTTPParamExpiresV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyIDV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignatureV2+"="), Equals, true) + } + + // Error put object with URL + err = bucket.PutObjectWithURL(str, strings.NewReader(objectValue), ContentType("image/tiff")) + f.Close() + + // read log file,get http info + contents, err := ioutil.ReadFile(logName) + c.Assert(err, IsNil) + + httpContent := string(contents) + //fmt.Println(httpContent) + + c.Assert(strings.Contains(httpContent, "signStr"), Equals, true) + c.Assert(strings.Contains(httpContent, "Method:"), Equals, true) + + // delete test bucket and log + os.Remove(logName) + client.DeleteBucket(testBucketName) + + client.Config.AuthVersion = oldType + client.Config.AdditionalHeaders = oldHeaders +} + +func (s *OssClientSuite) TestHttpLogSignUrl(c *C) { + s.HttpLogSignUrlTestFunc(c, AuthV1, []string{}) + s.HttpLogSignUrlTestFunc(c, AuthV2, []string{}) + s.HttpLogSignUrlTestFunc(c, AuthV2, []string{"host", "range", "user-agent"}) +} + +func (s *OssClientSuite) TestSetLimitUploadSpeed(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.LimitUploadSpeed(100) + + goVersion := runtime.Version() + pSlice := strings.Split(strings.ToLower(goVersion), ".") + + // compare with go1.7 + if len(pSlice) >= 2 { + if pSlice[0] > "go1" { + c.Assert(err, IsNil) + } else if pSlice[0] == "go1" { + subVersion, _ := strconv.Atoi(pSlice[1]) + if subVersion >= 7 { + c.Assert(err, IsNil) + } else { + c.Assert(err, NotNil) + } + } else { + c.Assert(err, NotNil) + } + } +} + +func (s *OssClientSuite) TestBucketEncyptionError(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(5) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // SetBucketEncryption:AES256 ,"123" + encryptionRule := ServerEncryptionRule{} + encryptionRule.SSEDefault.SSEAlgorithm = string(AESAlgorithm) + encryptionRule.SSEDefault.KMSMasterKeyID = "123" + + var responseHeader http.Header + err = client.SetBucketEncryption(bucketName, encryptionRule, GetResponseHeader(&responseHeader)) + c.Assert(err, NotNil) + requestId := GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + // GetBucketEncryption + _, err = client.GetBucketEncryption(bucketName, GetResponseHeader(&responseHeader)) + c.Assert(err, NotNil) + requestId = GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + // Get default bucket info + bucketResult, err := client.GetBucketInfo(bucketName) + c.Assert(err, IsNil) + + c.Assert(bucketResult.BucketInfo.SseRule.SSEAlgorithm, Equals, "") + c.Assert(bucketResult.BucketInfo.SseRule.KMSMasterKeyID, Equals, "") + c.Assert(bucketResult.BucketInfo.Versioning, Equals, "") + + err = client.DeleteBucket(bucketName) + c.Assert(err, IsNil) +} + +func (s *OssClientSuite) TestBucketEncryptionPutAndGetAndDelete(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(5) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // SetBucketEncryption:KMS ,"" + encryptionRule := ServerEncryptionRule{} + encryptionRule.SSEDefault.SSEAlgorithm = string(KMSAlgorithm) + + var responseHeader http.Header + err = client.SetBucketEncryption(bucketName, encryptionRule, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + requestId := GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + // GetBucketEncryption + getResult, err := client.GetBucketEncryption(bucketName, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + requestId = GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + // check encryption value + c.Assert(encryptionRule.SSEDefault.SSEAlgorithm, Equals, getResult.SSEDefault.SSEAlgorithm) + c.Assert(encryptionRule.SSEDefault.KMSMasterKeyID, Equals, getResult.SSEDefault.KMSMasterKeyID) + + // delete bucket encyption + err = client.DeleteBucketEncryption(bucketName, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + requestId = GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + // GetBucketEncryption failure + _, err = client.GetBucketEncryption(bucketName, GetResponseHeader(&responseHeader)) + c.Assert(err, NotNil) + requestId = GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + // Get default bucket info + bucketResult, err := client.GetBucketInfo(bucketName) + c.Assert(err, IsNil) + + c.Assert(bucketResult.BucketInfo.SseRule.SSEAlgorithm, Equals, "") + c.Assert(bucketResult.BucketInfo.SseRule.KMSMasterKeyID, Equals, "") + c.Assert(bucketResult.BucketInfo.Versioning, Equals, "") + + err = client.DeleteBucket(bucketName) + c.Assert(err, IsNil) +} + +func (s *OssClientSuite) TestBucketEncryptionWithSm4(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(5) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // SetBucketEncryption:SM4 ,"" + encryptionRule := ServerEncryptionRule{} + encryptionRule.SSEDefault.SSEAlgorithm = string(SM4Algorithm) + + var responseHeader http.Header + err = client.SetBucketEncryption(bucketName, encryptionRule, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + requestId := GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + // GetBucketEncryption + getResult, err := client.GetBucketEncryption(bucketName, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + requestId = GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + // check encryption value + c.Assert(getResult.SSEDefault.SSEAlgorithm, Equals, string(SM4Algorithm)) + c.Assert(getResult.SSEDefault.KMSMasterKeyID, Equals, "") + c.Assert(getResult.SSEDefault.KMSDataEncryption, Equals, "") + + // Get default bucket info + bucketResult, err := client.GetBucketInfo(bucketName) + c.Assert(err, IsNil) + + c.Assert(bucketResult.BucketInfo.SseRule.SSEAlgorithm, Equals, string(SM4Algorithm)) + c.Assert(bucketResult.BucketInfo.SseRule.KMSMasterKeyID, Equals, "") + c.Assert(bucketResult.BucketInfo.SseRule.KMSDataEncryption, Equals, "") + + err = client.DeleteBucket(bucketName) + c.Assert(err, IsNil) +} + +func (s *OssClientSuite) TestBucketEncryptionWithKmsSm4(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(5) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // SetBucketEncryption:SM4 ,"" + encryptionRule := ServerEncryptionRule{} + encryptionRule.SSEDefault.SSEAlgorithm = string(KMSAlgorithm) + encryptionRule.SSEDefault.KMSDataEncryption = string(SM4Algorithm) + + var responseHeader http.Header + err = client.SetBucketEncryption(bucketName, encryptionRule, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + requestId := GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + // GetBucketEncryption + getResult, err := client.GetBucketEncryption(bucketName, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + requestId = GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + // check encryption value + c.Assert(getResult.SSEDefault.SSEAlgorithm, Equals, string(KMSAlgorithm)) + c.Assert(getResult.SSEDefault.KMSMasterKeyID, Equals, "") + c.Assert(getResult.SSEDefault.KMSDataEncryption, Equals, string(SM4Algorithm)) + + // Get default bucket info + bucketResult, err := client.GetBucketInfo(bucketName) + c.Assert(err, IsNil) + + c.Assert(bucketResult.BucketInfo.SseRule.SSEAlgorithm, Equals, string(KMSAlgorithm)) + c.Assert(bucketResult.BucketInfo.SseRule.KMSMasterKeyID, Equals, "") + c.Assert(bucketResult.BucketInfo.SseRule.KMSDataEncryption, Equals, string(SM4Algorithm)) + + err = client.DeleteBucket(bucketName) + c.Assert(err, IsNil) +} + +func (s *OssClientSuite) TestBucketEncyptionPutObjectSuccess(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(5) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // SetBucketEncryption:KMS ,"" + encryptionRule := ServerEncryptionRule{} + encryptionRule.SSEDefault.SSEAlgorithm = string(KMSAlgorithm) + + var responseHeader http.Header + err = client.SetBucketEncryption(bucketName, encryptionRule, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + requestId := GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + // GetBucketEncryption + getResult, err := client.GetBucketEncryption(bucketName, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + requestId = GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + // check encryption value + c.Assert(encryptionRule.SSEDefault.SSEAlgorithm, Equals, getResult.SSEDefault.SSEAlgorithm) + c.Assert(encryptionRule.SSEDefault.KMSMasterKeyID, Equals, getResult.SSEDefault.KMSMasterKeyID) + + // Get default bucket info + bucketResult, err := client.GetBucketInfo(bucketName) + c.Assert(err, IsNil) + + c.Assert(bucketResult.BucketInfo.SseRule.SSEAlgorithm, Equals, "KMS") + c.Assert(bucketResult.BucketInfo.SseRule.KMSMasterKeyID, Equals, "") + c.Assert(bucketResult.BucketInfo.Versioning, Equals, "") + + err = client.DeleteBucket(bucketName) + c.Assert(err, IsNil) +} + +func (s *OssClientSuite) TestBucketEncyptionPutObjectError(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(5) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // SetBucketEncryption:KMS ,"" + encryptionRule := ServerEncryptionRule{} + encryptionRule.SSEDefault.SSEAlgorithm = string(KMSAlgorithm) + kmsId := "123" + encryptionRule.SSEDefault.KMSMasterKeyID = kmsId + + var responseHeader http.Header + err = client.SetBucketEncryption(bucketName, encryptionRule, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + requestId := GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + // GetBucketEncryption + getResult, err := client.GetBucketEncryption(bucketName, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + requestId = GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + // check encryption value + c.Assert(encryptionRule.SSEDefault.SSEAlgorithm, Equals, getResult.SSEDefault.SSEAlgorithm) + c.Assert(encryptionRule.SSEDefault.KMSMasterKeyID, Equals, getResult.SSEDefault.KMSMasterKeyID) + + // Get default bucket info + bucketResult, err := client.GetBucketInfo(bucketName) + c.Assert(err, IsNil) + + c.Assert(bucketResult.BucketInfo.SseRule.SSEAlgorithm, Equals, "KMS") + c.Assert(bucketResult.BucketInfo.SseRule.KMSMasterKeyID, Equals, kmsId) + c.Assert(bucketResult.BucketInfo.Versioning, Equals, "") + + // put and get object failure + bucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + + // put object failure + objectName := objectNamePrefix + RandStr(8) + context := RandStr(100) + err = bucket.PutObject(objectName, strings.NewReader(context)) + c.Assert(err, NotNil) + + err = client.DeleteBucket(bucketName) + c.Assert(err, IsNil) +} + +func (s *OssClientSuite) TestBucketTaggingOperation(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(5) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + var respHeader http.Header + + // Bucket Tagging + var tagging Tagging + tagging.Tags = []Tag{Tag{Key: "testkey2", Value: "testvalue2"}} + err = client.SetBucketTagging(bucketName, tagging, GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + c.Assert(GetRequestId(respHeader) != "", Equals, true) + + getResult, err := client.GetBucketTagging(bucketName) + c.Assert(err, IsNil) + c.Assert(getResult.Tags[0].Key, Equals, tagging.Tags[0].Key) + c.Assert(getResult.Tags[0].Value, Equals, tagging.Tags[0].Value) + + // delete BucketTagging + err = client.DeleteBucketTagging(bucketName, GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + c.Assert(GetRequestId(respHeader) != "", Equals, true) + + getResult, err = client.GetBucketTagging(bucketName, GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + c.Assert(GetRequestId(respHeader) != "", Equals, true) + c.Assert(len(getResult.Tags), Equals, 0) + + err = client.DeleteBucket(bucketName) + c.Assert(err, IsNil) +} + +func (s *OssClientSuite) TestListBucketsTagging(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName1 := bucketNamePrefix + RandLowStr(5) + err = client.CreateBucket(bucketName1) + c.Assert(err, IsNil) + + bucketName2 := bucketNamePrefix + RandLowStr(5) + err = client.CreateBucket(bucketName2) + c.Assert(err, IsNil) + + // Bucket Tagging + var tagging Tagging + tagging.Tags = []Tag{Tag{Key: "testkey", Value: "testvalue"}} + err = client.SetBucketTagging(bucketName1, tagging) + c.Assert(err, IsNil) + + // list bucket + listResult, err := client.ListBuckets(TagKey("testkey")) + c.Assert(err, IsNil) + c.Assert(len(listResult.Buckets), Equals, 1) + c.Assert(listResult.Buckets[0].Name, Equals, bucketName1) + + client.DeleteBucket(bucketName1) + client.DeleteBucket(bucketName2) +} + +func (s *OssClientSuite) TestGetBucketStat(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(5) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + + // put object + objectName := objectNamePrefix + RandLowStr(5) + err = bucket.PutObject(objectName, strings.NewReader(RandStr(10))) + c.Assert(err, IsNil) + + bucket.DeleteObject(objectName) + err = bucket.PutObject(objectName, strings.NewReader(RandStr(10))) + c.Assert(err, IsNil) + bucket.DeleteObject(objectName) + + _, err = client.GetBucketStat(bucketName) + c.Assert(err, IsNil) + + client.DeleteBucket(bucketName) +} + +func (s *OssBucketSuite) TestGetBucketVersioning(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + + var respHeader http.Header + err = client.CreateBucket(bucketName, GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + c.Assert(GetRequestId(respHeader) != "", Equals, true) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + // get bucket version success + versioningResult, err := client.GetBucketVersioning(bucketName, GetResponseHeader(&respHeader)) + c.Assert(versioningResult.Status, Equals, "Enabled") + c.Assert(GetRequestId(respHeader) != "", Equals, true) + + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssClientSuite) TestBucketPolicy(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(5) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + var responseHeader http.Header + ret, err := client.GetBucketPolicy(bucketName, GetResponseHeader(&responseHeader)) + c.Assert(err, NotNil) + requestId := GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + policyInfo := ` + { + "Version":"1", + "Statement":[ + { + "Action":[ + "oss:GetObject", + "oss:PutObject" + ], + "Effect":"Deny", + "Principal":"[123456790]", + "Resource":["acs:oss:*:1234567890:*/*"] + } + ] + }` + + err = client.SetBucketPolicy(bucketName, policyInfo, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + requestId = GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + ret, err = client.GetBucketPolicy(bucketName, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + testLogger.Println("policy:", ret) + c.Assert(ret, Equals, policyInfo) + requestId = GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + err = client.DeleteBucketPolicy(bucketName, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + requestId = GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + client.DeleteBucket(bucketName) +} + +func (s *OssClientSuite) TestBucketPolicyNegative(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(5) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + var responseHeader http.Header + _, err = client.GetBucketPolicy(bucketName, GetResponseHeader(&responseHeader)) + c.Assert(err, NotNil) + requestId := GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + // Setting the Version is 2, this is error policy + errPolicy := ` + { + "Version":"2", + "Statement":[ + { + "Action":[ + "oss:GetObject", + "oss:PutObject" + ], + "Effect":"Deny", + "Principal":"[123456790]", + "Resource":["acs:oss:*:1234567890:*/*"] + } + ] + }` + err = client.SetBucketPolicy(bucketName, errPolicy, GetResponseHeader(&responseHeader)) + c.Assert(err, NotNil) + testLogger.Println("err:", err) + requestId = GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + err = client.DeleteBucketPolicy(bucketName, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + + bucketNameEmpty := bucketNamePrefix + RandLowStr(5) + client.DeleteBucket(bucketNameEmpty) + + err = client.DeleteBucketPolicy(bucketNameEmpty, GetResponseHeader(&responseHeader)) + c.Assert(err, NotNil) + requestId = GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + client.DeleteBucket(bucketName) +} + +func (s *OssClientSuite) TestSetBucketRequestPayment(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(5) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + reqPayConf := RequestPaymentConfiguration{ + Payer: "Requester", + } + err = client.SetBucketRequestPayment(bucketName, reqPayConf) + c.Assert(err, IsNil) + + ret, err := client.GetBucketRequestPayment(bucketName) + c.Assert(err, IsNil) + c.Assert(ret.Payer, Equals, "Requester") + + client.DeleteBucket(bucketName) + c.Assert(err, IsNil) +} + +func (s *OssClientSuite) TestSetBucketRequestPaymentNegative(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(5) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + reqPayConf := RequestPaymentConfiguration{ + Payer: "Requesterttttt", // this is a error configuration + } + err = client.SetBucketRequestPayment(bucketName, reqPayConf) + c.Assert(err, NotNil) + + ret, err := client.GetBucketRequestPayment(bucketName) + c.Assert(err, IsNil) + c.Assert(ret.Payer, Equals, "BucketOwner") + + client.DeleteBucket(bucketName) + c.Assert(err, IsNil) +} + +func (s *OssClientSuite) TestBucketQos(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + ret, err := client.GetUserQoSInfo() + c.Assert(err, IsNil) + testLogger.Println("QosInfo:", ret) + + bucketName := bucketNamePrefix + RandLowStr(5) + _ = client.DeleteBucket(bucketName) + + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + _, err = client.GetBucketQosInfo(bucketName) + c.Assert(err, NotNil) + + // case 1 set BucketQoSConfiguration every member + five := 5 + four := 4 + totalQps := 200 + qosConf := BucketQoSConfiguration{ + TotalUploadBandwidth: &five, + IntranetUploadBandwidth: &four, + ExtranetUploadBandwidth: &four, + TotalDownloadBandwidth: &four, + IntranetDownloadBandwidth: &four, + ExtranetDownloadBandwidth: &four, + TotalQPS: &totalQps, + IntranetQPS: &totalQps, + ExtranetQPS: &totalQps, + } + var responseHeader http.Header + err = client.SetBucketQoSInfo(bucketName, qosConf, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + requestId := GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + // wait a moment for configuration effect + time.Sleep(time.Second) + + retQos, err := client.GetBucketQosInfo(bucketName) + c.Assert(err, IsNil) + + // set qosConf default value + qosConf.XMLName.Local = "QoSConfiguration" + c.Assert(struct2string(retQos, c), Equals, struct2string(qosConf, c)) + + // case 2 set BucketQoSConfiguration not every member + qosConfNo := BucketQoSConfiguration{ + TotalUploadBandwidth: &five, + IntranetUploadBandwidth: &four, + ExtranetUploadBandwidth: &four, + TotalDownloadBandwidth: &four, + IntranetDownloadBandwidth: &four, + ExtranetDownloadBandwidth: &four, + TotalQPS: &totalQps, + } + err = client.SetBucketQoSInfo(bucketName, qosConfNo) + c.Assert(err, IsNil) + + // wait a moment for configuration effect + time.Sleep(time.Second) + + retQos, err = client.GetBucketQosInfo(bucketName) + c.Assert(err, IsNil) + + // set qosConfNo default value + qosConfNo.XMLName.Local = "QoSConfiguration" + defNum := -1 + qosConfNo.IntranetQPS = &defNum + qosConfNo.ExtranetQPS = &defNum + c.Assert(struct2string(retQos, c), Equals, struct2string(qosConfNo, c)) + + err = client.DeleteBucketQosInfo(bucketName) + c.Assert(err, IsNil) + + // wait a moment for configuration effect + time.Sleep(time.Second) + + _, err = client.GetBucketQosInfo(bucketName) + c.Assert(err, NotNil) + + // this is a error qos configuration + to := *ret.TotalUploadBandwidth + 2 + qosErrConf := BucketQoSConfiguration{ + TotalUploadBandwidth: &to, // this exceed user TotalUploadBandwidth + IntranetUploadBandwidth: &four, + ExtranetUploadBandwidth: &four, + TotalDownloadBandwidth: &four, + IntranetDownloadBandwidth: &four, + ExtranetDownloadBandwidth: &four, + TotalQPS: &totalQps, + IntranetQPS: &totalQps, + ExtranetQPS: &totalQps, + } + err = client.SetBucketQoSInfo(bucketName, qosErrConf) + c.Assert(err, NotNil) + + err = client.DeleteBucketQosInfo(bucketName) + c.Assert(err, IsNil) + + err = client.DeleteBucket(bucketName) + c.Assert(err, IsNil) +} + +// struct to string +func struct2string(obj interface{}, c *C) string { + t := reflect.TypeOf(obj) + v := reflect.ValueOf(obj) + + var data = make(map[string]interface{}) + for i := 0; i < t.NumField(); i++ { + data[t.Field(i).Name] = v.Field(i).Interface() + } + str, err := json.Marshal(data) + c.Assert(err, IsNil) + return string(str) +} + +type TestCredentials struct { +} + +func (testCreInf *TestCredentials) GetAccessKeyID() string { + return os.Getenv("OSS_TEST_ACCESS_KEY_ID") +} + +func (testCreInf *TestCredentials) GetAccessKeySecret() string { + return os.Getenv("OSS_TEST_ACCESS_KEY_SECRET") +} + +func (testCreInf *TestCredentials) GetSecurityToken() string { + return "" +} + +type TestCredentialsProvider struct { +} + +func (testInfBuild *TestCredentialsProvider) GetCredentials() Credentials { + return &TestCredentials{} +} + +func (s *OssClientSuite) TestClientCredentialInfBuild(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + var defaultBuild TestCredentialsProvider + client, err := New(endpoint, "", "", SetCredentialsProvider(&defaultBuild)) + c.Assert(err, IsNil) + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +func (s *OssClientSuite) TestClientSetLocalIpError(c *C) { + // create client and bucket + ipAddr, err := net.ResolveIPAddr("ip", "127.0.0.1") + c.Assert(err, IsNil) + localTCPAddr := &(net.TCPAddr{IP: ipAddr.IP}) + client, err := New(endpoint, accessID, accessKey, SetLocalAddr(localTCPAddr)) + c.Assert(err, IsNil) + + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketNameTest) + c.Assert(err, NotNil) +} + +func (s *OssClientSuite) TestClientSetLocalIpSuccess(c *C) { + //get local ip + conn, err := net.Dial("udp", "8.8.8.8:80") + c.Assert(err, IsNil) + localAddr := conn.LocalAddr().(*net.UDPAddr) + localIp := localAddr.IP.String() + conn.Close() + + ipAddr, err := net.ResolveIPAddr("ip", localIp) + c.Assert(err, IsNil) + localTCPAddr := &(net.TCPAddr{IP: ipAddr.IP}) + client, err := New(endpoint, accessID, accessKey, SetLocalAddr(localTCPAddr)) + c.Assert(err, IsNil) + + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestCreateBucketInvalidName +func (s *OssClientSuite) TestCreateBucketInvalidName(c *C) { + var bucketNameTest = "-" + bucketNamePrefix + RandLowStr(6) + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + // Create + err = client.CreateBucket(bucketNameTest) + c.Assert(err, NotNil) +} + +// TestClientProcessEndpointSuccess +func (s *OssClientSuite) TestClientProcessEndpointSuccess(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + + testEndpoint := endpoint + "/" + "sina.com" + "?" + "para=abc" + + client, err := New(testEndpoint, accessID, accessKey) + c.Assert(err, IsNil) + + // Create + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + // delete + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestClientProcessEndpointSuccess +func (s *OssClientSuite) TestClientProcessEndpointError(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + + testEndpoint := "https://127.0.0.1/" + endpoint + + client, err := New(testEndpoint, accessID, accessKey) + c.Assert(err, IsNil) + + // Create + err = client.CreateBucket(bucketNameTest) + c.Assert(err, NotNil) +} + +// TestClientBucketError +func (s *OssClientSuite) TestClientBucketError(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := "-" + RandLowStr(5) + _, err = client.Bucket(bucketName) + c.Assert(err, NotNil) +} + +func (s *OssClientSuite) TestSetBucketInventory(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(5) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // encryption config + var invSseOss InvSseOss + invSseKms := InvSseKms{ + KmsId: "keyId", + } + var invEncryption InvEncryption + + bl := true + // not any encryption + invConfig := InventoryConfiguration{ + Id: "report1", + IsEnabled: &bl, + Prefix: "filterPrefix/", + OSSBucketDestination: OSSBucketDestination{ + Format: "CSV", + AccountId: accountID, + RoleArn: stsARN, + Bucket: "acs:oss:::" + bucketName, + Prefix: "prefix1", + }, + Frequency: "Daily", + IncludedObjectVersions: "All", + OptionalFields: OptionalFields{ + Field: []string{ + "Size", "LastModifiedDate", "ETag", "StorageClass", "IsMultipartUploaded", "EncryptionStatus", + }, + }, + } + + // case 1: not any encryption + err = client.SetBucketInventory(bucketName, invConfig) + c.Assert(err, IsNil) + + // case 2: use kms encryption + invConfig.Id = "report2" + invEncryption.SseKms = &invSseKms + invEncryption.SseOss = nil + invConfig.OSSBucketDestination.Encryption = &invEncryption + err = client.SetBucketInventory(bucketName, invConfig) + c.Assert(err, IsNil) + + // case 3: use SseOss encryption + invConfig.Id = "report3" + invEncryption.SseKms = nil + invEncryption.SseOss = &invSseOss + invConfig.OSSBucketDestination.Encryption = &invEncryption + err = client.SetBucketInventory(bucketName, invConfig) + c.Assert(err, IsNil) + + //case 4: use two type encryption + invConfig.Id = "report4" + invEncryption.SseKms = &invSseKms + invEncryption.SseOss = &invSseOss + invConfig.OSSBucketDestination.Encryption = &invEncryption + err = client.SetBucketInventory(bucketName, invConfig) + c.Assert(err, NotNil) + + err = client.DeleteBucket(bucketName) + c.Assert(err, IsNil) +} + +func (s *OssClientSuite) TestBucketInventory(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(5) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bl := true + invConfig := InventoryConfiguration{ + Id: "report1", + IsEnabled: &bl, + Prefix: "filterPrefix/", + OSSBucketDestination: OSSBucketDestination{ + Format: "CSV", + AccountId: accountID, + RoleArn: stsARN, + Bucket: "acs:oss:::" + bucketName, + Prefix: "prefix1", + }, + Frequency: "Daily", + IncludedObjectVersions: "All", + OptionalFields: OptionalFields{ + Field: []string{ + "Size", "LastModifiedDate", "ETag", "StorageClass", "IsMultipartUploaded", "EncryptionStatus", + }, + }, + } + + // case 1: test SetBucketInventory + err = client.SetBucketInventory(bucketName, invConfig) + c.Assert(err, IsNil) + + // case 2: test GetBucketInventory + out, err := client.GetBucketInventory(bucketName, "report1") + c.Assert(err, IsNil) + invConfig.XMLName.Local = "InventoryConfiguration" + invConfig.OSSBucketDestination.XMLName.Local = "OSSBucketDestination" + invConfig.OptionalFields.XMLName.Local = "OptionalFields" + c.Assert(struct2string(invConfig, c), Equals, struct2string(out, c)) + + // case 3: test ListBucketInventory + invConfig2 := InventoryConfiguration{ + Id: "report2", + IsEnabled: &bl, + Prefix: "filterPrefix/", + OSSBucketDestination: OSSBucketDestination{ + Format: "CSV", + AccountId: accountID, + RoleArn: stsARN, + Bucket: "acs:oss:::" + bucketName, + Prefix: "prefix1", + }, + Frequency: "Daily", + IncludedObjectVersions: "All", + OptionalFields: OptionalFields{ + Field: []string{ + "Size", "LastModifiedDate", "ETag", "StorageClass", "IsMultipartUploaded", "EncryptionStatus", + }, + }, + } + invConfig2.XMLName.Local = "InventoryConfiguration" + invConfig2.OSSBucketDestination.XMLName.Local = "OSSBucketDestination" + invConfig2.OptionalFields.XMLName.Local = "OptionalFields" + + err = client.SetBucketInventory(bucketName, invConfig2) + c.Assert(err, IsNil) + + listInvConf, err := client.ListBucketInventory(bucketName, "", Marker("report1"), MaxKeys(2)) + c.Assert(err, IsNil) + var listInvLocal ListInventoryConfigurationsResult + listInvLocal.InventoryConfiguration = []InventoryConfiguration{ + invConfig, + invConfig2, + } + bo := false + listInvLocal.IsTruncated = &bo + listInvLocal.XMLName.Local = "ListInventoryConfigurationsResult" + c.Assert(struct2string(listInvLocal, c), Equals, struct2string(listInvConf, c)) + + for i := 3; i < 109; i++ { + invConfig2 := InventoryConfiguration{ + Id: "report" + strconv.Itoa(i), + IsEnabled: &bl, + Prefix: "filterPrefix/", + OSSBucketDestination: OSSBucketDestination{ + Format: "CSV", + AccountId: accountID, + RoleArn: stsARN, + Bucket: "acs:oss:::" + bucketName, + Prefix: "prefix1", + }, + Frequency: "Daily", + IncludedObjectVersions: "All", + OptionalFields: OptionalFields{ + Field: []string{ + "Size", "LastModifiedDate", "ETag", "StorageClass", "IsMultipartUploaded", "EncryptionStatus", + }, + }, + } + err = client.SetBucketInventory(bucketName, invConfig2) + c.Assert(err, IsNil) + } + token := "" + for { + listInvConf1, err := client.ListBucketInventory(bucketName, token) + c.Assert(err, IsNil) + token = listInvConf1.NextContinuationToken + testLogger.Println(listInvConf1.NextContinuationToken, *listInvConf1.IsTruncated, token) + if *listInvConf1.IsTruncated == false { + break + } else { + c.Assert(listInvConf1.NextContinuationToken, Equals, "report91") + } + } + + // case 4: test DeleteBucketInventory + for i := 1; i < 109; i++ { + err = client.DeleteBucketInventory(bucketName, "report"+strconv.Itoa(i)) + c.Assert(err, IsNil) + } + + err = client.DeleteBucket(bucketName) + c.Assert(err, IsNil) +} + +func (s *OssClientSuite) TestBucketInventoryNegative(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(5) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bl := true + invConfigErr := InventoryConfiguration{ + Id: "report1", + IsEnabled: &bl, + Prefix: "filterPrefix/", + OSSBucketDestination: OSSBucketDestination{ + Format: "CSV", + AccountId: accountID, + RoleArn: stsARN, + Bucket: "test", + Prefix: "prefix1", + }, + Frequency: "Daily", + IncludedObjectVersions: "All", + OptionalFields: OptionalFields{ + Field: []string{ + "Size", "LastModifiedDate", "ETag", "StorageClass", "IsMultipartUploaded", "EncryptionStatus", + }, + }, + } + // case 1: test SetBucketInventory + err = client.SetBucketInventory(bucketName, invConfigErr) + c.Assert(err, NotNil) + + // case 2: test GetBucketInventory + _, err = client.GetBucketInventory(bucketName, "report1") + c.Assert(err, NotNil) + + // case 3: test ListBucketInventory + _, err = client.ListBucketInventory(bucketName, "", Marker("report1"), MaxKeys(2)) + c.Assert(err, NotNil) + + // case 4: test DeleteBucketInventory + err = client.DeleteBucketInventory(bucketName, "report1") + c.Assert(err, IsNil) + + err = client.DeleteBucket(bucketName) + c.Assert(err, IsNil) +} + +func (s *OssClientSuite) TestBucketAsyncTask(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(5) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + objectName := objectNamePrefix + RandLowStr(6) + + // set asyn task,IgnoreSameKey is false + asynConf := AsyncFetchTaskConfiguration{ + Url: "http://www.baidu.com", + Object: objectName, + Host: "", + ContentMD5: "", + Callback: "", + StorageClass: "", + IgnoreSameKey: false, + } + + asynResult, err := client.SetBucketAsyncTask(bucketName, asynConf) + c.Assert(err, IsNil) + c.Assert(len(asynResult.TaskId) > 0, Equals, true) + + // get asyn task + asynTask, err := client.GetBucketAsyncTask(bucketName, asynResult.TaskId) + c.Assert(err, IsNil) + c.Assert(asynResult.TaskId, Equals, asynTask.TaskId) + c.Assert(len(asynTask.State) > 0, Equals, true) + c.Assert(asynConf.Url, Equals, asynTask.TaskInfo.Url) + c.Assert(asynConf.Object, Equals, asynTask.TaskInfo.Object) + c.Assert(asynConf.Callback, Equals, asynTask.TaskInfo.Callback) + c.Assert(asynConf.IgnoreSameKey, Equals, asynTask.TaskInfo.IgnoreSameKey) + + // test again,IgnoreSameKey is true + asynConf.IgnoreSameKey = true + asynResult, err = client.SetBucketAsyncTask(bucketName, asynConf) + c.Assert(err, IsNil) + c.Assert(len(asynResult.TaskId) > 0, Equals, true) + + asynTask, err = client.GetBucketAsyncTask(bucketName, asynResult.TaskId) + c.Assert(asynConf.IgnoreSameKey, Equals, asynTask.TaskInfo.IgnoreSameKey) + + err = client.DeleteBucket(bucketName) + c.Assert(err, IsNil) +} + +func (s *OssClientSuite) TestClientOptionHeader(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + + var respHeader http.Header + err = client.CreateBucket(bucketName, GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + c.Assert(GetRequestId(respHeader) != "", Equals, true) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + // get bucket version success,use payer + options := []Option{RequestPayer(BucketOwner), GetResponseHeader(&respHeader)} + versioningResult, err := client.GetBucketVersioning(bucketName, options...) + c.Assert(versioningResult.Status, Equals, "Enabled") + c.Assert(GetRequestId(respHeader) != "", Equals, true) + + //list buckets,use payer + _, err = client.ListBuckets(options...) + c.Assert(err, IsNil) + + ForceDeleteBucket(client, bucketName, c) +} + +// compare with go1.7 +func compareVersion(goVersion string) bool { + nowVersion := runtime.Version() + nowVersion = strings.Replace(nowVersion, "go", "", -1) + pSlice1 := strings.Split(goVersion, ".") + pSlice2 := strings.Split(nowVersion, ".") + for k, v := range pSlice2 { + n2, _ := strconv.Atoi(string(v)) + n1, _ := strconv.Atoi(string(pSlice1[k])) + if n2 > n1 { + return true + } + if n2 < n1 { + return false + } + } + return true +} + +func homeHandler(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/redirectTo", http.StatusFound) +} +func targetHandler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "You have been redirected here!") +} + +func (s *OssClientSuite) TestClientRedirect(c *C) { + // must go1.7.0 onward + if !compareVersion("1.7.0") { + return + } + + // get port + rand.Seed(time.Now().Unix()) + port := 10000 + rand.Intn(10000) + + // start http server + httpAddr := fmt.Sprintf("127.0.0.1:%d", port) + mux := http.NewServeMux() + mux.HandleFunc("/redirectTo", targetHandler) + mux.HandleFunc("/", homeHandler) + svr := &http.Server{ + Addr: httpAddr, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, + Handler: mux, + } + + go func() { + svr.ListenAndServe() + }() + + url := "http://" + httpAddr + + // create client 1,redirect disable + client1, err := New(endpoint, accessID, accessKey, RedirectEnabled(false)) + resp, err := client1.Conn.client.Get(url) + c.Assert(err, IsNil) + c.Assert(resp.StatusCode, Equals, http.StatusFound) + resp.Body.Close() + + // create client2, redirect enabled + client2, err := New(endpoint, accessID, accessKey, RedirectEnabled(true)) + resp, err = client2.Conn.client.Get(url) + c.Assert(err, IsNil) + c.Assert(resp.StatusCode, Equals, 200) + data, err := ioutil.ReadAll(resp.Body) + c.Assert(string(data), Equals, "You have been redirected here!") + resp.Body.Close() +} + +// TestInitiateBucketWormSuccess +func (s *OssClientSuite) TestInitiateBucketWormSuccess(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + // InitiateBucketWorm + wormId, err := client.InitiateBucketWorm(bucketNameTest, 10) + c.Assert(err, IsNil) + c.Assert(len(wormId) > 0, Equals, true) + + // GetBucketWorm + wormConfig, err := client.GetBucketWorm(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(wormConfig.WormId, Equals, wormId) + c.Assert(wormConfig.State, Equals, "InProgress") + c.Assert(wormConfig.RetentionPeriodInDays, Equals, 10) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestInitiateBucketWormFailure +func (s *OssClientSuite) TestInitiateBucketWormFailure(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + // bucket not exist + wormId, err := client.InitiateBucketWorm(bucketNameTest, 10) + c.Assert(err, NotNil) + c.Assert(len(wormId), Equals, 0) +} + +// TestAbortBucketWorm +func (s *OssClientSuite) TestAbortBucketWorm(c *C) { + var bucketNameTest = bucketNamePrefix + RandLowStr(6) + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + // InitiateBucketWorm + wormId, err := client.InitiateBucketWorm(bucketNameTest, 10) + c.Assert(err, IsNil) + c.Assert(len(wormId) > 0, Equals, true) + + // GetBucketWorm success + wormConfig, err := client.GetBucketWorm(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(wormConfig.WormId, Equals, wormId) + c.Assert(wormConfig.State, Equals, "InProgress") + c.Assert(wormConfig.RetentionPeriodInDays, Equals, 10) + + // abort worm + err = client.AbortBucketWorm(bucketNameTest) + c.Assert(err, IsNil) + + // GetBucketWorm failure + _, err = client.GetBucketWorm(bucketNameTest) + c.Assert(err, NotNil) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestCompleteBucketWorm +func (s *OssClientSuite) TestCompleteBucketWorm(c *C) { + var bucketNameTest = bucketNamePrefix + "-worm-" + RandLowStr(6) + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + // InitiateBucketWorm + wormId, err := client.InitiateBucketWorm(bucketNameTest, 1) + c.Assert(err, IsNil) + c.Assert(len(wormId) > 0, Equals, true) + + // GetBucketWorm + wormConfig, err := client.GetBucketWorm(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(wormConfig.WormId, Equals, wormId) + c.Assert(wormConfig.State, Equals, "InProgress") + c.Assert(wormConfig.RetentionPeriodInDays, Equals, 1) + + // CompleteBucketWorm + err = client.CompleteBucketWorm(bucketNameTest, wormId) + c.Assert(err, IsNil) + + // GetBucketWorm again + wormConfig, err = client.GetBucketWorm(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(wormConfig.WormId, Equals, wormId) + c.Assert(wormConfig.State, Equals, "Locked") + c.Assert(wormConfig.RetentionPeriodInDays, Equals, 1) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} + +// TestExtendBucketWorm +func (s *OssClientSuite) TestExtendBucketWorm(c *C) { + var bucketNameTest = bucketNamePrefix + "-worm-" + RandLowStr(6) + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + err = client.CreateBucket(bucketNameTest) + c.Assert(err, IsNil) + + // InitiateBucketWorm + wormId, err := client.InitiateBucketWorm(bucketNameTest, 1) + c.Assert(err, IsNil) + c.Assert(len(wormId) > 0, Equals, true) + + // CompleteBucketWorm + err = client.CompleteBucketWorm(bucketNameTest, wormId) + c.Assert(err, IsNil) + + // GetBucketWorm + wormConfig, err := client.GetBucketWorm(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(wormConfig.WormId, Equals, wormId) + c.Assert(wormConfig.State, Equals, "Locked") + c.Assert(wormConfig.RetentionPeriodInDays, Equals, 1) + + // CompleteBucketWorm + err = client.ExtendBucketWorm(bucketNameTest, 2, wormId) + c.Assert(err, IsNil) + + // GetBucketWorm again + wormConfig, err = client.GetBucketWorm(bucketNameTest) + c.Assert(err, IsNil) + c.Assert(wormConfig.WormId, Equals, wormId) + c.Assert(wormConfig.State, Equals, "Locked") + c.Assert(wormConfig.RetentionPeriodInDays, Equals, 2) + + err = client.DeleteBucket(bucketNameTest) + c.Assert(err, IsNil) +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/conf.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/conf.go new file mode 100644 index 00000000..55e5e370 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/conf.go @@ -0,0 +1,185 @@ +package oss + +import ( + "bytes" + "fmt" + "log" + "net" + "os" + "time" +) + +// Define the level of the output log +const ( + LogOff = iota + Error + Warn + Info + Debug +) + +// LogTag Tag for each level of log +var LogTag = []string{"[error]", "[warn]", "[info]", "[debug]"} + +// HTTPTimeout defines HTTP timeout. +type HTTPTimeout struct { + ConnectTimeout time.Duration + ReadWriteTimeout time.Duration + HeaderTimeout time.Duration + LongTimeout time.Duration + IdleConnTimeout time.Duration +} + +// HTTPMaxConns defines max idle connections and max idle connections per host +type HTTPMaxConns struct { + MaxIdleConns int + MaxIdleConnsPerHost int +} + +// CredentialInf is interface for get AccessKeyID,AccessKeySecret,SecurityToken +type Credentials interface { + GetAccessKeyID() string + GetAccessKeySecret() string + GetSecurityToken() string +} + +// CredentialInfBuild is interface for get CredentialInf +type CredentialsProvider interface { + GetCredentials() Credentials +} + +type defaultCredentials struct { + config *Config +} + +func (defCre *defaultCredentials) GetAccessKeyID() string { + return defCre.config.AccessKeyID +} + +func (defCre *defaultCredentials) GetAccessKeySecret() string { + return defCre.config.AccessKeySecret +} + +func (defCre *defaultCredentials) GetSecurityToken() string { + return defCre.config.SecurityToken +} + +type defaultCredentialsProvider struct { + config *Config +} + +func (defBuild *defaultCredentialsProvider) GetCredentials() Credentials { + return &defaultCredentials{config: defBuild.config} +} + +// Config defines oss configuration +type Config struct { + Endpoint string // OSS endpoint + AccessKeyID string // AccessId + AccessKeySecret string // AccessKey + RetryTimes uint // Retry count by default it's 5. + UserAgent string // SDK name/version/system information + IsDebug bool // Enable debug mode. Default is false. + Timeout uint // Timeout in seconds. By default it's 60. + SecurityToken string // STS Token + IsCname bool // If cname is in the endpoint. + HTTPTimeout HTTPTimeout // HTTP timeout + HTTPMaxConns HTTPMaxConns // Http max connections + IsUseProxy bool // Flag of using proxy. + ProxyHost string // Flag of using proxy host. + IsAuthProxy bool // Flag of needing authentication. + ProxyUser string // Proxy user + ProxyPassword string // Proxy password + IsEnableMD5 bool // Flag of enabling MD5 for upload. + MD5Threshold int64 // Memory footprint threshold for each MD5 computation (16MB is the default), in byte. When the data is more than that, temp file is used. + IsEnableCRC bool // Flag of enabling CRC for upload. + LogLevel int // Log level + Logger *log.Logger // For write log + UploadLimitSpeed int // Upload limit speed:KB/s, 0 is unlimited + UploadLimiter *OssLimiter // Bandwidth limit reader for upload + CredentialsProvider CredentialsProvider // User provides interface to get AccessKeyID, AccessKeySecret, SecurityToken + LocalAddr net.Addr // local client host info + UserSetUa bool // UserAgent is set by user or not + AuthVersion AuthVersionType // v1 or v2 signature,default is v1 + AdditionalHeaders []string // special http headers needed to be sign + RedirectEnabled bool // only effective from go1.7 onward, enable http redirect or not +} + +// LimitUploadSpeed uploadSpeed:KB/s, 0 is unlimited,default is 0 +func (config *Config) LimitUploadSpeed(uploadSpeed int) error { + if uploadSpeed < 0 { + return fmt.Errorf("invalid argument, the value of uploadSpeed is less than 0") + } else if uploadSpeed == 0 { + config.UploadLimitSpeed = 0 + config.UploadLimiter = nil + return nil + } + + var err error + config.UploadLimiter, err = GetOssLimiter(uploadSpeed) + if err == nil { + config.UploadLimitSpeed = uploadSpeed + } + return err +} + +// WriteLog output log function +func (config *Config) WriteLog(LogLevel int, format string, a ...interface{}) { + if config.LogLevel < LogLevel || config.Logger == nil { + return + } + + var logBuffer bytes.Buffer + logBuffer.WriteString(LogTag[LogLevel-1]) + logBuffer.WriteString(fmt.Sprintf(format, a...)) + config.Logger.Printf("%s", logBuffer.String()) +} + +// for get Credentials +func (config *Config) GetCredentials() Credentials { + return config.CredentialsProvider.GetCredentials() +} + +// getDefaultOssConfig gets the default configuration. +func getDefaultOssConfig() *Config { + config := Config{} + + config.Endpoint = "" + config.AccessKeyID = "" + config.AccessKeySecret = "" + config.RetryTimes = 5 + config.IsDebug = false + config.UserAgent = userAgent() + config.Timeout = 60 // Seconds + config.SecurityToken = "" + config.IsCname = false + + config.HTTPTimeout.ConnectTimeout = time.Second * 30 // 30s + config.HTTPTimeout.ReadWriteTimeout = time.Second * 60 // 60s + config.HTTPTimeout.HeaderTimeout = time.Second * 60 // 60s + config.HTTPTimeout.LongTimeout = time.Second * 300 // 300s + config.HTTPTimeout.IdleConnTimeout = time.Second * 50 // 50s + config.HTTPMaxConns.MaxIdleConns = 100 + config.HTTPMaxConns.MaxIdleConnsPerHost = 100 + + config.IsUseProxy = false + config.ProxyHost = "" + config.IsAuthProxy = false + config.ProxyUser = "" + config.ProxyPassword = "" + + config.MD5Threshold = 16 * 1024 * 1024 // 16MB + config.IsEnableMD5 = false + config.IsEnableCRC = true + + config.LogLevel = LogOff + config.Logger = log.New(os.Stdout, "", log.LstdFlags) + + provider := &defaultCredentialsProvider{config: &config} + config.CredentialsProvider = provider + + config.AuthVersion = AuthV1 + config.RedirectEnabled = true + + return &config +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/conn.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/conn.go new file mode 100644 index 00000000..6e9db0f9 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/conn.go @@ -0,0 +1,793 @@ +package oss + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "encoding/json" + "encoding/xml" + "fmt" + "hash" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "os" + "sort" + "strconv" + "strings" + "time" +) + +// Conn defines OSS Conn +type Conn struct { + config *Config + url *urlMaker + client *http.Client +} + +var signKeyList = []string{"acl", "uploads", "location", "cors", + "logging", "website", "referer", "lifecycle", + "delete", "append", "tagging", "objectMeta", + "uploadId", "partNumber", "security-token", + "position", "img", "style", "styleName", + "replication", "replicationProgress", + "replicationLocation", "cname", "bucketInfo", + "comp", "qos", "live", "status", "vod", + "startTime", "endTime", "symlink", + "x-oss-process", "response-content-type", "x-oss-traffic-limit", + "response-content-language", "response-expires", + "response-cache-control", "response-content-disposition", + "response-content-encoding", "udf", "udfName", "udfImage", + "udfId", "udfImageDesc", "udfApplication", "comp", + "udfApplicationLog", "restore", "callback", "callback-var", "qosInfo", + "policy", "stat", "encryption", "versions", "versioning", "versionId", "requestPayment", + "x-oss-request-payer", "sequential", + "inventory", "inventoryId", "continuation-token", "asyncFetch", + "worm", "wormId", "wormExtend"} + +// init initializes Conn +func (conn *Conn) init(config *Config, urlMaker *urlMaker, client *http.Client) error { + if client == nil { + // New transport + transport := newTransport(conn, config) + + // Proxy + if conn.config.IsUseProxy { + proxyURL, err := url.Parse(config.ProxyHost) + if err != nil { + return err + } + if config.IsAuthProxy { + if config.ProxyPassword != "" { + proxyURL.User = url.UserPassword(config.ProxyUser, config.ProxyPassword) + } else { + proxyURL.User = url.User(config.ProxyUser) + } + } + transport.Proxy = http.ProxyURL(proxyURL) + } + client = &http.Client{Transport: transport} + if !config.RedirectEnabled { + disableHTTPRedirect(client) + } + } + + conn.config = config + conn.url = urlMaker + conn.client = client + + return nil +} + +// Do sends request and returns the response +func (conn Conn) Do(method, bucketName, objectName string, params map[string]interface{}, headers map[string]string, + data io.Reader, initCRC uint64, listener ProgressListener) (*Response, error) { + urlParams := conn.getURLParams(params) + subResource := conn.getSubResource(params) + uri := conn.url.getURL(bucketName, objectName, urlParams) + resource := conn.getResource(bucketName, objectName, subResource) + return conn.doRequest(method, uri, resource, headers, data, initCRC, listener) +} + +// DoURL sends the request with signed URL and returns the response result. +func (conn Conn) DoURL(method HTTPMethod, signedURL string, headers map[string]string, + data io.Reader, initCRC uint64, listener ProgressListener) (*Response, error) { + // Get URI from signedURL + uri, err := url.ParseRequestURI(signedURL) + if err != nil { + return nil, err + } + + m := strings.ToUpper(string(method)) + req := &http.Request{ + Method: m, + URL: uri, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: make(http.Header), + Host: uri.Host, + } + + tracker := &readerTracker{completedBytes: 0} + fd, crc := conn.handleBody(req, data, initCRC, listener, tracker) + if fd != nil { + defer func() { + fd.Close() + os.Remove(fd.Name()) + }() + } + + if conn.config.IsAuthProxy { + auth := conn.config.ProxyUser + ":" + conn.config.ProxyPassword + basic := "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) + req.Header.Set("Proxy-Authorization", basic) + } + + req.Header.Set(HTTPHeaderHost, req.Host) + req.Header.Set(HTTPHeaderUserAgent, conn.config.UserAgent) + + if headers != nil { + for k, v := range headers { + req.Header.Set(k, v) + } + } + + // Transfer started + event := newProgressEvent(TransferStartedEvent, 0, req.ContentLength, 0) + publishProgress(listener, event) + + if conn.config.LogLevel >= Debug { + conn.LoggerHTTPReq(req) + } + + resp, err := conn.client.Do(req) + if err != nil { + // Transfer failed + event = newProgressEvent(TransferFailedEvent, tracker.completedBytes, req.ContentLength, 0) + publishProgress(listener, event) + conn.config.WriteLog(Debug, "[Resp:%p]http error:%s\n", req, err.Error()) + return nil, err + } + + if conn.config.LogLevel >= Debug { + //print out http resp + conn.LoggerHTTPResp(req, resp) + } + + // Transfer completed + event = newProgressEvent(TransferCompletedEvent, tracker.completedBytes, req.ContentLength, 0) + publishProgress(listener, event) + + return conn.handleResponse(resp, crc) +} + +func (conn Conn) getURLParams(params map[string]interface{}) string { + // Sort + keys := make([]string, 0, len(params)) + for k := range params { + keys = append(keys, k) + } + sort.Strings(keys) + + // Serialize + var buf bytes.Buffer + for _, k := range keys { + if buf.Len() > 0 { + buf.WriteByte('&') + } + buf.WriteString(url.QueryEscape(k)) + if params[k] != nil { + buf.WriteString("=" + strings.Replace(url.QueryEscape(params[k].(string)), "+", "%20", -1)) + } + } + + return buf.String() +} + +func (conn Conn) getSubResource(params map[string]interface{}) string { + // Sort + keys := make([]string, 0, len(params)) + signParams := make(map[string]string) + for k := range params { + if conn.config.AuthVersion == AuthV2 { + encodedKey := url.QueryEscape(k) + keys = append(keys, encodedKey) + if params[k] != nil && params[k] != "" { + signParams[encodedKey] = strings.Replace(url.QueryEscape(params[k].(string)), "+", "%20", -1) + } + } else if conn.isParamSign(k) { + keys = append(keys, k) + if params[k] != nil { + signParams[k] = params[k].(string) + } + } + } + sort.Strings(keys) + + // Serialize + var buf bytes.Buffer + for _, k := range keys { + if buf.Len() > 0 { + buf.WriteByte('&') + } + buf.WriteString(k) + if _, ok := signParams[k]; ok { + buf.WriteString("=" + signParams[k]) + } + } + return buf.String() +} + +func (conn Conn) isParamSign(paramKey string) bool { + for _, k := range signKeyList { + if paramKey == k { + return true + } + } + return false +} + +// getResource gets canonicalized resource +func (conn Conn) getResource(bucketName, objectName, subResource string) string { + if subResource != "" { + subResource = "?" + subResource + } + if bucketName == "" { + if conn.config.AuthVersion == AuthV2 { + return url.QueryEscape("/") + subResource + } + return fmt.Sprintf("/%s%s", bucketName, subResource) + } + if conn.config.AuthVersion == AuthV2 { + return url.QueryEscape("/"+bucketName+"/") + strings.Replace(url.QueryEscape(objectName), "+", "%20", -1) + subResource + } + return fmt.Sprintf("/%s/%s%s", bucketName, objectName, subResource) +} + +func (conn Conn) doRequest(method string, uri *url.URL, canonicalizedResource string, headers map[string]string, + data io.Reader, initCRC uint64, listener ProgressListener) (*Response, error) { + method = strings.ToUpper(method) + req := &http.Request{ + Method: method, + URL: uri, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: make(http.Header), + Host: uri.Host, + } + + tracker := &readerTracker{completedBytes: 0} + fd, crc := conn.handleBody(req, data, initCRC, listener, tracker) + if fd != nil { + defer func() { + fd.Close() + os.Remove(fd.Name()) + }() + } + + if conn.config.IsAuthProxy { + auth := conn.config.ProxyUser + ":" + conn.config.ProxyPassword + basic := "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) + req.Header.Set("Proxy-Authorization", basic) + } + + date := time.Now().UTC().Format(http.TimeFormat) + req.Header.Set(HTTPHeaderDate, date) + req.Header.Set(HTTPHeaderHost, req.Host) + req.Header.Set(HTTPHeaderUserAgent, conn.config.UserAgent) + + akIf := conn.config.GetCredentials() + if akIf.GetSecurityToken() != "" { + req.Header.Set(HTTPHeaderOssSecurityToken, akIf.GetSecurityToken()) + } + + if headers != nil { + for k, v := range headers { + req.Header.Set(k, v) + } + } + + conn.signHeader(req, canonicalizedResource) + + // Transfer started + event := newProgressEvent(TransferStartedEvent, 0, req.ContentLength, 0) + publishProgress(listener, event) + + if conn.config.LogLevel >= Debug { + conn.LoggerHTTPReq(req) + } + + resp, err := conn.client.Do(req) + + if err != nil { + // Transfer failed + event = newProgressEvent(TransferFailedEvent, tracker.completedBytes, req.ContentLength, 0) + publishProgress(listener, event) + conn.config.WriteLog(Debug, "[Resp:%p]http error:%s\n", req, err.Error()) + return nil, err + } + + if conn.config.LogLevel >= Debug { + //print out http resp + conn.LoggerHTTPResp(req, resp) + } + + // Transfer completed + event = newProgressEvent(TransferCompletedEvent, tracker.completedBytes, req.ContentLength, 0) + publishProgress(listener, event) + + return conn.handleResponse(resp, crc) +} + +func (conn Conn) signURL(method HTTPMethod, bucketName, objectName string, expiration int64, params map[string]interface{}, headers map[string]string) string { + akIf := conn.config.GetCredentials() + if akIf.GetSecurityToken() != "" { + params[HTTPParamSecurityToken] = akIf.GetSecurityToken() + } + + m := strings.ToUpper(string(method)) + req := &http.Request{ + Method: m, + Header: make(http.Header), + } + + if conn.config.IsAuthProxy { + auth := conn.config.ProxyUser + ":" + conn.config.ProxyPassword + basic := "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) + req.Header.Set("Proxy-Authorization", basic) + } + + req.Header.Set(HTTPHeaderDate, strconv.FormatInt(expiration, 10)) + req.Header.Set(HTTPHeaderUserAgent, conn.config.UserAgent) + + if headers != nil { + for k, v := range headers { + req.Header.Set(k, v) + } + } + + if conn.config.AuthVersion == AuthV2 { + params[HTTPParamSignatureVersion] = "OSS2" + params[HTTPParamExpiresV2] = strconv.FormatInt(expiration, 10) + params[HTTPParamAccessKeyIDV2] = conn.config.AccessKeyID + additionalList, _ := conn.getAdditionalHeaderKeys(req) + if len(additionalList) > 0 { + params[HTTPParamAdditionalHeadersV2] = strings.Join(additionalList, ";") + } + } + + subResource := conn.getSubResource(params) + canonicalizedResource := conn.getResource(bucketName, objectName, subResource) + signedStr := conn.getSignedStr(req, canonicalizedResource, akIf.GetAccessKeySecret()) + + if conn.config.AuthVersion == AuthV1 { + params[HTTPParamExpires] = strconv.FormatInt(expiration, 10) + params[HTTPParamAccessKeyID] = akIf.GetAccessKeyID() + params[HTTPParamSignature] = signedStr + } else if conn.config.AuthVersion == AuthV2 { + params[HTTPParamSignatureV2] = signedStr + } + urlParams := conn.getURLParams(params) + return conn.url.getSignURL(bucketName, objectName, urlParams) +} + +func (conn Conn) signRtmpURL(bucketName, channelName, playlistName string, expiration int64) string { + params := map[string]interface{}{} + if playlistName != "" { + params[HTTPParamPlaylistName] = playlistName + } + expireStr := strconv.FormatInt(expiration, 10) + params[HTTPParamExpires] = expireStr + + akIf := conn.config.GetCredentials() + if akIf.GetAccessKeyID() != "" { + params[HTTPParamAccessKeyID] = akIf.GetAccessKeyID() + if akIf.GetSecurityToken() != "" { + params[HTTPParamSecurityToken] = akIf.GetSecurityToken() + } + signedStr := conn.getRtmpSignedStr(bucketName, channelName, playlistName, expiration, akIf.GetAccessKeySecret(), params) + params[HTTPParamSignature] = signedStr + } + + urlParams := conn.getURLParams(params) + return conn.url.getSignRtmpURL(bucketName, channelName, urlParams) +} + +// handleBody handles request body +func (conn Conn) handleBody(req *http.Request, body io.Reader, initCRC uint64, + listener ProgressListener, tracker *readerTracker) (*os.File, hash.Hash64) { + var file *os.File + var crc hash.Hash64 + reader := body + readerLen, err := GetReaderLen(reader) + if err == nil { + req.ContentLength = readerLen + } + req.Header.Set(HTTPHeaderContentLength, strconv.FormatInt(req.ContentLength, 10)) + + // MD5 + if body != nil && conn.config.IsEnableMD5 && req.Header.Get(HTTPHeaderContentMD5) == "" { + md5 := "" + reader, md5, file, _ = calcMD5(body, req.ContentLength, conn.config.MD5Threshold) + req.Header.Set(HTTPHeaderContentMD5, md5) + } + + // CRC + if reader != nil && conn.config.IsEnableCRC { + crc = NewCRC(CrcTable(), initCRC) + reader = TeeReader(reader, crc, req.ContentLength, listener, tracker) + } + + // HTTP body + rc, ok := reader.(io.ReadCloser) + if !ok && reader != nil { + rc = ioutil.NopCloser(reader) + } + + if conn.isUploadLimitReq(req) { + limitReader := &LimitSpeedReader{ + reader: rc, + ossLimiter: conn.config.UploadLimiter, + } + req.Body = limitReader + } else { + req.Body = rc + } + return file, crc +} + +// isUploadLimitReq: judge limit upload speed or not +func (conn Conn) isUploadLimitReq(req *http.Request) bool { + if conn.config.UploadLimitSpeed == 0 || conn.config.UploadLimiter == nil { + return false + } + + if req.Method != "GET" && req.Method != "DELETE" && req.Method != "HEAD" { + if req.ContentLength > 0 { + return true + } + } + return false +} + +func tryGetFileSize(f *os.File) int64 { + fInfo, _ := f.Stat() + return fInfo.Size() +} + +// handleResponse handles response +func (conn Conn) handleResponse(resp *http.Response, crc hash.Hash64) (*Response, error) { + var cliCRC uint64 + var srvCRC uint64 + + statusCode := resp.StatusCode + if statusCode >= 400 && statusCode <= 505 { + // 4xx and 5xx indicate that the operation has error occurred + var respBody []byte + respBody, err := readResponseBody(resp) + if err != nil { + return nil, err + } + + if len(respBody) == 0 { + err = ServiceError{ + StatusCode: statusCode, + RequestID: resp.Header.Get(HTTPHeaderOssRequestID), + } + } else { + // Response contains storage service error object, unmarshal + srvErr, errIn := serviceErrFromXML(respBody, resp.StatusCode, + resp.Header.Get(HTTPHeaderOssRequestID)) + if errIn != nil { // error unmarshaling the error response + err = fmt.Errorf("oss: service returned invalid response body, status = %s, RequestId = %s", resp.Status, resp.Header.Get(HTTPHeaderOssRequestID)) + } else { + err = srvErr + } + } + + return &Response{ + StatusCode: resp.StatusCode, + Headers: resp.Header, + Body: ioutil.NopCloser(bytes.NewReader(respBody)), // restore the body + }, err + } else if statusCode >= 300 && statusCode <= 307 { + // OSS use 3xx, but response has no body + err := fmt.Errorf("oss: service returned %d,%s", resp.StatusCode, resp.Status) + return &Response{ + StatusCode: resp.StatusCode, + Headers: resp.Header, + Body: resp.Body, + }, err + } + + if conn.config.IsEnableCRC && crc != nil { + cliCRC = crc.Sum64() + } + srvCRC, _ = strconv.ParseUint(resp.Header.Get(HTTPHeaderOssCRC64), 10, 64) + + // 2xx, successful + return &Response{ + StatusCode: resp.StatusCode, + Headers: resp.Header, + Body: resp.Body, + ClientCRC: cliCRC, + ServerCRC: srvCRC, + }, nil +} + +// LoggerHTTPReq Print the header information of the http request +func (conn Conn) LoggerHTTPReq(req *http.Request) { + var logBuffer bytes.Buffer + logBuffer.WriteString(fmt.Sprintf("[Req:%p]Method:%s\t", req, req.Method)) + logBuffer.WriteString(fmt.Sprintf("Host:%s\t", req.URL.Host)) + logBuffer.WriteString(fmt.Sprintf("Path:%s\t", req.URL.Path)) + logBuffer.WriteString(fmt.Sprintf("Query:%s\t", req.URL.RawQuery)) + logBuffer.WriteString(fmt.Sprintf("Header info:")) + + for k, v := range req.Header { + var valueBuffer bytes.Buffer + for j := 0; j < len(v); j++ { + if j > 0 { + valueBuffer.WriteString(" ") + } + valueBuffer.WriteString(v[j]) + } + logBuffer.WriteString(fmt.Sprintf("\t%s:%s", k, valueBuffer.String())) + } + conn.config.WriteLog(Debug, "%s\n", logBuffer.String()) +} + +// LoggerHTTPResp Print Response to http request +func (conn Conn) LoggerHTTPResp(req *http.Request, resp *http.Response) { + var logBuffer bytes.Buffer + logBuffer.WriteString(fmt.Sprintf("[Resp:%p]StatusCode:%d\t", req, resp.StatusCode)) + logBuffer.WriteString(fmt.Sprintf("Header info:")) + for k, v := range resp.Header { + var valueBuffer bytes.Buffer + for j := 0; j < len(v); j++ { + if j > 0 { + valueBuffer.WriteString(" ") + } + valueBuffer.WriteString(v[j]) + } + logBuffer.WriteString(fmt.Sprintf("\t%s:%s", k, valueBuffer.String())) + } + conn.config.WriteLog(Debug, "%s\n", logBuffer.String()) +} + +func calcMD5(body io.Reader, contentLen, md5Threshold int64) (reader io.Reader, b64 string, tempFile *os.File, err error) { + if contentLen == 0 || contentLen > md5Threshold { + // Huge body, use temporary file + tempFile, err = ioutil.TempFile(os.TempDir(), TempFilePrefix) + if tempFile != nil { + io.Copy(tempFile, body) + tempFile.Seek(0, os.SEEK_SET) + md5 := md5.New() + io.Copy(md5, tempFile) + sum := md5.Sum(nil) + b64 = base64.StdEncoding.EncodeToString(sum[:]) + tempFile.Seek(0, os.SEEK_SET) + reader = tempFile + } + } else { + // Small body, use memory + buf, _ := ioutil.ReadAll(body) + sum := md5.Sum(buf) + b64 = base64.StdEncoding.EncodeToString(sum[:]) + reader = bytes.NewReader(buf) + } + return +} + +func readResponseBody(resp *http.Response) ([]byte, error) { + defer resp.Body.Close() + out, err := ioutil.ReadAll(resp.Body) + if err == io.EOF { + err = nil + } + return out, err +} + +func serviceErrFromXML(body []byte, statusCode int, requestID string) (ServiceError, error) { + var storageErr ServiceError + + if err := xml.Unmarshal(body, &storageErr); err != nil { + return storageErr, err + } + + storageErr.StatusCode = statusCode + storageErr.RequestID = requestID + storageErr.RawMessage = string(body) + return storageErr, nil +} + +func xmlUnmarshal(body io.Reader, v interface{}) error { + data, err := ioutil.ReadAll(body) + if err != nil { + return err + } + return xml.Unmarshal(data, v) +} + +func jsonUnmarshal(body io.Reader, v interface{}) error { + data, err := ioutil.ReadAll(body) + if err != nil { + return err + } + return json.Unmarshal(data, v) +} + +// timeoutConn handles HTTP timeout +type timeoutConn struct { + conn net.Conn + timeout time.Duration + longTimeout time.Duration +} + +func newTimeoutConn(conn net.Conn, timeout time.Duration, longTimeout time.Duration) *timeoutConn { + conn.SetReadDeadline(time.Now().Add(longTimeout)) + return &timeoutConn{ + conn: conn, + timeout: timeout, + longTimeout: longTimeout, + } +} + +func (c *timeoutConn) Read(b []byte) (n int, err error) { + c.SetReadDeadline(time.Now().Add(c.timeout)) + n, err = c.conn.Read(b) + c.SetReadDeadline(time.Now().Add(c.longTimeout)) + return n, err +} + +func (c *timeoutConn) Write(b []byte) (n int, err error) { + c.SetWriteDeadline(time.Now().Add(c.timeout)) + n, err = c.conn.Write(b) + c.SetReadDeadline(time.Now().Add(c.longTimeout)) + return n, err +} + +func (c *timeoutConn) Close() error { + return c.conn.Close() +} + +func (c *timeoutConn) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +func (c *timeoutConn) RemoteAddr() net.Addr { + return c.conn.RemoteAddr() +} + +func (c *timeoutConn) SetDeadline(t time.Time) error { + return c.conn.SetDeadline(t) +} + +func (c *timeoutConn) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +func (c *timeoutConn) SetWriteDeadline(t time.Time) error { + return c.conn.SetWriteDeadline(t) +} + +// UrlMaker builds URL and resource +const ( + urlTypeCname = 1 + urlTypeIP = 2 + urlTypeAliyun = 3 +) + +type urlMaker struct { + Scheme string // HTTP or HTTPS + NetLoc string // Host or IP + Type int // 1 CNAME, 2 IP, 3 ALIYUN + IsProxy bool // Proxy +} + +// Init parses endpoint +func (um *urlMaker) Init(endpoint string, isCname bool, isProxy bool) error { + if strings.HasPrefix(endpoint, "http://") { + um.Scheme = "http" + um.NetLoc = endpoint[len("http://"):] + } else if strings.HasPrefix(endpoint, "https://") { + um.Scheme = "https" + um.NetLoc = endpoint[len("https://"):] + } else { + um.Scheme = "http" + um.NetLoc = endpoint + } + + //use url.Parse() to get real host + strUrl := um.Scheme + "://" + um.NetLoc + url, err := url.Parse(strUrl) + if err != nil { + return err + } + + um.NetLoc = url.Host + host, _, err := net.SplitHostPort(um.NetLoc) + if err != nil { + host = um.NetLoc + if host[0] == '[' && host[len(host)-1] == ']' { + host = host[1 : len(host)-1] + } + } + + ip := net.ParseIP(host) + if ip != nil { + um.Type = urlTypeIP + } else if isCname { + um.Type = urlTypeCname + } else { + um.Type = urlTypeAliyun + } + um.IsProxy = isProxy + + return nil +} + +// getURL gets URL +func (um urlMaker) getURL(bucket, object, params string) *url.URL { + host, path := um.buildURL(bucket, object) + addr := "" + if params == "" { + addr = fmt.Sprintf("%s://%s%s", um.Scheme, host, path) + } else { + addr = fmt.Sprintf("%s://%s%s?%s", um.Scheme, host, path, params) + } + uri, _ := url.ParseRequestURI(addr) + return uri +} + +// getSignURL gets sign URL +func (um urlMaker) getSignURL(bucket, object, params string) string { + host, path := um.buildURL(bucket, object) + return fmt.Sprintf("%s://%s%s?%s", um.Scheme, host, path, params) +} + +// getSignRtmpURL Build Sign Rtmp URL +func (um urlMaker) getSignRtmpURL(bucket, channelName, params string) string { + host, path := um.buildURL(bucket, "live") + + channelName = url.QueryEscape(channelName) + channelName = strings.Replace(channelName, "+", "%20", -1) + + return fmt.Sprintf("rtmp://%s%s/%s?%s", host, path, channelName, params) +} + +// buildURL builds URL +func (um urlMaker) buildURL(bucket, object string) (string, string) { + var host = "" + var path = "" + + object = url.QueryEscape(object) + object = strings.Replace(object, "+", "%20", -1) + + if um.Type == urlTypeCname { + host = um.NetLoc + path = "/" + object + } else if um.Type == urlTypeIP { + if bucket == "" { + host = um.NetLoc + path = "/" + } else { + host = um.NetLoc + path = fmt.Sprintf("/%s/%s", bucket, object) + } + } else { + if bucket == "" { + host = um.NetLoc + path = "/" + } else { + host = bucket + "." + um.NetLoc + path = "/" + object + } + } + + return host, path +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/conn_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/conn_test.go new file mode 100644 index 00000000..be57867e --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/conn_test.go @@ -0,0 +1,192 @@ +package oss + +import ( + "net/http" + "os" + "strings" + "time" + + . "gopkg.in/check.v1" +) + +type OssConnSuite struct{} + +var _ = Suite(&OssConnSuite{}) + +func (s *OssConnSuite) TestURLMarker(c *C) { + um := urlMaker{} + um.Init("docs.github.com", true, false) + c.Assert(um.Type, Equals, urlTypeCname) + c.Assert(um.Scheme, Equals, "http") + c.Assert(um.NetLoc, Equals, "docs.github.com") + + c.Assert(um.getURL("bucket", "object", "params").String(), Equals, "http://docs.github.com/object?params") + c.Assert(um.getURL("bucket", "object", "").String(), Equals, "http://docs.github.com/object") + c.Assert(um.getURL("", "object", "").String(), Equals, "http://docs.github.com/object") + + var conn Conn + conn.config = getDefaultOssConfig() + conn.config.AuthVersion = AuthV1 + c.Assert(conn.getResource("bucket", "object", "subres"), Equals, "/bucket/object?subres") + c.Assert(conn.getResource("bucket", "object", ""), Equals, "/bucket/object") + c.Assert(conn.getResource("", "object", ""), Equals, "/") + + um.Init("https://docs.github.com", true, false) + c.Assert(um.Type, Equals, urlTypeCname) + c.Assert(um.Scheme, Equals, "https") + c.Assert(um.NetLoc, Equals, "docs.github.com") + + um.Init("http://docs.github.com", true, false) + c.Assert(um.Type, Equals, urlTypeCname) + c.Assert(um.Scheme, Equals, "http") + c.Assert(um.NetLoc, Equals, "docs.github.com") + + um.Init("docs.github.com:8080", false, true) + c.Assert(um.Type, Equals, urlTypeAliyun) + c.Assert(um.Scheme, Equals, "http") + c.Assert(um.NetLoc, Equals, "docs.github.com:8080") + + c.Assert(um.getURL("bucket", "object", "params").String(), Equals, "http://bucket.docs.github.com:8080/object?params") + c.Assert(um.getURL("bucket", "object", "").String(), Equals, "http://bucket.docs.github.com:8080/object") + c.Assert(um.getURL("", "object", "").String(), Equals, "http://docs.github.com:8080/") + c.Assert(conn.getResource("bucket", "object", "subres"), Equals, "/bucket/object?subres") + c.Assert(conn.getResource("bucket", "object", ""), Equals, "/bucket/object") + c.Assert(conn.getResource("", "object", ""), Equals, "/") + + um.Init("https://docs.github.com:8080", false, true) + c.Assert(um.Type, Equals, urlTypeAliyun) + c.Assert(um.Scheme, Equals, "https") + c.Assert(um.NetLoc, Equals, "docs.github.com:8080") + + um.Init("127.0.0.1", false, true) + c.Assert(um.Type, Equals, urlTypeIP) + c.Assert(um.Scheme, Equals, "http") + c.Assert(um.NetLoc, Equals, "127.0.0.1") + + um.Init("http://127.0.0.1", false, false) + c.Assert(um.Type, Equals, urlTypeIP) + c.Assert(um.Scheme, Equals, "http") + c.Assert(um.NetLoc, Equals, "127.0.0.1") + c.Assert(um.getURL("bucket", "object", "params").String(), Equals, "http://127.0.0.1/bucket/object?params") + c.Assert(um.getURL("", "object", "params").String(), Equals, "http://127.0.0.1/?params") + + um.Init("https://127.0.0.1:8080", false, false) + c.Assert(um.Type, Equals, urlTypeIP) + c.Assert(um.Scheme, Equals, "https") + c.Assert(um.NetLoc, Equals, "127.0.0.1:8080") + + um.Init("http://[2401:b180::dc]", false, false) + c.Assert(um.Type, Equals, urlTypeIP) + c.Assert(um.Scheme, Equals, "http") + c.Assert(um.NetLoc, Equals, "[2401:b180::dc]") + + um.Init("https://[2401:b180::dc]:8080", false, false) + c.Assert(um.Type, Equals, urlTypeIP) + c.Assert(um.Scheme, Equals, "https") + c.Assert(um.NetLoc, Equals, "[2401:b180::dc]:8080") +} + +func (s *OssConnSuite) TestAuth(c *C) { + endpoint := "https://github.com/" + cfg := getDefaultOssConfig() + cfg.AuthVersion = AuthV1 + um := urlMaker{} + um.Init(endpoint, false, false) + conn := Conn{cfg, &um, nil} + uri := um.getURL("bucket", "object", "") + req := &http.Request{ + Method: "PUT", + URL: uri, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: make(http.Header), + Host: uri.Host, + } + + req.Header.Set("Content-Type", "text/html") + req.Header.Set("Date", "Thu, 17 Nov 2005 18:49:58 GMT") + req.Header.Set("Host", endpoint) + req.Header.Set("X-OSS-Meta-Your", "your") + req.Header.Set("X-OSS-Meta-Author", "foo@bar.com") + req.Header.Set("X-OSS-Magic", "abracadabra") + req.Header.Set("Content-Md5", "ODBGOERFMDMzQTczRUY3NUE3NzA5QzdFNUYzMDQxNEM=") + + conn.signHeader(req, conn.getResource("bucket", "object", "")) + testLogger.Println("AUTHORIZATION:", req.Header.Get(HTTPHeaderAuthorization)) +} + +func (s *OssConnSuite) TestConnToolFunc(c *C) { + err := CheckRespCode(202, []int{}) + c.Assert(err, NotNil) + + err = CheckRespCode(202, []int{404}) + c.Assert(err, NotNil) + + err = CheckRespCode(202, []int{202, 404}) + c.Assert(err, IsNil) + + srvErr, err := serviceErrFromXML([]byte(""), 312, "") + c.Assert(err, NotNil) + c.Assert(srvErr.StatusCode, Equals, 0) + + srvErr, err = serviceErrFromXML([]byte("ABC"), 312, "") + c.Assert(err, NotNil) + c.Assert(srvErr.StatusCode, Equals, 0) + + srvErr, err = serviceErrFromXML([]byte(""), 312, "") + c.Assert(err, IsNil) + c.Assert(srvErr.StatusCode, Equals, 312) + + unexpect := UnexpectedStatusCodeError{[]int{200}, 202} + c.Assert(len(unexpect.Error()) > 0, Equals, true) + c.Assert(unexpect.Got(), Equals, 202) + + fd, err := os.Open("../sample/BingWallpaper-2015-11-07.jpg") + c.Assert(err, IsNil) + fd.Close() + var out ProcessObjectResult + err = jsonUnmarshal(fd, &out) + c.Assert(err, NotNil) +} + +func (s *OssConnSuite) TestSignRtmpURL(c *C) { + cfg := getDefaultOssConfig() + + um := urlMaker{} + um.Init(endpoint, false, false) + conn := Conn{cfg, &um, nil} + + //Anonymous + channelName := "test-sign-rtmp-url" + playlistName := "playlist.m3u8" + expiration := time.Now().Unix() + 3600 + signedRtmpURL := conn.signRtmpURL(bucketName, channelName, playlistName, expiration) + playURL := getPublishURL(bucketName, channelName) + hasPrefix := strings.HasPrefix(signedRtmpURL, playURL) + c.Assert(hasPrefix, Equals, true) + + //empty playlist name + playlistName = "" + signedRtmpURL = conn.signRtmpURL(bucketName, channelName, playlistName, expiration) + playURL = getPublishURL(bucketName, channelName) + hasPrefix = strings.HasPrefix(signedRtmpURL, playURL) + c.Assert(hasPrefix, Equals, true) +} + +func (s *OssConnSuite) TestGetRtmpSignedStr(c *C) { + cfg := getDefaultOssConfig() + um := urlMaker{} + um.Init(endpoint, false, false) + conn := Conn{cfg, &um, nil} + + akIf := conn.config.GetCredentials() + + //Anonymous + channelName := "test-get-rtmp-signed-str" + playlistName := "playlist.m3u8" + expiration := time.Now().Unix() + 3600 + params := map[string]interface{}{} + signedStr := conn.getRtmpSignedStr(bucketName, channelName, playlistName, expiration, akIf.GetAccessKeySecret(), params) + c.Assert(signedStr, Equals, "") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/const.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/const.go new file mode 100644 index 00000000..cf3329f9 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/const.go @@ -0,0 +1,247 @@ +package oss + +import "os" + +// ACLType bucket/object ACL +type ACLType string + +const ( + // ACLPrivate definition : private read and write + ACLPrivate ACLType = "private" + + // ACLPublicRead definition : public read and private write + ACLPublicRead ACLType = "public-read" + + // ACLPublicReadWrite definition : public read and public write + ACLPublicReadWrite ACLType = "public-read-write" + + // ACLDefault Object. It's only applicable for object. + ACLDefault ACLType = "default" +) + +// bucket versioning status +type VersioningStatus string + +const ( + // Versioning Status definition: Enabled + VersionEnabled VersioningStatus = "Enabled" + + // Versioning Status definition: Suspended + VersionSuspended VersioningStatus = "Suspended" +) + +// MetadataDirectiveType specifying whether use the metadata of source object when copying object. +type MetadataDirectiveType string + +const ( + // MetaCopy the target object's metadata is copied from the source one + MetaCopy MetadataDirectiveType = "COPY" + + // MetaReplace the target object's metadata is created as part of the copy request (not same as the source one) + MetaReplace MetadataDirectiveType = "REPLACE" +) + +// TaggingDirectiveType specifying whether use the tagging of source object when copying object. +type TaggingDirectiveType string + +const ( + // TaggingCopy the target object's tagging is copied from the source one + TaggingCopy TaggingDirectiveType = "COPY" + + // TaggingReplace the target object's tagging is created as part of the copy request (not same as the source one) + TaggingReplace TaggingDirectiveType = "REPLACE" +) + +// AlgorithmType specifying the server side encryption algorithm name +type AlgorithmType string + +const ( + KMSAlgorithm AlgorithmType = "KMS" + AESAlgorithm AlgorithmType = "AES256" + SM4Algorithm AlgorithmType = "SM4" +) + +// StorageClassType bucket storage type +type StorageClassType string + +const ( + // StorageStandard standard + StorageStandard StorageClassType = "Standard" + + // StorageIA infrequent access + StorageIA StorageClassType = "IA" + + // StorageArchive archive + StorageArchive StorageClassType = "Archive" + + // StorageColdArchive cold archive + StorageColdArchive StorageClassType = "ColdArchive" +) + +//RedundancyType bucket data Redundancy type +type DataRedundancyType string + +const ( + // RedundancyLRS Local redundancy, default value + RedundancyLRS DataRedundancyType = "LRS" + + // RedundancyZRS Same city redundancy + RedundancyZRS DataRedundancyType = "ZRS" +) + +// PayerType the type of request payer +type PayerType string + +const ( + // Requester the requester who send the request + Requester PayerType = "Requester" + + // BucketOwner the requester who send the request + BucketOwner PayerType = "BucketOwner" +) + +//RestoreMode the restore mode for coldArchive object +type RestoreMode string + +const ( + //RestoreExpedited object will be restored in 1 hour + RestoreExpedited RestoreMode = "Expedited" + + //RestoreStandard object will be restored in 2-5 hours + RestoreStandard RestoreMode = "Standard" + + //RestoreBulk object will be restored in 5-10 hours + RestoreBulk RestoreMode = "Bulk" +) + +// HTTPMethod HTTP request method +type HTTPMethod string + +const ( + // HTTPGet HTTP GET + HTTPGet HTTPMethod = "GET" + + // HTTPPut HTTP PUT + HTTPPut HTTPMethod = "PUT" + + // HTTPHead HTTP HEAD + HTTPHead HTTPMethod = "HEAD" + + // HTTPPost HTTP POST + HTTPPost HTTPMethod = "POST" + + // HTTPDelete HTTP DELETE + HTTPDelete HTTPMethod = "DELETE" +) + +// HTTP headers +const ( + HTTPHeaderAcceptEncoding string = "Accept-Encoding" + HTTPHeaderAuthorization = "Authorization" + HTTPHeaderCacheControl = "Cache-Control" + HTTPHeaderContentDisposition = "Content-Disposition" + HTTPHeaderContentEncoding = "Content-Encoding" + HTTPHeaderContentLength = "Content-Length" + HTTPHeaderContentMD5 = "Content-MD5" + HTTPHeaderContentType = "Content-Type" + HTTPHeaderContentLanguage = "Content-Language" + HTTPHeaderDate = "Date" + HTTPHeaderEtag = "ETag" + HTTPHeaderExpires = "Expires" + HTTPHeaderHost = "Host" + HTTPHeaderLastModified = "Last-Modified" + HTTPHeaderRange = "Range" + HTTPHeaderLocation = "Location" + HTTPHeaderOrigin = "Origin" + HTTPHeaderServer = "Server" + HTTPHeaderUserAgent = "User-Agent" + HTTPHeaderIfModifiedSince = "If-Modified-Since" + HTTPHeaderIfUnmodifiedSince = "If-Unmodified-Since" + HTTPHeaderIfMatch = "If-Match" + HTTPHeaderIfNoneMatch = "If-None-Match" + HTTPHeaderACReqMethod = "Access-Control-Request-Method" + HTTPHeaderACReqHeaders = "Access-Control-Request-Headers" + + HTTPHeaderOssACL = "X-Oss-Acl" + HTTPHeaderOssMetaPrefix = "X-Oss-Meta-" + HTTPHeaderOssObjectACL = "X-Oss-Object-Acl" + HTTPHeaderOssSecurityToken = "X-Oss-Security-Token" + HTTPHeaderOssServerSideEncryption = "X-Oss-Server-Side-Encryption" + HTTPHeaderOssServerSideEncryptionKeyID = "X-Oss-Server-Side-Encryption-Key-Id" + HTTPHeaderOssServerSideDataEncryption = "X-Oss-Server-Side-Data-Encryption" + HTTPHeaderSSECAlgorithm = "X-Oss-Server-Side-Encryption-Customer-Algorithm" + HTTPHeaderSSECKey = "X-Oss-Server-Side-Encryption-Customer-Key" + HTTPHeaderSSECKeyMd5 = "X-Oss-Server-Side-Encryption-Customer-Key-MD5" + HTTPHeaderOssCopySource = "X-Oss-Copy-Source" + HTTPHeaderOssCopySourceRange = "X-Oss-Copy-Source-Range" + HTTPHeaderOssCopySourceIfMatch = "X-Oss-Copy-Source-If-Match" + HTTPHeaderOssCopySourceIfNoneMatch = "X-Oss-Copy-Source-If-None-Match" + HTTPHeaderOssCopySourceIfModifiedSince = "X-Oss-Copy-Source-If-Modified-Since" + HTTPHeaderOssCopySourceIfUnmodifiedSince = "X-Oss-Copy-Source-If-Unmodified-Since" + HTTPHeaderOssMetadataDirective = "X-Oss-Metadata-Directive" + HTTPHeaderOssNextAppendPosition = "X-Oss-Next-Append-Position" + HTTPHeaderOssRequestID = "X-Oss-Request-Id" + HTTPHeaderOssCRC64 = "X-Oss-Hash-Crc64ecma" + HTTPHeaderOssSymlinkTarget = "X-Oss-Symlink-Target" + HTTPHeaderOssStorageClass = "X-Oss-Storage-Class" + HTTPHeaderOssCallback = "X-Oss-Callback" + HTTPHeaderOssCallbackVar = "X-Oss-Callback-Var" + HTTPHeaderOssRequester = "X-Oss-Request-Payer" + HTTPHeaderOssTagging = "X-Oss-Tagging" + HTTPHeaderOssTaggingDirective = "X-Oss-Tagging-Directive" + HTTPHeaderOssTrafficLimit = "X-Oss-Traffic-Limit" + HTTPHeaderOssForbidOverWrite = "X-Oss-Forbid-Overwrite" + HTTPHeaderOssRangeBehavior = "X-Oss-Range-Behavior" + HTTPHeaderOssTaskID = "X-Oss-Task-Id" +) + +// HTTP Param +const ( + HTTPParamExpires = "Expires" + HTTPParamAccessKeyID = "OSSAccessKeyId" + HTTPParamSignature = "Signature" + HTTPParamSecurityToken = "security-token" + HTTPParamPlaylistName = "playlistName" + + HTTPParamSignatureVersion = "x-oss-signature-version" + HTTPParamExpiresV2 = "x-oss-expires" + HTTPParamAccessKeyIDV2 = "x-oss-access-key-id" + HTTPParamSignatureV2 = "x-oss-signature" + HTTPParamAdditionalHeadersV2 = "x-oss-additional-headers" +) + +// Other constants +const ( + MaxPartSize = 5 * 1024 * 1024 * 1024 // Max part size, 5GB + MinPartSize = 100 * 1024 // Min part size, 100KB + + FilePermMode = os.FileMode(0664) // Default file permission + + TempFilePrefix = "oss-go-temp-" // Temp file prefix + TempFileSuffix = ".temp" // Temp file suffix + + CheckpointFileSuffix = ".cp" // Checkpoint file suffix + + NullVersion = "null" + + Version = "v2.1.6" // Go SDK version +) + +// FrameType +const ( + DataFrameType = 8388609 + ContinuousFrameType = 8388612 + EndFrameType = 8388613 + MetaEndFrameCSVType = 8388614 + MetaEndFrameJSONType = 8388615 +) + +// AuthVersion the version of auth +type AuthVersionType string + +const ( + // AuthV1 v1 + AuthV1 AuthVersionType = "v1" + // AuthV2 v2 + AuthV2 AuthVersionType = "v2" +) diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crc.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crc.go new file mode 100644 index 00000000..c96694f2 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crc.go @@ -0,0 +1,123 @@ +package oss + +import ( + "hash" + "hash/crc64" +) + +// digest represents the partial evaluation of a checksum. +type digest struct { + crc uint64 + tab *crc64.Table +} + +// NewCRC creates a new hash.Hash64 computing the CRC64 checksum +// using the polynomial represented by the Table. +func NewCRC(tab *crc64.Table, init uint64) hash.Hash64 { return &digest{init, tab} } + +// Size returns the number of bytes sum will return. +func (d *digest) Size() int { return crc64.Size } + +// BlockSize returns the hash's underlying block size. +// The Write method must be able to accept any amount +// of data, but it may operate more efficiently if all writes +// are a multiple of the block size. +func (d *digest) BlockSize() int { return 1 } + +// Reset resets the hash to its initial state. +func (d *digest) Reset() { d.crc = 0 } + +// Write (via the embedded io.Writer interface) adds more data to the running hash. +// It never returns an error. +func (d *digest) Write(p []byte) (n int, err error) { + d.crc = crc64.Update(d.crc, d.tab, p) + return len(p), nil +} + +// Sum64 returns CRC64 value. +func (d *digest) Sum64() uint64 { return d.crc } + +// Sum returns hash value. +func (d *digest) Sum(in []byte) []byte { + s := d.Sum64() + return append(in, byte(s>>56), byte(s>>48), byte(s>>40), byte(s>>32), byte(s>>24), byte(s>>16), byte(s>>8), byte(s)) +} + +// gf2Dim dimension of GF(2) vectors (length of CRC) +const gf2Dim int = 64 + +func gf2MatrixTimes(mat []uint64, vec uint64) uint64 { + var sum uint64 + for i := 0; vec != 0; i++ { + if vec&1 != 0 { + sum ^= mat[i] + } + + vec >>= 1 + } + return sum +} + +func gf2MatrixSquare(square []uint64, mat []uint64) { + for n := 0; n < gf2Dim; n++ { + square[n] = gf2MatrixTimes(mat, mat[n]) + } +} + +// CRC64Combine combines CRC64 +func CRC64Combine(crc1 uint64, crc2 uint64, len2 uint64) uint64 { + var even [gf2Dim]uint64 // Even-power-of-two zeros operator + var odd [gf2Dim]uint64 // Odd-power-of-two zeros operator + + // Degenerate case + if len2 == 0 { + return crc1 + } + + // Put operator for one zero bit in odd + odd[0] = crc64.ECMA // CRC64 polynomial + var row uint64 = 1 + for n := 1; n < gf2Dim; n++ { + odd[n] = row + row <<= 1 + } + + // Put operator for two zero bits in even + gf2MatrixSquare(even[:], odd[:]) + + // Put operator for four zero bits in odd + gf2MatrixSquare(odd[:], even[:]) + + // Apply len2 zeros to crc1, first square will put the operator for one zero byte, eight zero bits, in even + for { + // Apply zeros operator for this bit of len2 + gf2MatrixSquare(even[:], odd[:]) + + if len2&1 != 0 { + crc1 = gf2MatrixTimes(even[:], crc1) + } + + len2 >>= 1 + + // If no more bits set, then done + if len2 == 0 { + break + } + + // Another iteration of the loop with odd and even swapped + gf2MatrixSquare(odd[:], even[:]) + if len2&1 != 0 { + crc1 = gf2MatrixTimes(odd[:], crc1) + } + len2 >>= 1 + + // If no more bits set, then done + if len2 == 0 { + break + } + } + + // Return combined CRC + crc1 ^= crc2 + return crc1 +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crc_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crc_test.go new file mode 100644 index 00000000..12548915 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crc_test.go @@ -0,0 +1,492 @@ +package oss + +import ( + "crypto/md5" + "encoding/base64" + "hash/crc64" + "io" + "io/ioutil" + "math/rand" + "os" + "strings" + + . "gopkg.in/check.v1" +) + +type OssCrcSuite struct { + client *Client + bucket *Bucket +} + +var _ = Suite(&OssCrcSuite{}) + +// SetUpSuite runs once when the suite starts running +func (s *OssCrcSuite) SetUpSuite(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + s.client = client + + s.client.CreateBucket(bucketName) + + bucket, err := s.client.Bucket(bucketName) + c.Assert(err, IsNil) + s.bucket = bucket + + testLogger.Println("test crc started") +} + +// TearDownSuite runs before each test or benchmark starts running +func (s *OssCrcSuite) TearDownSuite(c *C) { + // Delete part + keyMarker := KeyMarker("") + uploadIDMarker := UploadIDMarker("") + for { + lmur, err := s.bucket.ListMultipartUploads(keyMarker, uploadIDMarker) + c.Assert(err, IsNil) + for _, upload := range lmur.Uploads { + var imur = InitiateMultipartUploadResult{Bucket: s.bucket.BucketName, + Key: upload.Key, UploadID: upload.UploadID} + err = s.bucket.AbortMultipartUpload(imur) + c.Assert(err, IsNil) + } + keyMarker = KeyMarker(lmur.NextKeyMarker) + uploadIDMarker = UploadIDMarker(lmur.NextUploadIDMarker) + if !lmur.IsTruncated { + break + } + } + + // Delete objects + marker := Marker("") + for { + lor, err := s.bucket.ListObjects(marker) + c.Assert(err, IsNil) + for _, object := range lor.Objects { + err = s.bucket.DeleteObject(object.Key) + c.Assert(err, IsNil) + } + marker = Marker(lor.NextMarker) + if !lor.IsTruncated { + break + } + } + + // Delete bucket + err := s.client.DeleteBucket(s.bucket.BucketName) + c.Assert(err, IsNil) + + testLogger.Println("test crc completed") +} + +// SetUpTest runs after each test or benchmark runs +func (s *OssCrcSuite) SetUpTest(c *C) { + err := removeTempFiles("../oss", ".jpg") + c.Assert(err, IsNil) +} + +// TearDownTest runs once after all tests or benchmarks have finished running +func (s *OssCrcSuite) TearDownTest(c *C) { + err := removeTempFiles("../oss", ".jpg") + c.Assert(err, IsNil) +} + +// TestCRCGolden tests OSS's CRC64 +func (s *OssCrcSuite) TestCRCGolden(c *C) { + type crcTest struct { + out uint64 + in string + } + + var crcGolden = []crcTest{ + {0x0, ""}, + {0x3420000000000000, "a"}, + {0x36c4200000000000, "ab"}, + {0x3776c42000000000, "abc"}, + {0x336776c420000000, "abcd"}, + {0x32d36776c4200000, "abcde"}, + {0x3002d36776c42000, "abcdef"}, + {0x31b002d36776c420, "abcdefg"}, + {0xe21b002d36776c4, "abcdefgh"}, + {0x8b6e21b002d36776, "abcdefghi"}, + {0x7f5b6e21b002d367, "abcdefghij"}, + {0x8ec0e7c835bf9cdf, "Discard medicine more than two years old."}, + {0xc7db1759e2be5ab4, "He who has a shady past knows that nice guys finish last."}, + {0xfbf9d9603a6fa020, "I wouldn't marry him with a ten foot pole."}, + {0xeafc4211a6daa0ef, "Free! Free!/A trip/to Mars/for 900/empty jars/Burma Shave"}, + {0x3e05b21c7a4dc4da, "The days of the digital watch are numbered. -Tom Stoppard"}, + {0x5255866ad6ef28a6, "Nepal premier won't resign."}, + {0x8a79895be1e9c361, "For every action there is an equal and opposite government program."}, + {0x8878963a649d4916, "His money is twice tainted: 'taint yours and 'taint mine."}, + {0xa7b9d53ea87eb82f, "There is no reason for any individual to have a computer in their home. -Ken Olsen, 1977"}, + {0xdb6805c0966a2f9c, "It's a tiny change to the code and not completely disgusting. - Bob Manchek"}, + {0xf3553c65dacdadd2, "size: a.out: bad magic"}, + {0x9d5e034087a676b9, "The major problem is with sendmail. -Mark Horton"}, + {0xa6db2d7f8da96417, "Give me a rock, paper and scissors and I will move the world. CCFestoon"}, + {0x325e00cd2fe819f9, "If the enemy is within range, then so are you."}, + {0x88c6600ce58ae4c6, "It's well we cannot hear the screams/That we create in others' dreams."}, + {0x28c4a3f3b769e078, "You remind me of a TV show, but that's all right: I watch it anyway."}, + {0xa698a34c9d9f1dca, "C is as portable as Stonehedge!!"}, + {0xf6c1e2a8c26c5cfc, "Even if I could be Shakespeare, I think I should still choose to be Faraday. - A. Huxley"}, + {0xd402559dfe9b70c, "The fugacity of a constituent in a mixture of gases at a given temperature is proportional to its mole fraction. Lewis-Randall Rule"}, + {0xdb6efff26aa94946, "How can you write a big system without C++? -Paul Glick"}, + } + + var tab = crc64.MakeTable(crc64.ISO) + + for i := 0; i < len(crcGolden); i++ { + golden := crcGolden[i] + crc := NewCRC(tab, 0) + io.WriteString(crc, golden.in) + sum := crc.Sum64() + + c.Assert(sum, Equals, golden.out) + } +} + +// testCRC64Combine tests CRC64 on vector[0..pos] which should have CRC64 crc. +// Also test CRC64Combine on vector[] split in two. +func testCRC64Combine(c *C, str string, pos int, crc uint64) { + tabECMA := crc64.MakeTable(crc64.ECMA) + + // Test CRC64 + hash := crc64.New(tabECMA) + io.WriteString(hash, str) + crc1 := hash.Sum64() + c.Assert(crc1, Equals, crc) + + // Test CRC64 combine + hash = crc64.New(tabECMA) + io.WriteString(hash, str[0:pos]) + crc1 = hash.Sum64() + + hash = crc64.New(tabECMA) + io.WriteString(hash, str[pos:len(str)]) + crc2 := hash.Sum64() + + crc1 = CRC64Combine(crc1, crc2, uint64(len(str)-pos)) + c.Assert(crc1, Equals, crc) +} + +// TestCRCCombine tests CRC64Combine +func (s *OssCrcSuite) TestCRCCombine(c *C) { + str := "123456789" + testCRC64Combine(c, str, (len(str)+1)>>1, 0x995DC9BBDF1939FA) + + str = "This is a test of the emergency broadcast system." + testCRC64Combine(c, str, (len(str)+1)>>1, 0x27DB187FC15BBC72) +} + +// TestCRCRepeatedCombine tests CRC64Combine +func (s *OssCrcSuite) TestCRCRepeatedCombine(c *C) { + tab := crc64.MakeTable(crc64.ECMA) + str := "Even if I could be Shakespeare, I think I should still choose to be Faraday. - A. Huxley" + + for i := 0; i <= len(str); i++ { + hash := crc64.New(tab) + io.WriteString(hash, string(str[0:i])) + prev := hash.Sum64() + + hash = crc64.New(tab) + io.WriteString(hash, string(str[i:len(str)])) + post := hash.Sum64() + + crc := CRC64Combine(prev, post, uint64(len(str)-i)) + testLogger.Println("TestCRCRepeatedCombine:", prev, post, crc, i, len(str)) + c.Assert(crc == 0x7AD25FAFA1710407, Equals, true) + } +} + +// TestCRCRandomCombine tests CRC64Combine +func (s *OssCrcSuite) TestCRCRandomCombine(c *C) { + tab := crc64.MakeTable(crc64.ECMA) + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + + body, err := ioutil.ReadFile(fileName) + c.Assert(err, IsNil) + + for i := 0; i < 10; i++ { + fileParts, err := SplitFileByPartNum(fileName, 1+rand.Intn(9999)) + c.Assert(err, IsNil) + + var crc uint64 + for _, part := range fileParts { + calc := NewCRC(tab, 0) + calc.Write(body[part.Offset : part.Offset+part.Size]) + crc = CRC64Combine(crc, calc.Sum64(), (uint64)(part.Size)) + } + + testLogger.Println("TestCRCRandomCombine:", crc, i, fileParts) + c.Assert(crc == 0x2B612D24FFF64222, Equals, true) + } +} + +// TestEnableCRCAndMD5 tests MD5 and CRC check +func (s *OssCrcSuite) TestEnableCRCAndMD5(c *C) { + objectName := objectNamePrefix + RandStr(8) + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + newFileName := "BingWallpaper-2015-11-07-2.jpg" + objectValue := "空山新雨后,天气晚来秋。明月松间照,清泉石上流。竹喧归浣女,莲动下渔舟。随意春芳歇,王孙自可留。" + + client, err := New(endpoint, accessID, accessKey, EnableCRC(true), EnableMD5(true), MD5ThresholdCalcInMemory(200*1024)) + c.Assert(err, IsNil) + bucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + + // PutObject + err = bucket.PutObject(objectName, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // GetObject + body, err := bucket.GetObject(objectName) + c.Assert(err, IsNil) + _, err = ioutil.ReadAll(body) + c.Assert(err, IsNil) + body.Close() + + // GetObjectWithCRC + getResult, err := bucket.DoGetObject(&GetObjectRequest{objectName}, nil) + c.Assert(err, IsNil) + str, err := readBody(getResult.Response.Body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + c.Assert(getResult.ClientCRC.Sum64(), Equals, getResult.ServerCRC) + + // PutObjectFromFile + err = bucket.PutObjectFromFile(objectName, fileName) + c.Assert(err, IsNil) + + // GetObjectToFile + err = bucket.GetObjectToFile(objectName, newFileName) + c.Assert(err, IsNil) + eq, err := compareFiles(fileName, newFileName) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // DeleteObject + err = bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // AppendObject + var nextPos int64 + nextPos, err = bucket.AppendObject(objectName, strings.NewReader(objectValue), nextPos) + c.Assert(err, IsNil) + nextPos, err = bucket.AppendObject(objectName, strings.NewReader(objectValue), nextPos) + c.Assert(err, IsNil) + + err = bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + request := &AppendObjectRequest{ + ObjectKey: objectName, + Reader: strings.NewReader(objectValue), + Position: 0, + } + appendResult, err := bucket.DoAppendObject(request, []Option{InitCRC(0)}) + c.Assert(err, IsNil) + request.Position = appendResult.NextPosition + appendResult, err = bucket.DoAppendObject(request, []Option{InitCRC(appendResult.CRC)}) + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // MultipartUpload + chunks, err := SplitFileByPartSize(fileName, 100*1024) + imurUpload, err := bucket.InitiateMultipartUpload(objectName) + c.Assert(err, IsNil) + var partsUpload []UploadPart + + for _, chunk := range chunks { + part, err := bucket.UploadPartFromFile(imurUpload, fileName, chunk.Offset, chunk.Size, (int)(chunk.Number)) + c.Assert(err, IsNil) + partsUpload = append(partsUpload, part) + } + + _, err = bucket.CompleteMultipartUpload(imurUpload, partsUpload) + c.Assert(err, IsNil) + + // Check MultipartUpload + err = bucket.GetObjectToFile(objectName, newFileName) + c.Assert(err, IsNil) + eq, err = compareFiles(fileName, newFileName) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // DeleteObjects + _, err = bucket.DeleteObjects([]string{objectName}) + c.Assert(err, IsNil) +} + +// TestDisableCRCAndMD5 disables MD5 and CRC +func (s *OssCrcSuite) TestDisableCRCAndMD5(c *C) { + objectName := objectNamePrefix + RandStr(8) + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + newFileName := "BingWallpaper-2015-11-07-3.jpg" + objectValue := "中岁颇好道,晚家南山陲。兴来每独往,胜事空自知。行到水穷处,坐看云起时。偶然值林叟,谈笑无还期。" + + client, err := New(endpoint, accessID, accessKey, EnableCRC(false), EnableMD5(false)) + c.Assert(err, IsNil) + bucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + + // PutObject + err = bucket.PutObject(objectName, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // GetObject + body, err := bucket.GetObject(objectName) + c.Assert(err, IsNil) + _, err = ioutil.ReadAll(body) + c.Assert(err, IsNil) + body.Close() + + // GetObjectWithCRC + getResult, err := bucket.DoGetObject(&GetObjectRequest{objectName}, nil) + c.Assert(err, IsNil) + str, err := readBody(getResult.Response.Body) + c.Assert(err, IsNil) + c.Assert(str, Equals, objectValue) + + // PutObjectFromFile + err = bucket.PutObjectFromFile(objectName, fileName) + c.Assert(err, IsNil) + + // GetObjectToFile + err = bucket.GetObjectToFile(objectName, newFileName) + c.Assert(err, IsNil) + eq, err := compareFiles(fileName, newFileName) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // DeleteObject + err = bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // AppendObject + var nextPos int64 + nextPos, err = bucket.AppendObject(objectName, strings.NewReader(objectValue), nextPos) + c.Assert(err, IsNil) + nextPos, err = bucket.AppendObject(objectName, strings.NewReader(objectValue), nextPos) + c.Assert(err, IsNil) + + err = bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + request := &AppendObjectRequest{ + ObjectKey: objectName, + Reader: strings.NewReader(objectValue), + Position: 0, + } + appendResult, err := bucket.DoAppendObject(request, []Option{InitCRC(0)}) + c.Assert(err, IsNil) + request.Position = appendResult.NextPosition + appendResult, err = bucket.DoAppendObject(request, []Option{InitCRC(appendResult.CRC)}) + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // MultipartUpload + chunks, err := SplitFileByPartSize(fileName, 100*1024) + imurUpload, err := bucket.InitiateMultipartUpload(objectName) + c.Assert(err, IsNil) + var partsUpload []UploadPart + + for _, chunk := range chunks { + part, err := bucket.UploadPartFromFile(imurUpload, fileName, chunk.Offset, chunk.Size, (int)(chunk.Number)) + c.Assert(err, IsNil) + partsUpload = append(partsUpload, part) + } + + _, err = bucket.CompleteMultipartUpload(imurUpload, partsUpload) + c.Assert(err, IsNil) + + // Check MultipartUpload + err = bucket.GetObjectToFile(objectName, newFileName) + c.Assert(err, IsNil) + eq, err = compareFiles(fileName, newFileName) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // DeleteObjects + _, err = bucket.DeleteObjects([]string{objectName}) + c.Assert(err, IsNil) +} + +// TestSpecifyContentMD5 specifies MD5 +func (s *OssCrcSuite) TestSpecifyContentMD5(c *C) { + objectName := objectNamePrefix + RandStr(8) + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + objectValue := "积雨空林烟火迟,蒸藜炊黍饷东菑。漠漠水田飞白鹭,阴阴夏木啭黄鹂。山中习静观朝槿,松下清斋折露葵。野老与人争席罢,海鸥何事更相疑。" + + mh := md5.Sum([]byte(objectValue)) + md5B64 := base64.StdEncoding.EncodeToString(mh[:]) + + // PutObject + err := s.bucket.PutObject(objectName, strings.NewReader(objectValue), ContentMD5(md5B64)) + c.Assert(err, IsNil) + + // PutObjectFromFile + file, err := os.Open(fileName) + md5 := md5.New() + io.Copy(md5, file) + mdHex := base64.StdEncoding.EncodeToString(md5.Sum(nil)[:]) + err = s.bucket.PutObjectFromFile(objectName, fileName, ContentMD5(mdHex)) + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // AppendObject + var nextPos int64 + nextPos, err = s.bucket.AppendObject(objectName, strings.NewReader(objectValue), nextPos) + c.Assert(err, IsNil) + nextPos, err = s.bucket.AppendObject(objectName, strings.NewReader(objectValue), nextPos) + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + request := &AppendObjectRequest{ + ObjectKey: objectName, + Reader: strings.NewReader(objectValue), + Position: 0, + } + appendResult, err := s.bucket.DoAppendObject(request, []Option{InitCRC(0)}) + c.Assert(err, IsNil) + request.Position = appendResult.NextPosition + appendResult, err = s.bucket.DoAppendObject(request, []Option{InitCRC(appendResult.CRC)}) + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // MultipartUpload + imurUpload, err := s.bucket.InitiateMultipartUpload(objectName) + c.Assert(err, IsNil) + + var partsUpload []UploadPart + part, err := s.bucket.UploadPart(imurUpload, strings.NewReader(objectValue), (int64)(len([]byte(objectValue))), 1) + c.Assert(err, IsNil) + partsUpload = append(partsUpload, part) + + _, err = s.bucket.CompleteMultipartUpload(imurUpload, partsUpload) + c.Assert(err, IsNil) + + // DeleteObject + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +// TestAppendObjectNegative +func (s *OssCrcSuite) TestAppendObjectNegative(c *C) { + objectName := objectNamePrefix + RandStr(8) + objectValue := "空山不见人,但闻人语响。返影入深林,复照青苔上。" + + nextPos, err := s.bucket.AppendObject(objectName, strings.NewReader(objectValue), 0, InitCRC(0)) + c.Assert(err, IsNil) + + nextPos, err = s.bucket.AppendObject(objectName, strings.NewReader(objectValue), nextPos, InitCRC(0)) + c.Assert(err, NotNil) + c.Assert(strings.HasPrefix(err.Error(), "oss: the crc"), Equals, true) +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/aes_ctr.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/aes_ctr.go new file mode 100644 index 00000000..ba7ab3f6 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/aes_ctr.go @@ -0,0 +1,68 @@ +package osscrypto + +import ( + "crypto/aes" + "crypto/cipher" + "io" +) + +type aesCtr struct { + encrypter cipher.Stream + decrypter cipher.Stream +} + +func newAesCtr(cd CipherData) (Cipher, error) { + block, err := aes.NewCipher(cd.Key) + if err != nil { + return nil, err + } + + encrypter := cipher.NewCTR(block, cd.IV) + decrypter := cipher.NewCTR(block, cd.IV) + return &aesCtr{encrypter, decrypter}, nil +} + +func (c *aesCtr) Encrypt(src io.Reader) io.Reader { + reader := &ctrEncryptReader{ + encrypter: c.encrypter, + src: src, + } + return reader +} + +type ctrEncryptReader struct { + encrypter cipher.Stream + src io.Reader +} + +func (reader *ctrEncryptReader) Read(data []byte) (int, error) { + plainText := make([]byte, len(data), len(data)) + n, err := reader.src.Read(plainText) + if n > 0 { + plainText = plainText[0:n] + reader.encrypter.XORKeyStream(data, plainText) + } + return n, err +} + +func (c *aesCtr) Decrypt(src io.Reader) io.Reader { + return &ctrDecryptReader{ + decrypter: c.decrypter, + src: src, + } +} + +type ctrDecryptReader struct { + decrypter cipher.Stream + src io.Reader +} + +func (reader *ctrDecryptReader) Read(data []byte) (int, error) { + cryptoText := make([]byte, len(data), len(data)) + n, err := reader.src.Read(cryptoText) + if n > 0 { + cryptoText = cryptoText[0:n] + reader.decrypter.XORKeyStream(data, cryptoText) + } + return n, err +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/aes_ctr_cipher.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/aes_ctr_cipher.go new file mode 100644 index 00000000..fa60bd0e --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/aes_ctr_cipher.go @@ -0,0 +1,153 @@ +package osscrypto + +import ( + "io" +) + +const ( + aesKeySize = 32 + ivSize = 16 +) + +// aesCtrCipherBuilder for building ContentCipher +type aesCtrCipherBuilder struct { + MasterCipher MasterCipher +} + +// aesCtrCipher will use aes ctr algorithm +type aesCtrCipher struct { + CipherData CipherData + Cipher Cipher +} + +// CreateAesCtrCipher creates ContentCipherBuilder +func CreateAesCtrCipher(cipher MasterCipher) ContentCipherBuilder { + return aesCtrCipherBuilder{MasterCipher: cipher} +} + +// createCipherData create CipherData for encrypt object data +func (builder aesCtrCipherBuilder) createCipherData() (CipherData, error) { + var cd CipherData + var err error + err = cd.RandomKeyIv(aesKeySize, ivSize) + if err != nil { + return cd, err + } + + cd.WrapAlgorithm = builder.MasterCipher.GetWrapAlgorithm() + cd.CEKAlgorithm = AesCtrAlgorithm + cd.MatDesc = builder.MasterCipher.GetMatDesc() + + // EncryptedKey + cd.EncryptedKey, err = builder.MasterCipher.Encrypt(cd.Key) + if err != nil { + return cd, err + } + + // EncryptedIV + cd.EncryptedIV, err = builder.MasterCipher.Encrypt(cd.IV) + if err != nil { + return cd, err + } + + return cd, nil +} + +// contentCipherCD is used to create ContentCipher with CipherData +func (builder aesCtrCipherBuilder) contentCipherCD(cd CipherData) (ContentCipher, error) { + cipher, err := newAesCtr(cd) + if err != nil { + return nil, err + } + + return &aesCtrCipher{ + CipherData: cd, + Cipher: cipher, + }, nil +} + +// ContentCipher is used to create ContentCipher interface +func (builder aesCtrCipherBuilder) ContentCipher() (ContentCipher, error) { + cd, err := builder.createCipherData() + if err != nil { + return nil, err + } + return builder.contentCipherCD(cd) +} + +// ContentCipherEnv is used to create a decrption ContentCipher from Envelope +func (builder aesCtrCipherBuilder) ContentCipherEnv(envelope Envelope) (ContentCipher, error) { + var cd CipherData + cd.EncryptedKey = make([]byte, len(envelope.CipherKey)) + copy(cd.EncryptedKey, []byte(envelope.CipherKey)) + + plainKey, err := builder.MasterCipher.Decrypt([]byte(envelope.CipherKey)) + if err != nil { + return nil, err + } + cd.Key = make([]byte, len(plainKey)) + copy(cd.Key, plainKey) + + cd.EncryptedIV = make([]byte, len(envelope.IV)) + copy(cd.EncryptedIV, []byte(envelope.IV)) + + plainIV, err := builder.MasterCipher.Decrypt([]byte(envelope.IV)) + if err != nil { + return nil, err + } + + cd.IV = make([]byte, len(plainIV)) + copy(cd.IV, plainIV) + + cd.MatDesc = envelope.MatDesc + cd.WrapAlgorithm = envelope.WrapAlg + cd.CEKAlgorithm = envelope.CEKAlg + + return builder.contentCipherCD(cd) +} + +// GetMatDesc is used to get MasterCipher's MatDesc +func (builder aesCtrCipherBuilder) GetMatDesc() string { + return builder.MasterCipher.GetMatDesc() +} + +// EncryptContents will generate a random key and iv and encrypt the data using ctr +func (cc *aesCtrCipher) EncryptContent(src io.Reader) (io.ReadCloser, error) { + reader := cc.Cipher.Encrypt(src) + return &CryptoEncrypter{Body: src, Encrypter: reader}, nil +} + +// DecryptContent is used to decrypt object using ctr +func (cc *aesCtrCipher) DecryptContent(src io.Reader) (io.ReadCloser, error) { + reader := cc.Cipher.Decrypt(src) + return &CryptoDecrypter{Body: src, Decrypter: reader}, nil +} + +// GetCipherData is used to get cipher data information +func (cc *aesCtrCipher) GetCipherData() *CipherData { + return &(cc.CipherData) +} + +// GetCipherData returns cipher data +func (cc *aesCtrCipher) GetEncryptedLen(plainTextLen int64) int64 { + // AES CTR encryption mode does not change content length + return plainTextLen +} + +// GetAlignLen is used to get align length +func (cc *aesCtrCipher) GetAlignLen() int { + return len(cc.CipherData.IV) +} + +// Clone is used to create a new aesCtrCipher from itself +func (cc *aesCtrCipher) Clone(cd CipherData) (ContentCipher, error) { + cipher, err := newAesCtr(cd) + if err != nil { + return nil, err + } + + return &aesCtrCipher{ + CipherData: cd, + Cipher: cipher, + }, nil +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/aes_ctr_cipher_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/aes_ctr_cipher_test.go new file mode 100644 index 00000000..05f6a1ec --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/aes_ctr_cipher_test.go @@ -0,0 +1,41 @@ +package osscrypto + +import ( + . "gopkg.in/check.v1" +) + +func (s *OssCryptoBucketSuite) TestContentEncryptCipherError(c *C) { + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + cc, err := contentProvider.ContentCipher() + c.Assert(err, IsNil) + + var cipherData CipherData + cipherData.RandomKeyIv(31, 15) + + _, err = cc.Clone(cipherData) + c.Assert(err, NotNil) +} + +func (s *OssCryptoBucketSuite) TestCreateCipherDataError(c *C) { + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, "", "") + contentProvider := CreateAesCtrCipher(masterRsaCipher) + + v := contentProvider.(aesCtrCipherBuilder) + _, err := v.createCipherData() + c.Assert(err, NotNil) +} + +func (s *OssCryptoBucketSuite) TestContentCipherCDError(c *C) { + var cd CipherData + + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, "", "") + contentProvider := CreateAesCtrCipher(masterRsaCipher) + + v := contentProvider.(aesCtrCipherBuilder) + _, err := v.contentCipherCD(cd) + c.Assert(err, NotNil) +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/cipher.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/cipher.go new file mode 100644 index 00000000..7b512eae --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/cipher.go @@ -0,0 +1,69 @@ +package osscrypto + +import ( + "io" +) + +// Cipher is interface for encryption or decryption of an object +type Cipher interface { + Encrypter + Decrypter +} + +// Encrypter is interface with only encrypt method +type Encrypter interface { + Encrypt(io.Reader) io.Reader +} + +// Decrypter is interface with only decrypt method +type Decrypter interface { + Decrypt(io.Reader) io.Reader +} + +// CryptoEncrypter provides close method for Encrypter +type CryptoEncrypter struct { + Body io.Reader + Encrypter io.Reader + isClosed bool +} + +// Close lets the CryptoEncrypter satisfy io.ReadCloser interface +func (rc *CryptoEncrypter) Close() error { + rc.isClosed = true + if closer, ok := rc.Body.(io.ReadCloser); ok { + return closer.Close() + } + return nil +} + +// Read lets the CryptoEncrypter satisfy io.ReadCloser interface +func (rc *CryptoEncrypter) Read(b []byte) (int, error) { + if rc.isClosed { + return 0, io.EOF + } + return rc.Encrypter.Read(b) +} + +// CryptoDecrypter provides close method for Decrypter +type CryptoDecrypter struct { + Body io.Reader + Decrypter io.Reader + isClosed bool +} + +// Close lets the CryptoDecrypter satisfy io.ReadCloser interface +func (rc *CryptoDecrypter) Close() error { + rc.isClosed = true + if closer, ok := rc.Body.(io.ReadCloser); ok { + return closer.Close() + } + return nil +} + +// Read lets the CryptoDecrypter satisfy io.ReadCloser interface +func (rc *CryptoDecrypter) Read(b []byte) (int, error) { + if rc.isClosed { + return 0, io.EOF + } + return rc.Decrypter.Read(b) +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/cipher_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/cipher_test.go new file mode 100644 index 00000000..4cb013d7 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/cipher_test.go @@ -0,0 +1,32 @@ +package osscrypto + +import ( + "io" + "strings" + + . "gopkg.in/check.v1" +) + +func (s *OssCryptoBucketSuite) TestAesCtr(c *C) { + var cipherData CipherData + cipherData.RandomKeyIv(32, 16) + cipher, _ := newAesCtr(cipherData) + + byteReader := strings.NewReader(RandLowStr(100)) + enReader := cipher.Encrypt(byteReader) + encrypter := &CryptoEncrypter{Body: byteReader, Encrypter: enReader} + encrypter.Close() + buff := make([]byte, 10) + n, err := encrypter.Read(buff) + c.Assert(n, Equals, 0) + c.Assert(err, Equals, io.EOF) + + deReader := cipher.Encrypt(byteReader) + Decrypter := &CryptoDecrypter{Body: byteReader, Decrypter: deReader} + Decrypter.Close() + buff = make([]byte, 10) + n, err = Decrypter.Read(buff) + c.Assert(n, Equals, 0) + c.Assert(err, Equals, io.EOF) + +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_bucket.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_bucket.go new file mode 100644 index 00000000..1efeeeb2 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_bucket.go @@ -0,0 +1,539 @@ +package osscrypto + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "hash" + "hash/crc64" + "io" + "net/http" + "os" + "strconv" + + kms "github.com/aliyun/alibaba-cloud-sdk-go/services/kms" + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// MasterCipherManager is interface for getting master key with MatDesc(material desc) +// If you may use different master keys for encrypting and decrypting objects,each master +// key must have a unique, non-emtpy, unalterable MatDesc(json string format) and you must provide this interface +// If you always use the same master key for encrypting and decrypting objects, MatDesc +// can be empty and you don't need to provide this interface +// +// matDesc map[string]string:is converted by matDesc json string +// return: []string the secret key information,such as {"rsa-public-key","rsa-private-key"} or {"non-rsa-key"} +type MasterCipherManager interface { + GetMasterKey(matDesc map[string]string) ([]string, error) +} + +// ExtraCipherBuilder is interface for creating a decrypt ContentCipher with Envelope +// If the objects you need to decrypt are neither encrypted with ContentCipherBuilder +// you provided, nor encrypted with rsa and ali kms master keys, you must provide this interface +// +// ContentCipher the interface used to decrypt objects +type ExtraCipherBuilder interface { + GetDecryptCipher(envelope Envelope, cm MasterCipherManager) (ContentCipher, error) +} + +// CryptoBucketOption CryptoBucket option such as SetAliKmsClient, SetMasterCipherManager, SetDecryptCipherManager. +type CryptoBucketOption func(*CryptoBucket) + +// SetAliKmsClient set field AliKmsClient of CryptoBucket +// If the objects you need to decrypt are encrypted with ali kms master key,but not with ContentCipherBuilder +// you provided, you must provide this interface +func SetAliKmsClient(client *kms.Client) CryptoBucketOption { + return func(bucket *CryptoBucket) { + bucket.AliKmsClient = client + } +} + +// SetMasterCipherManager set field MasterCipherManager of CryptoBucket +func SetMasterCipherManager(manager MasterCipherManager) CryptoBucketOption { + return func(bucket *CryptoBucket) { + bucket.MasterCipherManager = manager + } +} + +// SetExtraCipherBuilder set field ExtraCipherBuilder of CryptoBucket +func SetExtraCipherBuilder(extraBuilder ExtraCipherBuilder) CryptoBucketOption { + return func(bucket *CryptoBucket) { + bucket.ExtraCipherBuilder = extraBuilder + } +} + +// DefaultExtraCipherBuilder is Default implementation of the ExtraCipherBuilder for rsa and kms master keys +type DefaultExtraCipherBuilder struct { + AliKmsClient *kms.Client +} + +// GetDecryptCipher is used to get ContentCipher for decrypt object +func (decb *DefaultExtraCipherBuilder) GetDecryptCipher(envelope Envelope, cm MasterCipherManager) (ContentCipher, error) { + if cm == nil { + return nil, fmt.Errorf("DefaultExtraCipherBuilder GetDecryptCipher error,MasterCipherManager is nil") + } + + if envelope.CEKAlg != AesCtrAlgorithm { + return nil, fmt.Errorf("DefaultExtraCipherBuilder GetDecryptCipher error,not supported content algorithm %s", envelope.CEKAlg) + } + + if envelope.WrapAlg != RsaCryptoWrap && envelope.WrapAlg != KmsAliCryptoWrap { + return nil, fmt.Errorf("DefaultExtraCipherBuilder GetDecryptCipher error,not supported envelope wrap algorithm %s", envelope.WrapAlg) + } + + matDesc := make(map[string]string) + if envelope.MatDesc != "" { + err := json.Unmarshal([]byte(envelope.MatDesc), &matDesc) + if err != nil { + return nil, err + } + } + + masterKeys, err := cm.GetMasterKey(matDesc) + if err != nil { + return nil, err + } + + var contentCipher ContentCipher + if envelope.WrapAlg == RsaCryptoWrap { + // for rsa master key + if len(masterKeys) != 2 { + return nil, fmt.Errorf("rsa keys count must be 2,now is %d", len(masterKeys)) + } + rsaCipher, err := CreateMasterRsa(matDesc, masterKeys[0], masterKeys[1]) + if err != nil { + return nil, err + } + aesCtrBuilder := CreateAesCtrCipher(rsaCipher) + contentCipher, err = aesCtrBuilder.ContentCipherEnv(envelope) + + } else if envelope.WrapAlg == KmsAliCryptoWrap { + // for kms master key + if len(masterKeys) != 1 { + return nil, fmt.Errorf("non-rsa keys count must be 1,now is %d", len(masterKeys)) + } + + if decb.AliKmsClient == nil { + return nil, fmt.Errorf("aliyun kms client is nil") + } + + kmsCipher, err := CreateMasterAliKms(matDesc, masterKeys[0], decb.AliKmsClient) + if err != nil { + return nil, err + } + aesCtrBuilder := CreateAesCtrCipher(kmsCipher) + contentCipher, err = aesCtrBuilder.ContentCipherEnv(envelope) + } else { + // to do + // for master keys which are neither rsa nor kms + } + + return contentCipher, err +} + +// CryptoBucket implements the operations for encrypting and decrypting objects +// ContentCipherBuilder is used to encrypt and decrypt objects by default +// when the object's MatDesc which you want to decrypt is emtpy or same to the +// master key's MatDesc you provided in ContentCipherBuilder, sdk try to +// use ContentCipherBuilder to decrypt +type CryptoBucket struct { + oss.Bucket + ContentCipherBuilder ContentCipherBuilder + ExtraCipherBuilder ExtraCipherBuilder + MasterCipherManager MasterCipherManager + AliKmsClient *kms.Client +} + +// GetCryptoBucket create a client encyrption bucket +func GetCryptoBucket(client *oss.Client, bucketName string, builder ContentCipherBuilder, + options ...CryptoBucketOption) (*CryptoBucket, error) { + var cryptoBucket CryptoBucket + cryptoBucket.Client = *client + cryptoBucket.BucketName = bucketName + cryptoBucket.ContentCipherBuilder = builder + + for _, option := range options { + option(&cryptoBucket) + } + + if cryptoBucket.ExtraCipherBuilder == nil { + cryptoBucket.ExtraCipherBuilder = &DefaultExtraCipherBuilder{AliKmsClient: cryptoBucket.AliKmsClient} + } + + return &cryptoBucket, nil +} + +// PutObject creates a new object and encyrpt it on client side when uploading to oss +func (bucket CryptoBucket) PutObject(objectKey string, reader io.Reader, options ...oss.Option) error { + options = bucket.AddEncryptionUaSuffix(options) + cc, err := bucket.ContentCipherBuilder.ContentCipher() + if err != nil { + return err + } + + cryptoReader, err := cc.EncryptContent(reader) + if err != nil { + return err + } + + var request *oss.PutObjectRequest + srcLen, err := oss.GetReaderLen(reader) + if err != nil { + request = &oss.PutObjectRequest{ + ObjectKey: objectKey, + Reader: cryptoReader, + } + } else { + encryptedLen := cc.GetEncryptedLen(srcLen) + request = &oss.PutObjectRequest{ + ObjectKey: objectKey, + Reader: oss.LimitReadCloser(cryptoReader, encryptedLen), + } + } + + opts := addCryptoHeaders(options, cc.GetCipherData()) + resp, err := bucket.DoPutObject(request, opts) + if err != nil { + return err + } + defer resp.Body.Close() + + return err +} + +// GetObject downloads the object from oss +// If the object is encrypted, sdk decrypt it automaticly +func (bucket CryptoBucket) GetObject(objectKey string, options ...oss.Option) (io.ReadCloser, error) { + options = bucket.AddEncryptionUaSuffix(options) + result, err := bucket.DoGetObject(&oss.GetObjectRequest{ObjectKey: objectKey}, options) + if err != nil { + return nil, err + } + return result.Response, nil +} + +// GetObjectToFile downloads the object from oss to local file +// If the object is encrypted, sdk decrypt it automaticly +func (bucket CryptoBucket) GetObjectToFile(objectKey, filePath string, options ...oss.Option) error { + options = bucket.AddEncryptionUaSuffix(options) + tempFilePath := filePath + oss.TempFileSuffix + + // Calls the API to actually download the object. Returns the result instance. + result, err := bucket.DoGetObject(&oss.GetObjectRequest{ObjectKey: objectKey}, options) + if err != nil { + return err + } + defer result.Response.Close() + + // If the local file does not exist, create a new one. If it exists, overwrite it. + fd, err := os.OpenFile(tempFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, oss.FilePermMode) + if err != nil { + return err + } + + // Copy the data to the local file path. + _, err = io.Copy(fd, result.Response.Body) + fd.Close() + if err != nil { + return err + } + + // Compares the CRC value + hasRange, _, _ := oss.IsOptionSet(options, oss.HTTPHeaderRange) + encodeOpt, _ := oss.FindOption(options, oss.HTTPHeaderAcceptEncoding, nil) + acceptEncoding := "" + if encodeOpt != nil { + acceptEncoding = encodeOpt.(string) + } + if bucket.GetConfig().IsEnableCRC && !hasRange && acceptEncoding != "gzip" { + result.Response.ClientCRC = result.ClientCRC.Sum64() + err = oss.CheckCRC(result.Response, "GetObjectToFile") + if err != nil { + os.Remove(tempFilePath) + return err + } + } + + return os.Rename(tempFilePath, filePath) +} + +// DoGetObject is the actual API that gets the encrypted or not encrypted object. +// It's the internal function called by other public APIs. +func (bucket CryptoBucket) DoGetObject(request *oss.GetObjectRequest, options []oss.Option) (*oss.GetObjectResult, error) { + options = bucket.AddEncryptionUaSuffix(options) + + // first,we must head object + metaInfo, err := bucket.GetObjectDetailedMeta(request.ObjectKey) + if err != nil { + return nil, err + } + + isEncryptedObj := isEncryptedObject(metaInfo) + if !isEncryptedObj { + return bucket.Bucket.DoGetObject(request, options) + } + + envelope, err := getEnvelopeFromHeader(metaInfo) + if err != nil { + return nil, err + } + + if !isValidContentAlg(envelope.CEKAlg) { + return nil, fmt.Errorf("not supported content algorithm %s,object:%s", envelope.CEKAlg, request.ObjectKey) + } + + if !envelope.IsValid() { + return nil, fmt.Errorf("getEnvelopeFromHeader error,object:%s", request.ObjectKey) + } + + // use ContentCipherBuilder to decrpt object by default + encryptMatDesc := bucket.ContentCipherBuilder.GetMatDesc() + var cc ContentCipher + err = nil + if envelope.MatDesc == encryptMatDesc { + cc, err = bucket.ContentCipherBuilder.ContentCipherEnv(envelope) + } else { + cc, err = bucket.ExtraCipherBuilder.GetDecryptCipher(envelope, bucket.MasterCipherManager) + } + + if err != nil { + return nil, fmt.Errorf("%s,object:%s", err.Error(), request.ObjectKey) + } + + discardFrontAlignLen := int64(0) + uRange, err := oss.GetRangeConfig(options) + if err != nil { + return nil, err + } + + if uRange != nil && uRange.HasStart { + // process range to align key size + adjustStart := adjustRangeStart(uRange.Start, cc) + discardFrontAlignLen = uRange.Start - adjustStart + if discardFrontAlignLen > 0 { + uRange.Start = adjustStart + options = oss.DeleteOption(options, oss.HTTPHeaderRange) + options = append(options, oss.NormalizedRange(oss.GetRangeString(*uRange))) + } + + // seek iv + cipherData := cc.GetCipherData().Clone() + cipherData.SeekIV(uint64(adjustStart)) + cc, _ = cc.Clone(cipherData) + } + + params, _ := oss.GetRawParams(options) + resp, err := bucket.Do("GET", request.ObjectKey, params, options, nil, nil) + if err != nil { + return nil, err + } + + result := &oss.GetObjectResult{ + Response: resp, + } + + // CRC + var crcCalc hash.Hash64 + hasRange, _, _ := oss.IsOptionSet(options, oss.HTTPHeaderRange) + if bucket.GetConfig().IsEnableCRC && !hasRange { + crcCalc = crc64.New(oss.CrcTable()) + result.ServerCRC = resp.ServerCRC + result.ClientCRC = crcCalc + } + + // Progress + listener := oss.GetProgressListener(options) + contentLen, _ := strconv.ParseInt(resp.Headers.Get(oss.HTTPHeaderContentLength), 10, 64) + resp.Body = oss.TeeReader(resp.Body, crcCalc, contentLen, listener, nil) + resp.Body, err = cc.DecryptContent(resp.Body) + if err == nil && discardFrontAlignLen > 0 { + resp.Body = &oss.DiscardReadCloser{ + RC: resp.Body, + Discard: int(discardFrontAlignLen)} + } + return result, err +} + +// PutObjectFromFile creates a new object from the local file +// the object will be encrypted automaticly on client side when uploaded to oss +func (bucket CryptoBucket) PutObjectFromFile(objectKey, filePath string, options ...oss.Option) error { + options = bucket.AddEncryptionUaSuffix(options) + fd, err := os.Open(filePath) + if err != nil { + return err + } + defer fd.Close() + + opts := oss.AddContentType(options, filePath, objectKey) + cc, err := bucket.ContentCipherBuilder.ContentCipher() + if err != nil { + return err + } + + cryptoReader, err := cc.EncryptContent(fd) + if err != nil { + return err + } + + var request *oss.PutObjectRequest + srcLen, err := oss.GetReaderLen(fd) + if err != nil { + request = &oss.PutObjectRequest{ + ObjectKey: objectKey, + Reader: cryptoReader, + } + } else { + encryptedLen := cc.GetEncryptedLen(srcLen) + request = &oss.PutObjectRequest{ + ObjectKey: objectKey, + Reader: oss.LimitReadCloser(cryptoReader, encryptedLen), + } + } + + opts = addCryptoHeaders(opts, cc.GetCipherData()) + resp, err := bucket.DoPutObject(request, opts) + if err != nil { + return err + } + defer resp.Body.Close() + return nil +} + +// AppendObject please refer to Bucket.AppendObject +func (bucket CryptoBucket) AppendObject(objectKey string, reader io.Reader, appendPosition int64, options ...oss.Option) (int64, error) { + return 0, fmt.Errorf("CryptoBucket doesn't support AppendObject") +} + +// DoAppendObject please refer to Bucket.DoAppendObject +func (bucket CryptoBucket) DoAppendObject(request *oss.AppendObjectRequest, options []oss.Option) (*oss.AppendObjectResult, error) { + return nil, fmt.Errorf("CryptoBucket doesn't support DoAppendObject") +} + +// PutObjectWithURL please refer to Bucket.PutObjectWithURL +func (bucket CryptoBucket) PutObjectWithURL(signedURL string, reader io.Reader, options ...oss.Option) error { + return fmt.Errorf("CryptoBucket doesn't support PutObjectWithURL") +} + +// PutObjectFromFileWithURL please refer to Bucket.PutObjectFromFileWithURL +func (bucket CryptoBucket) PutObjectFromFileWithURL(signedURL, filePath string, options ...oss.Option) error { + return fmt.Errorf("CryptoBucket doesn't support PutObjectFromFileWithURL") +} + +// DoPutObjectWithURL please refer to Bucket.DoPutObjectWithURL +func (bucket CryptoBucket) DoPutObjectWithURL(signedURL string, reader io.Reader, options []oss.Option) (*oss.Response, error) { + return nil, fmt.Errorf("CryptoBucket doesn't support DoPutObjectWithURL") +} + +// GetObjectWithURL please refer to Bucket.GetObjectWithURL +func (bucket CryptoBucket) GetObjectWithURL(signedURL string, options ...oss.Option) (io.ReadCloser, error) { + return nil, fmt.Errorf("CryptoBucket doesn't support GetObjectWithURL") +} + +// GetObjectToFileWithURL please refer to Bucket.GetObjectToFileWithURL +func (bucket CryptoBucket) GetObjectToFileWithURL(signedURL, filePath string, options ...oss.Option) error { + return fmt.Errorf("CryptoBucket doesn't support GetObjectToFileWithURL") +} + +// DoGetObjectWithURL please refer to Bucket.DoGetObjectWithURL +func (bucket CryptoBucket) DoGetObjectWithURL(signedURL string, options []oss.Option) (*oss.GetObjectResult, error) { + return nil, fmt.Errorf("CryptoBucket doesn't support DoGetObjectWithURL") +} + +// ProcessObject please refer to Bucket.ProcessObject +func (bucket CryptoBucket) ProcessObject(objectKey string, process string, options ...oss.Option) (oss.ProcessObjectResult, error) { + var out oss.ProcessObjectResult + return out, fmt.Errorf("CryptoBucket doesn't support ProcessObject") +} + +func (bucket CryptoBucket) AddEncryptionUaSuffix(options []oss.Option) []oss.Option { + var outOption []oss.Option + bSet, _, _ := oss.IsOptionSet(options, oss.HTTPHeaderUserAgent) + if bSet || bucket.Client.Config.UserSetUa { + outOption = options + return outOption + } + outOption = append(options, oss.UserAgentHeader(bucket.Client.Config.UserAgent+"/"+EncryptionUaSuffix)) + return outOption +} + +// isEncryptedObject judge the object is encrypted or not +func isEncryptedObject(headers http.Header) bool { + encrptedKey := headers.Get(oss.HTTPHeaderOssMetaPrefix + OssClientSideEncryptionKey) + return len(encrptedKey) > 0 +} + +// addCryptoHeaders save Envelope information in oss meta +func addCryptoHeaders(options []oss.Option, cd *CipherData) []oss.Option { + opts := []oss.Option{} + + // convert content-md5 + md5Option, _ := oss.FindOption(options, oss.HTTPHeaderContentMD5, nil) + if md5Option != nil { + opts = append(opts, oss.Meta(OssClientSideEncryptionUnencryptedContentMD5, md5Option.(string))) + options = oss.DeleteOption(options, oss.HTTPHeaderContentMD5) + } + + // convert content-length + lenOption, _ := oss.FindOption(options, oss.HTTPHeaderContentLength, nil) + if lenOption != nil { + opts = append(opts, oss.Meta(OssClientSideEncryptionUnencryptedContentLength, lenOption.(string))) + options = oss.DeleteOption(options, oss.HTTPHeaderContentLength) + } + + opts = append(opts, options...) + + // matDesc + if cd.MatDesc != "" { + opts = append(opts, oss.Meta(OssClientSideEncryptionMatDesc, cd.MatDesc)) + } + + // encrypted key + strEncryptedKey := base64.StdEncoding.EncodeToString(cd.EncryptedKey) + opts = append(opts, oss.Meta(OssClientSideEncryptionKey, strEncryptedKey)) + + // encrypted iv + strEncryptedIV := base64.StdEncoding.EncodeToString(cd.EncryptedIV) + opts = append(opts, oss.Meta(OssClientSideEncryptionStart, strEncryptedIV)) + + // wrap alg + opts = append(opts, oss.Meta(OssClientSideEncryptionWrapAlg, cd.WrapAlgorithm)) + + // cek alg + opts = append(opts, oss.Meta(OssClientSideEncryptionCekAlg, cd.CEKAlgorithm)) + + return opts +} + +func getEnvelopeFromHeader(header http.Header) (Envelope, error) { + var envelope Envelope + envelope.IV = header.Get(oss.HTTPHeaderOssMetaPrefix + OssClientSideEncryptionStart) + decodedIV, err := base64.StdEncoding.DecodeString(envelope.IV) + if err != nil { + return envelope, err + } + envelope.IV = string(decodedIV) + + envelope.CipherKey = header.Get(oss.HTTPHeaderOssMetaPrefix + OssClientSideEncryptionKey) + decodedKey, err := base64.StdEncoding.DecodeString(envelope.CipherKey) + if err != nil { + return envelope, err + } + envelope.CipherKey = string(decodedKey) + + envelope.MatDesc = header.Get(oss.HTTPHeaderOssMetaPrefix + OssClientSideEncryptionMatDesc) + envelope.WrapAlg = header.Get(oss.HTTPHeaderOssMetaPrefix + OssClientSideEncryptionWrapAlg) + envelope.CEKAlg = header.Get(oss.HTTPHeaderOssMetaPrefix + OssClientSideEncryptionCekAlg) + envelope.UnencryptedMD5 = header.Get(oss.HTTPHeaderOssMetaPrefix + OssClientSideEncryptionUnencryptedContentMD5) + envelope.UnencryptedContentLen = header.Get(oss.HTTPHeaderOssMetaPrefix + OssClientSideEncryptionUnencryptedContentLength) + return envelope, err +} + +func isValidContentAlg(algName string) bool { + // now content encyrption only support aec/ctr algorithm + return algName == AesCtrAlgorithm +} + +func adjustRangeStart(start int64, cc ContentCipher) int64 { + alignLen := int64(cc.GetAlignLen()) + return (start / alignLen) * alignLen +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_bucket_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_bucket_test.go new file mode 100644 index 00000000..a2c4d117 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_bucket_test.go @@ -0,0 +1,1226 @@ +package osscrypto + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "io" + "io/ioutil" + "log" + "math/rand" + math_rand "math/rand" + "net/http" + "os" + "strings" + "testing" + "time" + + kms "github.com/aliyun/alibaba-cloud-sdk-go/services/kms" + "github.com/aliyun/aliyun-oss-go-sdk/oss" + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { + TestingT(t) +} + +type OssCryptoBucketSuite struct { +} + +var _ = Suite(&OssCryptoBucketSuite{}) + +var ( + matDesc = make(map[string]string) + + rsaPublicKey string = `-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCokfiAVXXf5ImFzKDw+XO/UByW +6mse2QsIgz3ZwBtMNu59fR5zttSx+8fB7vR4CN3bTztrP9A6bjoN0FFnhlQ3vNJC +5MFO1PByrE/MNd5AAfSVba93I6sx8NSk5MzUCA4NJzAUqYOEWGtGBcom6kEF6MmR +1EKib1Id8hpooY5xaQIDAQAB +-----END PUBLIC KEY-----` + + rsaPrivateKey string = `-----BEGIN PRIVATE KEY----- +MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAKiR+IBVdd/kiYXM +oPD5c79QHJbqax7ZCwiDPdnAG0w27n19HnO21LH7x8Hu9HgI3dtPO2s/0DpuOg3Q +UWeGVDe80kLkwU7U8HKsT8w13kAB9JVtr3cjqzHw1KTkzNQIDg0nMBSpg4RYa0YF +yibqQQXoyZHUQqJvUh3yGmihjnFpAgMBAAECgYA49RmCQ14QyKevDfVTdvYlLmx6 +kbqgMbYIqk+7w611kxoCTMR9VMmJWgmk/Zic9mIAOEVbd7RkCdqT0E+xKzJJFpI2 +ZHjrlwb21uqlcUqH1Gn+wI+jgmrafrnKih0kGucavr/GFi81rXixDrGON9KBE0FJ +cPVdc0XiQAvCBnIIAQJBANXu3htPH0VsSznfqcDE+w8zpoAJdo6S/p30tcjsDQnx +l/jYV4FXpErSrtAbmI013VYkdJcghNSLNUXppfk2e8UCQQDJt5c07BS9i2SDEXiz +byzqCfXVzkdnDj9ry9mba1dcr9B9NCslVelXDGZKvQUBqNYCVxg398aRfWlYDTjU +IoVVAkAbTyjPN6R4SkC4HJMg5oReBmvkwFCAFsemBk0GXwuzD0IlJAjXnAZ+/rIO +ItewfwXIL1Mqz53lO/gK+q6TR585AkB304KUIoWzjyF3JqLP3IQOxzns92u9EV6l +V2P+CkbMPXiZV6sls6I4XppJXX2i3bu7iidN3/dqJ9izQK94fMU9AkBZvgsIPCot +y1/POIbv9LtnviDKrmpkXgVQSU4BmTPvXwTJm8APC7P/horSh3SVf1zgmnsyjm9D +hO92gGc+4ajL +-----END PRIVATE KEY-----` + + rsaPublicKeyPks1 string = `-----BEGIN RSA PUBLIC KEY----- +MIGJAoGBAKiR+IBVdd/kiYXMoPD5c79QHJbqax7ZCwiDPdnAG0w27n19HnO21LH7 +x8Hu9HgI3dtPO2s/0DpuOg3QUWeGVDe80kLkwU7U8HKsT8w13kAB9JVtr3cjqzHw +1KTkzNQIDg0nMBSpg4RYa0YFyibqQQXoyZHUQqJvUh3yGmihjnFpAgMBAAE= +-----END RSA PUBLIC KEY-----` + + rsaPrivateKeyPks1 string = `-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQCokfiAVXXf5ImFzKDw+XO/UByW6mse2QsIgz3ZwBtMNu59fR5z +ttSx+8fB7vR4CN3bTztrP9A6bjoN0FFnhlQ3vNJC5MFO1PByrE/MNd5AAfSVba93 +I6sx8NSk5MzUCA4NJzAUqYOEWGtGBcom6kEF6MmR1EKib1Id8hpooY5xaQIDAQAB +AoGAOPUZgkNeEMinrw31U3b2JS5sepG6oDG2CKpPu8OtdZMaAkzEfVTJiVoJpP2Y +nPZiADhFW3e0ZAnak9BPsSsySRaSNmR465cG9tbqpXFKh9Rp/sCPo4Jq2n65yood +JBrnGr6/xhYvNa14sQ6xjjfSgRNBSXD1XXNF4kALwgZyCAECQQDV7t4bTx9FbEs5 +36nAxPsPM6aACXaOkv6d9LXI7A0J8Zf42FeBV6RK0q7QG5iNNd1WJHSXIITUizVF +6aX5NnvFAkEAybeXNOwUvYtkgxF4s28s6gn11c5HZw4/a8vZm2tXXK/QfTQrJVXp +VwxmSr0FAajWAlcYN/fGkX1pWA041CKFVQJAG08ozzekeEpAuByTIOaEXgZr5MBQ +gBbHpgZNBl8Lsw9CJSQI15wGfv6yDiLXsH8FyC9TKs+d5Tv4Cvquk0efOQJAd9OC +lCKFs48hdyaiz9yEDsc57PdrvRFepVdj/gpGzD14mVerJbOiOF6aSV19ot27u4on +Td/3aifYs0CveHzFPQJAWb4LCDwqLctfzziG7/S7Z74gyq5qZF4FUElOAZkz718E +yZvADwuz/4aK0od0lX9c4Jp7Mo5vQ4TvdoBnPuGoyw== +-----END RSA PRIVATE KEY-----` +) + +var ( + // Endpoint/ID/Key + endpoint = os.Getenv("OSS_TEST_ENDPOINT") + accessID = os.Getenv("OSS_TEST_ACCESS_KEY_ID") + accessKey = os.Getenv("OSS_TEST_ACCESS_KEY_SECRET") + kmsID = os.Getenv("OSS_TEST_KMS_ID") + kmsRegion = os.Getenv("OSS_TEST_KMS_REGION") + kmsAccessID = accessID + kmsAccessKey = accessKey + bucketNamePrefix = "go-sdk-test-bucket-" + objectNamePrefix = "go-sdk-test-object-" +) + +var ( + logPath = "go_sdk_test_" + time.Now().Format("20060102_150405") + ".log" + testLogFile, _ = os.OpenFile(logPath, os.O_RDWR|os.O_CREATE, 0664) + testLogger = log.New(testLogFile, "", log.Ldate|log.Ltime|log.Lshortfile) + letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + timeoutInOperation = 3 * time.Second +) + +func RandStr(n int) string { + b := make([]rune, n) + randMarker := rand.New(rand.NewSource(time.Now().UnixNano())) + for i := range b { + b[i] = letters[randMarker.Intn(len(letters))] + } + return string(b) +} + +func RandLowStr(n int) string { + return strings.ToLower(RandStr(n)) +} + +func GetFileMD5(filePath string) (string, error) { + fd, err := os.Open(filePath) + if err != nil { + return "", err + } + defer fd.Close() + + md5 := md5.New() + _, err = io.Copy(md5, fd) + if err != nil { + return "", fmt.Errorf("buff copy error") + } + md5Str := hex.EncodeToString(md5.Sum(nil)) + return md5Str, nil +} + +func GetStringMd5(s string) string { + md5 := md5.New() + md5.Write([]byte(s)) + md5Str := hex.EncodeToString(md5.Sum(nil)) + return md5Str +} + +func ForceDeleteBucket(client *oss.Client, bucketName string, c *C) { + bucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + + // Delete Object + marker := oss.Marker("") + for { + lor, err := bucket.ListObjects(marker) + c.Assert(err, IsNil) + for _, object := range lor.Objects { + err = bucket.DeleteObject(object.Key) + c.Assert(err, IsNil) + } + marker = oss.Marker(lor.NextMarker) + if !lor.IsTruncated { + break + } + } + + // Delete Object Versions and DeleteMarks + keyMarker := oss.KeyMarker("") + versionIdMarker := oss.VersionIdMarker("") + options := []oss.Option{keyMarker, versionIdMarker} + for { + lor, err := bucket.ListObjectVersions(options...) + if err != nil { + break + } + + for _, object := range lor.ObjectDeleteMarkers { + err = bucket.DeleteObject(object.Key, oss.VersionId(object.VersionId)) + c.Assert(err, IsNil) + } + + for _, object := range lor.ObjectVersions { + err = bucket.DeleteObject(object.Key, oss.VersionId(object.VersionId)) + c.Assert(err, IsNil) + } + + keyMarker = oss.KeyMarker(lor.NextKeyMarker) + versionIdMarker := oss.VersionIdMarker(lor.NextVersionIdMarker) + options = []oss.Option{keyMarker, versionIdMarker} + + if !lor.IsTruncated { + break + } + } + + // Delete Part + keyMarker = oss.KeyMarker("") + uploadIDMarker := oss.UploadIDMarker("") + for { + lmur, err := bucket.ListMultipartUploads(keyMarker, uploadIDMarker) + c.Assert(err, IsNil) + for _, upload := range lmur.Uploads { + var imur = oss.InitiateMultipartUploadResult{Bucket: bucketName, + Key: upload.Key, UploadID: upload.UploadID} + err = bucket.AbortMultipartUpload(imur) + c.Assert(err, IsNil) + } + keyMarker = oss.KeyMarker(lmur.NextKeyMarker) + uploadIDMarker = oss.UploadIDMarker(lmur.NextUploadIDMarker) + if !lmur.IsTruncated { + break + } + } + + // delete live channel + strMarker := "" + for { + result, err := bucket.ListLiveChannel(oss.Marker(strMarker)) + c.Assert(err, IsNil) + + for _, channel := range result.LiveChannel { + err := bucket.DeleteLiveChannel(channel.Name) + c.Assert(err, IsNil) + } + + if result.IsTruncated { + strMarker = result.NextMarker + } else { + break + } + } + + // Delete Bucket + err = client.DeleteBucket(bucketName) + c.Assert(err, IsNil) +} + +func ReadBody(body io.ReadCloser) (string, error) { + data, err := ioutil.ReadAll(body) + body.Close() + if err != nil { + return "", err + } + return string(data), nil +} + +// SetUpSuite runs once when the suite starts running +func (s *OssCryptoBucketSuite) SetUpSuite(c *C) { +} + +// TearDownSuite runs before each test or benchmark starts running +func (s *OssCryptoBucketSuite) TearDownSuite(c *C) { +} + +// SetUpTest runs after each test or benchmark runs +func (s *OssCryptoBucketSuite) SetUpTest(c *C) { +} + +// TearDownTest runs once after all tests or benchmarks have finished running +func (s *OssCryptoBucketSuite) TearDownTest(c *C) { + +} + +func (s *OssCryptoBucketSuite) TestPutObjectNormalPks8(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + testMatDesc := make(map[string]string) + testMatDesc["desc"] = "test rsa key" + masterRsaCipher, _ := CreateMasterRsa(testMatDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + objectValue := RandStr(1023) + + // Put string + var respHeader http.Header + err = bucket.PutObject(objectName, strings.NewReader(objectValue), oss.GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + + // Check + body, err := bucket.GetObject(objectName) + c.Assert(err, IsNil) + text, err := ReadBody(body) + c.Assert(text, Equals, objectValue) + + // non-crypto bucket download + normalBucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + body, err = normalBucket.GetObject(objectName) + c.Assert(err, IsNil) + encryptText, err := ReadBody(body) + c.Assert(encryptText != objectValue, Equals, true) + + // acl + acl, err := bucket.GetObjectACL(objectName) + c.Assert(err, IsNil) + c.Assert(acl.ACL, Equals, "default") + + err = bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // put with meta + options := []oss.Option{ + oss.ObjectACL(oss.ACLPublicRead), + oss.Meta("myprop", "mypropval"), + } + err = bucket.PutObject(objectName, strings.NewReader(objectValue), options...) + c.Assert(err, IsNil) + + // Check + body, err = bucket.GetObject(objectName) + c.Assert(err, IsNil) + text, err = ReadBody(body) + c.Assert(err, IsNil) + c.Assert(text, Equals, objectValue) + + acl, err = bucket.GetObjectACL(objectName) + c.Assert(err, IsNil) + c.Assert(acl.ACL, Equals, string(oss.ACLPublicRead)) + + meta, err := bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + c.Assert(meta.Get("X-Oss-Meta-Myprop"), Equals, "mypropval") + + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestPutObjectNormalPks1(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKeyPks1, rsaPrivateKeyPks1) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + objectValue := RandStr(1023) + + // Put string + var respHeader http.Header + err = bucket.PutObject(objectName, strings.NewReader(objectValue), oss.GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + + // Check + body, err := bucket.GetObject(objectName) + c.Assert(err, IsNil) + text, err := ReadBody(body) + c.Assert(text, Equals, objectValue) + + // non-crypto bucket download + normalBucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + body, err = normalBucket.GetObject(objectName) + c.Assert(err, IsNil) + encryptText, err := ReadBody(body) + c.Assert(encryptText != objectValue, Equals, true) + + // acl + acl, err := bucket.GetObjectACL(objectName) + c.Assert(err, IsNil) + c.Assert(acl.ACL, Equals, "default") + + err = bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // put with meta + options := []oss.Option{ + oss.ObjectACL(oss.ACLPublicRead), + oss.Meta("myprop", "mypropval"), + } + err = bucket.PutObject(objectName, strings.NewReader(objectValue), options...) + c.Assert(err, IsNil) + + // Check + body, err = bucket.GetObject(objectName) + c.Assert(err, IsNil) + text, err = ReadBody(body) + c.Assert(err, IsNil) + c.Assert(text, Equals, objectValue) + + acl, err = bucket.GetObjectACL(objectName) + c.Assert(err, IsNil) + c.Assert(acl.ACL, Equals, string(oss.ACLPublicRead)) + + meta, err := bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + c.Assert(meta.Get("X-Oss-Meta-Myprop"), Equals, "mypropval") + + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestPutObjectEmptyPks1(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKeyPks1, rsaPrivateKeyPks1) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + objectValue := "" + + // Put empty string + var respHeader http.Header + err = bucket.PutObject(objectName, strings.NewReader(objectValue), oss.GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + + // Check + body, err := bucket.GetObject(objectName) + c.Assert(err, IsNil) + text, err := ReadBody(body) + c.Assert(text, Equals, objectValue) + + // non-crypto bucket download + normalBucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + body, err = normalBucket.GetObject(objectName) + c.Assert(err, IsNil) + encryptText, err := ReadBody(body) + c.Assert(encryptText == objectValue, Equals, true) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestPutObjectSmallSizePks1(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKeyPks1, rsaPrivateKeyPks1) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + objectValue := "123" + + var respHeader http.Header + err = bucket.PutObject(objectName, strings.NewReader(objectValue), oss.GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + + // Check + body, err := bucket.GetObject(objectName) + c.Assert(err, IsNil) + text, err := ReadBody(body) + c.Assert(text, Equals, objectValue) + + // non-crypto bucket download + normalBucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + body, err = normalBucket.GetObject(objectName) + c.Assert(err, IsNil) + encryptText, err := ReadBody(body) + c.Assert(encryptText != objectValue, Equals, true) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestPutObjectEmptyFilePks1(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKeyPks1, rsaPrivateKeyPks1) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + fileName := "oss-go-sdk-test-file-" + RandStr(5) + fo, err := os.Create(fileName) + c.Assert(err, IsNil) + _, err = fo.Write([]byte("")) + c.Assert(err, IsNil) + fo.Close() + + objectName := objectNamePrefix + RandStr(8) + + // file not exist + err = bucket.PutObjectFromFile(objectName, "/root1/abc.txt") + c.Assert(err, NotNil) + + err = bucket.PutObjectFromFile(objectName, fileName) + c.Assert(err, IsNil) + + downFileName := fileName + "-down" + + // Check + err = bucket.GetObjectToFile(objectName, downFileName) + c.Assert(err, IsNil) + + b1, err := ioutil.ReadFile(fileName) + b2, err := ioutil.ReadFile(downFileName) + c.Assert(len(b1), Equals, 0) + c.Assert(string(b1), Equals, string(b2)) + + os.Remove(downFileName) + os.Remove(fileName) + + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestKmsPutObjectNormal(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + kmsClient, err := kms.NewClientWithAccessKey(kmsRegion, kmsAccessID, kmsAccessKey) + c.Assert(err, IsNil) + + // crypto bucket + masterKmsCipher, _ := CreateMasterAliKms(matDesc, kmsID, kmsClient) + contentProvider := CreateAesCtrCipher(masterKmsCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + objectValue := RandStr(1023) + + // Put string + var respHeader http.Header + err = bucket.PutObject(objectName, strings.NewReader(objectValue), oss.GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + + // Check + body, err := bucket.GetObject(objectName) + c.Assert(err, IsNil) + text, err := ReadBody(body) + c.Assert(text, Equals, objectValue) + + // non-crypto bucket download + normalBucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + body, err = normalBucket.GetObject(objectName) + c.Assert(err, IsNil) + encryptText, err := ReadBody(body) + c.Assert(encryptText != objectValue, Equals, true) + + // acl + acl, err := bucket.GetObjectACL(objectName) + c.Assert(err, IsNil) + c.Assert(acl.ACL, Equals, "default") + + err = bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // put with meta + options := []oss.Option{ + oss.ObjectACL(oss.ACLPublicRead), + oss.Meta("myprop", "mypropval"), + } + err = bucket.PutObject(objectName, strings.NewReader(objectValue), options...) + c.Assert(err, IsNil) + + // Check + body, err = bucket.GetObject(objectName) + c.Assert(err, IsNil) + text, err = ReadBody(body) + c.Assert(err, IsNil) + c.Assert(text, Equals, objectValue) + + acl, err = bucket.GetObjectACL(objectName) + c.Assert(err, IsNil) + c.Assert(acl.ACL, Equals, string(oss.ACLPublicRead)) + + meta, err := bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + c.Assert(meta.Get("X-Oss-Meta-Myprop"), Equals, "mypropval") + + // put object error,bucket not exist + bucket.BucketName = bucket.BucketName + "-not-exist" + err = bucket.PutObject(objectName, strings.NewReader(objectValue), options...) + c.Assert(err, NotNil) + + ForceDeleteBucket(client, bucketName, c) +} + +type MockKmsManager struct { +} + +func (mg *MockKmsManager) GetMasterKey(matDesc map[string]string) ([]string, error) { + if len(matDesc) == 0 { + return nil, fmt.Errorf("not found") + } + + keyList := []string{kmsID} + return keyList, nil +} + +func (s *OssCryptoBucketSuite) TestRsaBucketDecrptObjectWithKmsSuccess(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + kmsClient, err := kms.NewClientWithAccessKey(kmsRegion, kmsAccessID, kmsAccessKey) + c.Assert(err, IsNil) + + // crypto bucket with kms + testMatDesc := make(map[string]string) + testMatDesc["desc"] = "test kms wrap" + masterKmsCipher, _ := CreateMasterAliKms(testMatDesc, kmsID, kmsClient) + contentProvider := CreateAesCtrCipher(masterKmsCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + objectValue := RandStr(1023) + + // Put string + var respHeader http.Header + err = bucket.PutObject(objectName, strings.NewReader(objectValue), oss.GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + + // crypto bucket with rsa + var masterManager MockKmsManager + var options []CryptoBucketOption + options = append(options, SetAliKmsClient(kmsClient)) + options = append(options, SetMasterCipherManager(&masterManager)) + + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + rsaProvider := CreateAesCtrCipher(masterRsaCipher) + rsaBucket, err := GetCryptoBucket(client, bucketName, rsaProvider, options...) + + // Check + body, err := rsaBucket.GetObject(objectName) + c.Assert(err, IsNil) + text, err := ReadBody(body) + c.Assert(text, Equals, objectValue) + + // non-crypto bucket download + normalBucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + body, err = normalBucket.GetObject(objectName) + c.Assert(err, IsNil) + encryptText, err := ReadBody(body) + c.Assert(encryptText != objectValue, Equals, true) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestRsaBucketDecrptObjectWithKmsError(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + kmsClient, err := kms.NewClientWithAccessKey(kmsRegion, kmsAccessID, kmsAccessKey) + c.Assert(err, IsNil) + + // crypto bucket with kms + testMatDesc := make(map[string]string) + testMatDesc["desc"] = "test kms wrap" + masterKmsCipher, _ := CreateMasterAliKms(testMatDesc, kmsID, kmsClient) + contentProvider := CreateAesCtrCipher(masterKmsCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + objectValue := RandStr(1023) + + // Put string + var respHeader http.Header + err = bucket.PutObject(objectName, strings.NewReader(objectValue), oss.GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + + // crypto bucket with rsa + var masterManager MockKmsManager + var options []CryptoBucketOption + + // kms client is nil + //options = append(options, SetAliKmsClient(kmsClient)) + + options = append(options, SetMasterCipherManager(&masterManager)) + + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + rsaProvider := CreateAesCtrCipher(masterRsaCipher) + rsaBucket, err := GetCryptoBucket(client, bucketName, rsaProvider, options...) + + // Check + _, err = rsaBucket.GetObject(objectName) + c.Assert(err, NotNil) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestRangeGetObject(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + contentLen := 1024 * 1024 + content := RandStr(contentLen) + err = bucket.PutObject(objectName, strings.NewReader(content)) + c.Assert(err, IsNil) + + // range get + for i := 0; i < 20; i++ { + math_rand.Seed(time.Now().UnixNano()) + rangeStart := rand.Intn(contentLen) + rangeEnd := rangeStart + rand.Intn(contentLen-rangeStart) + if rangeEnd == rangeStart || rangeStart >= contentLen-1 { + continue + } + + body, err := bucket.GetObject(objectName, oss.Range(int64(rangeStart), int64(rangeEnd))) + c.Assert(err, IsNil) + downText, err := ReadBody(body) + c.Assert(len(downText) > 0, Equals, true) + downMd5 := GetStringMd5(downText) + + srcText := content[rangeStart : rangeEnd+1] + srcMd5 := GetStringMd5(srcText) + + c.Assert(len(downText), Equals, len(srcText)) + c.Assert(downMd5, Equals, srcMd5) + } + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestGetNormalObject(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + // normal bucket + normalBucket, _ := client.Bucket(bucketName) + + objectName := objectNamePrefix + RandStr(8) + objectValue := RandStr(1023) + + // Put string + err = normalBucket.PutObject(objectName, strings.NewReader(objectValue)) + c.Assert(err, IsNil) + + // Check + body, err := bucket.GetObject(objectName) + c.Assert(err, IsNil) + text, err := ReadBody(body) + c.Assert(text, Equals, objectValue) + + // delete object + err = bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // get object again + body, err = bucket.GetObject(objectName) + c.Assert(err, NotNil) + + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestGetCryptoBucketNotSupport(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + objectValue := RandStr(1023) + + // AppendObject + _, err = bucket.AppendObject(objectName, strings.NewReader(objectValue), 0) + c.Assert(err, NotNil) + + // DoAppendObject + var request oss.AppendObjectRequest + var options []oss.Option + _, err = bucket.DoAppendObject(&request, options) + c.Assert(err, NotNil) + + // PutObjectWithURL + err = bucket.PutObjectWithURL("oss://bucket/object", strings.NewReader(objectValue)) + c.Assert(err, NotNil) + + // PutObjectFromFileWithURL + err = bucket.PutObjectFromFileWithURL("oss://bucket/object", "file.txt") + c.Assert(err, NotNil) + + // DoPutObjectWithURL + _, err = bucket.DoPutObjectWithURL("oss://bucket/object", strings.NewReader(objectValue), options) + c.Assert(err, NotNil) + + // GetObjectWithURL + _, err = bucket.GetObjectWithURL("oss://bucket/object") + c.Assert(err, NotNil) + + // GetObjectToFileWithURL + err = bucket.GetObjectToFileWithURL("oss://bucket/object", "file.txt") + c.Assert(err, NotNil) + + // DoGetObjectWithURL + _, err = bucket.DoGetObjectWithURL("oss://bucket/object", options) + c.Assert(err, NotNil) + + // ProcessObject + _, err = bucket.ProcessObject("oss://bucket/object", "") + c.Assert(err, NotNil) + + // DownloadFile + err = bucket.DownloadFile(objectName, "file.txt", 1024) + c.Assert(err, NotNil) + + // CopyFile + err = bucket.CopyFile("src-bucket", "src-object", "dest-object", 1024) + c.Assert(err, NotNil) + + // UploadFile + err = bucket.UploadFile(objectName, "file.txt", 1024) + c.Assert(err, NotNil) +} + +type MockRsaManager struct { +} + +func (mg *MockRsaManager) GetMasterKey(matDesc map[string]string) ([]string, error) { + if len(matDesc) == 0 { + return nil, fmt.Errorf("not found") + } + + keyList := []string{rsaPublicKey, rsaPrivateKey} + return keyList, nil +} + +func (s *OssCryptoBucketSuite) TestGetMasterKey(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + testMatDesc := make(map[string]string) + testMatDesc["desc"] = "test rsa key" + masterRsaCipher, _ := CreateMasterRsa(testMatDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + + fileName := "../../sample/BingWallpaper-2015-11-07.jpg" + srcMD5, err := GetFileMD5(fileName) + c.Assert(err, IsNil) + + err = bucket.PutObjectFromFile(objectName, fileName) + c.Assert(err, IsNil) + + // other crypto bucket + var rsaManager MockRsaManager + masterRsaCipherOther, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProviderOther := CreateAesCtrCipher(masterRsaCipherOther) + bucketOther, err := GetCryptoBucket(client, bucketName, contentProviderOther, SetMasterCipherManager(&rsaManager)) + + // download + downfileName := "test-go-sdk-file-" + RandLowStr(5) + ".jpg" + err = bucketOther.GetObjectToFile(objectName, downfileName) + c.Assert(err, IsNil) + downFileMD5, err := GetFileMD5(downfileName) + c.Assert(err, IsNil) + c.Assert(downFileMD5, Equals, srcMD5) + + // GetObjectToFile error + err = bucketOther.GetObjectToFile(objectName, "/root1/"+downfileName) + c.Assert(err, NotNil) + + os.Remove(downfileName) + ForceDeleteBucket(client, bucketName, c) +} + +type MockReader struct { + Reader io.Reader +} + +func (r *MockReader) Read(b []byte) (int, error) { + return r.Reader.Read(b) +} + +func (s *OssCryptoBucketSuite) TestPutObjectUnkownReaderLen(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + objectValue := RandStr(1023) + + srcMD5 := GetStringMd5(objectValue) + options := []oss.Option{oss.ContentMD5(srcMD5), oss.ContentLength(1023)} + + // Put string + mockReader := &MockReader{strings.NewReader(objectValue)} + err = bucket.PutObject(objectName, mockReader, options...) + c.Assert(err, IsNil) + + // Check + body, err := bucket.GetObject(objectName) + c.Assert(err, IsNil) + text, err := ReadBody(body) + c.Assert(text, Equals, objectValue) + + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestGetDecryptCipher(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + var rsaManager MockRsaManager + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider, SetMasterCipherManager(&rsaManager)) + + objectName := objectNamePrefix + RandStr(8) + objectValue := RandStr(1023) + + // Put string + var respHeader http.Header + err = bucket.PutObject(objectName, strings.NewReader(objectValue), oss.GetResponseHeader(&respHeader)) + c.Assert(err, IsNil) + + // first,we must head object + metaInfo, err := bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + + envelope, _ := getEnvelopeFromHeader(metaInfo) + + // test for getEnvelopeFromHeader + metaInfo.Set(oss.HTTPHeaderOssMetaPrefix+OssClientSideEncryptionKey, string([]byte{200, 200, 200})) + _, err = getEnvelopeFromHeader(metaInfo) + c.Assert(err, NotNil) + metaInfo.Set(oss.HTTPHeaderOssMetaPrefix+OssClientSideEncryptionKey, envelope.CipherKey) + + metaInfo.Set(oss.HTTPHeaderOssMetaPrefix+OssClientSideEncryptionStart, string([]byte{200, 200, 200})) + _, err = getEnvelopeFromHeader(metaInfo) + c.Assert(err, NotNil) + metaInfo.Set(oss.HTTPHeaderOssMetaPrefix+OssClientSideEncryptionKey, envelope.IV) + + // test for getDecryptCipher + CEKAlg := envelope.CEKAlg + envelope.CEKAlg = "" + _, err = bucket.ExtraCipherBuilder.GetDecryptCipher(envelope, bucket.MasterCipherManager) + c.Assert(err, NotNil) + envelope.CEKAlg = CEKAlg + + // matDesc is emtpy + bucket.MasterCipherManager = &MockRsaManager{} + _, err = bucket.ExtraCipherBuilder.GetDecryptCipher(envelope, bucket.MasterCipherManager) + c.Assert(err, NotNil) + + // MasterCipherManager is nil + bucket.MasterCipherManager = nil + _, err = bucket.ExtraCipherBuilder.GetDecryptCipher(envelope, bucket.MasterCipherManager) + c.Assert(err, NotNil) + + WrapAlg := envelope.WrapAlg + envelope.WrapAlg = "test" + _, err = bucket.ExtraCipherBuilder.GetDecryptCipher(envelope, bucket.MasterCipherManager) + c.Assert(err, NotNil) + envelope.WrapAlg = WrapAlg + + envelope.WrapAlg = KmsAliCryptoWrap + _, err = bucket.ExtraCipherBuilder.GetDecryptCipher(envelope, bucket.MasterCipherManager) + c.Assert(err, NotNil) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestGetObjectEncryptedByCppRsa(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // put object encrypted by cpp + bucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + + objectName := objectNamePrefix + RandStr(8) + srcJpgFile := "../../sample/test-client-encryption-src.jpg" + fileEncryptedByCpp := "../../sample/test-client-encryption-crypto-cpp-rsa.jpg" + + opts := []oss.Option{} + opts = append(opts, oss.Meta(OssClientSideEncryptionKey, "nyXOp7delQ/MQLjKQMhHLaT0w7u2yQoDLkSnK8MFg/MwYdh4na4/LS8LLbLcM18m8I/ObWUHU775I50sJCpdv+f4e0jLeVRRiDFWe+uo7Puc9j4xHj8YB3QlcIOFQiTxHIB6q+C+RA6lGwqqYVa+n3aV5uWhygyv1MWmESurppg=")) + opts = append(opts, oss.Meta(OssClientSideEncryptionStart, "De/S3T8wFjx7QPxAAFl7h7TeI2EsZlfCwox4WhLGng5DK2vNXxULmulMUUpYkdc9umqmDilgSy5Z3Foafw+v4JJThfw68T/9G2gxZLrQTbAlvFPFfPM9Ehk6cY4+8WpY32uN8w5vrHyoSZGr343NxCUGIp6fQ9sSuOLMoJg7hNw=")) + opts = append(opts, oss.Meta(OssClientSideEncryptionWrapAlg, "RSA/NONE/PKCS1Padding")) + opts = append(opts, oss.Meta(OssClientSideEncryptionCekAlg, "AES/CTR/NoPadding")) + err = bucket.PutObjectFromFile(objectName, fileEncryptedByCpp, opts...) + c.Assert(err, IsNil) + + // download with crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + cryptoBucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + downFileName := "oss-go-sdk-test-file-" + RandStr(5) + err = cryptoBucket.GetObjectToFile(objectName, downFileName) + c.Assert(err, IsNil) + + downMd5, _ := GetFileMD5(downFileName) + srcJpgMd5, _ := GetFileMD5(srcJpgFile) + c.Assert(downMd5, Equals, srcJpgMd5) + os.Remove(downFileName) + + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestGetObjectEncryptedByPythonRsa(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // put object encrypted by python + bucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + + objectName := objectNamePrefix + RandStr(8) + srcJpgFile := "../../sample/test-client-encryption-src.jpg" + fileEncryptedByCpp := "../../sample/test-client-encryption-crypto-python-rsa.jpg" + + opts := []oss.Option{} + opts = append(opts, oss.Meta(OssClientSideEncryptionKey, "ZNQM4g+JykUfOBMkfL8kbvChD3R23UH53sRyTg42h9H2ph8ZJJlo2tSP5Oi3nR5gJAwA/OTrruNq02M2Zt4N7zVWdbFArKbY/CkHpihVYOqsSU4Z8RmrNBm4QfC5om2WElRHNt8hlqhnvzhdorGDB5OoMQ8KvQqXDC53aM5OY64=")) + opts = append(opts, oss.Meta(OssClientSideEncryptionStart, "mZ6kts6kaMm++0akhQQZl+tj8gPWznZ+giHciCQTIzriwBzZZO4d85YZeBStuUPshdnO3QHK63/NH9QFL6pwpLiXI9UZxkGygkp82oB4jaF4HKoQ4ujd670pXLxpljBLnp0sCxiCIaf5Fzp4jgNCurXycY10/5DN7yPPtdw7dkk=")) + opts = append(opts, oss.Meta(OssClientSideEncryptionWrapAlg, "RSA/NONE/PKCS1Padding")) + opts = append(opts, oss.Meta(OssClientSideEncryptionCekAlg, "AES/CTR/NoPadding")) + err = bucket.PutObjectFromFile(objectName, fileEncryptedByCpp, opts...) + c.Assert(err, IsNil) + + // download with crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + cryptoBucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + downFileName := "oss-go-sdk-test-file-" + RandStr(5) + err = cryptoBucket.GetObjectToFile(objectName, downFileName) + c.Assert(err, IsNil) + + downMd5, _ := GetFileMD5(downFileName) + srcJpgMd5, _ := GetFileMD5(srcJpgFile) + c.Assert(downMd5, Equals, srcJpgMd5) + os.Remove(downFileName) + + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestRepeatedPutObjectFromFile(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + objectName := objectNamePrefix + RandStr(8) + srcJpgFile := "../../sample/test-client-encryption-src.jpg" + + // put object from file + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + cryptoBucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + err = cryptoBucket.PutObjectFromFile(objectName, srcJpgFile) + c.Assert(err, IsNil) + + downFileName := "oss-go-sdk-test-file-" + RandStr(5) + err = cryptoBucket.GetObjectToFile(objectName, downFileName) + c.Assert(err, IsNil) + + srcJpgMd5, _ := GetFileMD5(srcJpgFile) + downMd5, _ := GetFileMD5(downFileName) + c.Assert(len(srcJpgMd5) > 0, Equals, true) + c.Assert(len(downMd5) > 0, Equals, true) + c.Assert(downMd5, Equals, srcJpgMd5) + os.Remove(downFileName) + + err = cryptoBucket.PutObjectFromFile(objectName+"-other", srcJpgFile) + c.Assert(err, IsNil) + err = cryptoBucket.GetObjectToFile(objectName, downFileName) + c.Assert(err, IsNil) + downMd5, _ = GetFileMD5(downFileName) + c.Assert(downMd5, Equals, srcJpgMd5) + + os.Remove(downFileName) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestPutObjectEncryptionUserAgent(c *C) { + logName := "." + string(os.PathSeparator) + "test-go-sdk.log" + RandStr(5) + f, err := os.OpenFile(logName, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0660) + c.Assert(err, IsNil) + + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + client.Config.LogLevel = oss.Debug + client.Config.Logger = log.New(f, "", log.LstdFlags) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + objectName := objectNamePrefix + RandStr(8) + srcJpgFile := "../../sample/test-client-encryption-src.jpg" + + // put object from file + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + cryptoBucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + err = cryptoBucket.PutObjectFromFile(objectName, srcJpgFile) + c.Assert(err, IsNil) + + // read log file,get http info + contents, err := ioutil.ReadFile(logName) + c.Assert(err, IsNil) + + httpContent := string(contents) + c.Assert(strings.Contains(httpContent, EncryptionUaSuffix), Equals, true) + + f.Close() + os.Remove(logName) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestPutObjectNormalUserAgent(c *C) { + logName := "." + string(os.PathSeparator) + "test-go-sdk.log" + RandStr(5) + f, err := os.OpenFile(logName, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0660) + c.Assert(err, IsNil) + + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + client.Config.LogLevel = oss.Debug + client.Config.Logger = log.New(f, "", log.LstdFlags) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + objectName := objectNamePrefix + RandStr(8) + srcJpgFile := "../../sample/test-client-encryption-src.jpg" + + bucket, err := client.Bucket(bucketName) + + err = bucket.PutObjectFromFile(objectName, srcJpgFile) + c.Assert(err, IsNil) + + // read log file,get http info + contents, err := ioutil.ReadFile(logName) + c.Assert(err, IsNil) + + httpContent := string(contents) + c.Assert(strings.Contains(httpContent, EncryptionUaSuffix), Equals, false) + + f.Close() + os.Remove(logName) + ForceDeleteBucket(client, bucketName, c) +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_const.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_const.go new file mode 100644 index 00000000..73f46224 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_const.go @@ -0,0 +1,26 @@ +package osscrypto + +// for client sider encryption oss meta +const ( + OssClientSideEncryptionKey string = "client-side-encryption-key" + OssClientSideEncryptionStart = "client-side-encryption-start" + OssClientSideEncryptionCekAlg = "client-side-encryption-cek-alg" + OssClientSideEncryptionWrapAlg = "client-side-encryption-wrap-alg" + OssClientSideEncryptionMatDesc = "client-side-encryption-matdesc" + OssClientSideEncryptionUnencryptedContentLength = "client-side-encryption-unencrypted-content-length" + OssClientSideEncryptionUnencryptedContentMD5 = "client-side-encryption-unencrypted-content-md5" + OssClientSideEncryptionDataSize = "client-side-encryption-data-size" + OssClientSideEncryptionPartSize = "client-side-encryption-part-size" +) + +// encryption Algorithm +const ( + RsaCryptoWrap string = "RSA/NONE/PKCS1Padding" + KmsAliCryptoWrap string = "KMS/ALICLOUD" + AesCtrAlgorithm string = "AES/CTR/NoPadding" +) + +// user agent tag for client encryption +const ( + EncryptionUaSuffix string = "OssEncryptionClient" +) diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_download.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_download.go new file mode 100644 index 00000000..2c924e0d --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_download.go @@ -0,0 +1,12 @@ +package osscrypto + +import ( + "fmt" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// DownloadFile with multi part mode, temporarily not supported +func (bucket CryptoBucket) DownloadFile(objectKey, filePath string, partSize int64, options ...oss.Option) error { + return fmt.Errorf("CryptoBucket doesn't support DownloadFile") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_multicopy.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_multicopy.go new file mode 100644 index 00000000..331466c2 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_multicopy.go @@ -0,0 +1,12 @@ +package osscrypto + +import ( + "fmt" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// CopyFile with multi part mode, temporarily not supported +func (bucket CryptoBucket) CopyFile(srcBucketName, srcObjectKey, destObjectKey string, partSize int64, options ...oss.Option) error { + return fmt.Errorf("CryptoBucket doesn't support CopyFile") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_multipart.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_multipart.go new file mode 100644 index 00000000..afa07836 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_multipart.go @@ -0,0 +1,174 @@ +package osscrypto + +import ( + "fmt" + "io" + "os" + "strconv" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// PartCryptoContext save encryption or decryption information +type PartCryptoContext struct { + ContentCipher ContentCipher + DataSize int64 + PartSize int64 +} + +// Valid judge PartCryptoContext is valid or not +func (pcc PartCryptoContext) Valid() bool { + if pcc.ContentCipher == nil || pcc.DataSize == 0 || pcc.PartSize == 0 { + return false + } + return true +} + +// InitiateMultipartUpload initializes multipart upload for client encryption +// cryptoContext.PartSize and cryptoContext.DataSize are input parameter +// cryptoContext.PartSize must aligned to the secret iv length +// cryptoContext.ContentCipher is output parameter +// cryptoContext will be used in next API +func (bucket CryptoBucket) InitiateMultipartUpload(objectKey string, cryptoContext *PartCryptoContext, options ...oss.Option) (oss.InitiateMultipartUploadResult, error) { + options = bucket.AddEncryptionUaSuffix(options) + var imur oss.InitiateMultipartUploadResult + if cryptoContext == nil { + return imur, fmt.Errorf("error,cryptoContext is nil") + } + + if cryptoContext.PartSize <= 0 { + return imur, fmt.Errorf("invalid PartCryptoContext's PartSize %d", cryptoContext.PartSize) + } + + cc, err := bucket.ContentCipherBuilder.ContentCipher() + if err != nil { + return imur, err + } + + if cryptoContext.PartSize%int64(cc.GetAlignLen()) != 0 { + return imur, fmt.Errorf("PartCryptoContext's PartSize must be aligned to %d", cc.GetAlignLen()) + } + + opts := addCryptoHeaders(options, cc.GetCipherData()) + if cryptoContext.DataSize > 0 { + opts = append(opts, oss.Meta(OssClientSideEncryptionDataSize, strconv.FormatInt(cryptoContext.DataSize, 10))) + } + opts = append(opts, oss.Meta(OssClientSideEncryptionPartSize, strconv.FormatInt(cryptoContext.PartSize, 10))) + + imur, err = bucket.Bucket.InitiateMultipartUpload(objectKey, opts...) + if err == nil { + cryptoContext.ContentCipher = cc + } + return imur, err +} + +// UploadPart uploads parts to oss, the part data are encrypted automaticly on client side +// cryptoContext is the input parameter +func (bucket CryptoBucket) UploadPart(imur oss.InitiateMultipartUploadResult, reader io.Reader, + partSize int64, partNumber int, cryptoContext PartCryptoContext, options ...oss.Option) (oss.UploadPart, error) { + options = bucket.AddEncryptionUaSuffix(options) + var uploadPart oss.UploadPart + if cryptoContext.ContentCipher == nil { + return uploadPart, fmt.Errorf("error,cryptoContext is nil or cryptoContext.ContentCipher is nil") + } + + if partNumber < 1 { + return uploadPart, fmt.Errorf("partNumber:%d is smaller than 1", partNumber) + } + + if cryptoContext.PartSize%int64(cryptoContext.ContentCipher.GetAlignLen()) != 0 { + return uploadPart, fmt.Errorf("PartCryptoContext's PartSize must be aligned to %d", cryptoContext.ContentCipher.GetAlignLen()) + } + + cipherData := cryptoContext.ContentCipher.GetCipherData().Clone() + // caclulate iv based on part number + if partNumber > 1 { + cipherData.SeekIV(uint64(partNumber-1) * uint64(cryptoContext.PartSize)) + } + + // for parallel upload part + partCC, _ := cryptoContext.ContentCipher.Clone(cipherData) + + cryptoReader, err := partCC.EncryptContent(reader) + if err != nil { + return uploadPart, err + } + + request := &oss.UploadPartRequest{ + InitResult: &imur, + Reader: cryptoReader, + PartSize: partCC.GetEncryptedLen(partSize), + PartNumber: partNumber, + } + + opts := addCryptoHeaders(options, partCC.GetCipherData()) + if cryptoContext.DataSize > 0 { + opts = append(opts, oss.Meta(OssClientSideEncryptionDataSize, strconv.FormatInt(cryptoContext.DataSize, 10))) + } + opts = append(opts, oss.Meta(OssClientSideEncryptionPartSize, strconv.FormatInt(cryptoContext.PartSize, 10))) + + result, err := bucket.Bucket.DoUploadPart(request, opts) + return result.Part, err +} + +// UploadPartFromFile uploads part from the file, the part data are encrypted automaticly on client side +// cryptoContext is the input parameter +func (bucket CryptoBucket) UploadPartFromFile(imur oss.InitiateMultipartUploadResult, filePath string, + startPosition, partSize int64, partNumber int, cryptoContext PartCryptoContext, options ...oss.Option) (oss.UploadPart, error) { + options = bucket.AddEncryptionUaSuffix(options) + var uploadPart = oss.UploadPart{} + if cryptoContext.ContentCipher == nil { + return uploadPart, fmt.Errorf("error,cryptoContext is nil or cryptoContext.ContentCipher is nil") + } + + if cryptoContext.PartSize%int64(cryptoContext.ContentCipher.GetAlignLen()) != 0 { + return uploadPart, fmt.Errorf("PartCryptoContext's PartSize must be aligned to %d", cryptoContext.ContentCipher.GetAlignLen()) + } + + fd, err := os.Open(filePath) + if err != nil { + return uploadPart, err + } + defer fd.Close() + fd.Seek(startPosition, os.SEEK_SET) + + if partNumber < 1 { + return uploadPart, fmt.Errorf("partNumber:%d is smaller than 1", partNumber) + } + + cipherData := cryptoContext.ContentCipher.GetCipherData().Clone() + // calculate iv based on part number + if partNumber > 1 { + cipherData.SeekIV(uint64(partNumber-1) * uint64(cryptoContext.PartSize)) + } + + // for parallel upload part + partCC, _ := cryptoContext.ContentCipher.Clone(cipherData) + cryptoReader, err := partCC.EncryptContent(fd) + if err != nil { + return uploadPart, err + } + + encryptedLen := partCC.GetEncryptedLen(partSize) + opts := addCryptoHeaders(options, partCC.GetCipherData()) + if cryptoContext.DataSize > 0 { + opts = append(opts, oss.Meta(OssClientSideEncryptionDataSize, strconv.FormatInt(cryptoContext.DataSize, 10))) + } + opts = append(opts, oss.Meta(OssClientSideEncryptionPartSize, strconv.FormatInt(cryptoContext.PartSize, 10))) + + request := &oss.UploadPartRequest{ + InitResult: &imur, + Reader: cryptoReader, + PartSize: encryptedLen, + PartNumber: partNumber, + } + result, err := bucket.Bucket.DoUploadPart(request, opts) + return result.Part, err +} + +// UploadPartCopy uploads part copy +func (bucket CryptoBucket) UploadPartCopy(imur oss.InitiateMultipartUploadResult, srcBucketName, srcObjectKey string, + startPosition, partSize int64, partNumber int, cryptoContext PartCryptoContext, options ...oss.Option) (oss.UploadPart, error) { + var uploadPart = oss.UploadPart{} + return uploadPart, fmt.Errorf("CryptoBucket doesn't support UploadPartCopy") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_multipart_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_multipart_test.go new file mode 100644 index 00000000..1283c79b --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_multipart_test.go @@ -0,0 +1,758 @@ +// multipart test + +package osscrypto + +import ( + "io/ioutil" + "os" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" + . "gopkg.in/check.v1" +) + +func (s *OssCryptoBucketSuite) TestMultipartUpload(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + fileName := "../../sample/BingWallpaper-2015-11-07.jpg" + + srcMD5, err := GetFileMD5(fileName) + c.Assert(err, IsNil) + + fileInfo, err := os.Stat(fileName) + dataSize := fileInfo.Size() + c.Assert(err, IsNil) + + options := []oss.Option{oss.Meta("my", "myprop")} + var cryptoContext PartCryptoContext + cryptoContext.DataSize = dataSize + cryptoContext.PartSize = (dataSize / 16 / 3) * 16 + imur, err := bucket.InitiateMultipartUpload(objectName, &cryptoContext, options...) + c.Assert(err, IsNil) + + chunks, err := oss.SplitFileByPartSize(fileName, cryptoContext.PartSize) + c.Assert(err, IsNil) + + fd, err := os.Open(fileName) + c.Assert(err, IsNil) + defer fd.Close() + + var parts []oss.UploadPart + for _, chunk := range chunks { + fd.Seek(chunk.Offset, os.SEEK_SET) + part, err := bucket.UploadPart(imur, fd, chunk.Size, chunk.Number, cryptoContext) + c.Assert(err, IsNil) + parts = append(parts, part) + } + + _, err = bucket.CompleteMultipartUpload(imur, parts) + c.Assert(err, IsNil) + + meta, err := bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + + c.Assert(meta.Get("X-Oss-Meta-My"), Equals, "myprop") + c.Assert(meta.Get("X-Oss-Object-Type"), Equals, "Multipart") + + downfileName := "test-go-sdk-file-" + RandLowStr(5) + err = bucket.GetObjectToFile(objectName, downfileName) + c.Assert(err, IsNil) + + downFileMD5, err := GetFileMD5(downfileName) + c.Assert(err, IsNil) + c.Assert(downFileMD5, Equals, srcMD5) + + os.Remove(downfileName) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestMultipartUploadFromFile(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + fileName := "../../sample/BingWallpaper-2015-11-07.jpg" + + srcMD5, err := GetFileMD5(fileName) + c.Assert(err, IsNil) + + fileInfo, err := os.Stat(fileName) + dataSize := fileInfo.Size() + c.Assert(err, IsNil) + + options := []oss.Option{oss.Meta("my", "myprop")} + var cryptoContext PartCryptoContext + cryptoContext.DataSize = dataSize + cryptoContext.PartSize = (dataSize / 16 / 3) * 16 + imur, err := bucket.InitiateMultipartUpload(objectName, &cryptoContext, options...) + c.Assert(err, IsNil) + + chunks, err := oss.SplitFileByPartSize(fileName, cryptoContext.PartSize) + c.Assert(err, IsNil) + + var parts []oss.UploadPart + for _, chunk := range chunks { + part, err := bucket.UploadPartFromFile(imur, fileName, chunk.Offset, chunk.Size, chunk.Number, cryptoContext) + c.Assert(err, IsNil) + parts = append(parts, part) + } + + _, err = bucket.CompleteMultipartUpload(imur, parts) + c.Assert(err, IsNil) + + meta, err := bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + + c.Assert(meta.Get("X-Oss-Meta-My"), Equals, "myprop") + c.Assert(meta.Get("X-Oss-Object-Type"), Equals, "Multipart") + + downfileName := "test-go-sdk-file-" + RandLowStr(5) + ".jpg" + err = bucket.GetObjectToFile(objectName, downfileName) + c.Assert(err, IsNil) + + downFileMD5, err := GetFileMD5(downfileName) + c.Assert(err, IsNil) + c.Assert(downFileMD5, Equals, srcMD5) + + os.Remove(downfileName) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestMultipartUploadFromSmallSizeFile(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + fileName := "oss-go-sdk-test-file-" + RandStr(5) + fo, err := os.Create(fileName) + c.Assert(err, IsNil) + _, err = fo.Write([]byte("123")) + c.Assert(err, IsNil) + fo.Close() + + srcMD5, err := GetFileMD5(fileName) + c.Assert(err, IsNil) + + fileInfo, err := os.Stat(fileName) + dataSize := fileInfo.Size() + c.Assert(err, IsNil) + + options := []oss.Option{oss.Meta("my", "myprop")} + var cryptoContext PartCryptoContext + cryptoContext.DataSize = dataSize + cryptoContext.PartSize = 16 + imur, err := bucket.InitiateMultipartUpload(objectName, &cryptoContext, options...) + c.Assert(err, IsNil) + + chunks, err := oss.SplitFileByPartSize(fileName, cryptoContext.PartSize) + c.Assert(err, IsNil) + + var parts []oss.UploadPart + for _, chunk := range chunks { + part, err := bucket.UploadPartFromFile(imur, fileName, chunk.Offset, chunk.Size, chunk.Number, cryptoContext) + c.Assert(err, IsNil) + parts = append(parts, part) + } + + _, err = bucket.CompleteMultipartUpload(imur, parts) + c.Assert(err, IsNil) + + meta, err := bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + + c.Assert(meta.Get("X-Oss-Meta-My"), Equals, "myprop") + c.Assert(meta.Get("X-Oss-Object-Type"), Equals, "Multipart") + + downfileName := "test-go-sdk-file-" + RandLowStr(5) + ".jpg" + err = bucket.GetObjectToFile(objectName, downfileName) + c.Assert(err, IsNil) + + downFileMD5, err := GetFileMD5(downfileName) + c.Assert(err, IsNil) + c.Assert(downFileMD5, Equals, srcMD5) + + os.Remove(fileName) + os.Remove(downfileName) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestListUploadedPartsNormal(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + fileName := "../../sample/BingWallpaper-2015-11-07.jpg" + + srcMD5, err := GetFileMD5(fileName) + c.Assert(err, IsNil) + + fileInfo, err := os.Stat(fileName) + dataSize := fileInfo.Size() + c.Assert(err, IsNil) + + // Upload + var cryptoContext PartCryptoContext + cryptoContext.DataSize = dataSize + cryptoContext.PartSize = (dataSize / 16 / 3) * 16 + imurUpload, err := bucket.InitiateMultipartUpload(objectName, &cryptoContext) + chunks, err := oss.SplitFileByPartSize(fileName, cryptoContext.PartSize) + c.Assert(err, IsNil) + + var partsUpload []oss.UploadPart + for _, chunk := range chunks { + part, err := bucket.UploadPartFromFile(imurUpload, fileName, chunk.Offset, chunk.Size, (int)(chunk.Number), cryptoContext) + c.Assert(err, IsNil) + partsUpload = append(partsUpload, part) + } + + // List + lupr, err := bucket.ListUploadedParts(imurUpload) + c.Assert(err, IsNil) + c.Assert(len(lupr.UploadedParts), Equals, len(chunks)) + + // Complete + _, err = bucket.CompleteMultipartUpload(imurUpload, partsUpload) + c.Assert(err, IsNil) + + // Download + downfileName := "test-go-sdk-file-" + RandLowStr(5) + ".jpg" + err = bucket.GetObjectToFile(objectName, downfileName) + + downFileMD5, err := GetFileMD5(downfileName) + c.Assert(err, IsNil) + c.Assert(downFileMD5, Equals, srcMD5) + + os.Remove(downfileName) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestListUploadedPartsComplete(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + fileName := "../../sample/BingWallpaper-2015-11-07.jpg" + + srcMD5, err := GetFileMD5(fileName) + c.Assert(err, IsNil) + + fileInfo, err := os.Stat(fileName) + dataSize := fileInfo.Size() + c.Assert(err, IsNil) + + // Upload + var cryptoContext PartCryptoContext + cryptoContext.DataSize = dataSize + cryptoContext.PartSize = (dataSize / 16 / 3) * 16 + imurUpload, err := bucket.InitiateMultipartUpload(objectName, &cryptoContext) + chunks, err := oss.SplitFileByPartSize(fileName, cryptoContext.PartSize) + c.Assert(err, IsNil) + + var partsUpload []oss.UploadPart + i := 0 + // upload excepted the last part + for ; i < len(chunks)-1; i++ { + part, err := bucket.UploadPartFromFile(imurUpload, fileName, chunks[i].Offset, chunks[i].Size, (int)(chunks[i].Number), cryptoContext) + c.Assert(err, IsNil) + partsUpload = append(partsUpload, part) + } + + // List + lupr, err := bucket.ListUploadedParts(imurUpload) + c.Assert(err, IsNil) + c.Assert(len(lupr.UploadedParts), Equals, len(chunks)-1) + + lmur, err := bucket.ListMultipartUploads() + c.Assert(err, IsNil) + c.Assert(len(lmur.Uploads), Equals, 1) + + // upload the last part with list part result + part, err := bucket.UploadPartFromFile(imurUpload, fileName, chunks[i].Offset, chunks[i].Size, (int)(chunks[i].Number), cryptoContext) + c.Assert(err, IsNil) + partsUpload = append(partsUpload, part) + + // Complete + _, err = bucket.CompleteMultipartUpload(imurUpload, partsUpload) + c.Assert(err, IsNil) + + // Download + downfileName := "test-go-sdk-file-" + RandLowStr(5) + ".jpg" + err = bucket.GetObjectToFile(objectName, downfileName) + + downFileMD5, err := GetFileMD5(downfileName) + c.Assert(err, IsNil) + c.Assert(downFileMD5, Equals, srcMD5) + + os.Remove(downfileName) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestListUploadedPartsAbortUseInit(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + fileName := "../../sample/BingWallpaper-2015-11-07.jpg" + + fileInfo, err := os.Stat(fileName) + dataSize := fileInfo.Size() + c.Assert(err, IsNil) + + // Upload + var cryptoContext PartCryptoContext + cryptoContext.DataSize = dataSize + cryptoContext.PartSize = (dataSize / 16 / 3) * 16 + imurUpload, err := bucket.InitiateMultipartUpload(objectName, &cryptoContext) + chunks, err := oss.SplitFileByPartSize(fileName, cryptoContext.PartSize) + c.Assert(err, IsNil) + c.Assert(cryptoContext.Valid(), Equals, true) + + var partsUpload []oss.UploadPart + i := 0 + // upload excepted the last part + for ; i < len(chunks)-1; i++ { + part, err := bucket.UploadPartFromFile(imurUpload, fileName, chunks[i].Offset, chunks[i].Size, (int)(chunks[i].Number), cryptoContext) + c.Assert(err, IsNil) + partsUpload = append(partsUpload, part) + } + + // List + lupr, err := bucket.ListUploadedParts(imurUpload) + c.Assert(err, IsNil) + c.Assert(len(lupr.UploadedParts), Equals, len(chunks)-1) + + lmur, err := bucket.ListMultipartUploads() + c.Assert(err, IsNil) + c.Assert(len(lmur.Uploads), Equals, 1) + + // abort upload + err = bucket.AbortMultipartUpload(imurUpload) + c.Assert(err, IsNil) + + // list again + lupr, err = bucket.ListUploadedParts(imurUpload) + c.Assert(err, NotNil) + + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestListUploadedPartsAbortUseList(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + fileName := "../../sample/BingWallpaper-2015-11-07.jpg" + + fileInfo, err := os.Stat(fileName) + dataSize := fileInfo.Size() + c.Assert(err, IsNil) + + // Upload + var cryptoContext PartCryptoContext + cryptoContext.DataSize = dataSize + cryptoContext.PartSize = (dataSize / 16 / 3) * 16 + imurUpload, err := bucket.InitiateMultipartUpload(objectName, &cryptoContext) + chunks, err := oss.SplitFileByPartSize(fileName, cryptoContext.PartSize) + c.Assert(err, IsNil) + + var partsUpload []oss.UploadPart + i := 0 + // upload excepted the last part + for ; i < len(chunks)-1; i++ { + part, err := bucket.UploadPartFromFile(imurUpload, fileName, chunks[i].Offset, chunks[i].Size, (int)(chunks[i].Number), cryptoContext) + c.Assert(err, IsNil) + partsUpload = append(partsUpload, part) + } + + // List + lupr, err := bucket.ListUploadedParts(imurUpload) + c.Assert(err, IsNil) + c.Assert(len(lupr.UploadedParts), Equals, len(chunks)-1) + + // abort upload + err = bucket.AbortMultipartUpload(imurUpload) + c.Assert(err, IsNil) + + // list again + lupr, err = bucket.ListUploadedParts(imurUpload) + c.Assert(err, NotNil) + + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestInitiateMultipartUpload(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + context := RandStr(ivSize * 1024 * 10) + fileName := "test-go-sdk-file-" + RandStr(5) + + err = ioutil.WriteFile(fileName, []byte(context), 0666) + c.Assert(err, IsNil) + + fileInfo, err := os.Stat(fileName) + dataSize := fileInfo.Size() + c.Assert(err, IsNil) + + var cryptoContext PartCryptoContext + cryptoContext.DataSize = dataSize + cryptoContext.PartSize = ivSize * 1024 + imurUpload, err := bucket.InitiateMultipartUpload(objectName, &cryptoContext) + c.Assert(err, IsNil) + + err = bucket.AbortMultipartUpload(imurUpload) + c.Assert(err, IsNil) + + cryptoContext.DataSize = dataSize + cryptoContext.PartSize = ivSize / 2 + imurUpload, err = bucket.InitiateMultipartUpload(objectName, &cryptoContext) + c.Assert(err, NotNil) + + os.Remove(fileName) + + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestUploadPartError(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + context := RandStr(ivSize * 1024 * 10) + fileName := "test-go-sdk-file-" + RandStr(5) + + err = ioutil.WriteFile(fileName, []byte(context), 0666) + c.Assert(err, IsNil) + + fileInfo, err := os.Stat(fileName) + dataSize := fileInfo.Size() + c.Assert(err, IsNil) + + var cryptoContext PartCryptoContext + cryptoContext.DataSize = dataSize + cryptoContext.PartSize = ivSize * 1024 + imur, err := bucket.InitiateMultipartUpload(objectName, &cryptoContext) + c.Assert(err, IsNil) + + chunks, err := oss.SplitFileByPartSize(fileName, cryptoContext.PartSize) + c.Assert(err, IsNil) + + fd, err := os.Open(fileName) + c.Assert(err, IsNil) + for _, chunk := range chunks { + fd.Seek(chunk.Offset, os.SEEK_SET) + _, err := bucket.UploadPart(imur, fd, chunk.Size+1, chunk.Number, cryptoContext) + c.Assert(err, NotNil) + } + + for _, chunk := range chunks { + fd.Seek(chunk.Offset, os.SEEK_SET) + _, err := bucket.UploadPart(imur, fd, chunk.Size, 0, cryptoContext) + c.Assert(err, NotNil) + } + fd.Close() + + err = bucket.AbortMultipartUpload(imur) + c.Assert(err, IsNil) + os.Remove(fileName) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestUploadPartFromFileError(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + context := RandStr(ivSize * 1024 * 10) + fileName := "test-go-sdk-file-" + RandStr(5) + + err = ioutil.WriteFile(fileName, []byte(context), 0666) + c.Assert(err, IsNil) + + fileInfo, err := os.Stat(fileName) + dataSize := fileInfo.Size() + c.Assert(err, IsNil) + + var cryptoContext PartCryptoContext + cryptoContext.DataSize = dataSize + cryptoContext.PartSize = ivSize * 1024 + imur, err := bucket.InitiateMultipartUpload(objectName, &cryptoContext) + c.Assert(err, IsNil) + + chunks, err := oss.SplitFileByPartSize(fileName, cryptoContext.PartSize) + c.Assert(err, IsNil) + + for _, chunk := range chunks { + _, err := bucket.UploadPartFromFile(imur, fileName+".test", chunk.Offset, chunk.Size, chunk.Number, cryptoContext) + c.Assert(err, NotNil) + } + + _, err = bucket.UploadPartFromFile(imur, fileName+".test", chunks[0].Offset, chunks[0].Size+1, chunks[0].Number, cryptoContext) + c.Assert(err, NotNil) + + _, err = bucket.UploadPartFromFile(imur, fileName+".test", chunks[0].Offset, chunks[0].Size, 0, cryptoContext) + c.Assert(err, NotNil) + + err = bucket.AbortMultipartUpload(imur) + c.Assert(err, IsNil) + os.Remove(fileName) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestUploadPartCopyError(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + context := RandStr(ivSize * 1024 * 10) + fileName := "test-go-sdk-file-" + RandStr(5) + + err = ioutil.WriteFile(fileName, []byte(context), 0666) + c.Assert(err, IsNil) + + fileInfo, err := os.Stat(fileName) + dataSize := fileInfo.Size() + c.Assert(err, IsNil) + + var cryptoContext PartCryptoContext + cryptoContext.DataSize = dataSize + cryptoContext.PartSize = ivSize * 1024 + imur, err := bucket.InitiateMultipartUpload(objectName, &cryptoContext) + c.Assert(err, IsNil) + + chunks, err := oss.SplitFileByPartSize(fileName, cryptoContext.PartSize) + c.Assert(err, IsNil) + + _, err = bucket.UploadPartCopy(imur, bucketName, objectName, 0, chunks[0].Size, 1, cryptoContext) + c.Assert(err, NotNil) + + err = bucket.AbortMultipartUpload(imur) + c.Assert(err, IsNil) + os.Remove(fileName) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestMultipartUploadFromFileError(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + fileName := "../../sample/BingWallpaper-2015-11-07.jpg" + + fileInfo, err := os.Stat(fileName) + dataSize := fileInfo.Size() + c.Assert(err, IsNil) + + options := []oss.Option{oss.Meta("my", "myprop")} + var cryptoContext PartCryptoContext + cryptoContext.DataSize = dataSize + cryptoContext.PartSize = -1 + _, err = bucket.InitiateMultipartUpload(objectName, nil, options...) + c.Assert(err, NotNil) + + _, err = bucket.InitiateMultipartUpload(objectName, &cryptoContext, options...) + c.Assert(err, NotNil) + + cryptoContext.PartSize = (dataSize / 16 / 3) * 16 + imur, err := bucket.InitiateMultipartUpload(objectName, &cryptoContext, options...) + c.Assert(err, IsNil) + + chunks, err := oss.SplitFileByPartSize(fileName, cryptoContext.PartSize) + c.Assert(err, IsNil) + + bakCC := cryptoContext.ContentCipher + cryptoContext.ContentCipher = nil + + i := 0 + // upload excepted the last part + for ; i < len(chunks); i++ { + _, err = bucket.UploadPartFromFile(imur, fileName, chunks[i].Offset, chunks[i].Size, (int)(chunks[i].Number), cryptoContext) + c.Assert(err, NotNil) + + } + + i = 0 + cryptoContext.ContentCipher = bakCC + cryptoContext.PartSize -= 1 + for ; i < len(chunks); i++ { + _, err = bucket.UploadPartFromFile(imur, fileName, chunks[i].Offset, chunks[i].Size, (int)(chunks[i].Number), cryptoContext) + c.Assert(err, NotNil) + } + + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssCryptoBucketSuite) TestMultipartUploadPartError(c *C) { + // create a bucket with default proprety + client, err := oss.New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + // crypto bucket + masterRsaCipher, _ := CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + contentProvider := CreateAesCtrCipher(masterRsaCipher) + bucket, err := GetCryptoBucket(client, bucketName, contentProvider) + + objectName := objectNamePrefix + RandStr(8) + fileName := "../../sample/BingWallpaper-2015-11-07.jpg" + + fileInfo, err := os.Stat(fileName) + dataSize := fileInfo.Size() + c.Assert(err, IsNil) + + options := []oss.Option{oss.Meta("my", "myprop")} + var cryptoContext PartCryptoContext + cryptoContext.DataSize = dataSize + cryptoContext.PartSize = (dataSize / 16 / 3) * 16 + imur, err := bucket.InitiateMultipartUpload(objectName, &cryptoContext, options...) + c.Assert(err, IsNil) + + chunks, err := oss.SplitFileByPartSize(fileName, cryptoContext.PartSize) + c.Assert(err, IsNil) + + bakCC := cryptoContext.ContentCipher + cryptoContext.ContentCipher = nil + + fd, err := os.Open(fileName) + c.Assert(err, IsNil) + + for _, chunk := range chunks { + fd.Seek(chunk.Offset, os.SEEK_SET) + _, err = bucket.UploadPart(imur, fd, chunk.Size, chunk.Number, cryptoContext) + c.Assert(err, NotNil) + } + + cryptoContext.ContentCipher = bakCC + cryptoContext.PartSize -= 1 + for _, chunk := range chunks { + _, err = bucket.UploadPart(imur, fd, chunk.Size, chunk.Number, cryptoContext) + c.Assert(err, NotNil) + } + + ForceDeleteBucket(client, bucketName, c) +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_type.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_type.go new file mode 100644 index 00000000..c6893279 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_type.go @@ -0,0 +1,128 @@ +package osscrypto + +import ( + "crypto/rand" + "encoding/binary" + "fmt" + "io" + math_rand "math/rand" + "time" +) + +// MasterCipher encrypt or decrpt CipherData +// support master key: rsa && ali kms +type MasterCipher interface { + Encrypt([]byte) ([]byte, error) + Decrypt([]byte) ([]byte, error) + GetWrapAlgorithm() string + GetMatDesc() string +} + +// ContentCipherBuilder is used to create ContentCipher for encryting object's data +type ContentCipherBuilder interface { + ContentCipher() (ContentCipher, error) + ContentCipherEnv(Envelope) (ContentCipher, error) + GetMatDesc() string +} + +// ContentCipher is used to encrypt or decrypt object's data +type ContentCipher interface { + EncryptContent(io.Reader) (io.ReadCloser, error) + DecryptContent(io.Reader) (io.ReadCloser, error) + Clone(cd CipherData) (ContentCipher, error) + GetEncryptedLen(int64) int64 + GetCipherData() *CipherData + GetAlignLen() int +} + +// Envelope is stored in oss object's meta +type Envelope struct { + IV string + CipherKey string + MatDesc string + WrapAlg string + CEKAlg string + UnencryptedMD5 string + UnencryptedContentLen string +} + +func (el Envelope) IsValid() bool { + return len(el.IV) > 0 && + len(el.CipherKey) > 0 && + len(el.WrapAlg) > 0 && + len(el.CEKAlg) > 0 +} + +func (el Envelope) String() string { + return fmt.Sprintf("IV=%s&CipherKey=%s&WrapAlg=%s&CEKAlg=%s", el.IV, el.CipherKey, el.WrapAlg, el.CEKAlg) +} + +// CipherData is secret key information +type CipherData struct { + IV []byte + Key []byte + MatDesc string + WrapAlgorithm string + CEKAlgorithm string + EncryptedIV []byte + EncryptedKey []byte +} + +func (cd *CipherData) RandomKeyIv(keyLen int, ivLen int) error { + math_rand.Seed(time.Now().UnixNano()) + + // Key + cd.Key = make([]byte, keyLen) + if _, err := io.ReadFull(rand.Reader, cd.Key); err != nil { + return err + } + + // sizeof uint64 + if ivLen < 8 { + return fmt.Errorf("ivLen:%d less than 8", ivLen) + } + + // IV:reserve 8 bytes + cd.IV = make([]byte, ivLen) + if _, err := io.ReadFull(rand.Reader, cd.IV[0:ivLen-8]); err != nil { + return err + } + + // only use 4 byte,in order not to overflow when SeekIV() + randNumber := math_rand.Uint32() + cd.SetIV(uint64(randNumber)) + return nil +} + +func (cd *CipherData) SetIV(iv uint64) { + ivLen := len(cd.IV) + binary.BigEndian.PutUint64(cd.IV[ivLen-8:], iv) +} + +func (cd *CipherData) GetIV() uint64 { + ivLen := len(cd.IV) + return binary.BigEndian.Uint64(cd.IV[ivLen-8:]) +} + +func (cd *CipherData) SeekIV(startPos uint64) { + cd.SetIV(cd.GetIV() + startPos/uint64(len(cd.IV))) +} + +func (cd *CipherData) Clone() CipherData { + var cloneCd CipherData + cloneCd = *cd + + cloneCd.Key = make([]byte, len(cd.Key)) + copy(cloneCd.Key, cd.Key) + + cloneCd.IV = make([]byte, len(cd.IV)) + copy(cloneCd.IV, cd.IV) + + cloneCd.EncryptedIV = make([]byte, len(cd.EncryptedIV)) + copy(cloneCd.EncryptedIV, cd.EncryptedIV) + + cloneCd.EncryptedKey = make([]byte, len(cd.EncryptedKey)) + copy(cloneCd.EncryptedKey, cd.EncryptedKey) + + return cloneCd +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_upload.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_upload.go new file mode 100644 index 00000000..78be17db --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/crypto_upload.go @@ -0,0 +1,12 @@ +package osscrypto + +import ( + "fmt" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// UploadFile with multi part mode +func (bucket CryptoBucket) UploadFile(objectKey, filePath string, partSize int64, options ...oss.Option) error { + return fmt.Errorf("CryptoBucket doesn't support UploadFile") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/master_alikms_cipher.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/master_alikms_cipher.go new file mode 100644 index 00000000..3c55ff1a --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/master_alikms_cipher.go @@ -0,0 +1,85 @@ +package osscrypto + +import ( + "encoding/base64" + "encoding/json" + "fmt" + + kms "github.com/aliyun/alibaba-cloud-sdk-go/services/kms" +) + +// CreateMasterAliKms Create master key interface implemented by ali kms +// matDesc will be converted to json string +func CreateMasterAliKms(matDesc map[string]string, kmsID string, kmsClient *kms.Client) (MasterCipher, error) { + var masterCipher MasterAliKmsCipher + if kmsID == "" || kmsClient == nil { + return masterCipher, fmt.Errorf("kmsID is empty or kmsClient is nil") + } + + var jsonDesc string + if len(matDesc) > 0 { + b, err := json.Marshal(matDesc) + if err != nil { + return masterCipher, err + } + jsonDesc = string(b) + } + + masterCipher.MatDesc = jsonDesc + masterCipher.KmsID = kmsID + masterCipher.KmsClient = kmsClient + return masterCipher, nil +} + +// MasterAliKmsCipher ali kms master key interface +type MasterAliKmsCipher struct { + MatDesc string + KmsID string + KmsClient *kms.Client +} + +// GetWrapAlgorithm get master key wrap algorithm +func (mrc MasterAliKmsCipher) GetWrapAlgorithm() string { + return KmsAliCryptoWrap +} + +// GetMatDesc get master key describe +func (mkms MasterAliKmsCipher) GetMatDesc() string { + return mkms.MatDesc +} + +// Encrypt encrypt data by ali kms +// Mainly used to encrypt object's symmetric secret key and iv +func (mkms MasterAliKmsCipher) Encrypt(plainData []byte) ([]byte, error) { + // kms Plaintext must be base64 encoded + base64Plain := base64.StdEncoding.EncodeToString(plainData) + request := kms.CreateEncryptRequest() + request.RpcRequest.Scheme = "https" + request.RpcRequest.Method = "POST" + request.RpcRequest.AcceptFormat = "json" + + request.KeyId = mkms.KmsID + request.Plaintext = base64Plain + + response, err := mkms.KmsClient.Encrypt(request) + if err != nil { + return nil, err + } + return base64.StdEncoding.DecodeString(response.CiphertextBlob) +} + +// Decrypt decrypt data by ali kms +// Mainly used to decrypt object's symmetric secret key and iv +func (mkms MasterAliKmsCipher) Decrypt(cryptoData []byte) ([]byte, error) { + base64Crypto := base64.StdEncoding.EncodeToString(cryptoData) + request := kms.CreateDecryptRequest() + request.RpcRequest.Scheme = "https" + request.RpcRequest.Method = "POST" + request.RpcRequest.AcceptFormat = "json" + request.CiphertextBlob = string(base64Crypto) + response, err := mkms.KmsClient.Decrypt(request) + if err != nil { + return nil, err + } + return base64.StdEncoding.DecodeString(response.Plaintext) +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/master_alikms_cipher_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/master_alikms_cipher_test.go new file mode 100644 index 00000000..70cc509b --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/master_alikms_cipher_test.go @@ -0,0 +1,89 @@ +package osscrypto + +import ( + crypto_rand "crypto/rand" + "encoding/base64" + "io" + "math/rand" + "time" + + kms "github.com/aliyun/alibaba-cloud-sdk-go/services/kms" + . "gopkg.in/check.v1" +) + +func (s *OssCryptoBucketSuite) TestKmsClient(c *C) { + rand.Seed(time.Now().UnixNano()) + kmsClient, err := kms.NewClientWithAccessKey(kmsRegion, kmsAccessID, kmsAccessKey) + c.Assert(err, IsNil) + + // encrypte + enReq := kms.CreateEncryptRequest() + enReq.RpcRequest.Scheme = "https" + enReq.RpcRequest.Method = "POST" + enReq.RpcRequest.AcceptFormat = "json" + + enReq.KeyId = kmsID + + buff := make([]byte, 10) + _, err = io.ReadFull(crypto_rand.Reader, buff) + c.Assert(err, IsNil) + enReq.Plaintext = base64.StdEncoding.EncodeToString(buff) + + enResponse, err := kmsClient.Encrypt(enReq) + c.Assert(err, IsNil) + + // decrypte + deReq := kms.CreateDecryptRequest() + deReq.RpcRequest.Scheme = "https" + deReq.RpcRequest.Method = "POST" + deReq.RpcRequest.AcceptFormat = "json" + deReq.CiphertextBlob = enResponse.CiphertextBlob + deResponse, err := kmsClient.Decrypt(deReq) + c.Assert(err, IsNil) + c.Assert(deResponse.Plaintext, Equals, enReq.Plaintext) +} + +func (s *OssCryptoBucketSuite) TestMasterAliKmsCipherSuccess(c *C) { + + kmsClient, err := kms.NewClientWithAccessKey(kmsRegion, kmsAccessID, kmsAccessKey) + c.Assert(err, IsNil) + + masterCipher, _ := CreateMasterAliKms(matDesc, kmsID, kmsClient) + + var cd CipherData + err = cd.RandomKeyIv(aesKeySize, ivSize) + c.Assert(err, IsNil) + + cd.WrapAlgorithm = masterCipher.GetWrapAlgorithm() + cd.CEKAlgorithm = KmsAliCryptoWrap + cd.MatDesc = masterCipher.GetMatDesc() + + // EncryptedKey + cd.EncryptedKey, err = masterCipher.Encrypt(cd.Key) + + // EncryptedIV + cd.EncryptedIV, err = masterCipher.Encrypt(cd.IV) + + cloneData := cd.Clone() + + cloneData.Key, _ = masterCipher.Decrypt(cloneData.EncryptedKey) + cloneData.IV, _ = masterCipher.Decrypt(cloneData.EncryptedIV) + + c.Assert(string(cd.Key), Equals, string(cloneData.Key)) + c.Assert(string(cd.IV), Equals, string(cloneData.IV)) + +} + +func (s *OssCryptoBucketSuite) TestMasterAliKmsCipherError(c *C) { + kmsClient, err := kms.NewClientWithAccessKey(kmsRegion, kmsAccessID, kmsAccessKey) + c.Assert(err, IsNil) + + masterCipher, _ := CreateMasterAliKms(matDesc, kmsID, kmsClient) + v := masterCipher.(MasterAliKmsCipher) + v.KmsID = "" + _, err = v.Encrypt([]byte("hellow")) + c.Assert(err, NotNil) + + _, err = v.Decrypt([]byte("hellow")) + c.Assert(err, NotNil) +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/master_rsa_cipher.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/master_rsa_cipher.go new file mode 100644 index 00000000..616f3e12 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/master_rsa_cipher.go @@ -0,0 +1,102 @@ +package osscrypto + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/asn1" + "encoding/json" + "encoding/pem" + "fmt" +) + +// CreateMasterRsa Create master key interface implemented by rsa +// matDesc will be converted to json string +func CreateMasterRsa(matDesc map[string]string, publicKey string, privateKey string) (MasterCipher, error) { + var masterCipher MasterRsaCipher + var jsonDesc string + if len(matDesc) > 0 { + b, err := json.Marshal(matDesc) + if err != nil { + return masterCipher, err + } + jsonDesc = string(b) + } + masterCipher.MatDesc = jsonDesc + masterCipher.PublicKey = publicKey + masterCipher.PrivateKey = privateKey + return masterCipher, nil +} + +// MasterRsaCipher rsa master key interface +type MasterRsaCipher struct { + MatDesc string + PublicKey string + PrivateKey string +} + +// GetWrapAlgorithm get master key wrap algorithm +func (mrc MasterRsaCipher) GetWrapAlgorithm() string { + return RsaCryptoWrap +} + +// GetMatDesc get master key describe +func (mrc MasterRsaCipher) GetMatDesc() string { + return mrc.MatDesc +} + +// Encrypt encrypt data by rsa public key +// Mainly used to encrypt object's symmetric secret key and iv +func (mrc MasterRsaCipher) Encrypt(plainData []byte) ([]byte, error) { + block, _ := pem.Decode([]byte(mrc.PublicKey)) + if block == nil { + return nil, fmt.Errorf("pem.Decode public key error") + } + + var pub *rsa.PublicKey + if block.Type == "PUBLIC KEY" { + // pks8 format + pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, err + } + pub = pubInterface.(*rsa.PublicKey) + } else if block.Type == "RSA PUBLIC KEY" { + // pks1 format + pub = &rsa.PublicKey{} + _, err := asn1.Unmarshal(block.Bytes, pub) + if err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("not supported public key,type:%s", block.Type) + } + return rsa.EncryptPKCS1v15(rand.Reader, pub, plainData) +} + +// Decrypt Decrypt data by rsa private key +// Mainly used to decrypt object's symmetric secret key and iv +func (mrc MasterRsaCipher) Decrypt(cryptoData []byte) ([]byte, error) { + block, _ := pem.Decode([]byte(mrc.PrivateKey)) + if block == nil { + return nil, fmt.Errorf("pem.Decode private key error") + } + + if block.Type == "PRIVATE KEY" { + // pks8 format + privInterface, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + return rsa.DecryptPKCS1v15(rand.Reader, privInterface.(*rsa.PrivateKey), cryptoData) + } else if block.Type == "RSA PRIVATE KEY" { + // pks1 format + priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + return rsa.DecryptPKCS1v15(rand.Reader, priv, cryptoData) + } else { + return nil, fmt.Errorf("not supported private key,type:%s", block.Type) + } +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/master_rsa_cipher_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/master_rsa_cipher_test.go new file mode 100644 index 00000000..dca5adbd --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/crypto/master_rsa_cipher_test.go @@ -0,0 +1,41 @@ +package osscrypto + +import ( + "strings" + + . "gopkg.in/check.v1" +) + +func (s *OssCryptoBucketSuite) TestMasterRsaError(c *C) { + + masterRsaCipher, _ := CreateMasterRsa(matDesc, RandLowStr(100), rsaPrivateKey) + _, err := masterRsaCipher.Encrypt([]byte("123")) + c.Assert(err, NotNil) + + masterRsaCipher, _ = CreateMasterRsa(matDesc, rsaPublicKey, RandLowStr(100)) + _, err = masterRsaCipher.Decrypt([]byte("123")) + c.Assert(err, NotNil) + + testPrivateKey := rsaPrivateKey + []byte(testPrivateKey)[100] = testPrivateKey[90] + masterRsaCipher, _ = CreateMasterRsa(matDesc, rsaPublicKey, testPrivateKey) + _, err = masterRsaCipher.Decrypt([]byte("123")) + c.Assert(err, NotNil) + + masterRsaCipher, _ = CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + + var cipherData CipherData + err = cipherData.RandomKeyIv(aesKeySize/2, ivSize/4) + c.Assert(err, NotNil) + + masterRsaCipher, _ = CreateMasterRsa(matDesc, rsaPublicKey, rsaPrivateKey) + v := masterRsaCipher.(MasterRsaCipher) + + v.PublicKey = strings.Replace(rsaPublicKey, "PUBLIC KEY", "CERTIFICATE", -1) + _, err = v.Encrypt([]byte("HELLOW")) + c.Assert(err, NotNil) + + v.PrivateKey = strings.Replace(rsaPrivateKey, "PRIVATE KEY", "CERTIFICATE", -1) + _, err = v.Decrypt([]byte("HELLOW")) + c.Assert(err, NotNil) +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/download.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/download.go new file mode 100644 index 00000000..90c1b633 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/download.go @@ -0,0 +1,567 @@ +package oss + +import ( + "crypto/md5" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "hash" + "hash/crc64" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strconv" + "time" +) + +// DownloadFile downloads files with multipart download. +// +// objectKey the object key. +// filePath the local file to download from objectKey in OSS. +// partSize the part size in bytes. +// options object's constraints, check out GetObject for the reference. +// +// error it's nil when the call succeeds, otherwise it's an error object. +// +func (bucket Bucket) DownloadFile(objectKey, filePath string, partSize int64, options ...Option) error { + if partSize < 1 { + return errors.New("oss: part size smaller than 1") + } + + uRange, err := GetRangeConfig(options) + if err != nil { + return err + } + + cpConf := getCpConfig(options) + routines := getRoutines(options) + + var strVersionId string + versionId, _ := FindOption(options, "versionId", nil) + if versionId != nil { + strVersionId = versionId.(string) + } + + if cpConf != nil && cpConf.IsEnable { + cpFilePath := getDownloadCpFilePath(cpConf, bucket.BucketName, objectKey, strVersionId, filePath) + if cpFilePath != "" { + return bucket.downloadFileWithCp(objectKey, filePath, partSize, options, cpFilePath, routines, uRange) + } + } + + return bucket.downloadFile(objectKey, filePath, partSize, options, routines, uRange) +} + +func getDownloadCpFilePath(cpConf *cpConfig, srcBucket, srcObject, versionId, destFile string) string { + if cpConf.FilePath == "" && cpConf.DirPath != "" { + src := fmt.Sprintf("oss://%v/%v", srcBucket, srcObject) + absPath, _ := filepath.Abs(destFile) + cpFileName := getCpFileName(src, absPath, versionId) + cpConf.FilePath = cpConf.DirPath + string(os.PathSeparator) + cpFileName + } + return cpConf.FilePath +} + +// downloadWorkerArg is download worker's parameters +type downloadWorkerArg struct { + bucket *Bucket + key string + filePath string + options []Option + hook downloadPartHook + enableCRC bool +} + +// downloadPartHook is hook for test +type downloadPartHook func(part downloadPart) error + +var downloadPartHooker downloadPartHook = defaultDownloadPartHook + +func defaultDownloadPartHook(part downloadPart) error { + return nil +} + +// defaultDownloadProgressListener defines default ProgressListener, shields the ProgressListener in options of GetObject. +type defaultDownloadProgressListener struct { +} + +// ProgressChanged no-ops +func (listener *defaultDownloadProgressListener) ProgressChanged(event *ProgressEvent) { +} + +// downloadWorker +func downloadWorker(id int, arg downloadWorkerArg, jobs <-chan downloadPart, results chan<- downloadPart, failed chan<- error, die <-chan bool) { + for part := range jobs { + if err := arg.hook(part); err != nil { + failed <- err + break + } + + // Resolve options + r := Range(part.Start, part.End) + p := Progress(&defaultDownloadProgressListener{}) + + var respHeader http.Header + opts := make([]Option, len(arg.options)+3) + // Append orderly, can not be reversed! + opts = append(opts, arg.options...) + opts = append(opts, r, p, GetResponseHeader(&respHeader)) + + rd, err := arg.bucket.GetObject(arg.key, opts...) + if err != nil { + failed <- err + break + } + defer rd.Close() + + var crcCalc hash.Hash64 + if arg.enableCRC { + crcCalc = crc64.New(CrcTable()) + contentLen := part.End - part.Start + 1 + rd = ioutil.NopCloser(TeeReader(rd, crcCalc, contentLen, nil, nil)) + } + defer rd.Close() + + select { + case <-die: + return + default: + } + + fd, err := os.OpenFile(arg.filePath, os.O_WRONLY, FilePermMode) + if err != nil { + failed <- err + break + } + + _, err = fd.Seek(part.Start-part.Offset, os.SEEK_SET) + if err != nil { + fd.Close() + failed <- err + break + } + + startT := time.Now().UnixNano() / 1000 / 1000 / 1000 + _, err = io.Copy(fd, rd) + endT := time.Now().UnixNano() / 1000 / 1000 / 1000 + if err != nil { + arg.bucket.Client.Config.WriteLog(Debug, "download part error,cost:%d second,part number:%d,request id:%s,error:%s.\n", endT-startT, part.Index, GetRequestId(respHeader), err.Error()) + fd.Close() + failed <- err + break + } + + if arg.enableCRC { + part.CRC64 = crcCalc.Sum64() + } + + fd.Close() + results <- part + } +} + +// downloadScheduler +func downloadScheduler(jobs chan downloadPart, parts []downloadPart) { + for _, part := range parts { + jobs <- part + } + close(jobs) +} + +// downloadPart defines download part +type downloadPart struct { + Index int // Part number, starting from 0 + Start int64 // Start index + End int64 // End index + Offset int64 // Offset + CRC64 uint64 // CRC check value of part +} + +// getDownloadParts gets download parts +func getDownloadParts(objectSize, partSize int64, uRange *UnpackedRange) []downloadPart { + parts := []downloadPart{} + part := downloadPart{} + i := 0 + start, end := AdjustRange(uRange, objectSize) + for offset := start; offset < end; offset += partSize { + part.Index = i + part.Start = offset + part.End = GetPartEnd(offset, end, partSize) + part.Offset = start + part.CRC64 = 0 + parts = append(parts, part) + i++ + } + return parts +} + +// getObjectBytes gets object bytes length +func getObjectBytes(parts []downloadPart) int64 { + var ob int64 + for _, part := range parts { + ob += (part.End - part.Start + 1) + } + return ob +} + +// combineCRCInParts caculates the total CRC of continuous parts +func combineCRCInParts(dps []downloadPart) uint64 { + if dps == nil || len(dps) == 0 { + return 0 + } + + crc := dps[0].CRC64 + for i := 1; i < len(dps); i++ { + crc = CRC64Combine(crc, dps[i].CRC64, (uint64)(dps[i].End-dps[i].Start+1)) + } + + return crc +} + +// downloadFile downloads file concurrently without checkpoint. +func (bucket Bucket) downloadFile(objectKey, filePath string, partSize int64, options []Option, routines int, uRange *UnpackedRange) error { + tempFilePath := filePath + TempFileSuffix + listener := GetProgressListener(options) + + // If the file does not exist, create one. If exists, the download will overwrite it. + fd, err := os.OpenFile(tempFilePath, os.O_WRONLY|os.O_CREATE, FilePermMode) + if err != nil { + return err + } + fd.Close() + + // Get the object detailed meta for object whole size + // must delete header:range to get whole object size + skipOptions := DeleteOption(options, HTTPHeaderRange) + meta, err := bucket.GetObjectDetailedMeta(objectKey, skipOptions...) + if err != nil { + return err + } + + objectSize, err := strconv.ParseInt(meta.Get(HTTPHeaderContentLength), 10, 64) + if err != nil { + return err + } + + enableCRC := false + expectedCRC := (uint64)(0) + if bucket.GetConfig().IsEnableCRC && meta.Get(HTTPHeaderOssCRC64) != "" { + if uRange == nil || (!uRange.HasStart && !uRange.HasEnd) { + enableCRC = true + expectedCRC, _ = strconv.ParseUint(meta.Get(HTTPHeaderOssCRC64), 10, 64) + } + } + + // Get the parts of the file + parts := getDownloadParts(objectSize, partSize, uRange) + jobs := make(chan downloadPart, len(parts)) + results := make(chan downloadPart, len(parts)) + failed := make(chan error) + die := make(chan bool) + + var completedBytes int64 + totalBytes := getObjectBytes(parts) + event := newProgressEvent(TransferStartedEvent, 0, totalBytes, 0) + publishProgress(listener, event) + + // Start the download workers + arg := downloadWorkerArg{&bucket, objectKey, tempFilePath, options, downloadPartHooker, enableCRC} + for w := 1; w <= routines; w++ { + go downloadWorker(w, arg, jobs, results, failed, die) + } + + // Download parts concurrently + go downloadScheduler(jobs, parts) + + // Waiting for parts download finished + completed := 0 + for completed < len(parts) { + select { + case part := <-results: + completed++ + downBytes := (part.End - part.Start + 1) + completedBytes += downBytes + parts[part.Index].CRC64 = part.CRC64 + event = newProgressEvent(TransferDataEvent, completedBytes, totalBytes, downBytes) + publishProgress(listener, event) + case err := <-failed: + close(die) + event = newProgressEvent(TransferFailedEvent, completedBytes, totalBytes, 0) + publishProgress(listener, event) + return err + } + + if completed >= len(parts) { + break + } + } + + event = newProgressEvent(TransferCompletedEvent, completedBytes, totalBytes, 0) + publishProgress(listener, event) + + if enableCRC { + actualCRC := combineCRCInParts(parts) + err = CheckDownloadCRC(actualCRC, expectedCRC) + if err != nil { + return err + } + } + + return os.Rename(tempFilePath, filePath) +} + +// ----- Concurrent download with chcekpoint ----- + +const downloadCpMagic = "92611BED-89E2-46B6-89E5-72F273D4B0A3" + +type downloadCheckpoint struct { + Magic string // Magic + MD5 string // Checkpoint content MD5 + FilePath string // Local file + Object string // Key + ObjStat objectStat // Object status + Parts []downloadPart // All download parts + PartStat []bool // Parts' download status + Start int64 // Start point of the file + End int64 // End point of the file + enableCRC bool // Whether has CRC check + CRC uint64 // CRC check value +} + +type objectStat struct { + Size int64 // Object size + LastModified string // Last modified time + Etag string // Etag +} + +// isValid flags of checkpoint data is valid. It returns true when the data is valid and the checkpoint is valid and the object is not updated. +func (cp downloadCheckpoint) isValid(meta http.Header, uRange *UnpackedRange) (bool, error) { + // Compare the CP's Magic and the MD5 + cpb := cp + cpb.MD5 = "" + js, _ := json.Marshal(cpb) + sum := md5.Sum(js) + b64 := base64.StdEncoding.EncodeToString(sum[:]) + + if cp.Magic != downloadCpMagic || b64 != cp.MD5 { + return false, nil + } + + objectSize, err := strconv.ParseInt(meta.Get(HTTPHeaderContentLength), 10, 64) + if err != nil { + return false, err + } + + // Compare the object size, last modified time and etag + if cp.ObjStat.Size != objectSize || + cp.ObjStat.LastModified != meta.Get(HTTPHeaderLastModified) || + cp.ObjStat.Etag != meta.Get(HTTPHeaderEtag) { + return false, nil + } + + // Check the download range + if uRange != nil { + start, end := AdjustRange(uRange, objectSize) + if start != cp.Start || end != cp.End { + return false, nil + } + } + + return true, nil +} + +// load checkpoint from local file +func (cp *downloadCheckpoint) load(filePath string) error { + contents, err := ioutil.ReadFile(filePath) + if err != nil { + return err + } + + err = json.Unmarshal(contents, cp) + return err +} + +// dump funciton dumps to file +func (cp *downloadCheckpoint) dump(filePath string) error { + bcp := *cp + + // Calculate MD5 + bcp.MD5 = "" + js, err := json.Marshal(bcp) + if err != nil { + return err + } + sum := md5.Sum(js) + b64 := base64.StdEncoding.EncodeToString(sum[:]) + bcp.MD5 = b64 + + // Serialize + js, err = json.Marshal(bcp) + if err != nil { + return err + } + + // Dump + return ioutil.WriteFile(filePath, js, FilePermMode) +} + +// todoParts gets unfinished parts +func (cp downloadCheckpoint) todoParts() []downloadPart { + dps := []downloadPart{} + for i, ps := range cp.PartStat { + if !ps { + dps = append(dps, cp.Parts[i]) + } + } + return dps +} + +// getCompletedBytes gets completed size +func (cp downloadCheckpoint) getCompletedBytes() int64 { + var completedBytes int64 + for i, part := range cp.Parts { + if cp.PartStat[i] { + completedBytes += (part.End - part.Start + 1) + } + } + return completedBytes +} + +// prepare initiates download tasks +func (cp *downloadCheckpoint) prepare(meta http.Header, bucket *Bucket, objectKey, filePath string, partSize int64, uRange *UnpackedRange) error { + // CP + cp.Magic = downloadCpMagic + cp.FilePath = filePath + cp.Object = objectKey + + objectSize, err := strconv.ParseInt(meta.Get(HTTPHeaderContentLength), 10, 64) + if err != nil { + return err + } + + cp.ObjStat.Size = objectSize + cp.ObjStat.LastModified = meta.Get(HTTPHeaderLastModified) + cp.ObjStat.Etag = meta.Get(HTTPHeaderEtag) + + if bucket.GetConfig().IsEnableCRC && meta.Get(HTTPHeaderOssCRC64) != "" { + if uRange == nil || (!uRange.HasStart && !uRange.HasEnd) { + cp.enableCRC = true + cp.CRC, _ = strconv.ParseUint(meta.Get(HTTPHeaderOssCRC64), 10, 64) + } + } + + // Parts + cp.Parts = getDownloadParts(objectSize, partSize, uRange) + cp.PartStat = make([]bool, len(cp.Parts)) + for i := range cp.PartStat { + cp.PartStat[i] = false + } + + return nil +} + +func (cp *downloadCheckpoint) complete(cpFilePath, downFilepath string) error { + err := os.Rename(downFilepath, cp.FilePath) + if err != nil { + return err + } + return os.Remove(cpFilePath) +} + +// downloadFileWithCp downloads files with checkpoint. +func (bucket Bucket) downloadFileWithCp(objectKey, filePath string, partSize int64, options []Option, cpFilePath string, routines int, uRange *UnpackedRange) error { + tempFilePath := filePath + TempFileSuffix + listener := GetProgressListener(options) + + // Load checkpoint data. + dcp := downloadCheckpoint{} + err := dcp.load(cpFilePath) + if err != nil { + os.Remove(cpFilePath) + } + + // Get the object detailed meta for object whole size + // must delete header:range to get whole object size + skipOptions := DeleteOption(options, HTTPHeaderRange) + meta, err := bucket.GetObjectDetailedMeta(objectKey, skipOptions...) + if err != nil { + return err + } + + // Load error or data invalid. Re-initialize the download. + valid, err := dcp.isValid(meta, uRange) + if err != nil || !valid { + if err = dcp.prepare(meta, &bucket, objectKey, filePath, partSize, uRange); err != nil { + return err + } + os.Remove(cpFilePath) + } + + // Create the file if not exists. Otherwise the parts download will overwrite it. + fd, err := os.OpenFile(tempFilePath, os.O_WRONLY|os.O_CREATE, FilePermMode) + if err != nil { + return err + } + fd.Close() + + // Unfinished parts + parts := dcp.todoParts() + jobs := make(chan downloadPart, len(parts)) + results := make(chan downloadPart, len(parts)) + failed := make(chan error) + die := make(chan bool) + + completedBytes := dcp.getCompletedBytes() + event := newProgressEvent(TransferStartedEvent, completedBytes, dcp.ObjStat.Size, 0) + publishProgress(listener, event) + + // Start the download workers routine + arg := downloadWorkerArg{&bucket, objectKey, tempFilePath, options, downloadPartHooker, dcp.enableCRC} + for w := 1; w <= routines; w++ { + go downloadWorker(w, arg, jobs, results, failed, die) + } + + // Concurrently downloads parts + go downloadScheduler(jobs, parts) + + // Wait for the parts download finished + completed := 0 + for completed < len(parts) { + select { + case part := <-results: + completed++ + dcp.PartStat[part.Index] = true + dcp.Parts[part.Index].CRC64 = part.CRC64 + dcp.dump(cpFilePath) + downBytes := (part.End - part.Start + 1) + completedBytes += downBytes + event = newProgressEvent(TransferDataEvent, completedBytes, dcp.ObjStat.Size, downBytes) + publishProgress(listener, event) + case err := <-failed: + close(die) + event = newProgressEvent(TransferFailedEvent, completedBytes, dcp.ObjStat.Size, 0) + publishProgress(listener, event) + return err + } + + if completed >= len(parts) { + break + } + } + + event = newProgressEvent(TransferCompletedEvent, completedBytes, dcp.ObjStat.Size, 0) + publishProgress(listener, event) + + if dcp.enableCRC { + actualCRC := combineCRCInParts(dcp.Parts) + err = CheckDownloadCRC(actualCRC, dcp.CRC) + if err != nil { + return err + } + } + + return dcp.complete(cpFilePath, tempFilePath) +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/download_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/download_test.go new file mode 100644 index 00000000..6c737640 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/download_test.go @@ -0,0 +1,979 @@ +package oss + +import ( + "bytes" + "fmt" + "net/http" + "os" + "strings" + "time" + + . "gopkg.in/check.v1" +) + +type OssDownloadSuite struct { + client *Client + bucket *Bucket +} + +var _ = Suite(&OssDownloadSuite{}) + +// SetUpSuite runs once when the suite starts running +func (s *OssDownloadSuite) SetUpSuite(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + s.client = client + + s.client.CreateBucket(bucketName) + + bucket, err := s.client.Bucket(bucketName) + c.Assert(err, IsNil) + s.bucket = bucket + + testLogger.Println("test download started") +} + +// TearDownSuite runs before each test or benchmark starts running +func (s *OssDownloadSuite) TearDownSuite(c *C) { + // Delete part + keyMarker := KeyMarker("") + uploadIDMarker := UploadIDMarker("") + for { + lmur, err := s.bucket.ListMultipartUploads(keyMarker, uploadIDMarker) + c.Assert(err, IsNil) + for _, upload := range lmur.Uploads { + var imur = InitiateMultipartUploadResult{Bucket: s.bucket.BucketName, + Key: upload.Key, UploadID: upload.UploadID} + err = s.bucket.AbortMultipartUpload(imur) + c.Assert(err, IsNil) + } + keyMarker = KeyMarker(lmur.NextKeyMarker) + uploadIDMarker = UploadIDMarker(lmur.NextUploadIDMarker) + if !lmur.IsTruncated { + break + } + } + + // Delete objects + marker := Marker("") + for { + lor, err := s.bucket.ListObjects(marker) + c.Assert(err, IsNil) + for _, object := range lor.Objects { + err = s.bucket.DeleteObject(object.Key) + c.Assert(err, IsNil) + } + marker = Marker(lor.NextMarker) + if !lor.IsTruncated { + break + } + } + + // Delete bucket + err := s.client.DeleteBucket(s.bucket.BucketName) + c.Assert(err, IsNil) + + testLogger.Println("test download completed") +} + +// SetUpTest runs after each test or benchmark runs +func (s *OssDownloadSuite) SetUpTest(c *C) { + err := removeTempFiles("../oss", ".jpg") + c.Assert(err, IsNil) +} + +// TearDownTest runs once after all tests or benchmarks have finished running +func (s *OssDownloadSuite) TearDownTest(c *C) { + err := removeTempFiles("../oss", ".jpg") + c.Assert(err, IsNil) + + err = removeTempFiles("../oss", ".temp") + c.Assert(err, IsNil) +} + +// TestDownloadRoutineWithoutRecovery multipart downloads without checkpoint +func (s *OssDownloadSuite) TestDownloadRoutineWithoutRecovery(c *C) { + objectName := objectNamePrefix + RandStr(8) + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + newFile := RandStr(8) + ".jpg" + + // Upload a file + err := s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(3)) + c.Assert(err, IsNil) + + // Download the file by default + err = s.bucket.DownloadFile(objectName, newFile, 100*1024) + c.Assert(err, IsNil) + + // Check + eq, err := compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // Use 2 coroutines to download the file and total parts count is 5 + os.Remove(newFile) + err = s.bucket.DownloadFile(objectName, newFile, 100*1024, Routines(2)) + c.Assert(err, IsNil) + + // Check + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // Use 5 coroutines to download the file and the total parts count is 5. + os.Remove(newFile) + err = s.bucket.DownloadFile(objectName, newFile, 100*1024, Routines(5)) + c.Assert(err, IsNil) + + // Check + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // Use 10 coroutines to download the file and the total parts count is 5. + os.Remove(newFile) + err = s.bucket.DownloadFile(objectName, newFile, 100*1024, Routines(10)) + c.Assert(err, IsNil) + + // Check + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +// DownErrorHooker requests hook by downloadPart +func DownErrorHooker(part downloadPart) error { + if part.Index == 4 { + time.Sleep(time.Second) + return fmt.Errorf("ErrorHooker") + } + return nil +} + +// TestDownloadRoutineWithRecovery multi-routine resumable download +func (s *OssDownloadSuite) TestDownloadRoutineWithRecovery(c *C) { + objectName := objectNamePrefix + RandStr(8) + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + newFile := RandStr(8) + ".jpg" + + // Upload a file + err := s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(3)) + c.Assert(err, IsNil) + + // Download a file with default checkpoint + downloadPartHooker = DownErrorHooker + err = s.bucket.DownloadFile(objectName, newFile, 100*1024, Checkpoint(true, newFile+".cp")) + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "ErrorHooker") + downloadPartHooker = defaultDownloadPartHook + + // Check + dcp := downloadCheckpoint{} + err = dcp.load(newFile + ".cp") + c.Assert(err, IsNil) + c.Assert(dcp.Magic, Equals, downloadCpMagic) + c.Assert(len(dcp.MD5), Equals, len("LC34jZU5xK4hlxi3Qn3XGQ==")) + c.Assert(dcp.FilePath, Equals, newFile) + c.Assert(dcp.ObjStat.Size, Equals, int64(482048)) + c.Assert(len(dcp.ObjStat.LastModified) > 0, Equals, true) + c.Assert(dcp.ObjStat.Etag, Equals, "\"2351E662233817A7AE974D8C5B0876DD-5\"") + c.Assert(dcp.Object, Equals, objectName) + c.Assert(len(dcp.Parts), Equals, 5) + c.Assert(len(dcp.todoParts()), Equals, 1) + + err = s.bucket.DownloadFile(objectName, newFile, 100*1024, Checkpoint(true, newFile+".cp")) + c.Assert(err, IsNil) + //download success, checkpoint file has been deleted + err = dcp.load(newFile + ".cp") + c.Assert(err, NotNil) + + eq, err := compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // Resumable download with empty checkpoint file path + downloadPartHooker = DownErrorHooker + err = s.bucket.DownloadFile(objectName, newFile, 100*1024, Checkpoint(true, "")) + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "ErrorHooker") + downloadPartHooker = defaultDownloadPartHook + + dcp = downloadCheckpoint{} + err = dcp.load(newFile + ".cp") + c.Assert(err, NotNil) + + // Resumable download with checkpoint dir + os.Remove(newFile) + downloadPartHooker = DownErrorHooker + err = s.bucket.DownloadFile(objectName, newFile, 100*1024, CheckpointDir(true, "./")) + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "ErrorHooker") + downloadPartHooker = defaultDownloadPartHook + + // Check + dcp = downloadCheckpoint{} + cpConf := cpConfig{IsEnable: true, DirPath: "./"} + cpFilePath := getDownloadCpFilePath(&cpConf, s.bucket.BucketName, objectName, "",newFile) + err = dcp.load(cpFilePath) + c.Assert(err, IsNil) + c.Assert(dcp.Magic, Equals, downloadCpMagic) + c.Assert(len(dcp.MD5), Equals, len("LC34jZU5xK4hlxi3Qn3XGQ==")) + c.Assert(dcp.FilePath, Equals, newFile) + c.Assert(dcp.ObjStat.Size, Equals, int64(482048)) + c.Assert(len(dcp.ObjStat.LastModified) > 0, Equals, true) + c.Assert(dcp.ObjStat.Etag, Equals, "\"2351E662233817A7AE974D8C5B0876DD-5\"") + c.Assert(dcp.Object, Equals, objectName) + c.Assert(len(dcp.Parts), Equals, 5) + c.Assert(len(dcp.todoParts()), Equals, 1) + + err = s.bucket.DownloadFile(objectName, newFile, 100*1024, CheckpointDir(true, "./")) + c.Assert(err, IsNil) + //download success, checkpoint file has been deleted + err = dcp.load(cpFilePath) + c.Assert(err, NotNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // Resumable download with checkpoint at a time. No error is expected in the download procedure. + os.Remove(newFile) + err = s.bucket.DownloadFile(objectName, newFile, 100*1024, Checkpoint(true, newFile+".cp")) + c.Assert(err, IsNil) + + err = dcp.load(newFile + ".cp") + c.Assert(err, NotNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // Resumable download with checkpoint at a time. No error is expected in the download procedure. + os.Remove(newFile) + err = s.bucket.DownloadFile(objectName, newFile, 100*1024, Routines(10), Checkpoint(true, newFile+".cp")) + c.Assert(err, IsNil) + + err = dcp.load(newFile + ".cp") + c.Assert(err, NotNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +// TestDownloadOption options +func (s *OssDownloadSuite) TestDownloadOption(c *C) { + objectName := objectNamePrefix + RandStr(8) + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + newFile := RandStr(8) + ".jpg" + + // Upload the file + err := s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(3)) + c.Assert(err, IsNil) + + meta, err := s.bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + + // IfMatch + os.Remove(newFile) + err = s.bucket.DownloadFile(objectName, newFile, 100*1024, Routines(3), IfMatch(meta.Get("Etag"))) + c.Assert(err, IsNil) + + eq, err := compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // IfNoneMatch + os.Remove(newFile) + err = s.bucket.DownloadFile(objectName, newFile, 100*1024, Routines(3), IfNoneMatch(meta.Get("Etag"))) + c.Assert(err, NotNil) + + // IfMatch + err = s.bucket.DownloadFile(objectName, newFile, 100*1024, Routines(3), IfMatch(meta.Get("Etag"))) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // IfNoneMatch + err = s.bucket.DownloadFile(objectName, newFile, 100*1024, Routines(3), IfNoneMatch(meta.Get("Etag"))) + c.Assert(err, NotNil) +} + +// TestDownloadObjectChange tests the file is updated during the upload +func (s *OssDownloadSuite) TestDownloadObjectChange(c *C) { + objectName := objectNamePrefix + RandStr(8) + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + newFile := RandStr(8) + ".jpg" + + // Upload a file + err := s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(3)) + c.Assert(err, IsNil) + + // Download with default checkpoint + downloadPartHooker = DownErrorHooker + err = s.bucket.DownloadFile(objectName, newFile, 100*1024, Checkpoint(true, newFile+".cp")) + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "ErrorHooker") + downloadPartHooker = defaultDownloadPartHook + + err = s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(3)) + c.Assert(err, IsNil) + + err = s.bucket.DownloadFile(objectName, newFile, 100*1024, Checkpoint(true, newFile+".cp")) + c.Assert(err, IsNil) + + eq, err := compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) +} + +// TestDownloadNegative tests downloading negative +func (s *OssDownloadSuite) TestDownloadNegative(c *C) { + objectName := objectNamePrefix + RandStr(8) + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + newFile := RandStr(8) + ".jpg" + + // Upload a file + err := s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(3)) + c.Assert(err, IsNil) + + // Worker routine error + downloadPartHooker = DownErrorHooker + err = s.bucket.DownloadFile(objectName, newFile, 100*1024, Routines(2)) + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "ErrorHooker") + downloadPartHooker = defaultDownloadPartHook + + // Local file does not exist + err = s.bucket.DownloadFile(objectName, "/tmp/", 100*1024, Routines(2)) + c.Assert(err, NotNil) + + // Invalid part size + err = s.bucket.DownloadFile(objectName, newFile, 0, Routines(2)) + c.Assert(err, NotNil) + + err = s.bucket.DownloadFile(objectName, newFile, 1024*1024*1024*100, Routines(2)) + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Local file does not exist + err = s.bucket.DownloadFile(objectName, "/tmp/", 100*1024) + c.Assert(err, NotNil) + + err = s.bucket.DownloadFile(objectName, "/tmp/", 100*1024, Routines(2)) + c.Assert(err, NotNil) + + // Invalid part size + err = s.bucket.DownloadFile(objectName, newFile, -1) + c.Assert(err, NotNil) + + err = s.bucket.DownloadFile(objectName, newFile, 0, Routines(2)) + c.Assert(err, NotNil) + + err = s.bucket.DownloadFile(objectName, newFile, 1024*1024*1024*100) + c.Assert(err, NotNil) + + err = s.bucket.DownloadFile(objectName, newFile, 1024*1024*1024*100, Routines(2)) + c.Assert(err, NotNil) +} + +// TestDownloadWithRange tests concurrent downloading with range specified and checkpoint enabled +func (s *OssDownloadSuite) TestDownloadWithRange(c *C) { + objectName := objectNamePrefix + RandStr(8) + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + newFile := RandStr(8) + ".jpg" + newFileGet := RandStr(8) + "-.jpg" + + // Upload a file + err := s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(3)) + c.Assert(err, IsNil) + + fileSize, err := getFileSize(fileName) + c.Assert(err, IsNil) + + // Download with range, from 1024 to 4096 + os.Remove(newFile) + err = s.bucket.DownloadFile(objectName, newFile, 100*1024, Routines(3), Range(1024, 4095)) + c.Assert(err, IsNil) + + // Check + eq, err := compareFilesWithRange(fileName, 1024, newFile, 0, 3072) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + os.Remove(newFileGet) + err = s.bucket.GetObjectToFile(objectName, newFileGet, Range(1024, 4095)) + c.Assert(err, IsNil) + + // Compare get and download + eq, err = compareFiles(newFile, newFileGet) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // Download with range, from 1024 to 4096 + os.Remove(newFile) + err = s.bucket.DownloadFile(objectName, newFile, 1024, Routines(3), NormalizedRange("1024-4095")) + c.Assert(err, IsNil) + + // Check + eq, err = compareFilesWithRange(fileName, 1024, newFile, 0, 3072) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + os.Remove(newFileGet) + err = s.bucket.GetObjectToFile(objectName, newFileGet, NormalizedRange("1024-4095")) + c.Assert(err, IsNil) + + // Compare get and download + eq, err = compareFiles(newFile, newFileGet) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // Download with range, from 2048 to the end + os.Remove(newFile) + err = s.bucket.DownloadFile(objectName, newFile, 1024*1024, Routines(3), NormalizedRange("2048-")) + c.Assert(err, IsNil) + + // Check + eq, err = compareFilesWithRange(fileName, 2048, newFile, 0, fileSize-2048) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + os.Remove(newFileGet) + err = s.bucket.GetObjectToFile(objectName, newFileGet, NormalizedRange("2048-")) + c.Assert(err, IsNil) + + // Compare get and download + eq, err = compareFiles(newFile, newFileGet) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // Download with range, the last 4096 + os.Remove(newFile) + err = s.bucket.DownloadFile(objectName, newFile, 1024, Routines(3), NormalizedRange("-4096")) + c.Assert(err, IsNil) + + // Check + eq, err = compareFilesWithRange(fileName, fileSize-4096, newFile, 0, 4096) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + os.Remove(newFileGet) + err = s.bucket.GetObjectToFile(objectName, newFileGet, NormalizedRange("-4096")) + c.Assert(err, IsNil) + + // Compare get and download + eq, err = compareFiles(newFile, newFileGet) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +// TestDownloadWithCheckoutAndRange tests concurrent downloading with range specified and checkpoint enabled +func (s *OssDownloadSuite) TestDownloadWithCheckoutAndRange(c *C) { + objectName := objectNamePrefix + RandStr(8) + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + newFile := RandStr(8) + ".jpg" + newFileGet := RandStr(8) + "-get.jpg" + + // Upload a file + err := s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(3), Checkpoint(true, fileName+".cp")) + c.Assert(err, IsNil) + + fileSize, err := getFileSize(fileName) + c.Assert(err, IsNil) + + // Download with range, from 1024 to 4096 + os.Remove(newFile) + err = s.bucket.DownloadFile(objectName, newFile, 100*1024, Routines(3), Checkpoint(true, newFile+".cp"), Range(1024, 4095)) + c.Assert(err, IsNil) + + // Check + eq, err := compareFilesWithRange(fileName, 1024, newFile, 0, 3072) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + os.Remove(newFileGet) + err = s.bucket.GetObjectToFile(objectName, newFileGet, Range(1024, 4095)) + c.Assert(err, IsNil) + + // Compare get and download + eq, err = compareFiles(newFile, newFileGet) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // Download with range, from 1024 to 4096 + os.Remove(newFile) + err = s.bucket.DownloadFile(objectName, newFile, 1024, Routines(3), Checkpoint(true, newFile+".cp"), NormalizedRange("1024-4095")) + c.Assert(err, IsNil) + + // Check + eq, err = compareFilesWithRange(fileName, 1024, newFile, 0, 3072) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + os.Remove(newFileGet) + err = s.bucket.GetObjectToFile(objectName, newFileGet, NormalizedRange("1024-4095")) + c.Assert(err, IsNil) + + // Compare get and download + eq, err = compareFiles(newFile, newFileGet) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // Download with range, from 2048 to the end + os.Remove(newFile) + err = s.bucket.DownloadFile(objectName, newFile, 1024*1024, Routines(3), Checkpoint(true, newFile+".cp"), NormalizedRange("2048-")) + c.Assert(err, IsNil) + + // Check + eq, err = compareFilesWithRange(fileName, 2048, newFile, 0, fileSize-2048) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + os.Remove(newFileGet) + err = s.bucket.GetObjectToFile(objectName, newFileGet, NormalizedRange("2048-")) + c.Assert(err, IsNil) + + // Compare get and download + eq, err = compareFiles(newFile, newFileGet) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // Download with range, the last 4096 bytes + os.Remove(newFile) + err = s.bucket.DownloadFile(objectName, newFile, 1024, Routines(3), Checkpoint(true, newFile+".cp"), NormalizedRange("-4096")) + c.Assert(err, IsNil) + + // Check + eq, err = compareFilesWithRange(fileName, fileSize-4096, newFile, 0, 4096) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + os.Remove(newFileGet) + err = s.bucket.GetObjectToFile(objectName, newFileGet, NormalizedRange("-4096")) + c.Assert(err, IsNil) + + // Compare get and download + eq, err = compareFiles(newFile, newFileGet) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +// TestCombineCRCInDownloadParts tests combineCRCInParts +func (s *OssDownloadSuite) TestCombineCRCInDownloadParts(c *C) { + crc := combineCRCInParts(nil) + c.Assert(crc == 0, Equals, true) + + crc = combineCRCInParts(make([]downloadPart, 0)) + c.Assert(crc == 0, Equals, true) + + parts := make([]downloadPart, 1) + parts[0].CRC64 = 10278880121275185425 + crc = combineCRCInParts(parts) + c.Assert(crc == 10278880121275185425, Equals, true) + + parts = make([]downloadPart, 2) + parts[0].CRC64 = 6748440630437108969 + parts[0].Start = 0 + parts[0].End = 4 + parts[1].CRC64 = 10278880121275185425 + parts[1].Start = 5 + parts[1].End = 8 + crc = combineCRCInParts(parts) + c.Assert(crc == 11051210869376104954, Equals, true) +} + +func getFileSize(fileName string) (int64, error) { + file, err := os.Open(fileName) + if err != nil { + return 0, err + } + defer file.Close() + + stat, err := file.Stat() + if err != nil { + return 0, err + } + + return stat.Size(), nil +} + +// compareFilesWithRange compares the content between fileL and fileR with specified range +func compareFilesWithRange(fileL string, offsetL int64, fileR string, offsetR int64, size int64) (bool, error) { + finL, err := os.Open(fileL) + if err != nil { + return false, err + } + defer finL.Close() + finL.Seek(offsetL, os.SEEK_SET) + + finR, err := os.Open(fileR) + if err != nil { + return false, err + } + defer finR.Close() + finR.Seek(offsetR, os.SEEK_SET) + + statL, err := finL.Stat() + if err != nil { + return false, err + } + + statR, err := finR.Stat() + if err != nil { + return false, err + } + + if (offsetL+size > statL.Size()) || (offsetR+size > statR.Size()) { + return false, nil + } + + part := statL.Size() - offsetL + if part > 16*1024 { + part = 16 * 1024 + } + + bufL := make([]byte, part) + bufR := make([]byte, part) + for readN := int64(0); readN < size; { + n, _ := finL.Read(bufL) + if 0 == n { + break + } + + n, _ = finR.Read(bufR) + if 0 == n { + break + } + + tailer := part + if tailer > size-readN { + tailer = size - readN + } + readN += tailer + + if !bytes.Equal(bufL[0:tailer], bufR[0:tailer]) { + return false, nil + } + } + + return true, nil +} + +func (s *OssDownloadSuite) TestVersioningDownloadWithoutCheckPoint(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + // begin test + objectName := objectNamePrefix + RandStr(8) + fileName := "test-file-" + RandStr(8) + fileData := RandStr(500 * 1024) + CreateFile(fileName, fileData, c) + + newFile := RandStr(8) + ".jpg" + newFileGet := RandStr(8) + "-.jpg" + + // Upload a file + var respHeader http.Header + options := []Option{Routines(3), GetResponseHeader(&respHeader)} + err = bucket.UploadFile(objectName, fileName, 100*1024, options...) + c.Assert(err, IsNil) + versionId := GetVersionId(respHeader) + c.Assert(len(versionId) > 0, Equals, true) + + fileSize, err := getFileSize(fileName) + c.Assert(err, IsNil) + + // overwrite emtpy object + err = bucket.PutObject(objectName, strings.NewReader("")) + c.Assert(err, IsNil) + + // Download with range, from 1024 to 4096 + os.Remove(newFile) + options = []Option{Routines(3), Range(1024, 4095), VersionId(versionId)} + err = bucket.DownloadFile(objectName, newFile, 100*1024, options...) + c.Assert(err, IsNil) + + // Check + eq, err := compareFilesWithRange(fileName, 1024, newFile, 0, 3072) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + os.Remove(newFileGet) + options = []Option{Range(1024, 4095), VersionId(versionId)} + err = bucket.GetObjectToFile(objectName, newFileGet, options...) + c.Assert(err, IsNil) + + // Compare get and download + eq, err = compareFiles(newFile, newFileGet) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // Download with range, from 1024 to 4096 + os.Remove(newFile) + options = []Option{Routines(3), NormalizedRange("1024-4095"), VersionId(versionId)} + err = bucket.DownloadFile(objectName, newFile, 1024, options...) + c.Assert(err, IsNil) + + // Check + eq, err = compareFilesWithRange(fileName, 1024, newFile, 0, 3072) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + os.Remove(newFileGet) + options = []Option{NormalizedRange("1024-4095"), VersionId(versionId)} + err = bucket.GetObjectToFile(objectName, newFileGet, options...) + c.Assert(err, IsNil) + + // Compare get and download + eq, err = compareFiles(newFile, newFileGet) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // Download with range, from 2048 to the end + os.Remove(newFile) + options = []Option{NormalizedRange("2048-"), VersionId(versionId)} + err = bucket.DownloadFile(objectName, newFile, 1024*1024, options...) + c.Assert(err, IsNil) + + // Check + eq, err = compareFilesWithRange(fileName, 2048, newFile, 0, fileSize-2048) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + os.Remove(newFileGet) + options = []Option{NormalizedRange("2048-"), VersionId(versionId)} + err = bucket.GetObjectToFile(objectName, newFileGet, options...) + c.Assert(err, IsNil) + + // Compare get and download + eq, err = compareFiles(newFile, newFileGet) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // Download with range, the last 4096 + os.Remove(newFile) + options = []Option{Routines(3), NormalizedRange("-4096"), VersionId(versionId)} + err = bucket.DownloadFile(objectName, newFile, 1024, options...) + c.Assert(err, IsNil) + + // Check + eq, err = compareFilesWithRange(fileName, fileSize-4096, newFile, 0, 4096) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + os.Remove(newFileGet) + options = []Option{NormalizedRange("-4096"), VersionId(versionId)} + err = bucket.GetObjectToFile(objectName, newFileGet, options...) + c.Assert(err, IsNil) + + // Compare get and download + eq, err = compareFiles(newFile, newFileGet) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // download whole file + os.Remove(newFileGet) + options = []Option{Routines(3), VersionId(versionId)} + err = bucket.GetObjectToFile(objectName, newFileGet, options...) + c.Assert(err, IsNil) + + // Compare get and download + eq, err = compareFiles(fileName, newFileGet) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + os.Remove(fileName) + os.Remove(newFileGet) + err = bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssDownloadSuite) TestVersioningDownloadWithCheckPoint(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + // begin test + objectName := objectNamePrefix + RandStr(8) + fileName := "test-file-" + RandStr(8) + fileData := RandStr(500 * 1024) + CreateFile(fileName, fileData, c) + newFile := RandStr(8) + ".jpg" + + // Upload a file + var respHeader http.Header + options := []Option{Routines(3), GetResponseHeader(&respHeader)} + err = bucket.UploadFile(objectName, fileName, 100*1024, options...) + c.Assert(err, IsNil) + versionId := GetVersionId(respHeader) + c.Assert(len(versionId) > 0, Equals, true) + + // Resumable download with checkpoint dir + os.Remove(newFile) + downloadPartHooker = DownErrorHooker + options = []Option{CheckpointDir(true, "./"), VersionId(versionId)} + + strPayer := getPayer(options) + c.Assert(strPayer, Equals, "") + + err = bucket.DownloadFile(objectName, newFile, 100*1024, options...) + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "ErrorHooker") + + // download again + downloadPartHooker = defaultDownloadPartHook + options = []Option{CheckpointDir(true, "./"), VersionId(versionId), GetResponseHeader(&respHeader)} + err = bucket.DownloadFile(objectName, newFile, 100*1024, options...) + c.Assert(err, IsNil) + c.Assert(GetVersionId(respHeader), Equals, versionId) + + eq, err := compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + os.Remove(fileName) + os.Remove(newFile) + err = bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssDownloadSuite) TestdownloadFileChoiceOptions(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // begin test + objectName := objectNamePrefix + RandStr(8) + fileName := "test-file-" + RandStr(8) + fileData := RandStr(500 * 1024) + CreateFile(fileName, fileData, c) + newFile := RandStr(8) + ".jpg" + + // Upload a file + var respHeader http.Header + options := []Option{Routines(3), GetResponseHeader(&respHeader)} + err = bucket.UploadFile(objectName, fileName, 100*1024, options...) + c.Assert(err, IsNil) + + // Resumable download with checkpoint dir + os.Remove(newFile) + + // downloadFile with properties + options = []Option{ + ObjectACL(ACLPublicRead), + RequestPayer(Requester), + TrafficLimitHeader(1024 * 1024 * 8), + } + + err = bucket.DownloadFile(objectName, newFile, 100*1024, options...) + c.Assert(err, IsNil) + + eq, err := compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + os.Remove(fileName) + os.Remove(newFile) + err = bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + ForceDeleteBucket(client, bucketName, c) +} + +func (s *OssDownloadSuite) TestdownloadFileWithCpChoiceOptions(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // begin test + objectName := objectNamePrefix + RandStr(8) + fileName := "test-file-" + RandStr(8) + fileData := RandStr(500 * 1024) + CreateFile(fileName, fileData, c) + newFile := RandStr(8) + ".jpg" + + // Upload a file + var respHeader http.Header + options := []Option{Routines(3), GetResponseHeader(&respHeader)} + err = bucket.UploadFile(objectName, fileName, 100*1024, options...) + c.Assert(err, IsNil) + + // Resumable download with checkpoint dir + os.Remove(newFile) + + // DownloadFile with properties + options = []Option{ + ObjectACL(ACLPublicRead), + RequestPayer(Requester), + TrafficLimitHeader(1024 * 1024 * 8), + CheckpointDir(true, "./"), + } + + err = bucket.DownloadFile(objectName, newFile, 100*1024, options...) + c.Assert(err, IsNil) + + eq, err := compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + os.Remove(fileName) + os.Remove(newFile) + err = bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + ForceDeleteBucket(client, bucketName, c) +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/error.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/error.go new file mode 100644 index 00000000..a877211f --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/error.go @@ -0,0 +1,94 @@ +package oss + +import ( + "encoding/xml" + "fmt" + "net/http" + "strings" +) + +// ServiceError contains fields of the error response from Oss Service REST API. +type ServiceError struct { + XMLName xml.Name `xml:"Error"` + Code string `xml:"Code"` // The error code returned from OSS to the caller + Message string `xml:"Message"` // The detail error message from OSS + RequestID string `xml:"RequestId"` // The UUID used to uniquely identify the request + HostID string `xml:"HostId"` // The OSS server cluster's Id + Endpoint string `xml:"Endpoint"` + RawMessage string // The raw messages from OSS + StatusCode int // HTTP status code +} + +// Error implements interface error +func (e ServiceError) Error() string { + if e.Endpoint == "" { + return fmt.Sprintf("oss: service returned error: StatusCode=%d, ErrorCode=%s, ErrorMessage=\"%s\", RequestId=%s", + e.StatusCode, e.Code, e.Message, e.RequestID) + } + return fmt.Sprintf("oss: service returned error: StatusCode=%d, ErrorCode=%s, ErrorMessage=\"%s\", RequestId=%s, Endpoint=%s", + e.StatusCode, e.Code, e.Message, e.RequestID, e.Endpoint) +} + +// 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 // The expected HTTP stats code returned from OSS + got int // The actual HTTP status code from OSS +} + +// Error implements interface error +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("oss: status code from service response is %s; was expecting %s", + got, strings.Join(expected, " or ")) +} + +// Got is the actual status code returned by oss. +func (e UnexpectedStatusCodeError) Got() int { + return e.got +} + +// 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} +} + +// CRCCheckError is returned when crc check is inconsistent between client and server +type CRCCheckError struct { + clientCRC uint64 // Calculated CRC64 in client + serverCRC uint64 // Calculated CRC64 in server + operation string // Upload operations such as PutObject/AppendObject/UploadPart, etc + requestID string // The request id of this operation +} + +// Error implements interface error +func (e CRCCheckError) Error() string { + return fmt.Sprintf("oss: the crc of %s is inconsistent, client %d but server %d; request id is %s", + e.operation, e.clientCRC, e.serverCRC, e.requestID) +} + +func CheckDownloadCRC(clientCRC, serverCRC uint64) error { + if clientCRC == serverCRC { + return nil + } + return CRCCheckError{clientCRC, serverCRC, "DownloadFile", ""} +} + +func CheckCRC(resp *Response, operation string) error { + if resp.Headers.Get(HTTPHeaderOssCRC64) == "" || resp.ClientCRC == resp.ServerCRC { + return nil + } + return CRCCheckError{resp.ClientCRC, resp.ServerCRC, operation, resp.Headers.Get(HTTPHeaderOssRequestID)} +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/error_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/error_test.go new file mode 100644 index 00000000..49280cdf --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/error_test.go @@ -0,0 +1,111 @@ +package oss + +import ( + "math" + "net/http" + "strings" + + . "gopkg.in/check.v1" +) + +type OssErrorSuite struct{} + +var _ = Suite(&OssErrorSuite{}) + +func (s *OssErrorSuite) TestCheckCRCHasCRCInResp(c *C) { + headers := http.Header{ + "Expires": {"-1"}, + "Content-Length": {"0"}, + "Content-Encoding": {"gzip"}, + "X-Oss-Hash-Crc64ecma": {"0"}, + } + + resp := &Response{ + StatusCode: 200, + Headers: headers, + Body: nil, + ClientCRC: math.MaxUint64, + ServerCRC: math.MaxUint64, + } + + err := CheckCRC(resp, "test") + c.Assert(err, IsNil) +} + +func (s *OssErrorSuite) TestCheckCRCNotHasCRCInResp(c *C) { + headers := http.Header{ + "Expires": {"-1"}, + "Content-Length": {"0"}, + "Content-Encoding": {"gzip"}, + } + + resp := &Response{ + StatusCode: 200, + Headers: headers, + Body: nil, + ClientCRC: math.MaxUint64, + ServerCRC: math.MaxUint32, + } + + err := CheckCRC(resp, "test") + c.Assert(err, IsNil) +} + +func (s *OssErrorSuite) TestCheckCRCCNegative(c *C) { + headers := http.Header{ + "Expires": {"-1"}, + "Content-Length": {"0"}, + "Content-Encoding": {"gzip"}, + "X-Oss-Hash-Crc64ecma": {"0"}, + } + + resp := &Response{ + StatusCode: 200, + Headers: headers, + Body: nil, + ClientCRC: 0, + ServerCRC: math.MaxUint64, + } + + err := CheckCRC(resp, "test") + c.Assert(err, NotNil) + testLogger.Println("error:", err) +} + +func (s *OssErrorSuite) TestCheckDownloadCRC(c *C) { + err := CheckDownloadCRC(0xFBF9D9603A6FA020, 0xFBF9D9603A6FA020) + c.Assert(err, IsNil) + + err = CheckDownloadCRC(0, 0) + c.Assert(err, IsNil) + + err = CheckDownloadCRC(0xDB6EFFF26AA94946, 0) + c.Assert(err, NotNil) + testLogger.Println("error:", err) +} + +func (s *OssErrorSuite) TestServiceErrorEndPoint(c *C) { + xmlBody := ` + + AccessDenied + The bucket you visit is not belong to you. + 5C1B5E9BD79A6B9B6466166E + oss-c-sdk-test-verify-b.oss-cn-shenzhen.aliyuncs.com + ` + serverError, _ := serviceErrFromXML([]byte(xmlBody), 403, "5C1B5E9BD79A6B9B6466166E") + errMsg := serverError.Error() + c.Assert(strings.Contains(errMsg, "Endpoint="), Equals, false) + + xmlBodyWithEndPoint := ` + + AccessDenied + The bucket you are attempting to access must be addressed using the specified endpoint. Please send all future requests to this endpoint. + 5C1B595ED51820B569C6A12F + hello-hangzws.oss-cn-qingdao.aliyuncs.com + hello-hangzws + oss-cn-shenzhen.aliyuncs.com + ` + serverError, _ = serviceErrFromXML([]byte(xmlBodyWithEndPoint), 406, "5C1B595ED51820B569C6A12F") + errMsg = serverError.Error() + c.Assert(strings.Contains(errMsg, "Endpoint=oss-cn-shenzhen.aliyuncs.com"), Equals, true) +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/limit_reader_1_6.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/limit_reader_1_6.go new file mode 100644 index 00000000..943dc8fd --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/limit_reader_1_6.go @@ -0,0 +1,28 @@ +// +build !go1.7 + +// "golang.org/x/time/rate" is depended on golang context package go1.7 onward +// this file is only for build,not supports limit upload speed +package oss + +import ( + "fmt" + "io" +) + +const ( + perTokenBandwidthSize int = 1024 +) + +type OssLimiter struct { +} + +type LimitSpeedReader struct { + io.ReadCloser + reader io.Reader + ossLimiter *OssLimiter +} + +func GetOssLimiter(uploadSpeed int) (ossLimiter *OssLimiter, err error) { + err = fmt.Errorf("rate.Limiter is not supported below version go1.7") + return nil, err +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/limit_reader_1_7.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/limit_reader_1_7.go new file mode 100644 index 00000000..f6baf298 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/limit_reader_1_7.go @@ -0,0 +1,90 @@ +// +build go1.7 + +package oss + +import ( + "fmt" + "io" + "math" + "time" + + "golang.org/x/time/rate" +) + +const ( + perTokenBandwidthSize int = 1024 +) + +// OssLimiter wrapper rate.Limiter +type OssLimiter struct { + limiter *rate.Limiter +} + +// GetOssLimiter create OssLimiter +// uploadSpeed KB/s +func GetOssLimiter(uploadSpeed int) (ossLimiter *OssLimiter, err error) { + limiter := rate.NewLimiter(rate.Limit(uploadSpeed), uploadSpeed) + + // first consume the initial full token,the limiter will behave more accurately + limiter.AllowN(time.Now(), uploadSpeed) + + return &OssLimiter{ + limiter: limiter, + }, nil +} + +// LimitSpeedReader for limit bandwidth upload +type LimitSpeedReader struct { + io.ReadCloser + reader io.Reader + ossLimiter *OssLimiter +} + +// Read +func (r *LimitSpeedReader) Read(p []byte) (n int, err error) { + n = 0 + err = nil + start := 0 + burst := r.ossLimiter.limiter.Burst() + var end int + var tmpN int + var tc int + for start < len(p) { + if start+burst*perTokenBandwidthSize < len(p) { + end = start + burst*perTokenBandwidthSize + } else { + end = len(p) + } + + tmpN, err = r.reader.Read(p[start:end]) + if tmpN > 0 { + n += tmpN + start = n + } + + if err != nil { + return + } + + tc = int(math.Ceil(float64(tmpN) / float64(perTokenBandwidthSize))) + now := time.Now() + re := r.ossLimiter.limiter.ReserveN(now, tc) + if !re.OK() { + err = fmt.Errorf("LimitSpeedReader.Read() failure,ReserveN error,start:%d,end:%d,burst:%d,perTokenBandwidthSize:%d", + start, end, burst, perTokenBandwidthSize) + return + } + timeDelay := re.Delay() + time.Sleep(timeDelay) + } + return +} + +// Close ... +func (r *LimitSpeedReader) Close() error { + rc, ok := r.reader.(io.ReadCloser) + if ok { + return rc.Close() + } + return nil +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/livechannel.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/livechannel.go new file mode 100644 index 00000000..bf5ba070 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/livechannel.go @@ -0,0 +1,257 @@ +package oss + +import ( + "bytes" + "encoding/xml" + "fmt" + "io" + "net/http" + "strconv" + "time" +) + +// +// CreateLiveChannel create a live-channel +// +// channelName the name of the channel +// config configuration of the channel +// +// CreateLiveChannelResult the result of create live-channel +// error nil if success, otherwise error +// +func (bucket Bucket) CreateLiveChannel(channelName string, config LiveChannelConfiguration) (CreateLiveChannelResult, error) { + var out CreateLiveChannelResult + + bs, err := xml.Marshal(config) + if err != nil { + return out, err + } + + buffer := new(bytes.Buffer) + buffer.Write(bs) + + params := map[string]interface{}{} + params["live"] = nil + resp, err := bucket.do("PUT", channelName, params, nil, buffer, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// +// PutLiveChannelStatus Set the status of the live-channel: enabled/disabled +// +// channelName the name of the channel +// status enabled/disabled +// +// error nil if success, otherwise error +// +func (bucket Bucket) PutLiveChannelStatus(channelName, status string) error { + params := map[string]interface{}{} + params["live"] = nil + params["status"] = status + + resp, err := bucket.do("PUT", channelName, params, nil, nil, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return CheckRespCode(resp.StatusCode, []int{http.StatusOK}) +} + +// PostVodPlaylist create an playlist based on the specified playlist name, startTime and endTime +// +// channelName the name of the channel +// playlistName the name of the playlist, must end with ".m3u8" +// startTime the start time of the playlist +// endTime the endtime of the playlist +// +// error nil if success, otherwise error +// +func (bucket Bucket) PostVodPlaylist(channelName, playlistName string, startTime, endTime time.Time) error { + params := map[string]interface{}{} + params["vod"] = nil + params["startTime"] = strconv.FormatInt(startTime.Unix(), 10) + params["endTime"] = strconv.FormatInt(endTime.Unix(), 10) + + key := fmt.Sprintf("%s/%s", channelName, playlistName) + resp, err := bucket.do("POST", key, params, nil, nil, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return CheckRespCode(resp.StatusCode, []int{http.StatusOK}) +} + +// GetVodPlaylist get the playlist based on the specified channelName, startTime and endTime +// +// channelName the name of the channel +// startTime the start time of the playlist +// endTime the endtime of the playlist +// +// io.ReadCloser reader instance for reading data from response. It must be called close() after the usage and only valid when error is nil. +// error nil if success, otherwise error +// +func (bucket Bucket) GetVodPlaylist(channelName string, startTime, endTime time.Time) (io.ReadCloser, error) { + params := map[string]interface{}{} + params["vod"] = nil + params["startTime"] = strconv.FormatInt(startTime.Unix(), 10) + params["endTime"] = strconv.FormatInt(endTime.Unix(), 10) + + resp, err := bucket.do("GET", channelName, params, nil, nil, nil) + if err != nil { + return nil, err + } + + return resp.Body, nil +} + +// +// GetLiveChannelStat Get the state of the live-channel +// +// channelName the name of the channel +// +// LiveChannelStat the state of the live-channel +// error nil if success, otherwise error +// +func (bucket Bucket) GetLiveChannelStat(channelName string) (LiveChannelStat, error) { + var out LiveChannelStat + params := map[string]interface{}{} + params["live"] = nil + params["comp"] = "stat" + + resp, err := bucket.do("GET", channelName, params, nil, nil, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// +// GetLiveChannelInfo Get the configuration info of the live-channel +// +// channelName the name of the channel +// +// LiveChannelConfiguration the configuration info of the live-channel +// error nil if success, otherwise error +// +func (bucket Bucket) GetLiveChannelInfo(channelName string) (LiveChannelConfiguration, error) { + var out LiveChannelConfiguration + params := map[string]interface{}{} + params["live"] = nil + + resp, err := bucket.do("GET", channelName, params, nil, nil, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// +// GetLiveChannelHistory Get push records of live-channel +// +// channelName the name of the channel +// +// LiveChannelHistory push records +// error nil if success, otherwise error +// +func (bucket Bucket) GetLiveChannelHistory(channelName string) (LiveChannelHistory, error) { + var out LiveChannelHistory + params := map[string]interface{}{} + params["live"] = nil + params["comp"] = "history" + + resp, err := bucket.do("GET", channelName, params, nil, nil, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// +// ListLiveChannel list the live-channels +// +// options Prefix: filter by the name start with the value of "Prefix" +// MaxKeys: the maximum count returned +// Marker: cursor from which starting list +// +// ListLiveChannelResult live-channel list +// error nil if success, otherwise error +// +func (bucket Bucket) ListLiveChannel(options ...Option) (ListLiveChannelResult, error) { + var out ListLiveChannelResult + + params, err := GetRawParams(options) + if err != nil { + return out, err + } + + params["live"] = nil + + resp, err := bucket.do("GET", "", params, nil, nil, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// +// DeleteLiveChannel Delete the live-channel. When a client trying to stream the live-channel, the operation will fail. it will only delete the live-channel itself and the object generated by the live-channel will not be deleted. +// +// channelName the name of the channel +// +// error nil if success, otherwise error +// +func (bucket Bucket) DeleteLiveChannel(channelName string) error { + params := map[string]interface{}{} + params["live"] = nil + + if channelName == "" { + return fmt.Errorf("invalid argument: channel name is empty") + } + + resp, err := bucket.do("DELETE", channelName, params, nil, nil, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return CheckRespCode(resp.StatusCode, []int{http.StatusNoContent}) +} + +// +// SignRtmpURL Generate a RTMP push-stream signature URL for the trusted user to push the RTMP stream to the live-channel. +// +// channelName the name of the channel +// playlistName the name of the playlist, must end with ".m3u8" +// expires expiration (in seconds) +// +// string singed rtmp push stream url +// error nil if success, otherwise error +// +func (bucket Bucket) SignRtmpURL(channelName, playlistName string, expires int64) (string, error) { + if expires <= 0 { + return "", fmt.Errorf("invalid argument: %d, expires must greater than 0", expires) + } + expiration := time.Now().Unix() + expires + + return bucket.Client.Conn.signRtmpURL(bucket.BucketName, channelName, playlistName, expiration), nil +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/livechannel_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/livechannel_test.go new file mode 100644 index 00000000..26ed5ecd --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/livechannel_test.go @@ -0,0 +1,428 @@ +package oss + +import ( + "fmt" + "strings" + "time" + + . "gopkg.in/check.v1" +) + +type OssBucketLiveChannelSuite struct { + client *Client + bucket *Bucket +} + +var _ = Suite(&OssBucketLiveChannelSuite{}) + +// SetUpSuite Run once when the suite starts running +func (s *OssBucketLiveChannelSuite) SetUpSuite(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + s.client = client + + err = s.client.CreateBucket(bucketName) + c.Assert(err, IsNil) + time.Sleep(5 * time.Second) + + bucket, err := s.client.Bucket(bucketName) + c.Assert(err, IsNil) + s.bucket = bucket + + testLogger.Println("test livechannel started...") +} + +// TearDownSuite Run once after all tests or benchmarks +func (s *OssBucketLiveChannelSuite) TearDownSuite(c *C) { + marker := "" + for { + result, err := s.bucket.ListLiveChannel(Marker(marker)) + c.Assert(err, IsNil) + + for _, channel := range result.LiveChannel { + err := s.bucket.DeleteLiveChannel(channel.Name) + c.Assert(err, IsNil) + } + + if result.IsTruncated { + marker = result.NextMarker + } else { + break + } + } + + testLogger.Println("test livechannel done...") +} + +// SetUpTest Run before each test or benchmark starts +func (s *OssBucketLiveChannelSuite) SetUpTest(c *C) { + +} + +// TearDownTest Run after each test or benchmark runs. +func (s *OssBucketLiveChannelSuite) TearDownTest(c *C) { + +} + +// TestCreateLiveChannel +func (s *OssBucketLiveChannelSuite) TestCreateLiveChannel(c *C) { + channelName := "test-create-channel" + playlistName := "test-create-channel.m3u8" + + target := LiveChannelTarget{ + PlaylistName: playlistName, + Type: "HLS", + } + + config := LiveChannelConfiguration{ + Description: "livechannel for test", + Status: "enabled", + Target: target, + } + + result, err := s.bucket.CreateLiveChannel(channelName, config) + c.Assert(err, IsNil) + + playURL := getPlayURL(bucketName, channelName, playlistName) + publishURL := getPublishURL(bucketName, channelName) + c.Assert(result.PlayUrls[0], Equals, playURL) + c.Assert(result.PublishUrls[0], Equals, publishURL) + + err = s.bucket.DeleteLiveChannel(channelName) + c.Assert(err, IsNil) + + invalidType := RandStr(4) + invalidTarget := LiveChannelTarget{ + PlaylistName: playlistName, + Type: invalidType, + } + + invalidConfig := LiveChannelConfiguration{ + Description: "livechannel for test", + Status: "enabled", + Target: invalidTarget, + } + + _, err = s.bucket.CreateLiveChannel(channelName, invalidConfig) + c.Assert(err, NotNil) +} + +// TestDeleteLiveChannel +func (s *OssBucketLiveChannelSuite) TestDeleteLiveChannel(c *C) { + channelName := "test-delete-channel" + + target := LiveChannelTarget{ + Type: "HLS", + } + + config := LiveChannelConfiguration{ + Target: target, + } + + _, err := s.bucket.CreateLiveChannel(channelName, config) + c.Assert(err, IsNil) + + err = s.bucket.DeleteLiveChannel(channelName) + c.Assert(err, IsNil) + + emptyChannelName := "" + err = s.bucket.DeleteLiveChannel(emptyChannelName) + c.Assert(err, NotNil) + + longChannelName := RandStr(65) + err = s.bucket.DeleteLiveChannel(longChannelName) + c.Assert(err, NotNil) + + config, err = s.bucket.GetLiveChannelInfo(channelName) + c.Assert(err, NotNil) +} + +// TestGetLiveChannelInfo +func (s *OssBucketLiveChannelSuite) TestGetLiveChannelInfo(c *C) { + channelName := "test-get-channel-status" + + _, err := s.bucket.GetLiveChannelInfo(channelName) + c.Assert(err, NotNil) + + createCfg := LiveChannelConfiguration{ + Target: LiveChannelTarget{ + Type: "HLS", + FragDuration: 10, + FragCount: 4, + PlaylistName: "test-get-channel-status.m3u8", + }, + } + + _, err = s.bucket.CreateLiveChannel(channelName, createCfg) + c.Assert(err, IsNil) + + getCfg, err := s.bucket.GetLiveChannelInfo(channelName) + c.Assert(err, IsNil) + c.Assert("enabled", Equals, getCfg.Status) //The default value is enabled + c.Assert("HLS", Equals, getCfg.Target.Type) + c.Assert(10, Equals, getCfg.Target.FragDuration) + c.Assert(4, Equals, getCfg.Target.FragCount) + c.Assert("test-get-channel-status.m3u8", Equals, getCfg.Target.PlaylistName) + + err = s.bucket.DeleteLiveChannel(channelName) + c.Assert(err, IsNil) +} + +// TestPutLiveChannelStatus +func (s *OssBucketLiveChannelSuite) TestPutLiveChannelStatus(c *C) { + channelName := "test-put-channel-status" + + config := LiveChannelConfiguration{ + Status: "disabled", + Target: LiveChannelTarget{ + Type: "HLS", + }, + } + + _, err := s.bucket.CreateLiveChannel(channelName, config) + c.Assert(err, IsNil) + getCfg, err := s.bucket.GetLiveChannelInfo(channelName) + c.Assert(err, IsNil) + c.Assert("disabled", Equals, getCfg.Status) + + err = s.bucket.PutLiveChannelStatus(channelName, "enabled") + c.Assert(err, IsNil) + getCfg, err = s.bucket.GetLiveChannelInfo(channelName) + c.Assert(err, IsNil) + c.Assert("enabled", Equals, getCfg.Status) + + err = s.bucket.PutLiveChannelStatus(channelName, "disabled") + c.Assert(err, IsNil) + getCfg, err = s.bucket.GetLiveChannelInfo(channelName) + c.Assert(err, IsNil) + c.Assert("disabled", Equals, getCfg.Status) + + invalidStatus := RandLowStr(9) + err = s.bucket.PutLiveChannelStatus(channelName, invalidStatus) + c.Assert(err, NotNil) + + err = s.bucket.DeleteLiveChannel(channelName) + c.Assert(err, IsNil) +} + +// TestGetLiveChannelHistory +func (s *OssBucketLiveChannelSuite) TestGetLiveChannelHistory(c *C) { + channelName := "test-get-channel-history" + + _, err := s.bucket.GetLiveChannelHistory(channelName) + c.Assert(err, NotNil) + + config := LiveChannelConfiguration{ + Target: LiveChannelTarget{ + Type: "HLS", + }, + } + + _, err = s.bucket.CreateLiveChannel(channelName, config) + c.Assert(err, IsNil) + + history, err := s.bucket.GetLiveChannelHistory(channelName) + c.Assert(err, IsNil) + c.Assert(len(history.Record), Equals, 0) + + err = s.bucket.DeleteLiveChannel(channelName) + c.Assert(err, IsNil) +} + +// TestGetLiveChannelStat +func (s *OssBucketLiveChannelSuite) TestGetLiveChannelStat(c *C) { + channelName := "test-get-channel-stat" + + _, err := s.bucket.GetLiveChannelStat(channelName) + c.Assert(err, NotNil) + + config := LiveChannelConfiguration{ + Target: LiveChannelTarget{ + Type: "HLS", + }, + } + + _, err = s.bucket.CreateLiveChannel(channelName, config) + c.Assert(err, IsNil) + + stat, err := s.bucket.GetLiveChannelStat(channelName) + c.Assert(err, IsNil) + c.Assert(stat.Status, Equals, "Idle") + + err = s.bucket.DeleteLiveChannel(channelName) + c.Assert(err, IsNil) +} + +// TestPostVodPlaylist +func (s *OssBucketLiveChannelSuite) TestPostVodPlaylist(c *C) { + channelName := "test-post-vod-playlist" + playlistName := "test-post-vod-playlist.m3u8" + + config := LiveChannelConfiguration{ + Target: LiveChannelTarget{ + Type: "HLS", + }, + } + + _, err := s.bucket.CreateLiveChannel(channelName, config) + c.Assert(err, IsNil) + + endTime := time.Now().Add(-1 * time.Minute) + startTime := endTime.Add(-60 * time.Minute) + + err = s.bucket.PostVodPlaylist(channelName, playlistName, startTime, endTime) + c.Assert(err, NotNil) + + err = s.bucket.DeleteLiveChannel(channelName) + c.Assert(err, IsNil) +} + +// TestPostVodPlaylist +func (s *OssBucketLiveChannelSuite) TestGetVodPlaylist(c *C) { + channelName := "test-get-vod-playlist" + + config := LiveChannelConfiguration{ + Target: LiveChannelTarget{ + Type: "HLS", + }, + } + + _, err := s.bucket.CreateLiveChannel(channelName, config) + c.Assert(err, IsNil) + + endTime := time.Now().Add(-1 * time.Minute) + startTime := endTime.Add(-60 * time.Minute) + + _, err = s.bucket.GetVodPlaylist(channelName, startTime, endTime) + c.Assert(err, NotNil) + + err = s.bucket.DeleteLiveChannel(channelName) + c.Assert(err, IsNil) +} + +// TestListLiveChannel +func (s *OssBucketLiveChannelSuite) TestListLiveChannel(c *C) { + result, err := s.bucket.ListLiveChannel() + c.Assert(err, IsNil) + ok := compareListResult(result, "", "", "", 100, false, 0) + c.Assert(ok, Equals, true) + + prefix := "test-list-channel" + for i := 0; i < 200; i++ { + channelName := fmt.Sprintf("%s-%03d", prefix, i) + + config := LiveChannelConfiguration{ + Target: LiveChannelTarget{ + Type: "HLS", + }, + } + + _, err := s.bucket.CreateLiveChannel(channelName, config) + c.Assert(err, IsNil) + } + + result, err = s.bucket.ListLiveChannel() + c.Assert(err, IsNil) + nextMarker := fmt.Sprintf("%s-099", prefix) + ok = compareListResult(result, "", "", nextMarker, 100, true, 100) + c.Assert(ok, Equals, true) + + randPrefix := RandStr(5) + result, err = s.bucket.ListLiveChannel(Prefix(randPrefix)) + c.Assert(err, IsNil) + ok = compareListResult(result, randPrefix, "", "", 100, false, 0) + c.Assert(ok, Equals, true) + + marker := fmt.Sprintf("%s-100", prefix) + result, err = s.bucket.ListLiveChannel(Prefix(prefix), Marker(marker)) + c.Assert(err, IsNil) + ok = compareListResult(result, prefix, marker, "", 100, false, 99) + c.Assert(ok, Equals, true) + + maxKeys := 1000 + result, err = s.bucket.ListLiveChannel(MaxKeys(maxKeys)) + c.Assert(err, IsNil) + ok = compareListResult(result, "", "", "", maxKeys, false, 200) + + invalidMaxKeys := -1 + result, err = s.bucket.ListLiveChannel(MaxKeys(invalidMaxKeys)) + c.Assert(err, NotNil) + + for i := 0; i < 200; i++ { + channelName := fmt.Sprintf("%s-%03d", prefix, i) + err := s.bucket.DeleteLiveChannel(channelName) + c.Assert(err, IsNil) + } +} + +// TestSignRtmpURL +func (s *OssBucketLiveChannelSuite) TestSignRtmpURL(c *C) { + channelName := "test-sign-rtmp-url" + playlistName := "test-sign-rtmp-url.m3u8" + + config := LiveChannelConfiguration{ + Target: LiveChannelTarget{ + Type: "HLS", + PlaylistName: playlistName, + }, + } + + _, err := s.bucket.CreateLiveChannel(channelName, config) + c.Assert(err, IsNil) + + expires := int64(3600) + signedRtmpURL, err := s.bucket.SignRtmpURL(channelName, playlistName, expires) + c.Assert(err, IsNil) + playURL := getPublishURL(s.bucket.BucketName, channelName) + hasPrefix := strings.HasPrefix(signedRtmpURL, playURL) + c.Assert(hasPrefix, Equals, true) + + invalidExpires := int64(-1) + signedRtmpURL, err = s.bucket.SignRtmpURL(channelName, playlistName, invalidExpires) + c.Assert(err, NotNil) + + err = s.bucket.DeleteLiveChannel(channelName) + c.Assert(err, IsNil) +} + +// private +// getPlayURL Get the play url of the live channel +func getPlayURL(bucketName, channelName, playlistName string) string { + host := "" + useHTTPS := false + if strings.Contains(endpoint, "https://") { + host = endpoint[8:] + useHTTPS = true + } else if strings.Contains(endpoint, "http://") { + host = endpoint[7:] + } else { + host = endpoint + } + + if useHTTPS { + return fmt.Sprintf("https://%s.%s/%s/%s", bucketName, host, channelName, playlistName) + } + return fmt.Sprintf("http://%s.%s/%s/%s", bucketName, host, channelName, playlistName) +} + +// getPublistURL Get the push url of the live stream channel +func getPublishURL(bucketName, channelName string) string { + host := "" + if strings.Contains(endpoint, "https://") { + host = endpoint[8:] + } else if strings.Contains(endpoint, "http://") { + host = endpoint[7:] + } else { + host = endpoint + } + + return fmt.Sprintf("rtmp://%s.%s/live/%s", bucketName, host, channelName) +} + +func compareListResult(result ListLiveChannelResult, prefix, marker, nextMarker string, maxKey int, isTruncated bool, count int) bool { + if result.Prefix != prefix || result.Marker != marker || result.NextMarker != nextMarker || result.MaxKeys != maxKey || result.IsTruncated != isTruncated || len(result.LiveChannel) != count { + return false + } + + return true +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/mime.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/mime.go new file mode 100644 index 00000000..64f4dcc6 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/mime.go @@ -0,0 +1,572 @@ +package oss + +import ( + "mime" + "path" + "strings" +) + +var extToMimeType = map[string]string{ + ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ".xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template", + ".potx": "application/vnd.openxmlformats-officedocument.presentationml.template", + ".ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow", + ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + ".sldx": "application/vnd.openxmlformats-officedocument.presentationml.slide", + ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ".dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template", + ".xlam": "application/vnd.ms-excel.addin.macroEnabled.12", + ".xlsb": "application/vnd.ms-excel.sheet.binary.macroEnabled.12", + ".apk": "application/vnd.android.package-archive", + ".hqx": "application/mac-binhex40", + ".cpt": "application/mac-compactpro", + ".doc": "application/msword", + ".ogg": "application/ogg", + ".pdf": "application/pdf", + ".rtf": "text/rtf", + ".mif": "application/vnd.mif", + ".xls": "application/vnd.ms-excel", + ".ppt": "application/vnd.ms-powerpoint", + ".odc": "application/vnd.oasis.opendocument.chart", + ".odb": "application/vnd.oasis.opendocument.database", + ".odf": "application/vnd.oasis.opendocument.formula", + ".odg": "application/vnd.oasis.opendocument.graphics", + ".otg": "application/vnd.oasis.opendocument.graphics-template", + ".odi": "application/vnd.oasis.opendocument.image", + ".odp": "application/vnd.oasis.opendocument.presentation", + ".otp": "application/vnd.oasis.opendocument.presentation-template", + ".ods": "application/vnd.oasis.opendocument.spreadsheet", + ".ots": "application/vnd.oasis.opendocument.spreadsheet-template", + ".odt": "application/vnd.oasis.opendocument.text", + ".odm": "application/vnd.oasis.opendocument.text-master", + ".ott": "application/vnd.oasis.opendocument.text-template", + ".oth": "application/vnd.oasis.opendocument.text-web", + ".sxw": "application/vnd.sun.xml.writer", + ".stw": "application/vnd.sun.xml.writer.template", + ".sxc": "application/vnd.sun.xml.calc", + ".stc": "application/vnd.sun.xml.calc.template", + ".sxd": "application/vnd.sun.xml.draw", + ".std": "application/vnd.sun.xml.draw.template", + ".sxi": "application/vnd.sun.xml.impress", + ".sti": "application/vnd.sun.xml.impress.template", + ".sxg": "application/vnd.sun.xml.writer.global", + ".sxm": "application/vnd.sun.xml.math", + ".sis": "application/vnd.symbian.install", + ".wbxml": "application/vnd.wap.wbxml", + ".wmlc": "application/vnd.wap.wmlc", + ".wmlsc": "application/vnd.wap.wmlscriptc", + ".bcpio": "application/x-bcpio", + ".torrent": "application/x-bittorrent", + ".bz2": "application/x-bzip2", + ".vcd": "application/x-cdlink", + ".pgn": "application/x-chess-pgn", + ".cpio": "application/x-cpio", + ".csh": "application/x-csh", + ".dvi": "application/x-dvi", + ".spl": "application/x-futuresplash", + ".gtar": "application/x-gtar", + ".hdf": "application/x-hdf", + ".jar": "application/x-java-archive", + ".jnlp": "application/x-java-jnlp-file", + ".js": "application/x-javascript", + ".ksp": "application/x-kspread", + ".chrt": "application/x-kchart", + ".kil": "application/x-killustrator", + ".latex": "application/x-latex", + ".rpm": "application/x-rpm", + ".sh": "application/x-sh", + ".shar": "application/x-shar", + ".swf": "application/x-shockwave-flash", + ".sit": "application/x-stuffit", + ".sv4cpio": "application/x-sv4cpio", + ".sv4crc": "application/x-sv4crc", + ".tar": "application/x-tar", + ".tcl": "application/x-tcl", + ".tex": "application/x-tex", + ".man": "application/x-troff-man", + ".me": "application/x-troff-me", + ".ms": "application/x-troff-ms", + ".ustar": "application/x-ustar", + ".src": "application/x-wais-source", + ".zip": "application/zip", + ".m3u": "audio/x-mpegurl", + ".ra": "audio/x-pn-realaudio", + ".wav": "audio/x-wav", + ".wma": "audio/x-ms-wma", + ".wax": "audio/x-ms-wax", + ".pdb": "chemical/x-pdb", + ".xyz": "chemical/x-xyz", + ".bmp": "image/bmp", + ".gif": "image/gif", + ".ief": "image/ief", + ".png": "image/png", + ".wbmp": "image/vnd.wap.wbmp", + ".ras": "image/x-cmu-raster", + ".pnm": "image/x-portable-anymap", + ".pbm": "image/x-portable-bitmap", + ".pgm": "image/x-portable-graymap", + ".ppm": "image/x-portable-pixmap", + ".rgb": "image/x-rgb", + ".xbm": "image/x-xbitmap", + ".xpm": "image/x-xpixmap", + ".xwd": "image/x-xwindowdump", + ".css": "text/css", + ".rtx": "text/richtext", + ".tsv": "text/tab-separated-values", + ".jad": "text/vnd.sun.j2me.app-descriptor", + ".wml": "text/vnd.wap.wml", + ".wmls": "text/vnd.wap.wmlscript", + ".etx": "text/x-setext", + ".mxu": "video/vnd.mpegurl", + ".flv": "video/x-flv", + ".wm": "video/x-ms-wm", + ".wmv": "video/x-ms-wmv", + ".wmx": "video/x-ms-wmx", + ".wvx": "video/x-ms-wvx", + ".avi": "video/x-msvideo", + ".movie": "video/x-sgi-movie", + ".ice": "x-conference/x-cooltalk", + ".3gp": "video/3gpp", + ".ai": "application/postscript", + ".aif": "audio/x-aiff", + ".aifc": "audio/x-aiff", + ".aiff": "audio/x-aiff", + ".asc": "text/plain", + ".atom": "application/atom+xml", + ".au": "audio/basic", + ".bin": "application/octet-stream", + ".cdf": "application/x-netcdf", + ".cgm": "image/cgm", + ".class": "application/octet-stream", + ".dcr": "application/x-director", + ".dif": "video/x-dv", + ".dir": "application/x-director", + ".djv": "image/vnd.djvu", + ".djvu": "image/vnd.djvu", + ".dll": "application/octet-stream", + ".dmg": "application/octet-stream", + ".dms": "application/octet-stream", + ".dtd": "application/xml-dtd", + ".dv": "video/x-dv", + ".dxr": "application/x-director", + ".eps": "application/postscript", + ".exe": "application/octet-stream", + ".ez": "application/andrew-inset", + ".gram": "application/srgs", + ".grxml": "application/srgs+xml", + ".gz": "application/x-gzip", + ".htm": "text/html", + ".html": "text/html", + ".ico": "image/x-icon", + ".ics": "text/calendar", + ".ifb": "text/calendar", + ".iges": "model/iges", + ".igs": "model/iges", + ".jp2": "image/jp2", + ".jpe": "image/jpeg", + ".jpeg": "image/jpeg", + ".jpg": "image/jpeg", + ".kar": "audio/midi", + ".lha": "application/octet-stream", + ".lzh": "application/octet-stream", + ".m4a": "audio/mp4a-latm", + ".m4p": "audio/mp4a-latm", + ".m4u": "video/vnd.mpegurl", + ".m4v": "video/x-m4v", + ".mac": "image/x-macpaint", + ".mathml": "application/mathml+xml", + ".mesh": "model/mesh", + ".mid": "audio/midi", + ".midi": "audio/midi", + ".mov": "video/quicktime", + ".mp2": "audio/mpeg", + ".mp3": "audio/mpeg", + ".mp4": "video/mp4", + ".mpe": "video/mpeg", + ".mpeg": "video/mpeg", + ".mpg": "video/mpeg", + ".mpga": "audio/mpeg", + ".msh": "model/mesh", + ".nc": "application/x-netcdf", + ".oda": "application/oda", + ".ogv": "video/ogv", + ".pct": "image/pict", + ".pic": "image/pict", + ".pict": "image/pict", + ".pnt": "image/x-macpaint", + ".pntg": "image/x-macpaint", + ".ps": "application/postscript", + ".qt": "video/quicktime", + ".qti": "image/x-quicktime", + ".qtif": "image/x-quicktime", + ".ram": "audio/x-pn-realaudio", + ".rdf": "application/rdf+xml", + ".rm": "application/vnd.rn-realmedia", + ".roff": "application/x-troff", + ".sgm": "text/sgml", + ".sgml": "text/sgml", + ".silo": "model/mesh", + ".skd": "application/x-koan", + ".skm": "application/x-koan", + ".skp": "application/x-koan", + ".skt": "application/x-koan", + ".smi": "application/smil", + ".smil": "application/smil", + ".snd": "audio/basic", + ".so": "application/octet-stream", + ".svg": "image/svg+xml", + ".t": "application/x-troff", + ".texi": "application/x-texinfo", + ".texinfo": "application/x-texinfo", + ".tif": "image/tiff", + ".tiff": "image/tiff", + ".tr": "application/x-troff", + ".txt": "text/plain", + ".vrml": "model/vrml", + ".vxml": "application/voicexml+xml", + ".webm": "video/webm", + ".wrl": "model/vrml", + ".xht": "application/xhtml+xml", + ".xhtml": "application/xhtml+xml", + ".xml": "application/xml", + ".xsl": "application/xml", + ".xslt": "application/xslt+xml", + ".xul": "application/vnd.mozilla.xul+xml", + ".webp": "image/webp", + ".323": "text/h323", + ".aab": "application/x-authoware-bin", + ".aam": "application/x-authoware-map", + ".aas": "application/x-authoware-seg", + ".acx": "application/internet-property-stream", + ".als": "audio/X-Alpha5", + ".amc": "application/x-mpeg", + ".ani": "application/octet-stream", + ".asd": "application/astound", + ".asf": "video/x-ms-asf", + ".asn": "application/astound", + ".asp": "application/x-asap", + ".asr": "video/x-ms-asf", + ".asx": "video/x-ms-asf", + ".avb": "application/octet-stream", + ".awb": "audio/amr-wb", + ".axs": "application/olescript", + ".bas": "text/plain", + ".bin ": "application/octet-stream", + ".bld": "application/bld", + ".bld2": "application/bld2", + ".bpk": "application/octet-stream", + ".c": "text/plain", + ".cal": "image/x-cals", + ".cat": "application/vnd.ms-pkiseccat", + ".ccn": "application/x-cnc", + ".cco": "application/x-cocoa", + ".cer": "application/x-x509-ca-cert", + ".cgi": "magnus-internal/cgi", + ".chat": "application/x-chat", + ".clp": "application/x-msclip", + ".cmx": "image/x-cmx", + ".co": "application/x-cult3d-object", + ".cod": "image/cis-cod", + ".conf": "text/plain", + ".cpp": "text/plain", + ".crd": "application/x-mscardfile", + ".crl": "application/pkix-crl", + ".crt": "application/x-x509-ca-cert", + ".csm": "chemical/x-csml", + ".csml": "chemical/x-csml", + ".cur": "application/octet-stream", + ".dcm": "x-lml/x-evm", + ".dcx": "image/x-dcx", + ".der": "application/x-x509-ca-cert", + ".dhtml": "text/html", + ".dot": "application/msword", + ".dwf": "drawing/x-dwf", + ".dwg": "application/x-autocad", + ".dxf": "application/x-autocad", + ".ebk": "application/x-expandedbook", + ".emb": "chemical/x-embl-dl-nucleotide", + ".embl": "chemical/x-embl-dl-nucleotide", + ".epub": "application/epub+zip", + ".eri": "image/x-eri", + ".es": "audio/echospeech", + ".esl": "audio/echospeech", + ".etc": "application/x-earthtime", + ".evm": "x-lml/x-evm", + ".evy": "application/envoy", + ".fh4": "image/x-freehand", + ".fh5": "image/x-freehand", + ".fhc": "image/x-freehand", + ".fif": "application/fractals", + ".flr": "x-world/x-vrml", + ".fm": "application/x-maker", + ".fpx": "image/x-fpx", + ".fvi": "video/isivideo", + ".gau": "chemical/x-gaussian-input", + ".gca": "application/x-gca-compressed", + ".gdb": "x-lml/x-gdb", + ".gps": "application/x-gps", + ".h": "text/plain", + ".hdm": "text/x-hdml", + ".hdml": "text/x-hdml", + ".hlp": "application/winhlp", + ".hta": "application/hta", + ".htc": "text/x-component", + ".hts": "text/html", + ".htt": "text/webviewhtml", + ".ifm": "image/gif", + ".ifs": "image/ifs", + ".iii": "application/x-iphone", + ".imy": "audio/melody", + ".ins": "application/x-internet-signup", + ".ips": "application/x-ipscript", + ".ipx": "application/x-ipix", + ".isp": "application/x-internet-signup", + ".it": "audio/x-mod", + ".itz": "audio/x-mod", + ".ivr": "i-world/i-vrml", + ".j2k": "image/j2k", + ".jam": "application/x-jam", + ".java": "text/plain", + ".jfif": "image/pipeg", + ".jpz": "image/jpeg", + ".jwc": "application/jwc", + ".kjx": "application/x-kjx", + ".lak": "x-lml/x-lak", + ".lcc": "application/fastman", + ".lcl": "application/x-digitalloca", + ".lcr": "application/x-digitalloca", + ".lgh": "application/lgh", + ".lml": "x-lml/x-lml", + ".lmlpack": "x-lml/x-lmlpack", + ".log": "text/plain", + ".lsf": "video/x-la-asf", + ".lsx": "video/x-la-asf", + ".m13": "application/x-msmediaview", + ".m14": "application/x-msmediaview", + ".m15": "audio/x-mod", + ".m3url": "audio/x-mpegurl", + ".m4b": "audio/mp4a-latm", + ".ma1": "audio/ma1", + ".ma2": "audio/ma2", + ".ma3": "audio/ma3", + ".ma5": "audio/ma5", + ".map": "magnus-internal/imagemap", + ".mbd": "application/mbedlet", + ".mct": "application/x-mascot", + ".mdb": "application/x-msaccess", + ".mdz": "audio/x-mod", + ".mel": "text/x-vmel", + ".mht": "message/rfc822", + ".mhtml": "message/rfc822", + ".mi": "application/x-mif", + ".mil": "image/x-cals", + ".mio": "audio/x-mio", + ".mmf": "application/x-skt-lbs", + ".mng": "video/x-mng", + ".mny": "application/x-msmoney", + ".moc": "application/x-mocha", + ".mocha": "application/x-mocha", + ".mod": "audio/x-mod", + ".mof": "application/x-yumekara", + ".mol": "chemical/x-mdl-molfile", + ".mop": "chemical/x-mopac-input", + ".mpa": "video/mpeg", + ".mpc": "application/vnd.mpohun.certificate", + ".mpg4": "video/mp4", + ".mpn": "application/vnd.mophun.application", + ".mpp": "application/vnd.ms-project", + ".mps": "application/x-mapserver", + ".mpv2": "video/mpeg", + ".mrl": "text/x-mrml", + ".mrm": "application/x-mrm", + ".msg": "application/vnd.ms-outlook", + ".mts": "application/metastream", + ".mtx": "application/metastream", + ".mtz": "application/metastream", + ".mvb": "application/x-msmediaview", + ".mzv": "application/metastream", + ".nar": "application/zip", + ".nbmp": "image/nbmp", + ".ndb": "x-lml/x-ndb", + ".ndwn": "application/ndwn", + ".nif": "application/x-nif", + ".nmz": "application/x-scream", + ".nokia-op-logo": "image/vnd.nok-oplogo-color", + ".npx": "application/x-netfpx", + ".nsnd": "audio/nsnd", + ".nva": "application/x-neva1", + ".nws": "message/rfc822", + ".oom": "application/x-AtlasMate-Plugin", + ".p10": "application/pkcs10", + ".p12": "application/x-pkcs12", + ".p7b": "application/x-pkcs7-certificates", + ".p7c": "application/x-pkcs7-mime", + ".p7m": "application/x-pkcs7-mime", + ".p7r": "application/x-pkcs7-certreqresp", + ".p7s": "application/x-pkcs7-signature", + ".pac": "audio/x-pac", + ".pae": "audio/x-epac", + ".pan": "application/x-pan", + ".pcx": "image/x-pcx", + ".pda": "image/x-pda", + ".pfr": "application/font-tdpfr", + ".pfx": "application/x-pkcs12", + ".pko": "application/ynd.ms-pkipko", + ".pm": "application/x-perl", + ".pma": "application/x-perfmon", + ".pmc": "application/x-perfmon", + ".pmd": "application/x-pmd", + ".pml": "application/x-perfmon", + ".pmr": "application/x-perfmon", + ".pmw": "application/x-perfmon", + ".pnz": "image/png", + ".pot,": "application/vnd.ms-powerpoint", + ".pps": "application/vnd.ms-powerpoint", + ".pqf": "application/x-cprplayer", + ".pqi": "application/cprplayer", + ".prc": "application/x-prc", + ".prf": "application/pics-rules", + ".prop": "text/plain", + ".proxy": "application/x-ns-proxy-autoconfig", + ".ptlk": "application/listenup", + ".pub": "application/x-mspublisher", + ".pvx": "video/x-pv-pvx", + ".qcp": "audio/vnd.qcelp", + ".r3t": "text/vnd.rn-realtext3d", + ".rar": "application/octet-stream", + ".rc": "text/plain", + ".rf": "image/vnd.rn-realflash", + ".rlf": "application/x-richlink", + ".rmf": "audio/x-rmf", + ".rmi": "audio/mid", + ".rmm": "audio/x-pn-realaudio", + ".rmvb": "audio/x-pn-realaudio", + ".rnx": "application/vnd.rn-realplayer", + ".rp": "image/vnd.rn-realpix", + ".rt": "text/vnd.rn-realtext", + ".rte": "x-lml/x-gps", + ".rtg": "application/metastream", + ".rv": "video/vnd.rn-realvideo", + ".rwc": "application/x-rogerwilco", + ".s3m": "audio/x-mod", + ".s3z": "audio/x-mod", + ".sca": "application/x-supercard", + ".scd": "application/x-msschedule", + ".sct": "text/scriptlet", + ".sdf": "application/e-score", + ".sea": "application/x-stuffit", + ".setpay": "application/set-payment-initiation", + ".setreg": "application/set-registration-initiation", + ".shtml": "text/html", + ".shtm": "text/html", + ".shw": "application/presentations", + ".si6": "image/si6", + ".si7": "image/vnd.stiwap.sis", + ".si9": "image/vnd.lgtwap.sis", + ".slc": "application/x-salsa", + ".smd": "audio/x-smd", + ".smp": "application/studiom", + ".smz": "audio/x-smd", + ".spc": "application/x-pkcs7-certificates", + ".spr": "application/x-sprite", + ".sprite": "application/x-sprite", + ".sdp": "application/sdp", + ".spt": "application/x-spt", + ".sst": "application/vnd.ms-pkicertstore", + ".stk": "application/hyperstudio", + ".stl": "application/vnd.ms-pkistl", + ".stm": "text/html", + ".svf": "image/vnd", + ".svh": "image/svh", + ".svr": "x-world/x-svr", + ".swfl": "application/x-shockwave-flash", + ".tad": "application/octet-stream", + ".talk": "text/x-speech", + ".taz": "application/x-tar", + ".tbp": "application/x-timbuktu", + ".tbt": "application/x-timbuktu", + ".tgz": "application/x-compressed", + ".thm": "application/vnd.eri.thm", + ".tki": "application/x-tkined", + ".tkined": "application/x-tkined", + ".toc": "application/toc", + ".toy": "image/toy", + ".trk": "x-lml/x-gps", + ".trm": "application/x-msterminal", + ".tsi": "audio/tsplayer", + ".tsp": "application/dsptype", + ".ttf": "application/octet-stream", + ".ttz": "application/t-time", + ".uls": "text/iuls", + ".ult": "audio/x-mod", + ".uu": "application/x-uuencode", + ".uue": "application/x-uuencode", + ".vcf": "text/x-vcard", + ".vdo": "video/vdo", + ".vib": "audio/vib", + ".viv": "video/vivo", + ".vivo": "video/vivo", + ".vmd": "application/vocaltec-media-desc", + ".vmf": "application/vocaltec-media-file", + ".vmi": "application/x-dreamcast-vms-info", + ".vms": "application/x-dreamcast-vms", + ".vox": "audio/voxware", + ".vqe": "audio/x-twinvq-plugin", + ".vqf": "audio/x-twinvq", + ".vql": "audio/x-twinvq", + ".vre": "x-world/x-vream", + ".vrt": "x-world/x-vrt", + ".vrw": "x-world/x-vream", + ".vts": "workbook/formulaone", + ".wcm": "application/vnd.ms-works", + ".wdb": "application/vnd.ms-works", + ".web": "application/vnd.xara", + ".wi": "image/wavelet", + ".wis": "application/x-InstallShield", + ".wks": "application/vnd.ms-works", + ".wmd": "application/x-ms-wmd", + ".wmf": "application/x-msmetafile", + ".wmlscript": "text/vnd.wap.wmlscript", + ".wmz": "application/x-ms-wmz", + ".wpng": "image/x-up-wpng", + ".wps": "application/vnd.ms-works", + ".wpt": "x-lml/x-gps", + ".wri": "application/x-mswrite", + ".wrz": "x-world/x-vrml", + ".ws": "text/vnd.wap.wmlscript", + ".wsc": "application/vnd.wap.wmlscriptc", + ".wv": "video/wavelet", + ".wxl": "application/x-wxl", + ".x-gzip": "application/x-gzip", + ".xaf": "x-world/x-vrml", + ".xar": "application/vnd.xara", + ".xdm": "application/x-xdma", + ".xdma": "application/x-xdma", + ".xdw": "application/vnd.fujixerox.docuworks", + ".xhtm": "application/xhtml+xml", + ".xla": "application/vnd.ms-excel", + ".xlc": "application/vnd.ms-excel", + ".xll": "application/x-excel", + ".xlm": "application/vnd.ms-excel", + ".xlt": "application/vnd.ms-excel", + ".xlw": "application/vnd.ms-excel", + ".xm": "audio/x-mod", + ".xmz": "audio/x-mod", + ".xof": "x-world/x-vrml", + ".xpi": "application/x-xpinstall", + ".xsit": "text/xml", + ".yz1": "application/x-yz1", + ".z": "application/x-compress", + ".zac": "application/x-zaurus-zac", + ".json": "application/json", +} + +// TypeByExtension returns the MIME type associated with the file extension ext. +// gets the file's MIME type for HTTP header Content-Type +func TypeByExtension(filePath string) string { + typ := mime.TypeByExtension(path.Ext(filePath)) + if typ == "" { + typ = extToMimeType[strings.ToLower(path.Ext(filePath))] + } + return typ +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/model.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/model.go new file mode 100644 index 00000000..b0b4a502 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/model.go @@ -0,0 +1,69 @@ +package oss + +import ( + "hash" + "io" + "net/http" +) + +// Response defines HTTP response from OSS +type Response struct { + StatusCode int + Headers http.Header + Body io.ReadCloser + ClientCRC uint64 + ServerCRC uint64 +} + +func (r *Response) Read(p []byte) (n int, err error) { + return r.Body.Read(p) +} + +// Close close http reponse body +func (r *Response) Close() error { + return r.Body.Close() +} + +// PutObjectRequest is the request of DoPutObject +type PutObjectRequest struct { + ObjectKey string + Reader io.Reader +} + +// GetObjectRequest is the request of DoGetObject +type GetObjectRequest struct { + ObjectKey string +} + +// GetObjectResult is the result of DoGetObject +type GetObjectResult struct { + Response *Response + ClientCRC hash.Hash64 + ServerCRC uint64 +} + +// AppendObjectRequest is the requtest of DoAppendObject +type AppendObjectRequest struct { + ObjectKey string + Reader io.Reader + Position int64 +} + +// AppendObjectResult is the result of DoAppendObject +type AppendObjectResult struct { + NextPosition int64 + CRC uint64 +} + +// UploadPartRequest is the request of DoUploadPart +type UploadPartRequest struct { + InitResult *InitiateMultipartUploadResult + Reader io.Reader + PartSize int64 + PartNumber int +} + +// UploadPartResult is the result of DoUploadPart +type UploadPartResult struct { + Part UploadPart +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/multicopy.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/multicopy.go new file mode 100644 index 00000000..56ed8cad --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/multicopy.go @@ -0,0 +1,474 @@ +package oss + +import ( + "crypto/md5" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "os" + "strconv" +) + +// CopyFile is multipart copy object +// +// srcBucketName source bucket name +// srcObjectKey source object name +// destObjectKey target object name in the form of bucketname.objectkey +// partSize the part size in byte. +// options object's contraints. Check out function InitiateMultipartUpload. +// +// error it's nil if the operation succeeds, otherwise it's an error object. +// +func (bucket Bucket) CopyFile(srcBucketName, srcObjectKey, destObjectKey string, partSize int64, options ...Option) error { + destBucketName := bucket.BucketName + if partSize < MinPartSize || partSize > MaxPartSize { + return errors.New("oss: part size invalid range (1024KB, 5GB]") + } + + cpConf := getCpConfig(options) + routines := getRoutines(options) + + var strVersionId string + versionId, _ := FindOption(options, "versionId", nil) + if versionId != nil { + strVersionId = versionId.(string) + } + + if cpConf != nil && cpConf.IsEnable { + cpFilePath := getCopyCpFilePath(cpConf, srcBucketName, srcObjectKey, destBucketName, destObjectKey, strVersionId) + if cpFilePath != "" { + return bucket.copyFileWithCp(srcBucketName, srcObjectKey, destBucketName, destObjectKey, partSize, options, cpFilePath, routines) + } + } + + return bucket.copyFile(srcBucketName, srcObjectKey, destBucketName, destObjectKey, + partSize, options, routines) +} + +func getCopyCpFilePath(cpConf *cpConfig, srcBucket, srcObject, destBucket, destObject, versionId string) string { + if cpConf.FilePath == "" && cpConf.DirPath != "" { + dest := fmt.Sprintf("oss://%v/%v", destBucket, destObject) + src := fmt.Sprintf("oss://%v/%v", srcBucket, srcObject) + cpFileName := getCpFileName(src, dest, versionId) + cpConf.FilePath = cpConf.DirPath + string(os.PathSeparator) + cpFileName + } + return cpConf.FilePath +} + +// ----- Concurrently copy without checkpoint --------- + +// copyWorkerArg defines the copy worker arguments +type copyWorkerArg struct { + bucket *Bucket + imur InitiateMultipartUploadResult + srcBucketName string + srcObjectKey string + options []Option + hook copyPartHook +} + +// copyPartHook is the hook for testing purpose +type copyPartHook func(part copyPart) error + +var copyPartHooker copyPartHook = defaultCopyPartHook + +func defaultCopyPartHook(part copyPart) error { + return nil +} + +// copyWorker copies worker +func copyWorker(id int, arg copyWorkerArg, jobs <-chan copyPart, results chan<- UploadPart, failed chan<- error, die <-chan bool) { + for chunk := range jobs { + if err := arg.hook(chunk); err != nil { + failed <- err + break + } + chunkSize := chunk.End - chunk.Start + 1 + part, err := arg.bucket.UploadPartCopy(arg.imur, arg.srcBucketName, arg.srcObjectKey, + chunk.Start, chunkSize, chunk.Number, arg.options...) + if err != nil { + failed <- err + break + } + select { + case <-die: + return + default: + } + results <- part + } +} + +// copyScheduler +func copyScheduler(jobs chan copyPart, parts []copyPart) { + for _, part := range parts { + jobs <- part + } + close(jobs) +} + +// copyPart structure +type copyPart struct { + Number int // Part number (from 1 to 10,000) + Start int64 // The start index in the source file. + End int64 // The end index in the source file +} + +// getCopyParts calculates copy parts +func getCopyParts(objectSize, partSize int64) []copyPart { + parts := []copyPart{} + part := copyPart{} + i := 0 + for offset := int64(0); offset < objectSize; offset += partSize { + part.Number = i + 1 + part.Start = offset + part.End = GetPartEnd(offset, objectSize, partSize) + parts = append(parts, part) + i++ + } + return parts +} + +// getSrcObjectBytes gets the source file size +func getSrcObjectBytes(parts []copyPart) int64 { + var ob int64 + for _, part := range parts { + ob += (part.End - part.Start + 1) + } + return ob +} + +// copyFile is a concurrently copy without checkpoint +func (bucket Bucket) copyFile(srcBucketName, srcObjectKey, destBucketName, destObjectKey string, + partSize int64, options []Option, routines int) error { + descBucket, err := bucket.Client.Bucket(destBucketName) + srcBucket, err := bucket.Client.Bucket(srcBucketName) + listener := GetProgressListener(options) + + // choice valid options + headerOptions := ChoiceHeadObjectOption(options) + partOptions := ChoiceTransferPartOption(options) + completeOptions := ChoiceCompletePartOption(options) + abortOptions := ChoiceAbortPartOption(options) + + meta, err := srcBucket.GetObjectDetailedMeta(srcObjectKey, headerOptions...) + if err != nil { + return err + } + + objectSize, err := strconv.ParseInt(meta.Get(HTTPHeaderContentLength), 10, 0) + if err != nil { + return err + } + + // Get copy parts + parts := getCopyParts(objectSize, partSize) + // Initialize the multipart upload + imur, err := descBucket.InitiateMultipartUpload(destObjectKey, options...) + if err != nil { + return err + } + + jobs := make(chan copyPart, len(parts)) + results := make(chan UploadPart, len(parts)) + failed := make(chan error) + die := make(chan bool) + + var completedBytes int64 + totalBytes := getSrcObjectBytes(parts) + event := newProgressEvent(TransferStartedEvent, 0, totalBytes, 0) + publishProgress(listener, event) + + // Start to copy workers + arg := copyWorkerArg{descBucket, imur, srcBucketName, srcObjectKey, partOptions, copyPartHooker} + for w := 1; w <= routines; w++ { + go copyWorker(w, arg, jobs, results, failed, die) + } + + // Start the scheduler + go copyScheduler(jobs, parts) + + // Wait for the parts finished. + completed := 0 + ups := make([]UploadPart, len(parts)) + for completed < len(parts) { + select { + case part := <-results: + completed++ + ups[part.PartNumber-1] = part + copyBytes := (parts[part.PartNumber-1].End - parts[part.PartNumber-1].Start + 1) + completedBytes += copyBytes + event = newProgressEvent(TransferDataEvent, completedBytes, totalBytes, copyBytes) + publishProgress(listener, event) + case err := <-failed: + close(die) + descBucket.AbortMultipartUpload(imur, abortOptions...) + event = newProgressEvent(TransferFailedEvent, completedBytes, totalBytes, 0) + publishProgress(listener, event) + return err + } + + if completed >= len(parts) { + break + } + } + + event = newProgressEvent(TransferCompletedEvent, completedBytes, totalBytes, 0) + publishProgress(listener, event) + + // Complete the multipart upload + _, err = descBucket.CompleteMultipartUpload(imur, ups, completeOptions...) + if err != nil { + bucket.AbortMultipartUpload(imur, abortOptions...) + return err + } + return nil +} + +// ----- Concurrently copy with checkpoint ----- + +const copyCpMagic = "84F1F18C-FF1D-403B-A1D8-9DEB5F65910A" + +type copyCheckpoint struct { + Magic string // Magic + MD5 string // CP content MD5 + SrcBucketName string // Source bucket + SrcObjectKey string // Source object + DestBucketName string // Target bucket + DestObjectKey string // Target object + CopyID string // Copy ID + ObjStat objectStat // Object stat + Parts []copyPart // Copy parts + CopyParts []UploadPart // The uploaded parts + PartStat []bool // The part status +} + +// isValid checks if the data is valid which means CP is valid and object is not updated. +func (cp copyCheckpoint) isValid(meta http.Header) (bool, error) { + // Compare CP's magic number and the MD5. + cpb := cp + cpb.MD5 = "" + js, _ := json.Marshal(cpb) + sum := md5.Sum(js) + b64 := base64.StdEncoding.EncodeToString(sum[:]) + + if cp.Magic != downloadCpMagic || b64 != cp.MD5 { + return false, nil + } + + objectSize, err := strconv.ParseInt(meta.Get(HTTPHeaderContentLength), 10, 64) + if err != nil { + return false, err + } + + // Compare the object size and last modified time and etag. + if cp.ObjStat.Size != objectSize || + cp.ObjStat.LastModified != meta.Get(HTTPHeaderLastModified) || + cp.ObjStat.Etag != meta.Get(HTTPHeaderEtag) { + return false, nil + } + + return true, nil +} + +// load loads from the checkpoint file +func (cp *copyCheckpoint) load(filePath string) error { + contents, err := ioutil.ReadFile(filePath) + if err != nil { + return err + } + + err = json.Unmarshal(contents, cp) + return err +} + +// update updates the parts status +func (cp *copyCheckpoint) update(part UploadPart) { + cp.CopyParts[part.PartNumber-1] = part + cp.PartStat[part.PartNumber-1] = true +} + +// dump dumps the CP to the file +func (cp *copyCheckpoint) dump(filePath string) error { + bcp := *cp + + // Calculate MD5 + bcp.MD5 = "" + js, err := json.Marshal(bcp) + if err != nil { + return err + } + sum := md5.Sum(js) + b64 := base64.StdEncoding.EncodeToString(sum[:]) + bcp.MD5 = b64 + + // Serialization + js, err = json.Marshal(bcp) + if err != nil { + return err + } + + // Dump + return ioutil.WriteFile(filePath, js, FilePermMode) +} + +// todoParts returns unfinished parts +func (cp copyCheckpoint) todoParts() []copyPart { + dps := []copyPart{} + for i, ps := range cp.PartStat { + if !ps { + dps = append(dps, cp.Parts[i]) + } + } + return dps +} + +// getCompletedBytes returns finished bytes count +func (cp copyCheckpoint) getCompletedBytes() int64 { + var completedBytes int64 + for i, part := range cp.Parts { + if cp.PartStat[i] { + completedBytes += (part.End - part.Start + 1) + } + } + return completedBytes +} + +// prepare initializes the multipart upload +func (cp *copyCheckpoint) prepare(meta http.Header, srcBucket *Bucket, srcObjectKey string, destBucket *Bucket, destObjectKey string, + partSize int64, options []Option) error { + // CP + cp.Magic = copyCpMagic + cp.SrcBucketName = srcBucket.BucketName + cp.SrcObjectKey = srcObjectKey + cp.DestBucketName = destBucket.BucketName + cp.DestObjectKey = destObjectKey + + objectSize, err := strconv.ParseInt(meta.Get(HTTPHeaderContentLength), 10, 64) + if err != nil { + return err + } + + cp.ObjStat.Size = objectSize + cp.ObjStat.LastModified = meta.Get(HTTPHeaderLastModified) + cp.ObjStat.Etag = meta.Get(HTTPHeaderEtag) + + // Parts + cp.Parts = getCopyParts(objectSize, partSize) + cp.PartStat = make([]bool, len(cp.Parts)) + for i := range cp.PartStat { + cp.PartStat[i] = false + } + cp.CopyParts = make([]UploadPart, len(cp.Parts)) + + // Init copy + imur, err := destBucket.InitiateMultipartUpload(destObjectKey, options...) + if err != nil { + return err + } + cp.CopyID = imur.UploadID + + return nil +} + +func (cp *copyCheckpoint) complete(bucket *Bucket, parts []UploadPart, cpFilePath string, options []Option) error { + imur := InitiateMultipartUploadResult{Bucket: cp.DestBucketName, + Key: cp.DestObjectKey, UploadID: cp.CopyID} + _, err := bucket.CompleteMultipartUpload(imur, parts, options...) + if err != nil { + return err + } + os.Remove(cpFilePath) + return err +} + +// copyFileWithCp is concurrently copy with checkpoint +func (bucket Bucket) copyFileWithCp(srcBucketName, srcObjectKey, destBucketName, destObjectKey string, + partSize int64, options []Option, cpFilePath string, routines int) error { + descBucket, err := bucket.Client.Bucket(destBucketName) + srcBucket, err := bucket.Client.Bucket(srcBucketName) + listener := GetProgressListener(options) + + // Load CP data + ccp := copyCheckpoint{} + err = ccp.load(cpFilePath) + if err != nil { + os.Remove(cpFilePath) + } + + // choice valid options + headerOptions := ChoiceHeadObjectOption(options) + partOptions := ChoiceTransferPartOption(options) + completeOptions := ChoiceCompletePartOption(options) + + meta, err := srcBucket.GetObjectDetailedMeta(srcObjectKey, headerOptions...) + if err != nil { + return err + } + + // Load error or the CP data is invalid---reinitialize + valid, err := ccp.isValid(meta) + if err != nil || !valid { + if err = ccp.prepare(meta, srcBucket, srcObjectKey, descBucket, destObjectKey, partSize, options); err != nil { + return err + } + os.Remove(cpFilePath) + } + + // Unfinished parts + parts := ccp.todoParts() + imur := InitiateMultipartUploadResult{ + Bucket: destBucketName, + Key: destObjectKey, + UploadID: ccp.CopyID} + + jobs := make(chan copyPart, len(parts)) + results := make(chan UploadPart, len(parts)) + failed := make(chan error) + die := make(chan bool) + + completedBytes := ccp.getCompletedBytes() + event := newProgressEvent(TransferStartedEvent, completedBytes, ccp.ObjStat.Size, 0) + publishProgress(listener, event) + + // Start the worker coroutines + arg := copyWorkerArg{descBucket, imur, srcBucketName, srcObjectKey, partOptions, copyPartHooker} + for w := 1; w <= routines; w++ { + go copyWorker(w, arg, jobs, results, failed, die) + } + + // Start the scheduler + go copyScheduler(jobs, parts) + + // Wait for the parts completed. + completed := 0 + for completed < len(parts) { + select { + case part := <-results: + completed++ + ccp.update(part) + ccp.dump(cpFilePath) + copyBytes := (parts[part.PartNumber-1].End - parts[part.PartNumber-1].Start + 1) + completedBytes += copyBytes + event = newProgressEvent(TransferDataEvent, completedBytes, ccp.ObjStat.Size, copyBytes) + publishProgress(listener, event) + case err := <-failed: + close(die) + event = newProgressEvent(TransferFailedEvent, completedBytes, ccp.ObjStat.Size, 0) + publishProgress(listener, event) + return err + } + + if completed >= len(parts) { + break + } + } + + event = newProgressEvent(TransferCompletedEvent, completedBytes, ccp.ObjStat.Size, 0) + publishProgress(listener, event) + + return ccp.complete(descBucket, ccp.CopyParts, cpFilePath, completeOptions) +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/multicopy_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/multicopy_test.go new file mode 100644 index 00000000..4742a9bc --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/multicopy_test.go @@ -0,0 +1,669 @@ +package oss + +import ( + "fmt" + "net/http" + "os" + "strings" + "time" + + . "gopkg.in/check.v1" +) + +type OssCopySuite struct { + client *Client + bucket *Bucket +} + +var _ = Suite(&OssCopySuite{}) + +// SetUpSuite runs once when the suite starts running +func (s *OssCopySuite) SetUpSuite(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + s.client = client + + s.client.CreateBucket(bucketName) + + bucket, err := s.client.Bucket(bucketName) + c.Assert(err, IsNil) + s.bucket = bucket + + testLogger.Println("test copy started") +} + +// TearDownSuite runs before each test or benchmark starts running +func (s *OssCopySuite) TearDownSuite(c *C) { + // Delete Part + keyMarker := KeyMarker("") + uploadIDMarker := UploadIDMarker("") + for { + lmur, err := s.bucket.ListMultipartUploads(keyMarker, uploadIDMarker) + c.Assert(err, IsNil) + for _, upload := range lmur.Uploads { + var imur = InitiateMultipartUploadResult{Bucket: bucketName, + Key: upload.Key, UploadID: upload.UploadID} + err = s.bucket.AbortMultipartUpload(imur) + c.Assert(err, IsNil) + } + keyMarker = KeyMarker(lmur.NextKeyMarker) + uploadIDMarker = UploadIDMarker(lmur.NextUploadIDMarker) + if !lmur.IsTruncated { + break + } + } + + // Delete objects + marker := Marker("") + for { + lor, err := s.bucket.ListObjects(marker) + c.Assert(err, IsNil) + for _, object := range lor.Objects { + err = s.bucket.DeleteObject(object.Key) + c.Assert(err, IsNil) + } + marker = Marker(lor.NextMarker) + if !lor.IsTruncated { + break + } + } + + // Delete bucket + err := s.client.DeleteBucket(s.bucket.BucketName) + c.Assert(err, IsNil) + + testLogger.Println("test copy completed") +} + +// SetUpTest runs after each test or benchmark runs +func (s *OssCopySuite) SetUpTest(c *C) { + err := removeTempFiles("../oss", ".jpg") + c.Assert(err, IsNil) +} + +// TearDownTest runs once after all tests or benchmarks have finished running +func (s *OssCopySuite) TearDownTest(c *C) { + err := removeTempFiles("../oss", ".jpg") + c.Assert(err, IsNil) +} + +// TestCopyRoutineWithoutRecovery is multi-routine copy without resumable recovery +func (s *OssCopySuite) TestCopyRoutineWithoutRecovery(c *C) { + srcObjectName := objectNamePrefix + RandStr(8) + destObjectName := srcObjectName + "-dest" + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + newFile := "copy-new-file.jpg" + + // Upload source file + err := s.bucket.UploadFile(srcObjectName, fileName, 100*1024, Routines(3)) + c.Assert(err, IsNil) + os.Remove(newFile) + + // Does not specify parameter 'routines', by default it's single routine + err = s.bucket.CopyFile(bucketName, srcObjectName, destObjectName, 100*1024) + c.Assert(err, IsNil) + + err = s.bucket.GetObjectToFile(destObjectName, newFile) + c.Assert(err, IsNil) + + eq, err := compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(destObjectName) + c.Assert(err, IsNil) + os.Remove(newFile) + + // Specify one routine. + err = s.bucket.CopyFile(bucketName, srcObjectName, destObjectName, 100*1024, Routines(1)) + c.Assert(err, IsNil) + + err = s.bucket.GetObjectToFile(destObjectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(destObjectName) + c.Assert(err, IsNil) + os.Remove(newFile) + + // Specify three routines, which is less than parts count 5 + err = s.bucket.CopyFile(bucketName, srcObjectName, destObjectName, 100*1024, Routines(3)) + c.Assert(err, IsNil) + + err = s.bucket.GetObjectToFile(destObjectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(destObjectName) + c.Assert(err, IsNil) + os.Remove(newFile) + + // Specify 5 routines which is the same as parts count + err = s.bucket.CopyFile(bucketName, srcObjectName, destObjectName, 100*1024, Routines(5)) + c.Assert(err, IsNil) + + err = s.bucket.GetObjectToFile(destObjectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(destObjectName) + c.Assert(err, IsNil) + os.Remove(newFile) + + // Specify routine count 10, which is more than parts count + err = s.bucket.CopyFile(bucketName, srcObjectName, destObjectName, 100*1024, Routines(10)) + c.Assert(err, IsNil) + + err = s.bucket.GetObjectToFile(destObjectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(destObjectName) + c.Assert(err, IsNil) + os.Remove(newFile) + + // Invalid routine count, will use single routine + err = s.bucket.CopyFile(bucketName, srcObjectName, destObjectName, 100*1024, Routines(-1)) + c.Assert(err, IsNil) + + err = s.bucket.GetObjectToFile(destObjectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(destObjectName) + c.Assert(err, IsNil) + os.Remove(newFile) + + // Option + err = s.bucket.CopyFile(bucketName, srcObjectName, destObjectName, 100*1024, Routines(3), Meta("myprop", "mypropval")) + + meta, err := s.bucket.GetObjectDetailedMeta(destObjectName) + c.Assert(err, IsNil) + c.Assert(meta.Get("X-Oss-Meta-Myprop"), Equals, "mypropval") + + err = s.bucket.GetObjectToFile(destObjectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(destObjectName) + c.Assert(err, IsNil) + os.Remove(newFile) + + err = s.bucket.DeleteObject(srcObjectName) + c.Assert(err, IsNil) +} + +// CopyErrorHooker is a copypart request hook +func CopyErrorHooker(part copyPart) error { + if part.Number == 5 { + time.Sleep(time.Second) + return fmt.Errorf("ErrorHooker") + } + return nil +} + +// TestCopyRoutineWithoutRecoveryNegative is a multiple routines copy without checkpoint +func (s *OssCopySuite) TestCopyRoutineWithoutRecoveryNegative(c *C) { + srcObjectName := objectNamePrefix + RandStr(8) + destObjectName := srcObjectName + "-dest" + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + + // Upload source file + err := s.bucket.UploadFile(srcObjectName, fileName, 100*1024, Routines(3)) + c.Assert(err, IsNil) + + copyPartHooker = CopyErrorHooker + // Worker routine errors + err = s.bucket.CopyFile(bucketName, srcObjectName, destObjectName, 100*1024, Routines(2)) + + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "ErrorHooker") + copyPartHooker = defaultCopyPartHook + + // Source bucket does not exist + err = s.bucket.CopyFile("notexist", srcObjectName, destObjectName, 100*1024, Routines(2)) + c.Assert(err, NotNil) + + // Target object does not exist + err = s.bucket.CopyFile(bucketName, "notexist", destObjectName, 100*1024, Routines(2)) + + // The part size is invalid + err = s.bucket.CopyFile(bucketName, srcObjectName, destObjectName, 1024, Routines(2)) + c.Assert(err, NotNil) + + err = s.bucket.CopyFile(bucketName, srcObjectName, destObjectName, 1024*1024*1024*100, Routines(2)) + c.Assert(err, NotNil) + + // Delete the source file + err = s.bucket.DeleteObject(srcObjectName) + c.Assert(err, IsNil) +} + +// TestCopyRoutineWithRecovery is a multiple routines copy with resumable recovery +func (s *OssCopySuite) TestCopyRoutineWithRecovery(c *C) { + srcObjectName := objectNamePrefix + RandStr(8) + destObjectName := srcObjectName + "-dest" + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + newFile := RandStr(8) + ".jpg" + + // Upload source file + err := s.bucket.UploadFile(srcObjectName, fileName, 100*1024, Routines(3)) + c.Assert(err, IsNil) + os.Remove(newFile) + + // Routines default value, CP's default path is destObjectName+.cp + // Copy object with checkpoint enabled, single runtine. + // Copy 4 parts---the CopyErrorHooker makes sure the copy of part 5 will fail. + copyPartHooker = CopyErrorHooker + err = s.bucket.CopyFile(bucketName, srcObjectName, destObjectName, 1024*100, Checkpoint(true, destObjectName+".cp")) + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "ErrorHooker") + copyPartHooker = defaultCopyPartHook + + // Check CP + ccp := copyCheckpoint{} + err = ccp.load(destObjectName + ".cp") + c.Assert(err, IsNil) + c.Assert(ccp.Magic, Equals, copyCpMagic) + c.Assert(len(ccp.MD5), Equals, len("LC34jZU5xK4hlxi3Qn3XGQ==")) + c.Assert(ccp.SrcBucketName, Equals, bucketName) + c.Assert(ccp.SrcObjectKey, Equals, srcObjectName) + c.Assert(ccp.DestBucketName, Equals, bucketName) + c.Assert(ccp.DestObjectKey, Equals, destObjectName) + c.Assert(len(ccp.CopyID), Equals, len("3F79722737D1469980DACEDCA325BB52")) + c.Assert(ccp.ObjStat.Size, Equals, int64(482048)) + c.Assert(len(ccp.ObjStat.LastModified), Equals, len("2015-12-17 18:43:03 +0800 CST")) + c.Assert(ccp.ObjStat.Etag, Equals, "\"2351E662233817A7AE974D8C5B0876DD-5\"") + c.Assert(len(ccp.Parts), Equals, 5) + c.Assert(len(ccp.todoParts()), Equals, 1) + c.Assert(ccp.PartStat[4], Equals, false) + + // Second copy, finish the last part + err = s.bucket.CopyFile(bucketName, srcObjectName, destObjectName, 1024*100, Checkpoint(true, destObjectName+".cp")) + c.Assert(err, IsNil) + + err = s.bucket.GetObjectToFile(destObjectName, newFile) + c.Assert(err, IsNil) + + eq, err := compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(destObjectName) + c.Assert(err, IsNil) + os.Remove(newFile) + + err = ccp.load(fileName + ".cp") + c.Assert(err, NotNil) + + //multicopy with empty checkpoint path + copyPartHooker = CopyErrorHooker + err = s.bucket.CopyFile(bucketName, srcObjectName, destObjectName, 1024*100, Checkpoint(true, "")) + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "ErrorHooker") + copyPartHooker = defaultCopyPartHook + ccp = copyCheckpoint{} + err = ccp.load(destObjectName + ".cp") + c.Assert(err, NotNil) + + //multi copy with checkpoint dir + copyPartHooker = CopyErrorHooker + err = s.bucket.CopyFile(bucketName, srcObjectName, destObjectName, 1024*100, Routines(2), CheckpointDir(true, "./")) + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "ErrorHooker") + copyPartHooker = defaultCopyPartHook + + // Check CP + ccp = copyCheckpoint{} + cpConf := cpConfig{IsEnable: true, DirPath: "./"} + cpFilePath := getCopyCpFilePath(&cpConf, bucketName, srcObjectName, s.bucket.BucketName, destObjectName, "") + err = ccp.load(cpFilePath) + c.Assert(err, IsNil) + c.Assert(ccp.Magic, Equals, copyCpMagic) + c.Assert(len(ccp.MD5), Equals, len("LC34jZU5xK4hlxi3Qn3XGQ==")) + c.Assert(ccp.SrcBucketName, Equals, bucketName) + c.Assert(ccp.SrcObjectKey, Equals, srcObjectName) + c.Assert(ccp.DestBucketName, Equals, bucketName) + c.Assert(ccp.DestObjectKey, Equals, destObjectName) + c.Assert(len(ccp.CopyID), Equals, len("3F79722737D1469980DACEDCA325BB52")) + c.Assert(ccp.ObjStat.Size, Equals, int64(482048)) + c.Assert(len(ccp.ObjStat.LastModified), Equals, len("2015-12-17 18:43:03 +0800 CST")) + c.Assert(ccp.ObjStat.Etag, Equals, "\"2351E662233817A7AE974D8C5B0876DD-5\"") + c.Assert(len(ccp.Parts), Equals, 5) + c.Assert(len(ccp.todoParts()), Equals, 1) + c.Assert(ccp.PartStat[4], Equals, false) + + // Second copy, finish the last part. + err = s.bucket.CopyFile(bucketName, srcObjectName, destObjectName, 1024*100, Routines(2), CheckpointDir(true, "./")) + c.Assert(err, IsNil) + + err = s.bucket.GetObjectToFile(destObjectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(destObjectName) + c.Assert(err, IsNil) + os.Remove(newFile) + + err = ccp.load(srcObjectName + ".cp") + c.Assert(err, NotNil) + + // First copy without error. + err = s.bucket.CopyFile(bucketName, srcObjectName, destObjectName, 1024*100, Routines(3), Checkpoint(true, destObjectName+".cp")) + c.Assert(err, IsNil) + + err = s.bucket.GetObjectToFile(destObjectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(destObjectName) + c.Assert(err, IsNil) + os.Remove(newFile) + + // Copy with multiple coroutines, no errors. + err = s.bucket.CopyFile(bucketName, srcObjectName, destObjectName, 1024*100, Routines(10), Checkpoint(true, destObjectName+".cp")) + c.Assert(err, IsNil) + + err = s.bucket.GetObjectToFile(destObjectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(destObjectName) + c.Assert(err, IsNil) + os.Remove(newFile) + + // Option + err = s.bucket.CopyFile(bucketName, srcObjectName, destObjectName, 1024*100, Routines(5), Checkpoint(true, destObjectName+".cp"), Meta("myprop", "mypropval")) + c.Assert(err, IsNil) + + meta, err := s.bucket.GetObjectDetailedMeta(destObjectName) + c.Assert(err, IsNil) + c.Assert(meta.Get("X-Oss-Meta-Myprop"), Equals, "mypropval") + + err = s.bucket.GetObjectToFile(destObjectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(destObjectName) + c.Assert(err, IsNil) + os.Remove(newFile) + + // Delete the source file + err = s.bucket.DeleteObject(srcObjectName) + c.Assert(err, IsNil) +} + +// TestCopyRoutineWithRecoveryNegative is a multiple routineed copy without checkpoint +func (s *OssCopySuite) TestCopyRoutineWithRecoveryNegative(c *C) { + srcObjectName := objectNamePrefix + RandStr(8) + destObjectName := srcObjectName + "-dest" + + // Source bucket does not exist + err := s.bucket.CopyFile("notexist", srcObjectName, destObjectName, 100*1024, Checkpoint(true, destObjectName+".cp")) + c.Assert(err, NotNil) + c.Assert(err, NotNil) + + // Source object does not exist + err = s.bucket.CopyFile(bucketName, "notexist", destObjectName, 100*1024, Routines(2), Checkpoint(true, destObjectName+".cp")) + c.Assert(err, NotNil) + + // Specify part size is invalid. + err = s.bucket.CopyFile(bucketName, srcObjectName, destObjectName, 1024, Checkpoint(true, destObjectName+".cp")) + c.Assert(err, NotNil) + + err = s.bucket.CopyFile(bucketName, srcObjectName, destObjectName, 1024*1024*1024*100, Routines(2), Checkpoint(true, destObjectName+".cp")) + c.Assert(err, NotNil) +} + +// TestCopyFileCrossBucket is a cross bucket's direct copy. +func (s *OssCopySuite) TestCopyFileCrossBucket(c *C) { + destBucketName := bucketName + "-desc" + srcObjectName := objectNamePrefix + RandStr(8) + destObjectName := srcObjectName + "-dest" + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + newFile := RandStr(8) + ".jpg" + + destBucket, err := s.client.Bucket(destBucketName) + c.Assert(err, IsNil) + + // Create a target bucket + err = s.client.CreateBucket(destBucketName) + + // Upload source file + err = s.bucket.UploadFile(srcObjectName, fileName, 100*1024, Routines(3)) + c.Assert(err, IsNil) + os.Remove(newFile) + + // Copy files + err = destBucket.CopyFile(bucketName, srcObjectName, destObjectName, 1024*100, Routines(5), Checkpoint(true, destObjectName+".cp")) + c.Assert(err, IsNil) + + err = destBucket.GetObjectToFile(destObjectName, newFile) + c.Assert(err, IsNil) + + eq, err := compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = destBucket.DeleteObject(destObjectName) + c.Assert(err, IsNil) + os.Remove(newFile) + + // Copy file with options + err = destBucket.CopyFile(bucketName, srcObjectName, destObjectName, 1024*100, Routines(10), Checkpoint(true, "copy.cp"), Meta("myprop", "mypropval")) + c.Assert(err, IsNil) + + err = destBucket.GetObjectToFile(destObjectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = destBucket.DeleteObject(destObjectName) + c.Assert(err, IsNil) + os.Remove(newFile) + + // Delete target bucket + ForceDeleteBucket(s.client, destBucketName, c) +} + +func (s *OssCopySuite) TestVersioningCopyFileCrossBucket(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + // begin test + objectName := objectNamePrefix + RandStr(8) + fileName := "test-file-" + RandStr(8) + fileData := RandStr(500 * 1024) + CreateFile(fileName, fileData, c) + newFile := "test-file-" + RandStr(8) + destBucketName := bucketName + "-desc" + srcObjectName := objectNamePrefix + RandStr(8) + destObjectName := srcObjectName + "-dest" + + // Create dest bucket + err = client.CreateBucket(destBucketName) + c.Assert(err, IsNil) + destBucket, err := client.Bucket(destBucketName) + c.Assert(err, IsNil) + + err = client.SetBucketVersioning(destBucketName, versioningConfig) + c.Assert(err, IsNil) + + // Upload source file + var respHeader http.Header + options := []Option{Routines(3), GetResponseHeader(&respHeader)} + err = bucket.UploadFile(srcObjectName, fileName, 100*1024, options...) + versionId := GetVersionId(respHeader) + c.Assert(len(versionId) > 0, Equals, true) + + c.Assert(err, IsNil) + os.Remove(newFile) + + // overwrite emtpy object + err = bucket.PutObject(srcObjectName, strings.NewReader("")) + c.Assert(err, IsNil) + + // Copy files + var respCopyHeader http.Header + options = []Option{Routines(5), Checkpoint(true, destObjectName+".cp"), GetResponseHeader(&respCopyHeader), VersionId(versionId)} + err = destBucket.CopyFile(bucketName, srcObjectName, destObjectName, 1024*100, options...) + c.Assert(err, IsNil) + versionIdCopy := GetVersionId(respCopyHeader) + c.Assert(len(versionIdCopy) > 0, Equals, true) + + err = destBucket.GetObjectToFile(destObjectName, newFile, VersionId(versionIdCopy)) + c.Assert(err, IsNil) + + eq, err := compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = destBucket.DeleteObject(destObjectName) + c.Assert(err, IsNil) + os.Remove(newFile) + + // Copy file with options meta + options = []Option{Routines(10), Checkpoint(true, "copy.cp"), Meta("myprop", "mypropval"), GetResponseHeader(&respCopyHeader), VersionId(versionId)} + err = destBucket.CopyFile(bucketName, srcObjectName, destObjectName, 1024*100, options...) + c.Assert(err, IsNil) + versionIdCopy = GetVersionId(respCopyHeader) + + err = destBucket.GetObjectToFile(destObjectName, newFile, VersionId(versionIdCopy)) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + os.Remove(fileName) + os.Remove(newFile) + destBucket.DeleteObject(destObjectName) + bucket.DeleteObject(objectName) + ForceDeleteBucket(client, bucketName, c) + ForceDeleteBucket(client, destBucketName, c) +} + +// TestCopyFileChoiceOptions +func (s *OssCopySuite) TestCopyFileChoiceOptions(c *C) { + destBucketName := bucketName + "-desc" + srcObjectName := objectNamePrefix + RandStr(8) + destObjectName := srcObjectName + "-dest" + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + newFile := RandStr(8) + ".jpg" + + destBucket, err := s.client.Bucket(destBucketName) + c.Assert(err, IsNil) + + // Create a target bucket + err = s.client.CreateBucket(destBucketName) + + // Upload source file + err = s.bucket.UploadFile(srcObjectName, fileName, 100*1024, Routines(3)) + c.Assert(err, IsNil) + os.Remove(newFile) + + // copyfile with properties + options := []Option{ + ObjectACL(ACLPublicRead), + RequestPayer(Requester), + TrafficLimitHeader(1024 * 1024 * 8), + ObjectStorageClass(StorageArchive), + ServerSideEncryption("AES256"), + Routines(5), // without checkpoint + } + + // Copy files + err = destBucket.CopyFile(bucketName, srcObjectName, destObjectName, 1024*100, options...) + c.Assert(err, IsNil) + + // check object + meta, err := destBucket.GetObjectDetailedMeta(destObjectName) + c.Assert(err, IsNil) + c.Assert(meta.Get("X-Oss-Storage-Class"), Equals, "Archive") + c.Assert(meta.Get("X-Oss-Server-Side-Encryption"), Equals, "AES256") + + aclResult, err := destBucket.GetObjectACL(destObjectName) + c.Assert(aclResult.ACL, Equals, "public-read") + c.Assert(err, IsNil) + + err = destBucket.DeleteObject(destObjectName) + c.Assert(err, IsNil) + os.Remove(newFile) + + // Copy file with options + options = []Option{ + ObjectACL(ACLPublicRead), + RequestPayer(Requester), + TrafficLimitHeader(1024 * 1024 * 8), + ObjectStorageClass(StorageArchive), + ServerSideEncryption("AES256"), + Routines(10), + Checkpoint(true, "copy.cp"), // with checkpoint + } + + err = destBucket.CopyFile(bucketName, srcObjectName, destObjectName, 1024*100, options...) + c.Assert(err, IsNil) + + // check object + meta, err = destBucket.GetObjectDetailedMeta(destObjectName) + c.Assert(err, IsNil) + c.Assert(meta.Get("X-Oss-Storage-Class"), Equals, "Archive") + c.Assert(meta.Get("X-Oss-Server-Side-Encryption"), Equals, "AES256") + + aclResult, err = destBucket.GetObjectACL(destObjectName) + c.Assert(aclResult.ACL, Equals, "public-read") + c.Assert(err, IsNil) + + err = destBucket.DeleteObject(destObjectName) + c.Assert(err, IsNil) + os.Remove(newFile) + + // Delete target bucket + err = s.client.DeleteBucket(destBucketName) + c.Assert(err, IsNil) +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/multipart.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/multipart.go new file mode 100644 index 00000000..eed490a1 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/multipart.go @@ -0,0 +1,308 @@ +package oss + +import ( + "bytes" + "encoding/xml" + "io" + "net/http" + "net/url" + "os" + "sort" + "strconv" +) + +// InitiateMultipartUpload initializes multipart upload +// +// objectKey object name +// options the object constricts for upload. The valid options are CacheControl, ContentDisposition, ContentEncoding, Expires, +// ServerSideEncryption, Meta, check out the following link: +// https://help.aliyun.com/document_detail/oss/api-reference/multipart-upload/InitiateMultipartUpload.html +// +// InitiateMultipartUploadResult the return value of the InitiateMultipartUpload, which is used for calls later on such as UploadPartFromFile,UploadPartCopy. +// error it's nil if the operation succeeds, otherwise it's an error object. +// +func (bucket Bucket) InitiateMultipartUpload(objectKey string, options ...Option) (InitiateMultipartUploadResult, error) { + var imur InitiateMultipartUploadResult + opts := AddContentType(options, objectKey) + params, _ := GetRawParams(options) + _, ok := params["sequential"] + if ok { + // convert "" to nil + params["sequential"] = nil + } + params["uploads"] = nil + + resp, err := bucket.do("POST", objectKey, params, opts, nil, nil) + if err != nil { + return imur, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &imur) + return imur, err +} + +// UploadPart uploads parts +// +// After initializing a Multipart Upload, the upload Id and object key could be used for uploading the parts. +// Each part has its part number (ranges from 1 to 10,000). And for each upload Id, the part number identifies the position of the part in the whole file. +// And thus with the same part number and upload Id, another part upload will overwrite the data. +// Except the last one, minimal part size is 100KB. There's no limit on the last part size. +// +// imur the returned value of InitiateMultipartUpload. +// reader io.Reader the reader for the part's data. +// size the part size. +// partNumber the part number (ranges from 1 to 10,000). Invalid part number will lead to InvalidArgument error. +// +// UploadPart the return value of the upload part. It consists of PartNumber and ETag. It's valid when error is nil. +// error it's nil if the operation succeeds, otherwise it's an error object. +// +func (bucket Bucket) UploadPart(imur InitiateMultipartUploadResult, reader io.Reader, + partSize int64, partNumber int, options ...Option) (UploadPart, error) { + request := &UploadPartRequest{ + InitResult: &imur, + Reader: reader, + PartSize: partSize, + PartNumber: partNumber, + } + + result, err := bucket.DoUploadPart(request, options) + + return result.Part, err +} + +// UploadPartFromFile uploads part from the file. +// +// imur the return value of a successful InitiateMultipartUpload. +// filePath the local file path to upload. +// startPosition the start position in the local file. +// partSize the part size. +// partNumber the part number (from 1 to 10,000) +// +// UploadPart the return value consists of PartNumber and ETag. +// error it's nil if the operation succeeds, otherwise it's an error object. +// +func (bucket Bucket) UploadPartFromFile(imur InitiateMultipartUploadResult, filePath string, + startPosition, partSize int64, partNumber int, options ...Option) (UploadPart, error) { + var part = UploadPart{} + fd, err := os.Open(filePath) + if err != nil { + return part, err + } + defer fd.Close() + fd.Seek(startPosition, os.SEEK_SET) + + request := &UploadPartRequest{ + InitResult: &imur, + Reader: fd, + PartSize: partSize, + PartNumber: partNumber, + } + + result, err := bucket.DoUploadPart(request, options) + + return result.Part, err +} + +// DoUploadPart does the actual part upload. +// +// request part upload request +// +// UploadPartResult the result of uploading part. +// error it's nil if the operation succeeds, otherwise it's an error object. +// +func (bucket Bucket) DoUploadPart(request *UploadPartRequest, options []Option) (*UploadPartResult, error) { + listener := GetProgressListener(options) + options = append(options, ContentLength(request.PartSize)) + params := map[string]interface{}{} + params["partNumber"] = strconv.Itoa(request.PartNumber) + params["uploadId"] = request.InitResult.UploadID + resp, err := bucket.do("PUT", request.InitResult.Key, params, options, + &io.LimitedReader{R: request.Reader, N: request.PartSize}, listener) + if err != nil { + return &UploadPartResult{}, err + } + defer resp.Body.Close() + + part := UploadPart{ + ETag: resp.Headers.Get(HTTPHeaderEtag), + PartNumber: request.PartNumber, + } + + if bucket.GetConfig().IsEnableCRC { + err = CheckCRC(resp, "DoUploadPart") + if err != nil { + return &UploadPartResult{part}, err + } + } + + return &UploadPartResult{part}, nil +} + +// UploadPartCopy uploads part copy +// +// imur the return value of InitiateMultipartUpload +// copySrc source Object name +// startPosition the part's start index in the source file +// partSize the part size +// partNumber the part number, ranges from 1 to 10,000. If it exceeds the range OSS returns InvalidArgument error. +// options the constraints of source object for the copy. The copy happens only when these contraints are met. Otherwise it returns error. +// CopySourceIfNoneMatch, CopySourceIfModifiedSince CopySourceIfUnmodifiedSince, check out the following link for the detail +// https://help.aliyun.com/document_detail/oss/api-reference/multipart-upload/UploadPartCopy.html +// +// UploadPart the return value consists of PartNumber and ETag. +// error it's nil if the operation succeeds, otherwise it's an error object. +// +func (bucket Bucket) UploadPartCopy(imur InitiateMultipartUploadResult, srcBucketName, srcObjectKey string, + startPosition, partSize int64, partNumber int, options ...Option) (UploadPart, error) { + var out UploadPartCopyResult + var part UploadPart + var opts []Option + + //first find version id + versionIdKey := "versionId" + versionId, _ := FindOption(options, versionIdKey, nil) + if versionId == nil { + opts = []Option{CopySource(srcBucketName, url.QueryEscape(srcObjectKey)), + CopySourceRange(startPosition, partSize)} + } else { + opts = []Option{CopySourceVersion(srcBucketName, url.QueryEscape(srcObjectKey), versionId.(string)), + CopySourceRange(startPosition, partSize)} + options = DeleteOption(options, versionIdKey) + } + + opts = append(opts, options...) + + params := map[string]interface{}{} + params["partNumber"] = strconv.Itoa(partNumber) + params["uploadId"] = imur.UploadID + resp, err := bucket.do("PUT", imur.Key, params, opts, nil, nil) + if err != nil { + return part, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + if err != nil { + return part, err + } + part.ETag = out.ETag + part.PartNumber = partNumber + + return part, nil +} + +// CompleteMultipartUpload completes the multipart upload. +// +// imur the return value of InitiateMultipartUpload. +// parts the array of return value of UploadPart/UploadPartFromFile/UploadPartCopy. +// +// CompleteMultipartUploadResponse the return value when the call succeeds. Only valid when the error is nil. +// error it's nil if the operation succeeds, otherwise it's an error object. +// +func (bucket Bucket) CompleteMultipartUpload(imur InitiateMultipartUploadResult, + parts []UploadPart, options ...Option) (CompleteMultipartUploadResult, error) { + var out CompleteMultipartUploadResult + + sort.Sort(UploadParts(parts)) + cxml := completeMultipartUploadXML{} + cxml.Part = parts + bs, err := xml.Marshal(cxml) + if err != nil { + return out, err + } + buffer := new(bytes.Buffer) + buffer.Write(bs) + + params := map[string]interface{}{} + params["uploadId"] = imur.UploadID + resp, err := bucket.do("POST", imur.Key, params, options, buffer, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + return out, err +} + +// AbortMultipartUpload aborts the multipart upload. +// +// imur the return value of InitiateMultipartUpload. +// +// error it's nil if the operation succeeds, otherwise it's an error object. +// +func (bucket Bucket) AbortMultipartUpload(imur InitiateMultipartUploadResult, options ...Option) error { + params := map[string]interface{}{} + params["uploadId"] = imur.UploadID + resp, err := bucket.do("DELETE", imur.Key, params, options, nil, nil) + if err != nil { + return err + } + defer resp.Body.Close() + return CheckRespCode(resp.StatusCode, []int{http.StatusNoContent}) +} + +// ListUploadedParts lists the uploaded parts. +// +// imur the return value of InitiateMultipartUpload. +// +// ListUploadedPartsResponse the return value if it succeeds, only valid when error is nil. +// error it's nil if the operation succeeds, otherwise it's an error object. +// +func (bucket Bucket) ListUploadedParts(imur InitiateMultipartUploadResult, options ...Option) (ListUploadedPartsResult, error) { + var out ListUploadedPartsResult + options = append(options, EncodingType("url")) + + params := map[string]interface{}{} + params, err := GetRawParams(options) + if err != nil { + return out, err + } + + params["uploadId"] = imur.UploadID + resp, err := bucket.do("GET", imur.Key, params, options, nil, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + if err != nil { + return out, err + } + err = decodeListUploadedPartsResult(&out) + return out, err +} + +// ListMultipartUploads lists all ongoing multipart upload tasks +// +// options listObject's filter. Prefix specifies the returned object's prefix; KeyMarker specifies the returned object's start point in lexicographic order; +// MaxKeys specifies the max entries to return; Delimiter is the character for grouping object keys. +// +// ListMultipartUploadResponse the return value if it succeeds, only valid when error is nil. +// error it's nil if the operation succeeds, otherwise it's an error object. +// +func (bucket Bucket) ListMultipartUploads(options ...Option) (ListMultipartUploadResult, error) { + var out ListMultipartUploadResult + + options = append(options, EncodingType("url")) + params, err := GetRawParams(options) + if err != nil { + return out, err + } + params["uploads"] = nil + + resp, err := bucket.do("GET", "", params, options, nil, nil) + if err != nil { + return out, err + } + defer resp.Body.Close() + + err = xmlUnmarshal(resp.Body, &out) + if err != nil { + return out, err + } + err = decodeListMultipartUploadResult(&out) + return out, err +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/multipart_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/multipart_test.go new file mode 100644 index 00000000..57278592 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/multipart_test.go @@ -0,0 +1,986 @@ +// multipart test + +package oss + +import ( + "math/rand" + "net/http" + "os" + "strconv" + + . "gopkg.in/check.v1" +) + +type OssBucketMultipartSuite struct { + client *Client + bucket *Bucket +} + +var _ = Suite(&OssBucketMultipartSuite{}) + +// SetUpSuite runs once when the suite starts running +func (s *OssBucketMultipartSuite) SetUpSuite(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + s.client = client + + s.client.CreateBucket(bucketName) + + bucket, err := s.client.Bucket(bucketName) + c.Assert(err, IsNil) + s.bucket = bucket + + // Delete part + keyMarker := KeyMarker("") + uploadIDMarker := UploadIDMarker("") + for { + lmur, err := s.bucket.ListMultipartUploads(keyMarker, uploadIDMarker) + c.Assert(err, IsNil) + for _, upload := range lmur.Uploads { + var imur = InitiateMultipartUploadResult{Bucket: s.bucket.BucketName, + Key: upload.Key, UploadID: upload.UploadID} + err = s.bucket.AbortMultipartUpload(imur) + c.Assert(err, IsNil) + } + keyMarker = KeyMarker(lmur.NextKeyMarker) + uploadIDMarker = UploadIDMarker(lmur.NextUploadIDMarker) + if !lmur.IsTruncated { + break + } + } + + // Delete objects + marker := Marker("") + for { + lor, err := s.bucket.ListObjects(marker) + c.Assert(err, IsNil) + for _, object := range lor.Objects { + err = s.bucket.DeleteObject(object.Key) + c.Assert(err, IsNil) + } + marker = Marker(lor.NextMarker) + if !lor.IsTruncated { + break + } + } + + testLogger.Println("test multipart started") +} + +// TearDownSuite runs before each test or benchmark starts running +func (s *OssBucketMultipartSuite) TearDownSuite(c *C) { + // Delete part + keyMarker := KeyMarker("") + uploadIDMarker := UploadIDMarker("") + for { + lmur, err := s.bucket.ListMultipartUploads(keyMarker, uploadIDMarker) + c.Assert(err, IsNil) + for _, upload := range lmur.Uploads { + var imur = InitiateMultipartUploadResult{Bucket: s.bucket.BucketName, + Key: upload.Key, UploadID: upload.UploadID} + err = s.bucket.AbortMultipartUpload(imur) + c.Assert(err, IsNil) + } + keyMarker = KeyMarker(lmur.NextKeyMarker) + uploadIDMarker = UploadIDMarker(lmur.NextUploadIDMarker) + if !lmur.IsTruncated { + break + } + } + + // Delete objects + marker := Marker("") + for { + lor, err := s.bucket.ListObjects(marker) + c.Assert(err, IsNil) + for _, object := range lor.Objects { + err = s.bucket.DeleteObject(object.Key) + c.Assert(err, IsNil) + } + marker = Marker(lor.NextMarker) + if !lor.IsTruncated { + break + } + } + + // Delete bucket + err := s.client.DeleteBucket(s.bucket.BucketName) + c.Assert(err, IsNil) + + testLogger.Println("test multipart completed") +} + +// SetUpTest runs after each test or benchmark runs +func (s *OssBucketMultipartSuite) SetUpTest(c *C) { + err := removeTempFiles("../oss", ".jpg") + c.Assert(err, IsNil) +} + +// TearDownTest runs once after all tests or benchmarks have finished running +func (s *OssBucketMultipartSuite) TearDownTest(c *C) { + err := removeTempFiles("../oss", ".jpg") + c.Assert(err, IsNil) + + err = removeTempFiles("../oss", ".temp") + c.Assert(err, IsNil) + + err = removeTempFiles("../oss", ".txt1") + c.Assert(err, IsNil) + + err = removeTempFiles("../oss", ".txt2") + c.Assert(err, IsNil) +} + +// TestMultipartUpload +func (s *OssBucketMultipartSuite) TestMultipartUpload(c *C) { + objectName := objectNamePrefix + RandStr(8) + var fileName = "../sample/BingWallpaper-2015-11-07.jpg" + + chunks, err := SplitFileByPartNum(fileName, 3) + c.Assert(err, IsNil) + testLogger.Println("chunks:", chunks) + + options := []Option{ + Expires(futureDate), Meta("my", "myprop"), + } + + fd, err := os.Open(fileName) + c.Assert(err, IsNil) + defer fd.Close() + + imur, err := s.bucket.InitiateMultipartUpload(objectName, options...) + c.Assert(err, IsNil) + var parts []UploadPart + for _, chunk := range chunks { + fd.Seek(chunk.Offset, os.SEEK_SET) + part, err := s.bucket.UploadPart(imur, fd, chunk.Size, chunk.Number) + c.Assert(err, IsNil) + parts = append(parts, part) + } + + cmur, err := s.bucket.CompleteMultipartUpload(imur, parts) + c.Assert(err, IsNil) + testLogger.Println("cmur:", cmur) + + meta, err := s.bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + testLogger.Println("GetObjectDetailedMeta:", meta) + c.Assert(meta.Get("X-Oss-Meta-My"), Equals, "myprop") + c.Assert(meta.Get("Expires"), Equals, futureDate.Format(http.TimeFormat)) + c.Assert(meta.Get("X-Oss-Object-Type"), Equals, "Multipart") + + err = s.bucket.GetObjectToFile(objectName, "newpic1.jpg") + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +// TestMultipartUploadFromFile +func (s *OssBucketMultipartSuite) TestMultipartUploadFromFile(c *C) { + objectName := objectNamePrefix + RandStr(8) + var fileName = "../sample/BingWallpaper-2015-11-07.jpg" + + chunks, err := SplitFileByPartNum(fileName, 3) + c.Assert(err, IsNil) + testLogger.Println("chunks:", chunks) + + options := []Option{ + Expires(futureDate), Meta("my", "myprop"), + } + imur, err := s.bucket.InitiateMultipartUpload(objectName, options...) + c.Assert(err, IsNil) + var parts []UploadPart + for _, chunk := range chunks { + part, err := s.bucket.UploadPartFromFile(imur, fileName, chunk.Offset, chunk.Size, chunk.Number) + c.Assert(err, IsNil) + parts = append(parts, part) + } + + cmur, err := s.bucket.CompleteMultipartUpload(imur, parts) + c.Assert(err, IsNil) + testLogger.Println("cmur:", cmur) + + meta, err := s.bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + testLogger.Println("GetObjectDetailedMeta:", meta) + c.Assert(meta.Get("X-Oss-Meta-My"), Equals, "myprop") + c.Assert(meta.Get("Expires"), Equals, futureDate.Format(http.TimeFormat)) + c.Assert(meta.Get("X-Oss-Object-Type"), Equals, "Multipart") + + err = s.bucket.GetObjectToFile(objectName, "newpic1.jpg") + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +// TestUploadPartCopy +func (s *OssBucketMultipartSuite) TestUploadPartCopy(c *C) { + objectSrc := objectNamePrefix + RandStr(8) + "-src" + objectDest := objectNamePrefix + RandStr(8) + "-dest" + var fileName = "../sample/BingWallpaper-2015-11-07.jpg" + + chunks, err := SplitFileByPartNum(fileName, 3) + c.Assert(err, IsNil) + testLogger.Println("chunks:", chunks) + + err = s.bucket.PutObjectFromFile(objectSrc, fileName) + c.Assert(err, IsNil) + + options := []Option{ + Expires(futureDate), Meta("my", "myprop"), + } + imur, err := s.bucket.InitiateMultipartUpload(objectDest, options...) + c.Assert(err, IsNil) + var parts []UploadPart + for _, chunk := range chunks { + part, err := s.bucket.UploadPartCopy(imur, bucketName, objectSrc, chunk.Offset, chunk.Size, (int)(chunk.Number)) + c.Assert(err, IsNil) + parts = append(parts, part) + } + + cmur, err := s.bucket.CompleteMultipartUpload(imur, parts) + c.Assert(err, IsNil) + testLogger.Println("cmur:", cmur) + + meta, err := s.bucket.GetObjectDetailedMeta(objectDest) + c.Assert(err, IsNil) + testLogger.Println("GetObjectDetailedMeta:", meta) + c.Assert(meta.Get("X-Oss-Meta-My"), Equals, "myprop") + c.Assert(meta.Get("Expires"), Equals, futureDate.Format(http.TimeFormat)) + c.Assert(meta.Get("X-Oss-Object-Type"), Equals, "Multipart") + + err = s.bucket.GetObjectToFile(objectDest, "newpic2.jpg") + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(objectSrc) + c.Assert(err, IsNil) + err = s.bucket.DeleteObject(objectDest) + c.Assert(err, IsNil) +} + +func (s *OssBucketMultipartSuite) TestListUploadedParts(c *C) { + objectName := objectNamePrefix + RandStr(8) + objectSrc := objectName + "-src" + objectDest := objectName + "-dest" + var fileName = "../sample/BingWallpaper-2015-11-07.jpg" + + chunks, err := SplitFileByPartSize(fileName, 100*1024) + c.Assert(err, IsNil) + testLogger.Println("chunks:", chunks) + + err = s.bucket.PutObjectFromFile(objectSrc, fileName) + c.Assert(err, IsNil) + + // Upload + imurUpload, err := s.bucket.InitiateMultipartUpload(objectName) + var partsUpload []UploadPart + for _, chunk := range chunks { + part, err := s.bucket.UploadPartFromFile(imurUpload, fileName, chunk.Offset, chunk.Size, (int)(chunk.Number)) + c.Assert(err, IsNil) + partsUpload = append(partsUpload, part) + } + + // Copy + imurCopy, err := s.bucket.InitiateMultipartUpload(objectDest) + var partsCopy []UploadPart + for _, chunk := range chunks { + part, err := s.bucket.UploadPartCopy(imurCopy, bucketName, objectSrc, chunk.Offset, chunk.Size, (int)(chunk.Number)) + c.Assert(err, IsNil) + partsCopy = append(partsCopy, part) + } + + // List + lupr, err := s.bucket.ListUploadedParts(imurUpload) + c.Assert(err, IsNil) + testLogger.Println("lupr:", lupr) + c.Assert(len(lupr.UploadedParts), Equals, len(chunks)) + + lupr, err = s.bucket.ListUploadedParts(imurCopy) + c.Assert(err, IsNil) + testLogger.Println("lupr:", lupr) + c.Assert(len(lupr.UploadedParts), Equals, len(chunks)) + + lmur, err := s.bucket.ListMultipartUploads() + c.Assert(err, IsNil) + testLogger.Println("lmur:", lmur) + + // Complete + _, err = s.bucket.CompleteMultipartUpload(imurUpload, partsUpload) + c.Assert(err, IsNil) + _, err = s.bucket.CompleteMultipartUpload(imurCopy, partsCopy) + c.Assert(err, IsNil) + + // Download + err = s.bucket.GetObjectToFile(objectDest, "newpic3.jpg") + c.Assert(err, IsNil) + err = s.bucket.GetObjectToFile(objectName, "newpic4.jpg") + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + err = s.bucket.DeleteObject(objectDest) + c.Assert(err, IsNil) + err = s.bucket.DeleteObject(objectSrc) + c.Assert(err, IsNil) +} + +func (s *OssBucketMultipartSuite) TestAbortMultipartUpload(c *C) { + objectName := objectNamePrefix + RandStr(8) + objectSrc := objectName + "-src" + objectDest := objectName + "-dest" + var fileName = "../sample/BingWallpaper-2015-11-07.jpg" + + chunks, err := SplitFileByPartSize(fileName, 100*1024) + c.Assert(err, IsNil) + testLogger.Println("chunks:", chunks) + + err = s.bucket.PutObjectFromFile(objectSrc, fileName) + c.Assert(err, IsNil) + + // Upload + imurUpload, err := s.bucket.InitiateMultipartUpload(objectName) + var partsUpload []UploadPart + for _, chunk := range chunks { + part, err := s.bucket.UploadPartFromFile(imurUpload, fileName, chunk.Offset, chunk.Size, (int)(chunk.Number)) + c.Assert(err, IsNil) + partsUpload = append(partsUpload, part) + } + + // Copy + imurCopy, err := s.bucket.InitiateMultipartUpload(objectDest) + var partsCopy []UploadPart + for _, chunk := range chunks { + part, err := s.bucket.UploadPartCopy(imurCopy, bucketName, objectSrc, chunk.Offset, chunk.Size, (int)(chunk.Number)) + c.Assert(err, IsNil) + partsCopy = append(partsCopy, part) + } + + // List + lupr, err := s.bucket.ListUploadedParts(imurUpload) + c.Assert(err, IsNil) + testLogger.Println("lupr:", lupr) + c.Assert(len(lupr.UploadedParts), Equals, len(chunks)) + + lupr, err = s.bucket.ListUploadedParts(imurCopy) + c.Assert(err, IsNil) + testLogger.Println("lupr:", lupr) + c.Assert(len(lupr.UploadedParts), Equals, len(chunks)) + + lmur, err := s.bucket.ListMultipartUploads() + c.Assert(err, IsNil) + testLogger.Println("lmur:", lmur) + c.Assert(len(lmur.Uploads), Equals, 2) + + // Abort + err = s.bucket.AbortMultipartUpload(imurUpload) + c.Assert(err, IsNil) + err = s.bucket.AbortMultipartUpload(imurCopy) + c.Assert(err, IsNil) + + lmur, err = s.bucket.ListMultipartUploads() + c.Assert(err, IsNil) + testLogger.Println("lmur:", lmur) + c.Assert(len(lmur.Uploads), Equals, 0) + + // Download + err = s.bucket.GetObjectToFile(objectDest, "newpic3.jpg") + c.Assert(err, NotNil) + err = s.bucket.GetObjectToFile(objectName, "newpic4.jpg") + c.Assert(err, NotNil) +} + +// TestUploadPartCopyWithConstraints +func (s *OssBucketMultipartSuite) TestUploadPartCopyWithConstraints(c *C) { + objectSrc := objectNamePrefix + RandStr(8) + "-src" + objectDest := objectNamePrefix + RandStr(8) + "-dest" + var fileName = "../sample/BingWallpaper-2015-11-07.jpg" + + chunks, err := SplitFileByPartNum(fileName, 3) + c.Assert(err, IsNil) + testLogger.Println("chunks:", chunks) + + err = s.bucket.PutObjectFromFile(objectSrc, fileName) + c.Assert(err, IsNil) + + imur, err := s.bucket.InitiateMultipartUpload(objectDest) + var parts []UploadPart + for _, chunk := range chunks { + _, err = s.bucket.UploadPartCopy(imur, bucketName, objectSrc, chunk.Offset, chunk.Size, (int)(chunk.Number), + CopySourceIfModifiedSince(futureDate)) + c.Assert(err, NotNil) + } + + for _, chunk := range chunks { + _, err = s.bucket.UploadPartCopy(imur, bucketName, objectSrc, chunk.Offset, chunk.Size, (int)(chunk.Number), + CopySourceIfUnmodifiedSince(futureDate)) + c.Assert(err, IsNil) + } + + meta, err := s.bucket.GetObjectDetailedMeta(objectSrc) + c.Assert(err, IsNil) + testLogger.Println("GetObjectDetailedMeta:", meta) + + for _, chunk := range chunks { + _, err = s.bucket.UploadPartCopy(imur, bucketName, objectSrc, chunk.Offset, chunk.Size, (int)(chunk.Number), + CopySourceIfNoneMatch(meta.Get("Etag"))) + c.Assert(err, NotNil) + } + + for _, chunk := range chunks { + part, err := s.bucket.UploadPartCopy(imur, bucketName, objectSrc, chunk.Offset, chunk.Size, (int)(chunk.Number), + CopySourceIfMatch(meta.Get("Etag"))) + c.Assert(err, IsNil) + parts = append(parts, part) + } + + cmur, err := s.bucket.CompleteMultipartUpload(imur, parts) + c.Assert(err, IsNil) + testLogger.Println("cmur:", cmur) + + err = s.bucket.GetObjectToFile(objectDest, "newpic5.jpg") + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(objectSrc) + c.Assert(err, IsNil) + err = s.bucket.DeleteObject(objectDest) + c.Assert(err, IsNil) +} + +// TestMultipartUploadFromFileOutofOrder +func (s *OssBucketMultipartSuite) TestMultipartUploadFromFileOutofOrder(c *C) { + objectName := objectNamePrefix + RandStr(8) + var fileName = "../sample/BingWallpaper-2015-11-07.jpg" + + chunks, err := SplitFileByPartSize(fileName, 1024*100) + shuffleArray(chunks) + c.Assert(err, IsNil) + testLogger.Println("chunks:", chunks) + + imur, err := s.bucket.InitiateMultipartUpload(objectName) + var parts []UploadPart + for _, chunk := range chunks { + _, err := s.bucket.UploadPartFromFile(imur, fileName, chunk.Offset, chunk.Size, (int)(chunk.Number)) + c.Assert(err, IsNil) + } + // Double upload + for _, chunk := range chunks { + part, err := s.bucket.UploadPartFromFile(imur, fileName, chunk.Offset, chunk.Size, (int)(chunk.Number)) + c.Assert(err, IsNil) + parts = append(parts, part) + } + + cmur, err := s.bucket.CompleteMultipartUpload(imur, parts) + c.Assert(err, IsNil) + testLogger.Println("cmur:", cmur) + + err = s.bucket.GetObjectToFile(objectName, "newpic6.jpg") + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +// TestUploadPartCopyOutofOrder +func (s *OssBucketMultipartSuite) TestUploadPartCopyOutofOrder(c *C) { + objectSrc := objectNamePrefix + RandStr(8) + "-src" + objectDest := objectNamePrefix + RandStr(8) + "-dest" + var fileName = "../sample/BingWallpaper-2015-11-07.jpg" + + chunks, err := SplitFileByPartSize(fileName, 1024*100) + shuffleArray(chunks) + c.Assert(err, IsNil) + testLogger.Println("chunks:", chunks) + + err = s.bucket.PutObjectFromFile(objectSrc, fileName) + c.Assert(err, IsNil) + + imur, err := s.bucket.InitiateMultipartUpload(objectDest) + var parts []UploadPart + for _, chunk := range chunks { + _, err := s.bucket.UploadPartCopy(imur, bucketName, objectSrc, chunk.Offset, chunk.Size, (int)(chunk.Number)) + c.Assert(err, IsNil) + } + // Double copy + for _, chunk := range chunks { + part, err := s.bucket.UploadPartCopy(imur, bucketName, objectSrc, chunk.Offset, chunk.Size, (int)(chunk.Number)) + c.Assert(err, IsNil) + parts = append(parts, part) + } + + cmur, err := s.bucket.CompleteMultipartUpload(imur, parts) + c.Assert(err, IsNil) + testLogger.Println("cmur:", cmur) + + err = s.bucket.GetObjectToFile(objectDest, "newpic7.jpg") + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(objectSrc) + c.Assert(err, IsNil) + err = s.bucket.DeleteObject(objectDest) + c.Assert(err, IsNil) +} + +// TestMultipartUploadFromFileType +func (s *OssBucketMultipartSuite) TestMultipartUploadFromFileType(c *C) { + objectName := objectNamePrefix + RandStr(8) + ".jpg" + var fileName = "../sample/BingWallpaper-2015-11-07.jpg" + + chunks, err := SplitFileByPartNum(fileName, 4) + c.Assert(err, IsNil) + testLogger.Println("chunks:", chunks) + + imur, err := s.bucket.InitiateMultipartUpload(objectName) + var parts []UploadPart + for _, chunk := range chunks { + part, err := s.bucket.UploadPartFromFile(imur, fileName, chunk.Offset, chunk.Size, chunk.Number) + c.Assert(err, IsNil) + parts = append(parts, part) + } + + testLogger.Println("parts:", parts) + cmur, err := s.bucket.CompleteMultipartUpload(imur, parts) + c.Assert(err, IsNil) + testLogger.Println("cmur:", cmur) + + err = s.bucket.GetObjectToFile(objectName, "newpic8.jpg") + c.Assert(err, IsNil) + + meta, err := s.bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + c.Assert(meta.Get("Content-Type"), Equals, "image/jpeg") + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +func (s *OssBucketMultipartSuite) TestListMultipartUploads(c *C) { + objectName := objectNamePrefix + RandStr(8) + + imurs := []InitiateMultipartUploadResult{} + for i := 0; i < 20; i++ { + imur, err := s.bucket.InitiateMultipartUpload(objectName + strconv.Itoa(i)) + c.Assert(err, IsNil) + imurs = append(imurs, imur) + } + + lmpu, err := s.bucket.ListMultipartUploads() + c.Assert(err, IsNil) + c.Assert(len(lmpu.Uploads), Equals, 20) + + lmpu, err = s.bucket.ListMultipartUploads(MaxUploads(3)) + c.Assert(err, IsNil) + c.Assert(len(lmpu.Uploads), Equals, 3) + + lmpu, err = s.bucket.ListMultipartUploads(Prefix(objectName)) + c.Assert(err, IsNil) + c.Assert(len(lmpu.Uploads), Equals, 20) + + lmpu, err = s.bucket.ListMultipartUploads(Prefix(objectName + "1")) + c.Assert(err, IsNil) + c.Assert(len(lmpu.Uploads), Equals, 11) + + lmpu, err = s.bucket.ListMultipartUploads(Prefix(objectName + "22")) + c.Assert(err, IsNil) + c.Assert(len(lmpu.Uploads), Equals, 0) + + lmpu, err = s.bucket.ListMultipartUploads(KeyMarker(objectName + "10")) + c.Assert(err, IsNil) + c.Assert(len(lmpu.Uploads), Equals, 17) + + lmpu, err = s.bucket.ListMultipartUploads(KeyMarker(objectName+"10"), MaxUploads(3)) + c.Assert(err, IsNil) + c.Assert(len(lmpu.Uploads), Equals, 3) + + lmpu, err = s.bucket.ListMultipartUploads(Prefix(objectName), Delimiter("4")) + c.Assert(err, IsNil) + c.Assert(len(lmpu.Uploads), Equals, 18) + c.Assert(len(lmpu.CommonPrefixes), Equals, 2) + + upLoadIDStr := RandStr(3) + lmpu, err = s.bucket.ListMultipartUploads(KeyMarker(objectName+"12"), UploadIDMarker(upLoadIDStr)) + c.Assert(err, IsNil) + checkNum := 15 + for _, im := range imurs { + if im.Key == objectName+"12" && im.UploadID > upLoadIDStr { + checkNum = 16 + break + } + } + c.Assert(len(lmpu.Uploads), Equals, checkNum) + //testLogger.Println("UploadIDMarker", lmpu.Uploads) + + for _, imur := range imurs { + err = s.bucket.AbortMultipartUpload(imur) + c.Assert(err, IsNil) + } +} + +func (s *OssBucketMultipartSuite) TestListMultipartUploadsEncodingKey(c *C) { + prefix := objectNamePrefix + "让你任性让你狂" + RandStr(8) + + imurs := []InitiateMultipartUploadResult{} + for i := 0; i < 3; i++ { + imur, err := s.bucket.InitiateMultipartUpload(prefix + strconv.Itoa(i)) + c.Assert(err, IsNil) + imurs = append(imurs, imur) + } + + lmpu, err := s.bucket.ListMultipartUploads() + c.Assert(err, IsNil) + c.Assert(len(lmpu.Uploads), Equals, 3) + + lmpu, err = s.bucket.ListMultipartUploads(Prefix(prefix + "1")) + c.Assert(err, IsNil) + c.Assert(len(lmpu.Uploads), Equals, 1) + + lmpu, err = s.bucket.ListMultipartUploads(KeyMarker(prefix + "1")) + c.Assert(err, IsNil) + c.Assert(len(lmpu.Uploads), Equals, 1) + + lmpu, err = s.bucket.ListMultipartUploads(EncodingType("url")) + c.Assert(err, IsNil) + for i, upload := range lmpu.Uploads { + c.Assert(upload.Key, Equals, prefix+strconv.Itoa(i)) + } + + for _, imur := range imurs { + err = s.bucket.AbortMultipartUpload(imur) + c.Assert(err, IsNil) + } +} + +func (s *OssBucketMultipartSuite) TestMultipartNegative(c *C) { + objectName := objectNamePrefix + RandStr(8) + + // Key tool long + data := make([]byte, 100*1024) + imur, err := s.bucket.InitiateMultipartUpload(string(data)) + c.Assert(err, NotNil) + + // Invalid imur + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + fd, err := os.Open(fileName) + c.Assert(err, IsNil) + defer fd.Close() + + _, err = s.bucket.UploadPart(imur, fd, 1024, 1) + c.Assert(err, NotNil) + + _, err = s.bucket.UploadPartFromFile(imur, fileName, 0, 1024, 1) + c.Assert(err, NotNil) + + _, err = s.bucket.UploadPartCopy(imur, bucketName, fileName, 0, 1024, 1) + c.Assert(err, NotNil) + + err = s.bucket.AbortMultipartUpload(imur) + c.Assert(err, NotNil) + + _, err = s.bucket.ListUploadedParts(imur) + c.Assert(err, NotNil) + + // Invalid exist + imur, err = s.bucket.InitiateMultipartUpload(objectName) + c.Assert(err, IsNil) + + _, err = s.bucket.UploadPart(imur, fd, 1024, 1) + c.Assert(err, IsNil) + + _, err = s.bucket.UploadPart(imur, fd, 102400, 10001) + c.Assert(err, NotNil) + + // _, err = s.bucket.UploadPartFromFile(imur, fileName, 0, 1024, 1) + // c.Assert(err, IsNil) + + _, err = s.bucket.UploadPartFromFile(imur, fileName, 0, 102400, 10001) + c.Assert(err, NotNil) + + _, err = s.bucket.UploadPartCopy(imur, bucketName, fileName, 0, 1024, 1) + c.Assert(err, NotNil) + + _, err = s.bucket.UploadPartCopy(imur, bucketName, fileName, 0, 1024, 1000) + c.Assert(err, NotNil) + + err = s.bucket.AbortMultipartUpload(imur) + c.Assert(err, IsNil) + + // Invalid option + _, err = s.bucket.InitiateMultipartUpload(objectName, IfModifiedSince(futureDate)) + c.Assert(err, IsNil) +} + +func (s *OssBucketMultipartSuite) TestMultipartUploadFromFileBigFile(c *C) { + objectName := objectNamePrefix + RandStr(8) + bigFile := "D:\\tmp\\bigfile.zip" + newFile := "D:\\tmp\\newbigfile.zip" + + exist, err := isFileExist(bigFile) + c.Assert(err, IsNil) + if !exist { + return + } + + chunks, err := SplitFileByPartNum(bigFile, 64) + c.Assert(err, IsNil) + testLogger.Println("chunks:", chunks) + + imur, err := s.bucket.InitiateMultipartUpload(objectName) + var parts []UploadPart + start := GetNowSec() + for _, chunk := range chunks { + part, err := s.bucket.UploadPartFromFile(imur, bigFile, chunk.Offset, chunk.Size, (int)(chunk.Number)) + c.Assert(err, IsNil) + parts = append(parts, part) + } + end := GetNowSec() + testLogger.Println("Uplaod big file:", bigFile, "use sec:", end-start) + + testLogger.Println("parts:", parts) + _, err = s.bucket.CompleteMultipartUpload(imur, parts) + c.Assert(err, IsNil) + + start = GetNowSec() + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + end = GetNowSec() + testLogger.Println("Download big file:", bigFile, "use sec:", end-start) + + start = GetNowSec() + eq, err := compareFiles(bigFile, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + end = GetNowSec() + testLogger.Println("Compare big file:", bigFile, "use sec:", end-start) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +// TestUploadFile +func (s *OssBucketMultipartSuite) TestUploadFile(c *C) { + objectName := objectNamePrefix + RandStr(8) + var fileName = "../sample/BingWallpaper-2015-11-07.jpg" + newFile := RandStr(8) + ".jpg" + + // Upload with 100K part size + err := s.bucket.UploadFile(objectName, fileName, 100*1024) + c.Assert(err, IsNil) + + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err := compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Upload with part size equals to 1/4 of the file size + err = s.bucket.UploadFile(objectName, fileName, 482048/4) + c.Assert(err, IsNil) + + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Upload with part size equals to the file size + err = s.bucket.UploadFile(objectName, fileName, 482048) + c.Assert(err, IsNil) + + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Upload with part size is bigger than the file size + err = s.bucket.UploadFile(objectName, fileName, 482049) + c.Assert(err, IsNil) + + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Option + options := []Option{ + Expires(futureDate), + ObjectACL(ACLPublicRead), + Meta("myprop", "mypropval")} + err = s.bucket.UploadFile(objectName, fileName, 482049, options...) + c.Assert(err, IsNil) + + // Check + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + acl, err := s.bucket.GetObjectACL(objectName) + c.Assert(err, IsNil) + testLogger.Println("GetObjectAcl:", acl) + c.Assert(acl.ACL, Equals, "public-read") + + meta, err := s.bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + testLogger.Println("GetObjectDetailedMeta:", meta) + c.Assert(meta.Get("X-Oss-Meta-Myprop"), Equals, "mypropval") +} + +func (s *OssBucketMultipartSuite) TestUploadFileNegative(c *C) { + objectName := objectNamePrefix + RandStr(8) + var fileName = "../sample/BingWallpaper-2015-11-07.jpg" + + // Smaller than the required minimal part size (100KB) + err := s.bucket.UploadFile(objectName, fileName, 100*1024-1) + c.Assert(err, NotNil) + + // Bigger than the max part size (5G) + err = s.bucket.UploadFile(objectName, fileName, 1024*1024*1024*5+1) + c.Assert(err, NotNil) + + // File does not exist + err = s.bucket.UploadFile(objectName, "/root1/123abc9874", 1024*1024*1024) + c.Assert(err, NotNil) + + // Invalid key , key is empty. + err = s.bucket.UploadFile("", fileName, 100*1024) + c.Assert(err, NotNil) +} + +// TestDownloadFile +func (s *OssBucketMultipartSuite) TestDownloadFile(c *C) { + objectName := objectNamePrefix + RandStr(8) + var fileName = "../sample/BingWallpaper-2015-11-07.jpg" + newFile := RandStr(8) + ".jpg" + + err := s.bucket.UploadFile(objectName, fileName, 100*1024) + c.Assert(err, IsNil) + + // Download file with part size of 100K + err = s.bucket.DownloadFile(objectName, newFile, 100*1024) + c.Assert(err, IsNil) + + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err := compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // Download the file with part size equals to 1/4 of the file size + err = s.bucket.DownloadFile(objectName, newFile, 482048/4) + c.Assert(err, IsNil) + + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // Download the file with part size same as the file size + err = s.bucket.DownloadFile(objectName, newFile, 482048) + c.Assert(err, IsNil) + + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // Download the file with part size bigger than the file size + err = s.bucket.DownloadFile(objectName, newFile, 482049) + c.Assert(err, IsNil) + + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // Option + meta, err := s.bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + testLogger.Println("GetObjectDetailedMeta:", meta) + + // If-Match + err = s.bucket.DownloadFile(objectName, newFile, 482048/4, IfMatch(meta.Get("Etag"))) + c.Assert(err, IsNil) + + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + // If-None-Match + err = s.bucket.DownloadFile(objectName, newFile, 482048, IfNoneMatch(meta.Get("Etag"))) + c.Assert(err, NotNil) + + os.Remove(newFile) + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +func (s *OssBucketMultipartSuite) TestDownloadFileNegative(c *C) { + objectName := objectNamePrefix + RandStr(8) + newFile := RandStr(8) + ".jpg" + + // Smaller than the required minimal part size (100KB) + err := s.bucket.DownloadFile(objectName, newFile, 100*1024-1) + c.Assert(err, NotNil) + + // Bigger than the required max part size (5G) + err = s.bucket.DownloadFile(objectName, newFile, 1024*1024*1024+1) + c.Assert(err, NotNil) + + // File does not exist + err = s.bucket.DownloadFile(objectName, "/OSS/TEMP/ZIBI/QUQU/BALA", 1024*1024*1024+1) + c.Assert(err, NotNil) + + // Key does not exist + err = s.bucket.DownloadFile(objectName, newFile, 100*1024) + c.Assert(err, NotNil) +} + +// Private +func shuffleArray(chunks []FileChunk) []FileChunk { + for i := range chunks { + j := rand.Intn(i + 1) + chunks[i], chunks[j] = chunks[j], chunks[i] + } + return chunks +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/option.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/option.go new file mode 100644 index 00000000..779b00e0 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/option.go @@ -0,0 +1,638 @@ +package oss + +import ( + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "time" +) + +type optionType string + +const ( + optionParam optionType = "HTTPParameter" // URL parameter + optionHTTP optionType = "HTTPHeader" // HTTP header + optionArg optionType = "FuncArgument" // Function argument +) + +const ( + deleteObjectsQuiet = "delete-objects-quiet" + routineNum = "x-routine-num" + checkpointConfig = "x-cp-config" + initCRC64 = "init-crc64" + progressListener = "x-progress-listener" + storageClass = "storage-class" + responseHeader = "x-response-header" + redundancyType = "redundancy-type" +) + +type ( + optionValue struct { + Value interface{} + Type optionType + } + + // Option HTTP option + Option func(map[string]optionValue) error +) + +// ACL is an option to set X-Oss-Acl header +func ACL(acl ACLType) Option { + return setHeader(HTTPHeaderOssACL, string(acl)) +} + +// ContentType is an option to set Content-Type header +func ContentType(value string) Option { + return setHeader(HTTPHeaderContentType, value) +} + +// ContentLength is an option to set Content-Length header +func ContentLength(length int64) Option { + return setHeader(HTTPHeaderContentLength, strconv.FormatInt(length, 10)) +} + +// CacheControl is an option to set Cache-Control header +func CacheControl(value string) Option { + return setHeader(HTTPHeaderCacheControl, value) +} + +// ContentDisposition is an option to set Content-Disposition header +func ContentDisposition(value string) Option { + return setHeader(HTTPHeaderContentDisposition, value) +} + +// ContentEncoding is an option to set Content-Encoding header +func ContentEncoding(value string) Option { + return setHeader(HTTPHeaderContentEncoding, value) +} + +// ContentLanguage is an option to set Content-Language header +func ContentLanguage(value string) Option { + return setHeader(HTTPHeaderContentLanguage, value) +} + +// ContentMD5 is an option to set Content-MD5 header +func ContentMD5(value string) Option { + return setHeader(HTTPHeaderContentMD5, value) +} + +// Expires is an option to set Expires header +func Expires(t time.Time) Option { + return setHeader(HTTPHeaderExpires, t.Format(http.TimeFormat)) +} + +// Meta is an option to set Meta header +func Meta(key, value string) Option { + return setHeader(HTTPHeaderOssMetaPrefix+key, value) +} + +// Range is an option to set Range header, [start, end] +func Range(start, end int64) Option { + return setHeader(HTTPHeaderRange, fmt.Sprintf("bytes=%d-%d", start, end)) +} + +// NormalizedRange is an option to set Range header, such as 1024-2048 or 1024- or -2048 +func NormalizedRange(nr string) Option { + return setHeader(HTTPHeaderRange, fmt.Sprintf("bytes=%s", strings.TrimSpace(nr))) +} + +// AcceptEncoding is an option to set Accept-Encoding header +func AcceptEncoding(value string) Option { + return setHeader(HTTPHeaderAcceptEncoding, value) +} + +// IfModifiedSince is an option to set If-Modified-Since header +func IfModifiedSince(t time.Time) Option { + return setHeader(HTTPHeaderIfModifiedSince, t.Format(http.TimeFormat)) +} + +// IfUnmodifiedSince is an option to set If-Unmodified-Since header +func IfUnmodifiedSince(t time.Time) Option { + return setHeader(HTTPHeaderIfUnmodifiedSince, t.Format(http.TimeFormat)) +} + +// IfMatch is an option to set If-Match header +func IfMatch(value string) Option { + return setHeader(HTTPHeaderIfMatch, value) +} + +// IfNoneMatch is an option to set IfNoneMatch header +func IfNoneMatch(value string) Option { + return setHeader(HTTPHeaderIfNoneMatch, value) +} + +// CopySource is an option to set X-Oss-Copy-Source header +func CopySource(sourceBucket, sourceObject string) Option { + return setHeader(HTTPHeaderOssCopySource, "/"+sourceBucket+"/"+sourceObject) +} + +// CopySourceVersion is an option to set X-Oss-Copy-Source header,include versionId +func CopySourceVersion(sourceBucket, sourceObject string, versionId string) Option { + return setHeader(HTTPHeaderOssCopySource, "/"+sourceBucket+"/"+sourceObject+"?"+"versionId="+versionId) +} + +// CopySourceRange is an option to set X-Oss-Copy-Source header +func CopySourceRange(startPosition, partSize int64) Option { + val := "bytes=" + strconv.FormatInt(startPosition, 10) + "-" + + strconv.FormatInt((startPosition+partSize-1), 10) + return setHeader(HTTPHeaderOssCopySourceRange, val) +} + +// CopySourceIfMatch is an option to set X-Oss-Copy-Source-If-Match header +func CopySourceIfMatch(value string) Option { + return setHeader(HTTPHeaderOssCopySourceIfMatch, value) +} + +// CopySourceIfNoneMatch is an option to set X-Oss-Copy-Source-If-None-Match header +func CopySourceIfNoneMatch(value string) Option { + return setHeader(HTTPHeaderOssCopySourceIfNoneMatch, value) +} + +// CopySourceIfModifiedSince is an option to set X-Oss-CopySource-If-Modified-Since header +func CopySourceIfModifiedSince(t time.Time) Option { + return setHeader(HTTPHeaderOssCopySourceIfModifiedSince, t.Format(http.TimeFormat)) +} + +// CopySourceIfUnmodifiedSince is an option to set X-Oss-Copy-Source-If-Unmodified-Since header +func CopySourceIfUnmodifiedSince(t time.Time) Option { + return setHeader(HTTPHeaderOssCopySourceIfUnmodifiedSince, t.Format(http.TimeFormat)) +} + +// MetadataDirective is an option to set X-Oss-Metadata-Directive header +func MetadataDirective(directive MetadataDirectiveType) Option { + return setHeader(HTTPHeaderOssMetadataDirective, string(directive)) +} + +// ServerSideEncryption is an option to set X-Oss-Server-Side-Encryption header +func ServerSideEncryption(value string) Option { + return setHeader(HTTPHeaderOssServerSideEncryption, value) +} + +// ServerSideEncryptionKeyID is an option to set X-Oss-Server-Side-Encryption-Key-Id header +func ServerSideEncryptionKeyID(value string) Option { + return setHeader(HTTPHeaderOssServerSideEncryptionKeyID, value) +} + +// ServerSideDataEncryption is an option to set X-Oss-Server-Side-Data-Encryption header +func ServerSideDataEncryption(value string) Option { + return setHeader(HTTPHeaderOssServerSideDataEncryption, value) +} + +// SSECAlgorithm is an option to set X-Oss-Server-Side-Encryption-Customer-Algorithm header +func SSECAlgorithm(value string) Option { + return setHeader(HTTPHeaderSSECAlgorithm, value) +} + +// SSECKey is an option to set X-Oss-Server-Side-Encryption-Customer-Key header +func SSECKey(value string) Option { + return setHeader(HTTPHeaderSSECKey, value) +} + +// SSECKeyMd5 is an option to set X-Oss-Server-Side-Encryption-Customer-Key-Md5 header +func SSECKeyMd5(value string) Option { + return setHeader(HTTPHeaderSSECKeyMd5, value) +} + +// ObjectACL is an option to set X-Oss-Object-Acl header +func ObjectACL(acl ACLType) Option { + return setHeader(HTTPHeaderOssObjectACL, string(acl)) +} + +// symlinkTarget is an option to set X-Oss-Symlink-Target +func symlinkTarget(targetObjectKey string) Option { + return setHeader(HTTPHeaderOssSymlinkTarget, targetObjectKey) +} + +// Origin is an option to set Origin header +func Origin(value string) Option { + return setHeader(HTTPHeaderOrigin, value) +} + +// ObjectStorageClass is an option to set the storage class of object +func ObjectStorageClass(storageClass StorageClassType) Option { + return setHeader(HTTPHeaderOssStorageClass, string(storageClass)) +} + +// Callback is an option to set callback values +func Callback(callback string) Option { + return setHeader(HTTPHeaderOssCallback, callback) +} + +// CallbackVar is an option to set callback user defined values +func CallbackVar(callbackVar string) Option { + return setHeader(HTTPHeaderOssCallbackVar, callbackVar) +} + +// RequestPayer is an option to set payer who pay for the request +func RequestPayer(payerType PayerType) Option { + return setHeader(HTTPHeaderOssRequester, strings.ToLower(string(payerType))) +} + +// RequestPayerParam is an option to set payer who pay for the request +func RequestPayerParam(payerType PayerType) Option { + return addParam(strings.ToLower(HTTPHeaderOssRequester), strings.ToLower(string(payerType))) +} + +// SetTagging is an option to set object tagging +func SetTagging(tagging Tagging) Option { + if len(tagging.Tags) == 0 { + return nil + } + + taggingValue := "" + for index, tag := range tagging.Tags { + if index != 0 { + taggingValue += "&" + } + taggingValue += url.QueryEscape(tag.Key) + "=" + url.QueryEscape(tag.Value) + } + return setHeader(HTTPHeaderOssTagging, taggingValue) +} + +// TaggingDirective is an option to set X-Oss-Metadata-Directive header +func TaggingDirective(directive TaggingDirectiveType) Option { + return setHeader(HTTPHeaderOssTaggingDirective, string(directive)) +} + +// ACReqMethod is an option to set Access-Control-Request-Method header +func ACReqMethod(value string) Option { + return setHeader(HTTPHeaderACReqMethod, value) +} + +// ACReqHeaders is an option to set Access-Control-Request-Headers header +func ACReqHeaders(value string) Option { + return setHeader(HTTPHeaderACReqHeaders, value) +} + +// TrafficLimitHeader is an option to set X-Oss-Traffic-Limit +func TrafficLimitHeader(value int64) Option { + return setHeader(HTTPHeaderOssTrafficLimit, strconv.FormatInt(value, 10)) +} + +// UserAgentHeader is an option to set HTTPHeaderUserAgent +func UserAgentHeader(ua string) Option { + return setHeader(HTTPHeaderUserAgent, ua) +} + +// ForbidOverWrite is an option to set X-Oss-Forbid-Overwrite +func ForbidOverWrite(forbidWrite bool) Option { + if forbidWrite { + return setHeader(HTTPHeaderOssForbidOverWrite, "true") + } else { + return setHeader(HTTPHeaderOssForbidOverWrite, "false") + } +} + +// RangeBehavior is an option to set Range value, such as "standard" +func RangeBehavior(value string) Option { + return setHeader(HTTPHeaderOssRangeBehavior, value) +} + +// Delimiter is an option to set delimiler parameter +func Delimiter(value string) Option { + return addParam("delimiter", value) +} + +// Marker is an option to set marker parameter +func Marker(value string) Option { + return addParam("marker", value) +} + +// MaxKeys is an option to set maxkeys parameter +func MaxKeys(value int) Option { + return addParam("max-keys", strconv.Itoa(value)) +} + +// Prefix is an option to set prefix parameter +func Prefix(value string) Option { + return addParam("prefix", value) +} + +// EncodingType is an option to set encoding-type parameter +func EncodingType(value string) Option { + return addParam("encoding-type", value) +} + +// MaxUploads is an option to set max-uploads parameter +func MaxUploads(value int) Option { + return addParam("max-uploads", strconv.Itoa(value)) +} + +// KeyMarker is an option to set key-marker parameter +func KeyMarker(value string) Option { + return addParam("key-marker", value) +} + +// VersionIdMarker is an option to set version-id-marker parameter +func VersionIdMarker(value string) Option { + return addParam("version-id-marker", value) +} + +// VersionId is an option to set versionId parameter +func VersionId(value string) Option { + return addParam("versionId", value) +} + +// TagKey is an option to set tag key parameter +func TagKey(value string) Option { + return addParam("tag-key", value) +} + +// TagValue is an option to set tag value parameter +func TagValue(value string) Option { + return addParam("tag-value", value) +} + +// UploadIDMarker is an option to set upload-id-marker parameter +func UploadIDMarker(value string) Option { + return addParam("upload-id-marker", value) +} + +// MaxParts is an option to set max-parts parameter +func MaxParts(value int) Option { + return addParam("max-parts", strconv.Itoa(value)) +} + +// PartNumberMarker is an option to set part-number-marker parameter +func PartNumberMarker(value int) Option { + return addParam("part-number-marker", strconv.Itoa(value)) +} + +// Sequential is an option to set sequential parameter for InitiateMultipartUpload +func Sequential() Option { + return addParam("sequential", "") +} + +// ListType is an option to set List-type parameter for ListObjectsV2 +func ListType(value int) Option { + return addParam("list-type", strconv.Itoa(value)) +} + +// StartAfter is an option to set start-after parameter for ListObjectsV2 +func StartAfter(value string) Option { + return addParam("start-after", value) +} + +// ContinuationToken is an option to set Continuation-token parameter for ListObjectsV2 +func ContinuationToken(value string) Option { + if value == "" { + return addParam("continuation-token", nil) + } + return addParam("continuation-token", value) +} + +// FetchOwner is an option to set Fetch-owner parameter for ListObjectsV2 +func FetchOwner(value bool) Option { + if value { + return addParam("fetch-owner", "true") + } + return addParam("fetch-owner", "false") +} + +// DeleteObjectsQuiet false:DeleteObjects in verbose mode; true:DeleteObjects in quite mode. Default is false. +func DeleteObjectsQuiet(isQuiet bool) Option { + return addArg(deleteObjectsQuiet, isQuiet) +} + +// StorageClass bucket storage class +func StorageClass(value StorageClassType) Option { + return addArg(storageClass, value) +} + +// RedundancyType bucket data redundancy type +func RedundancyType(value DataRedundancyType) Option { + return addArg(redundancyType, value) +} + +// Checkpoint configuration +type cpConfig struct { + IsEnable bool + FilePath string + DirPath string +} + +// Checkpoint sets the isEnable flag and checkpoint file path for DownloadFile/UploadFile. +func Checkpoint(isEnable bool, filePath string) Option { + return addArg(checkpointConfig, &cpConfig{IsEnable: isEnable, FilePath: filePath}) +} + +// CheckpointDir sets the isEnable flag and checkpoint dir path for DownloadFile/UploadFile. +func CheckpointDir(isEnable bool, dirPath string) Option { + return addArg(checkpointConfig, &cpConfig{IsEnable: isEnable, DirPath: dirPath}) +} + +// Routines DownloadFile/UploadFile routine count +func Routines(n int) Option { + return addArg(routineNum, n) +} + +// InitCRC Init AppendObject CRC +func InitCRC(initCRC uint64) Option { + return addArg(initCRC64, initCRC) +} + +// Progress set progress listener +func Progress(listener ProgressListener) Option { + return addArg(progressListener, listener) +} + +// GetResponseHeader for get response http header +func GetResponseHeader(respHeader *http.Header) Option { + return addArg(responseHeader, respHeader) +} + +// ResponseContentType is an option to set response-content-type param +func ResponseContentType(value string) Option { + return addParam("response-content-type", value) +} + +// ResponseContentLanguage is an option to set response-content-language param +func ResponseContentLanguage(value string) Option { + return addParam("response-content-language", value) +} + +// ResponseExpires is an option to set response-expires param +func ResponseExpires(value string) Option { + return addParam("response-expires", value) +} + +// ResponseCacheControl is an option to set response-cache-control param +func ResponseCacheControl(value string) Option { + return addParam("response-cache-control", value) +} + +// ResponseContentDisposition is an option to set response-content-disposition param +func ResponseContentDisposition(value string) Option { + return addParam("response-content-disposition", value) +} + +// ResponseContentEncoding is an option to set response-content-encoding param +func ResponseContentEncoding(value string) Option { + return addParam("response-content-encoding", value) +} + +// Process is an option to set x-oss-process param +func Process(value string) Option { + return addParam("x-oss-process", value) +} + +// TrafficLimitParam is a option to set x-oss-traffic-limit +func TrafficLimitParam(value int64) Option { + return addParam("x-oss-traffic-limit", strconv.FormatInt(value, 10)) +} + +// SetHeader Allow users to set personalized http headers +func SetHeader(key string, value interface{}) Option { + return setHeader(key, value) +} + +// AddParam Allow users to set personalized http params +func AddParam(key string, value interface{}) Option { + return addParam(key, value) +} + +func setHeader(key string, value interface{}) Option { + return func(params map[string]optionValue) error { + if value == nil { + return nil + } + params[key] = optionValue{value, optionHTTP} + return nil + } +} + +func addParam(key string, value interface{}) Option { + return func(params map[string]optionValue) error { + if value == nil { + return nil + } + params[key] = optionValue{value, optionParam} + return nil + } +} + +func addArg(key string, value interface{}) Option { + return func(params map[string]optionValue) error { + if value == nil { + return nil + } + params[key] = optionValue{value, optionArg} + return nil + } +} + +func handleOptions(headers map[string]string, options []Option) error { + params := map[string]optionValue{} + for _, option := range options { + if option != nil { + if err := option(params); err != nil { + return err + } + } + } + + for k, v := range params { + if v.Type == optionHTTP { + headers[k] = v.Value.(string) + } + } + return nil +} + +func GetRawParams(options []Option) (map[string]interface{}, error) { + // Option + params := map[string]optionValue{} + for _, option := range options { + if option != nil { + if err := option(params); err != nil { + return nil, err + } + } + } + + paramsm := map[string]interface{}{} + // Serialize + for k, v := range params { + if v.Type == optionParam { + vs := params[k] + paramsm[k] = vs.Value.(string) + } + } + + return paramsm, nil +} + +func FindOption(options []Option, param string, defaultVal interface{}) (interface{}, error) { + params := map[string]optionValue{} + for _, option := range options { + if option != nil { + if err := option(params); err != nil { + return nil, err + } + } + } + + if val, ok := params[param]; ok { + return val.Value, nil + } + return defaultVal, nil +} + +func IsOptionSet(options []Option, option string) (bool, interface{}, error) { + params := map[string]optionValue{} + for _, option := range options { + if option != nil { + if err := option(params); err != nil { + return false, nil, err + } + } + } + + if val, ok := params[option]; ok { + return true, val.Value, nil + } + return false, nil, nil +} + +func DeleteOption(options []Option, strKey string) []Option { + var outOption []Option + params := map[string]optionValue{} + for _, option := range options { + if option != nil { + option(params) + _, exist := params[strKey] + if !exist { + outOption = append(outOption, option) + } else { + delete(params, strKey) + } + } + } + return outOption +} + +func GetRequestId(header http.Header) string { + return header.Get("x-oss-request-id") +} + +func GetVersionId(header http.Header) string { + return header.Get("x-oss-version-id") +} + +func GetCopySrcVersionId(header http.Header) string { + return header.Get("x-oss-copy-source-version-id") +} + +func GetDeleteMark(header http.Header) bool { + value := header.Get("x-oss-delete-marker") + if strings.ToUpper(value) == "TRUE" { + return true + } + return false +} + +func GetQosDelayTime(header http.Header) string { + return header.Get("x-oss-qos-delay-time") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/option_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/option_test.go new file mode 100644 index 00000000..f5c7e7d5 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/option_test.go @@ -0,0 +1,317 @@ +package oss + +import ( + "net/http" + + . "gopkg.in/check.v1" +) + +type OssOptionSuite struct{} + +var _ = Suite(&OssOptionSuite{}) + +type optionTestCase struct { + option Option + key string + value string +} + +var headerTestcases = []optionTestCase{ + { + option: Meta("User", "baymax"), + key: "X-Oss-Meta-User", + value: "baymax", + }, + { + option: ACL(ACLPrivate), + key: "X-Oss-Acl", + value: "private", + }, + { + option: ContentType("plain/text"), + key: "Content-Type", + value: "plain/text", + }, + { + option: CacheControl("no-cache"), + key: "Cache-Control", + value: "no-cache", + }, + { + option: ContentDisposition("Attachment; filename=example.txt"), + key: "Content-Disposition", + value: "Attachment; filename=example.txt", + }, + { + option: ContentEncoding("gzip"), + key: "Content-Encoding", + value: "gzip", + }, + { + option: Expires(pastDate), + key: "Expires", + value: pastDate.Format(http.TimeFormat), + }, + { + option: Range(0, 9), + key: "Range", + value: "bytes=0-9", + }, + { + option: Origin("localhost"), + key: "Origin", + value: "localhost", + }, + { + option: CopySourceRange(0, 9), + key: "X-Oss-Copy-Source-Range", + value: "bytes=0-8", + }, + { + option: IfModifiedSince(pastDate), + key: "If-Modified-Since", + value: pastDate.Format(http.TimeFormat), + }, + { + option: IfUnmodifiedSince(futureDate), + key: "If-Unmodified-Since", + value: futureDate.Format(http.TimeFormat), + }, + { + option: IfMatch("xyzzy"), + key: "If-Match", + value: "xyzzy", + }, + { + option: IfNoneMatch("xyzzy"), + key: "If-None-Match", + value: "xyzzy", + }, + { + option: CopySource("bucket_name", "object_name"), + key: "X-Oss-Copy-Source", + value: "/bucket_name/object_name", + }, + { + option: CopySourceIfModifiedSince(pastDate), + key: "X-Oss-Copy-Source-If-Modified-Since", + value: pastDate.Format(http.TimeFormat), + }, + { + option: CopySourceIfUnmodifiedSince(futureDate), + key: "X-Oss-Copy-Source-If-Unmodified-Since", + value: futureDate.Format(http.TimeFormat), + }, + { + option: CopySourceIfMatch("xyzzy"), + key: "X-Oss-Copy-Source-If-Match", + value: "xyzzy", + }, + { + option: CopySourceIfNoneMatch("xyzzy"), + key: "X-Oss-Copy-Source-If-None-Match", + value: "xyzzy", + }, + { + option: MetadataDirective(MetaCopy), + key: "X-Oss-Metadata-Directive", + value: "COPY", + }, + { + option: ServerSideEncryption("AES256"), + key: "X-Oss-Server-Side-Encryption", + value: "AES256", + }, + { + option: ObjectACL(ACLPrivate), + key: "X-Oss-Object-Acl", + value: "private", + }, + { + option: ObjectStorageClass(StorageStandard), + key: "X-Oss-Storage-Class", + value: "Standard", + }, + { + option: Callback("JTdCJTIyY2FsbGJhY2tVcmwlMjIlM0ElMjJleGFtcGxlLmNvbS9pbmRleC5odG1sJTIyJTdE"), + key: "X-Oss-Callback", + value: "JTdCJTIyY2FsbGJhY2tVcmwlMjIlM0ElMjJleGFtcGxlLmNvbS9pbmRleC5odG1sJTIyJTdE", + }, + { + option: CallbackVar("JTdCJTIyeCUzQXZhcjElMjIlM0ElMjJ2YWx1ZTElMjIlMkMlMjJ4JTNBdmFyMiUyMiUzQSUyMnZhbHVlMiUyMiU3RA=="), + key: "X-Oss-Callback-Var", + value: "JTdCJTIyeCUzQXZhcjElMjIlM0ElMjJ2YWx1ZTElMjIlMkMlMjJ4JTNBdmFyMiUyMiUzQSUyMnZhbHVlMiUyMiU3RA==", + }, + { + option: ContentLanguage("zh-CN"), + key: "Content-Language", + value: "zh-CN", + }, + { + option: ServerSideEncryptionKeyID("xossekid"), + key: "X-Oss-Server-Side-Encryption-Key-Id", + value: "xossekid", + }, +} + +func (s *OssOptionSuite) TestHeaderOptions(c *C) { + for _, testcase := range headerTestcases { + headers := make(map[string]optionValue) + err := testcase.option(headers) + c.Assert(err, IsNil) + + expected, actual := testcase.value, headers[testcase.key].Value + c.Assert(expected, Equals, actual) + } +} + +var paramTestCases = []optionTestCase{ + { + option: Delimiter("/"), + key: "delimiter", + value: "/", + }, + { + option: Marker("abc"), + key: "marker", + value: "abc", + }, + { + option: MaxKeys(150), + key: "max-keys", + value: "150", + }, + { + option: Prefix("fun"), + key: "prefix", + value: "fun", + }, + { + option: EncodingType("ascii"), + key: "encoding-type", + value: "ascii", + }, + { + option: MaxUploads(100), + key: "max-uploads", + value: "100", + }, + { + option: KeyMarker("abc"), + key: "key-marker", + value: "abc", + }, + { + option: UploadIDMarker("xyz"), + key: "upload-id-marker", + value: "xyz", + }, + { + option: MaxParts(1000), + key: "max-parts", + value: "1000", + }, + { + option: PartNumberMarker(1), + key: "part-number-marker", + value: "1", + }, + { + option: Process("image/format,png"), + key: "x-oss-process", + value: "image/format,png", + }, +} + +func (s *OssOptionSuite) TestParamOptions(c *C) { + for _, testcase := range paramTestCases { + params := make(map[string]optionValue) + err := testcase.option(params) + c.Assert(err, IsNil) + + expected, actual := testcase.value, params[testcase.key].Value + c.Assert(expected, Equals, actual) + } +} + +func (s *OssOptionSuite) TestHandleOptions(c *C) { + headers := make(map[string]string) + options := []Option{} + + for _, testcase := range headerTestcases { + options = append(options, testcase.option) + } + + err := handleOptions(headers, options) + c.Assert(err, IsNil) + + for _, testcase := range headerTestcases { + expected, actual := testcase.value, headers[testcase.key] + c.Assert(expected, Equals, actual) + } + + options = []Option{IfMatch(""), nil} + headers = map[string]string{} + err = handleOptions(headers, options) + c.Assert(err, IsNil) + c.Assert(len(headers), Equals, 1) +} + +func (s *OssOptionSuite) TestHandleParams(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + options := []Option{} + + for _, testcase := range paramTestCases { + options = append(options, testcase.option) + } + + params, err := GetRawParams(options) + c.Assert(err, IsNil) + + out := client.Conn.getURLParams(params) + c.Assert(len(out), Equals, 191) + + options = []Option{KeyMarker(""), nil} + + params, err = GetRawParams(options) + c.Assert(err, IsNil) + + out = client.Conn.getURLParams(params) + c.Assert(out, Equals, "key-marker=") +} + +func (s *OssOptionSuite) TestFindOption(c *C) { + options := []Option{} + + for _, testcase := range headerTestcases { + options = append(options, testcase.option) + } + + str, err := FindOption(options, "X-Oss-Acl", "") + c.Assert(err, IsNil) + c.Assert(str, Equals, "private") + + str, err = FindOption(options, "MyProp", "") + c.Assert(err, IsNil) + c.Assert(str, Equals, "") +} + +func (s *OssOptionSuite) TestDeleteOption(c *C) { + options := []Option{VersionId("123"), VersionIdMarker("456"), KeyMarker("789")} + str, err := FindOption(options, "versionId", "") + c.Assert(str, Equals, "123") + c.Assert(err, IsNil) + + skipOption := DeleteOption(options, "versionId") + str, err = FindOption(skipOption, "versionId", "") + c.Assert(str, Equals, "") + + str, err = FindOption(skipOption, "version-id-marker", "") + c.Assert(str, Equals, "456") + + str, err = FindOption(skipOption, "key-marker", "") + c.Assert(str, Equals, "789") + +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/progress.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/progress.go new file mode 100644 index 00000000..33cbebd5 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/progress.go @@ -0,0 +1,114 @@ +package oss + +import "io" + +// ProgressEventType defines transfer progress event type +type ProgressEventType int + +const ( + // TransferStartedEvent transfer started, set TotalBytes + TransferStartedEvent ProgressEventType = 1 + iota + // TransferDataEvent transfer data, set ConsumedBytes anmd TotalBytes + TransferDataEvent + // TransferCompletedEvent transfer completed + TransferCompletedEvent + // TransferFailedEvent transfer encounters an error + TransferFailedEvent +) + +// ProgressEvent defines progress event +type ProgressEvent struct { + ConsumedBytes int64 + TotalBytes int64 + RwBytes int64 + EventType ProgressEventType +} + +// ProgressListener listens progress change +type ProgressListener interface { + ProgressChanged(event *ProgressEvent) +} + +// -------------------- Private -------------------- + +func newProgressEvent(eventType ProgressEventType, consumed, total int64, rwBytes int64) *ProgressEvent { + return &ProgressEvent{ + ConsumedBytes: consumed, + TotalBytes: total, + RwBytes: rwBytes, + EventType: eventType} +} + +// publishProgress +func publishProgress(listener ProgressListener, event *ProgressEvent) { + if listener != nil && event != nil { + listener.ProgressChanged(event) + } +} + +type readerTracker struct { + completedBytes int64 +} + +type teeReader struct { + reader io.Reader + writer io.Writer + listener ProgressListener + consumedBytes int64 + totalBytes int64 + tracker *readerTracker +} + +// TeeReader returns a Reader that writes to w what it reads from r. +// All reads from r performed through it are matched with +// corresponding writes to w. There is no internal buffering - +// the write must complete before the read completes. +// Any error encountered while writing is reported as a read error. +func TeeReader(reader io.Reader, writer io.Writer, totalBytes int64, listener ProgressListener, tracker *readerTracker) io.ReadCloser { + return &teeReader{ + reader: reader, + writer: writer, + listener: listener, + consumedBytes: 0, + totalBytes: totalBytes, + tracker: tracker, + } +} + +func (t *teeReader) Read(p []byte) (n int, err error) { + n, err = t.reader.Read(p) + + // Read encountered error + if err != nil && err != io.EOF { + event := newProgressEvent(TransferFailedEvent, t.consumedBytes, t.totalBytes, 0) + publishProgress(t.listener, event) + } + + if n > 0 { + t.consumedBytes += int64(n) + // CRC + if t.writer != nil { + if n, err := t.writer.Write(p[:n]); err != nil { + return n, err + } + } + // Progress + if t.listener != nil { + event := newProgressEvent(TransferDataEvent, t.consumedBytes, t.totalBytes, int64(n)) + publishProgress(t.listener, event) + } + // Track + if t.tracker != nil { + t.tracker.completedBytes = t.consumedBytes + } + } + + return +} + +func (t *teeReader) Close() error { + if rc, ok := t.reader.(io.ReadCloser); ok { + return rc.Close() + } + return nil +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/progress_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/progress_test.go new file mode 100644 index 00000000..f92ebe18 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/progress_test.go @@ -0,0 +1,582 @@ +// bucket test + +package oss + +import ( + "bytes" + "io/ioutil" + "math/rand" + "os" + "strings" + "sync/atomic" + + . "gopkg.in/check.v1" +) + +type OssProgressSuite struct { + client *Client + bucket *Bucket +} + +var _ = Suite(&OssProgressSuite{}) + +// SetUpSuite runs once when the suite starts running +func (s *OssProgressSuite) SetUpSuite(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + s.client = client + + s.client.CreateBucket(bucketName) + + bucket, err := s.client.Bucket(bucketName) + c.Assert(err, IsNil) + s.bucket = bucket + + testLogger.Println("test progress started") +} + +// TearDownSuite runs before each test or benchmark starts running +func (s *OssProgressSuite) TearDownSuite(c *C) { + // Abort multipart uploads + keyMarker := KeyMarker("") + uploadIDMarker := UploadIDMarker("") + for { + lmu, err := s.bucket.ListMultipartUploads(keyMarker, uploadIDMarker) + c.Assert(err, IsNil) + for _, upload := range lmu.Uploads { + imur := InitiateMultipartUploadResult{Bucket: bucketName, Key: upload.Key, UploadID: upload.UploadID} + err = s.bucket.AbortMultipartUpload(imur) + c.Assert(err, IsNil) + } + keyMarker = KeyMarker(lmu.NextKeyMarker) + uploadIDMarker = UploadIDMarker(lmu.NextUploadIDMarker) + if !lmu.IsTruncated { + break + } + } + + // Delete objects + marker := Marker("") + for { + lor, err := s.bucket.ListObjects(marker) + c.Assert(err, IsNil) + for _, object := range lor.Objects { + err = s.bucket.DeleteObject(object.Key) + c.Assert(err, IsNil) + } + marker = Marker(lor.NextMarker) + if !lor.IsTruncated { + break + } + } + + // Delete bucket + err := s.client.DeleteBucket(s.bucket.BucketName) + c.Assert(err, IsNil) + + testLogger.Println("test progress completed") +} + +// SetUpTest runs after each test or benchmark runs +func (s *OssProgressSuite) SetUpTest(c *C) { + err := removeTempFiles("../oss", ".jpg") + c.Assert(err, IsNil) + + err = removeTempFiles("../oss", ".txt") + c.Assert(err, IsNil) + + err = removeTempFiles("../oss", ".html") + c.Assert(err, IsNil) +} + +// TearDownTest runs once after all tests or benchmarks have finished running +func (s *OssProgressSuite) TearDownTest(c *C) { + err := removeTempFiles("../oss", ".jpg") + c.Assert(err, IsNil) + + err = removeTempFiles("../oss", ".txt") + c.Assert(err, IsNil) + + err = removeTempFiles("../oss", ".html") + c.Assert(err, IsNil) +} + +// OssProgressListener is the progress listener +type OssProgressListener struct { + TotalRwBytes int64 +} + +// ProgressChanged handles progress event +func (listener *OssProgressListener) ProgressChanged(event *ProgressEvent) { + switch event.EventType { + case TransferStartedEvent: + testLogger.Printf("Transfer Started, ConsumedBytes: %d, TotalBytes %d.\n", + event.ConsumedBytes, event.TotalBytes) + case TransferDataEvent: + atomic.AddInt64(&listener.TotalRwBytes, event.RwBytes) + testLogger.Printf("Transfer Data, ConsumedBytes: %d, TotalBytes %d, %d%%.\n", + event.ConsumedBytes, event.TotalBytes, event.ConsumedBytes*100/event.TotalBytes) + case TransferCompletedEvent: + testLogger.Printf("Transfer Completed, ConsumedBytes: %d, TotalBytes %d.\n", + event.ConsumedBytes, event.TotalBytes) + case TransferFailedEvent: + testLogger.Printf("Transfer Failed, ConsumedBytes: %d, TotalBytes %d.\n", + event.ConsumedBytes, event.TotalBytes) + default: + } +} + +// TestPutObject +func (s *OssProgressSuite) TestPutObject(c *C) { + objectName := RandStr(8) + ".jpg" + localFile := "../sample/The Go Programming Language.html" + + fileInfo, err := os.Stat(localFile) + c.Assert(err, IsNil) + + // PutObject + fd, err := os.Open(localFile) + c.Assert(err, IsNil) + defer fd.Close() + + progressListener := OssProgressListener{} + err = s.bucket.PutObject(objectName, fd, Progress(&progressListener)) + c.Assert(err, IsNil) + c.Assert(progressListener.TotalRwBytes, Equals, fileInfo.Size()) + + // PutObjectFromFile + progressListener.TotalRwBytes = 0 + err = s.bucket.PutObjectFromFile(objectName, localFile, Progress(&progressListener)) + c.Assert(err, IsNil) + c.Assert(progressListener.TotalRwBytes, Equals, fileInfo.Size()) + + // DoPutObject + fd, err = os.Open(localFile) + c.Assert(err, IsNil) + defer fd.Close() + + request := &PutObjectRequest{ + ObjectKey: objectName, + Reader: fd, + } + + progressListener.TotalRwBytes = 0 + options := []Option{Progress(&progressListener)} + _, err = s.bucket.DoPutObject(request, options) + c.Assert(err, IsNil) + c.Assert(progressListener.TotalRwBytes, Equals, fileInfo.Size()) + + // PutObject size is 0 + progressListener.TotalRwBytes = 0 + err = s.bucket.PutObject(objectName, strings.NewReader(""), Progress(&progressListener)) + c.Assert(err, IsNil) + c.Assert(progressListener.TotalRwBytes, Equals, int64(0)) + + testLogger.Println("OssProgressSuite.TestPutObject") +} + +// TestSignURL +func (s *OssProgressSuite) SignURLTestFunc(c *C, authVersion AuthVersionType, extraHeaders []string) { + objectName := objectNamePrefix + RandStr(8) + filePath := RandLowStr(10) + content := RandStr(20) + CreateFile(filePath, content, c) + + oldType := s.bucket.Client.Config.AuthVersion + oldHeaders := s.bucket.Client.Config.AdditionalHeaders + s.bucket.Client.Config.AuthVersion = authVersion + s.bucket.Client.Config.AdditionalHeaders = extraHeaders + + // Sign URL for put + progressListener := OssProgressListener{} + str, err := s.bucket.SignURL(objectName, HTTPPut, 60, Progress(&progressListener)) + c.Assert(err, IsNil) + if s.bucket.Client.Config.AuthVersion == AuthV1 { + c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true) + } else { + c.Assert(strings.Contains(str, HTTPParamSignatureVersion+"=OSS2"), Equals, true) + c.Assert(strings.Contains(str, HTTPParamExpiresV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyIDV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignatureV2+"="), Equals, true) + } + + // Put object with URL + fd, err := os.Open(filePath) + c.Assert(err, IsNil) + defer fd.Close() + + err = s.bucket.PutObjectWithURL(str, fd, Progress(&progressListener)) + c.Assert(err, IsNil) + c.Assert(progressListener.TotalRwBytes, Equals, int64(len(content))) + + // Put object from file with URL + progressListener.TotalRwBytes = 0 + err = s.bucket.PutObjectFromFileWithURL(str, filePath, Progress(&progressListener)) + c.Assert(err, IsNil) + c.Assert(progressListener.TotalRwBytes, Equals, int64(len(content))) + + // DoPutObject + fd, err = os.Open(filePath) + c.Assert(err, IsNil) + defer fd.Close() + + progressListener.TotalRwBytes = 0 + options := []Option{Progress(&progressListener)} + _, err = s.bucket.DoPutObjectWithURL(str, fd, options) + c.Assert(err, IsNil) + c.Assert(progressListener.TotalRwBytes, Equals, int64(len(content))) + + // Sign URL for get + str, err = s.bucket.SignURL(objectName, HTTPGet, 60, Progress(&progressListener)) + c.Assert(err, IsNil) + if s.bucket.Client.Config.AuthVersion == AuthV1 { + c.Assert(strings.Contains(str, HTTPParamExpires+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyID+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignature+"="), Equals, true) + } else { + c.Assert(strings.Contains(str, HTTPParamSignatureVersion+"=OSS2"), Equals, true) + c.Assert(strings.Contains(str, HTTPParamExpiresV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamAccessKeyIDV2+"="), Equals, true) + c.Assert(strings.Contains(str, HTTPParamSignatureV2+"="), Equals, true) + } + + // Get object with URL + progressListener.TotalRwBytes = 0 + body, err := s.bucket.GetObjectWithURL(str, Progress(&progressListener)) + c.Assert(err, IsNil) + str, err = readBody(body) + c.Assert(err, IsNil) + c.Assert(str, Equals, content) + c.Assert(progressListener.TotalRwBytes, Equals, int64(len(content))) + + // Get object to file with URL + progressListener.TotalRwBytes = 0 + str, err = s.bucket.SignURL(objectName, HTTPGet, 10, Progress(&progressListener)) + c.Assert(err, IsNil) + + newFile := RandStr(10) + progressListener.TotalRwBytes = 0 + err = s.bucket.GetObjectToFileWithURL(str, newFile, Progress(&progressListener)) + c.Assert(progressListener.TotalRwBytes, Equals, int64(len(content))) + c.Assert(err, IsNil) + eq, err := compareFiles(filePath, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + os.Remove(filePath) + os.Remove(newFile) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + testLogger.Println("OssProgressSuite.TestSignURL") + + s.bucket.Client.Config.AuthVersion = oldType + s.bucket.Client.Config.AdditionalHeaders = oldHeaders +} + +func (s *OssProgressSuite) TestSignURL(c *C) { + s.SignURLTestFunc(c, AuthV1, []string{}) + s.SignURLTestFunc(c, AuthV2, []string{}) + s.SignURLTestFunc(c, AuthV2, []string{"host", "range", "user-agent"}) +} + +func (s *OssProgressSuite) TestPutObjectNegative(c *C) { + objectName := objectNamePrefix + RandStr(8) + localFile := "../sample/The Go Programming Language.html" + + // Invalid endpoint + client, err := New("http://oss-cn-taikang.aliyuncs.com", accessID, accessKey) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + c.Assert(err, IsNil) + + err = bucket.PutObjectFromFile(objectName, localFile, Progress(&OssProgressListener{})) + testLogger.Println(err) + c.Assert(err, NotNil) + + testLogger.Println("OssProgressSuite.TestPutObjectNegative") +} + +// TestAppendObject +func (s *OssProgressSuite) TestAppendObject(c *C) { + objectName := objectNamePrefix + RandStr(8) + objectValue := RandStr(100) + var val = []byte(objectValue) + var nextPos int64 + var midPos = 1 + rand.Intn(len(val)-1) + + // AppendObject + progressListener := OssProgressListener{} + nextPos, err := s.bucket.AppendObject(objectName, bytes.NewReader(val[0:midPos]), nextPos, Progress(&progressListener)) + c.Assert(err, IsNil) + c.Assert(progressListener.TotalRwBytes, Equals, nextPos) + + // DoAppendObject + request := &AppendObjectRequest{ + ObjectKey: objectName, + Reader: bytes.NewReader(val[midPos:]), + Position: nextPos, + } + options := []Option{Progress(&OssProgressListener{})} + _, err = s.bucket.DoAppendObject(request, options) + c.Assert(err, IsNil) + + testLogger.Println("OssProgressSuite.TestAppendObject") +} + +// TestMultipartUpload +func (s *OssProgressSuite) TestMultipartUpload(c *C) { + objectName := objectNamePrefix + RandStr(8) + var fileName = "../sample/BingWallpaper-2015-11-07.jpg" + + fileInfo, err := os.Stat(fileName) + c.Assert(err, IsNil) + + chunks, err := SplitFileByPartNum(fileName, 3) + c.Assert(err, IsNil) + testLogger.Println("chunks:", chunks) + + fd, err := os.Open(fileName) + c.Assert(err, IsNil) + defer fd.Close() + + // Initiate + progressListener := OssProgressListener{} + imur, err := s.bucket.InitiateMultipartUpload(objectName) + c.Assert(err, IsNil) + + // UploadPart + var parts []UploadPart + for _, chunk := range chunks { + fd.Seek(chunk.Offset, os.SEEK_SET) + part, err := s.bucket.UploadPart(imur, fd, chunk.Size, chunk.Number, Progress(&progressListener)) + c.Assert(err, IsNil) + parts = append(parts, part) + } + + // Complete + _, err = s.bucket.CompleteMultipartUpload(imur, parts) + c.Assert(err, IsNil) + c.Assert(progressListener.TotalRwBytes, Equals, fileInfo.Size()) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + testLogger.Println("OssProgressSuite.TestMultipartUpload") +} + +// TestMultipartUploadFromFile +func (s *OssProgressSuite) TestMultipartUploadFromFile(c *C) { + objectName := objectNamePrefix + RandStr(8) + var fileName = "../sample/BingWallpaper-2015-11-07.jpg" + fileInfo, err := os.Stat(fileName) + c.Assert(err, IsNil) + + chunks, err := SplitFileByPartNum(fileName, 3) + c.Assert(err, IsNil) + + // Initiate + imur, err := s.bucket.InitiateMultipartUpload(objectName) + c.Assert(err, IsNil) + + // UploadPart + progressListener := OssProgressListener{} + var parts []UploadPart + for _, chunk := range chunks { + part, err := s.bucket.UploadPartFromFile(imur, fileName, chunk.Offset, chunk.Size, chunk.Number, Progress(&progressListener)) + c.Assert(err, IsNil) + parts = append(parts, part) + } + + // Complete + _, err = s.bucket.CompleteMultipartUpload(imur, parts) + c.Assert(err, IsNil) + c.Assert(progressListener.TotalRwBytes, Equals, fileInfo.Size()) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + testLogger.Println("OssProgressSuite.TestMultipartUploadFromFile") +} + +// TestGetObject +func (s *OssProgressSuite) TestGetObject(c *C) { + objectName := objectNamePrefix + RandStr(8) + localFile := "../sample/BingWallpaper-2015-11-07.jpg" + newFile := "newpic-progress-1.jpg" + + fileInfo, err := os.Stat(localFile) + c.Assert(err, IsNil) + + progressListener := OssProgressListener{} + // PutObject + err = s.bucket.PutObjectFromFile(objectName, localFile, Progress(&progressListener)) + c.Assert(err, IsNil) + c.Assert(progressListener.TotalRwBytes, Equals, fileInfo.Size()) + + // GetObject + progressListener.TotalRwBytes = 0 + body, err := s.bucket.GetObject(objectName, Progress(&progressListener)) + c.Assert(err, IsNil) + _, err = ioutil.ReadAll(body) + c.Assert(err, IsNil) + body.Close() + c.Assert(progressListener.TotalRwBytes, Equals, fileInfo.Size()) + + // GetObjectToFile + progressListener.TotalRwBytes = 0 + err = s.bucket.GetObjectToFile(objectName, newFile, Progress(&progressListener)) + c.Assert(err, IsNil) + c.Assert(progressListener.TotalRwBytes, Equals, fileInfo.Size()) + + // DoGetObject + progressListener.TotalRwBytes = 0 + request := &GetObjectRequest{objectName} + options := []Option{Progress(&progressListener)} + result, err := s.bucket.DoGetObject(request, options) + c.Assert(err, IsNil) + _, err = ioutil.ReadAll(result.Response.Body) + c.Assert(err, IsNil) + result.Response.Body.Close() + c.Assert(progressListener.TotalRwBytes, Equals, fileInfo.Size()) + + // GetObject with range + progressListener.TotalRwBytes = 0 + body, err = s.bucket.GetObject(objectName, Range(1024, 4*1024), Progress(&progressListener)) + c.Assert(err, IsNil) + text, err := ioutil.ReadAll(body) + c.Assert(err, IsNil) + body.Close() + c.Assert(progressListener.TotalRwBytes, Equals, int64(len(text))) + + // PutObject size is 0 + progressListener.TotalRwBytes = 0 + err = s.bucket.PutObject(objectName, strings.NewReader(""), Progress(&progressListener)) + c.Assert(err, IsNil) + c.Assert(progressListener.TotalRwBytes, Equals, int64(0)) + + // GetObject size is 0 + progressListener.TotalRwBytes = 0 + body, err = s.bucket.GetObject(objectName, Progress(&progressListener)) + c.Assert(err, IsNil) + _, err = ioutil.ReadAll(body) + c.Assert(err, IsNil) + body.Close() + c.Assert(progressListener.TotalRwBytes, Equals, int64(0)) + + testLogger.Println("OssProgressSuite.TestGetObject") +} + +// TestGetObjectNegative +func (s *OssProgressSuite) TestGetObjectNegative(c *C) { + objectName := objectNamePrefix + RandStr(8) + localFile := "../sample/BingWallpaper-2015-11-07.jpg" + + // PutObject + err := s.bucket.PutObjectFromFile(objectName, localFile) + c.Assert(err, IsNil) + + // GetObject + body, err := s.bucket.GetObject(objectName, Progress(&OssProgressListener{})) + c.Assert(err, IsNil) + + buf := make([]byte, 4*1024) + n, err := body.Read(buf) + c.Assert(err, IsNil) + + //time.Sleep(70 * time.Second) TODO + + // Read should fail + for err == nil { + n, err = body.Read(buf) + n += n + } + c.Assert(err, NotNil) + body.Close() + + testLogger.Println("OssProgressSuite.TestGetObjectNegative") +} + +// TestUploadFile +func (s *OssProgressSuite) TestUploadFile(c *C) { + objectName := objectNamePrefix + RandStr(8) + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + + fileInfo, err := os.Stat(fileName) + c.Assert(err, IsNil) + + progressListener := OssProgressListener{} + err = s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(5), Progress(&progressListener)) + c.Assert(err, IsNil) + c.Assert(progressListener.TotalRwBytes, Equals, fileInfo.Size()) + + progressListener.TotalRwBytes = 0 + err = s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(3), Checkpoint(true, objectName+".cp"), Progress(&progressListener)) + c.Assert(err, IsNil) + c.Assert(progressListener.TotalRwBytes, Equals, fileInfo.Size()) + + testLogger.Println("OssProgressSuite.TestUploadFile") +} + +// TestDownloadFile +func (s *OssProgressSuite) TestDownloadFile(c *C) { + objectName := objectNamePrefix + RandStr(8) + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + newFile := "down-new-file-progress-2.jpg" + + fileInfo, err := os.Stat(fileName) + c.Assert(err, IsNil) + + // Upload + err = s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(3)) + c.Assert(err, IsNil) + + progressListener := OssProgressListener{} + err = s.bucket.DownloadFile(objectName, newFile, 100*1024, Routines(5), Progress(&progressListener)) + c.Assert(err, IsNil) + c.Assert(progressListener.TotalRwBytes, Equals, fileInfo.Size()) + + progressListener.TotalRwBytes = 0 + err = s.bucket.DownloadFile(objectName, newFile, 1024*1024, Routines(3), Progress(&progressListener)) + c.Assert(err, IsNil) + c.Assert(progressListener.TotalRwBytes, Equals, fileInfo.Size()) + + progressListener.TotalRwBytes = 0 + err = s.bucket.DownloadFile(objectName, newFile, 50*1024, Routines(3), Checkpoint(true, ""), Progress(&progressListener)) + c.Assert(err, IsNil) + c.Assert(progressListener.TotalRwBytes, Equals, fileInfo.Size()) + + testLogger.Println("OssProgressSuite.TestDownloadFile") +} + +// TestCopyFile +func (s *OssProgressSuite) TestCopyFile(c *C) { + srcObjectName := objectNamePrefix + RandStr(8) + destObjectName := srcObjectName + "-copy" + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + + fileInfo, err := os.Stat(fileName) + c.Assert(err, IsNil) + + // Upload + progressListener := OssProgressListener{} + err = s.bucket.UploadFile(srcObjectName, fileName, 100*1024, Routines(3), Progress(&progressListener)) + c.Assert(err, IsNil) + c.Assert(progressListener.TotalRwBytes, Equals, fileInfo.Size()) + + progressListener.TotalRwBytes = 0 + err = s.bucket.CopyFile(bucketName, srcObjectName, destObjectName, 100*1024, Routines(5), Progress(&progressListener)) + c.Assert(err, IsNil) + c.Assert(progressListener.TotalRwBytes, Equals, fileInfo.Size()) + + progressListener.TotalRwBytes = 0 + err = s.bucket.CopyFile(bucketName, srcObjectName, destObjectName, 1024*100, Routines(3), Checkpoint(true, ""), Progress(&progressListener)) + c.Assert(err, IsNil) + c.Assert(progressListener.TotalRwBytes, Equals, fileInfo.Size()) + + testLogger.Println("OssProgressSuite.TestCopyFile") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/redirect_1_6.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/redirect_1_6.go new file mode 100644 index 00000000..d09bc5eb --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/redirect_1_6.go @@ -0,0 +1,11 @@ +// +build !go1.7 + +package oss + +import "net/http" + +// http.ErrUseLastResponse only is defined go1.7 onward + +func disableHTTPRedirect(client *http.Client) { + +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/redirect_1_7.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/redirect_1_7.go new file mode 100644 index 00000000..5b0bb867 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/redirect_1_7.go @@ -0,0 +1,12 @@ +// +build go1.7 + +package oss + +import "net/http" + +// http.ErrUseLastResponse only is defined go1.7 onward +func disableHTTPRedirect(client *http.Client) { + client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/select_csv_objcet_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/select_csv_objcet_test.go new file mode 100644 index 00000000..d4068568 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/select_csv_objcet_test.go @@ -0,0 +1,627 @@ +package oss + +import ( + "io/ioutil" + "os" + "strconv" + "strings" + "io" + + . "gopkg.in/check.v1" +) + +type OssSelectCsvSuite struct { + client *Client + bucket *Bucket +} + +var _ = Suite(&OssSelectCsvSuite{}) + +func (s *OssSelectCsvSuite) SetUpSuite(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + s.client = client + s.client.Config.LogLevel = Error // Debug + // s.client.Config.Timeout = 5 + err = s.client.CreateBucket(bucketName) + c.Assert(err, IsNil) + bucket, err := s.client.Bucket(bucketName) + c.Assert(err, IsNil) + s.bucket = bucket + + testLogger.Println("test select csv started") +} + +func (s *OssSelectCsvSuite) TearDownSuite(c *C) { + // Delete objects + marker := Marker("") + for { + lor, err := s.bucket.ListObjects(marker) + c.Assert(err, IsNil) + for _, object := range lor.Objects { + err = s.bucket.DeleteObject(object.Key) + c.Assert(err, IsNil) + } + marker = Marker(lor.NextMarker) + if !lor.IsTruncated { + break + } + } + + err := s.client.DeleteBucket(bucketName) + c.Assert(err, IsNil) + + testLogger.Println("test select csv completed") +} + +func (s *OssSelectCsvSuite) SetUpTest(c *C) { + testLogger.Println("test func", c.TestName(), "start") +} + +func (s *OssSelectCsvSuite) TearDownTest(c *C) { + testLogger.Println("test func", c.TestName(), "succeed") +} + +// TestCreateSelectObjectMeta +func (s *OssSelectCsvSuite) TestCreateSelectCsvObjectMeta(c *C) { + key := "sample_data.csv" + localCsvFile := "../sample/sample_data.csv" + err := s.bucket.PutObjectFromFile(key, localCsvFile) + c.Assert(err, IsNil) + csvMeta := CsvMetaRequest{} + var bo bool + csvMeta.OverwriteIfExists = &bo + res, err := s.bucket.CreateSelectCsvObjectMeta(key, csvMeta) + c.Assert(err, IsNil) + l, err := readCsvLine(localCsvFile) + c.Assert(err, IsNil) + c.Assert(res.RowsCount, Equals, int64(l)) + + bo = true + csvMeta.OverwriteIfExists = &bo + csvMeta.InputSerialization.CSV.RecordDelimiter = "\n" + csvMeta.InputSerialization.CSV.FieldDelimiter = "," + csvMeta.InputSerialization.CSV.QuoteCharacter = "\"" + res, err = s.bucket.CreateSelectCsvObjectMeta(key, csvMeta) + c.Assert(err, IsNil) + c.Assert(res.RowsCount, Equals, int64(l)) + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectCsvSuite) TestSelectCsvObjectIsEmpty(c *C) { + key := "sample_data.csv" + localCsvFile := "../sample/sample_data.csv" + err := s.bucket.PutObjectFromFile(key, localCsvFile) + c.Assert(err, IsNil) + csvMeta := CsvMetaRequest{} + _, err = s.bucket.CreateSelectCsvObjectMeta(key, csvMeta) + c.Assert(err, IsNil) + selReq := SelectRequest{} + selReq.Expression = "select Year, StateAbbr, CityName, PopulationCount from ossobject where CityName != ''" + selReq.InputSerializationSelect.CsvBodyInput.FileHeaderInfo = "Use" + + body, err := s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer body.Close() + + p := make([]byte, 512) + n, err := body.Read(p) + c.Assert(err, IsNil) + c.Assert(n, Equals, 512) + p1 := make([]byte, 3) + _, err = body.Read(p1) + c.Assert(err, IsNil) + rets, err := ioutil.ReadAll(body) + c.Assert(err, IsNil) + str, err := readCsvIsEmpty(localCsvFile) + c.Assert(err, IsNil) + c.Assert(string(p)+string(p1)+string(rets), Equals, str) + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectCsvSuite) TestSelectObjectIntoFile(c *C) { + var bo bool = true + key := "sample_data.csv" + localCsvFile := "../sample/sample_data.csv" + err := s.bucket.PutObjectFromFile(key, localCsvFile) + c.Assert(err, IsNil) + csvMeta := CsvMetaRequest{ + InputSerialization: InputSerialization { + CSV: CSV { + RecordDelimiter: "\n", + FieldDelimiter: ",", + QuoteCharacter: "\"", + }, + }, + OverwriteIfExists: &bo, + } + res, err := s.bucket.CreateSelectCsvObjectMeta(key, csvMeta) + c.Assert(err, IsNil) + l, err := readCsvLine(localCsvFile) + c.Assert(err, IsNil) + c.Assert(res.RowsCount, Equals, int64(l)) + + selReq := SelectRequest{ + Expression:"select * from ossobject", + InputSerializationSelect: InputSerializationSelect { + CsvBodyInput :CSVSelectInput{ + FileHeaderInfo: "None", + CommentCharacter: "#", + RecordDelimiter: "\n", + FieldDelimiter: ",", + QuoteCharacter:"\"", + Range:"", + }, + }, + } + outfile := "sample_data_out.csv" + err = s.bucket.SelectObjectIntoFile(key, outfile, selReq) + c.Assert(err, IsNil) + + fd1, err := os.Open(outfile) + c.Assert(err,IsNil) + defer fd1.Close() + fd2, err := os.Open(localCsvFile) + c.Assert(err,IsNil) + defer fd2.Close() + str1, err := ioutil.ReadAll(fd1) + c.Assert(err,IsNil) + str2 ,err := ioutil.ReadAll(fd2) + c.Assert(err,IsNil) + c.Assert(string(str1), Equals, string(str2)) + + err = os.Remove(outfile) + c.Assert(err, IsNil) + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func(s *OssSelectCsvSuite) TestSelectCsvObjectRange(c *C) { + key := "sample_data.csv" + localCsvFile := "../sample/sample_data.csv" + err := s.bucket.PutObjectFromFile(key, localCsvFile) + c.Assert(err, IsNil) + csvMeta := CsvMetaRequest{} + _,err = s.bucket.CreateSelectCsvObjectMeta(key, csvMeta) + c.Assert(err, IsNil) + selReq := SelectRequest{} + selReq.Expression = "select Year,StateAbbr, CityName, Short_Question_Text from ossobject" + selReq.InputSerializationSelect.CsvBodyInput.FileHeaderInfo = "Use" + selReq.InputSerializationSelect.CsvBodyInput.Range = "0-2" + body, err := s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer body.Close() + rets, err := ioutil.ReadAll(body) + + str,err := readCsvRange(localCsvFile, 0, 2) + c.Assert(err, IsNil) + c.Assert(string(rets), Equals, str) + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func(s *OssSelectCsvSuite) TestSelectCsvObjectLike(c *C) { + key := "sample_data.csv" + localCsvFile := "../sample/sample_data.csv" + err := s.bucket.PutObjectFromFile(key, localCsvFile) + c.Assert(err, IsNil) + selReq := SelectRequest{} + selReq.Expression = "select Year, StateAbbr, CityName, Short_Question_Text from ossobject where Measure like '%blood pressure%Years'" + selReq.InputSerializationSelect.CsvBodyInput.FileHeaderInfo = "Use" + ret,err := s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer ret.Close() + ts, err := ioutil.ReadAll(ret) + c.Assert(err, IsNil) + str, err := readCsvLike(localCsvFile) + c.Assert(err, IsNil) + c.Assert(string(ts), Equals, str) + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func(s *OssSelectCsvSuite) TestSelectCsvObjectIntAggregation(c *C) { + key := "sample_data.csv" + localCsvFile := "../sample/sample_data.csv" + err := s.bucket.PutObjectFromFile(key, localCsvFile) + c.Assert(err, IsNil) + selReq := SelectRequest{} + selReq.Expression = `select avg(cast(year as int)), max(cast(year as int)), min(cast(year as int)) from ossobject where year = 2015` + selReq.InputSerializationSelect.CsvBodyInput.FileHeaderInfo = "Use" + ret,err := s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer ret.Close() + ts, err := ioutil.ReadAll(ret) + c.Assert(err, IsNil) + + c.Assert(string(ts), Equals, "2015,2015,2015\n") + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func(s *OssSelectCsvSuite) TestSelectCsvObjectFloatAggregation(c *C) { + key := "sample_data.csv" + localCsvFile := "../sample/sample_data.csv" + err := s.bucket.PutObjectFromFile(key, localCsvFile) + c.Assert(err, IsNil) + selReq := SelectRequest{} + selReq.Expression = `select avg(cast(data_value as double)), max(cast(data_value as double)), sum(cast(data_value as double)) from ossobject` + selReq.InputSerializationSelect.CsvBodyInput.FileHeaderInfo = "Use" + ret,err := s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer ret.Close() + ts, err := ioutil.ReadAll(ret) + strR := string(ts) + c.Assert(err, IsNil) + + avg, max, sum , err := readCsvFloatAgg(localCsvFile) + c.Assert(err, IsNil) + + s1 := strconv.FormatFloat(avg, 'f', 5, 32) + "," + s1 += strconv.FormatFloat(max, 'f', 5, 32) + "," + s1 += strconv.FormatFloat(sum, 'f', 5, 32) + "," + retS := "" + for _, v := range strings.Split(strR[:len(strR)-1], ",") { + vv, err := strconv.ParseFloat(v, 64) + c.Assert(err, IsNil) + retS += strconv.FormatFloat(vv, 'f', 5, 32) + "," + } + c.Assert(s1, Equals, retS) + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func(s *OssSelectCsvSuite) TestSelectCsvObjectConcat(c *C) { + key := "sample_data.csv" + localCsvFile := "../sample/sample_data.csv" + err := s.bucket.PutObjectFromFile(key, localCsvFile) + c.Assert(err, IsNil) + selReq := SelectRequest{} + selReq.Expression = `select Year,StateAbbr, CityName, Short_Question_Text from ossobject where (data_value || data_value_unit) = '14.8%'` + selReq.InputSerializationSelect.CsvBodyInput.FileHeaderInfo = "Use" + ret,err := s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer ret.Close() + ts, err := ioutil.ReadAll(ret) + c.Assert(err, IsNil) + + str, err := readCsvConcat(localCsvFile) + c.Assert(err, IsNil) + c.Assert(string(ts), Equals, str) + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectCsvSuite) TestSelectCsvObjectComplicateConcat(c *C) { + key := "sample_data.csv" + localCsvFile := "../sample/sample_data.csv" + err := s.bucket.PutObjectFromFile(key, localCsvFile) + c.Assert(err, IsNil) + selReq := SelectRequest{} + selReq.Expression = ` + select + Year,StateAbbr, CityName, Short_Question_Text, data_value, + data_value_unit, category, high_confidence_limit + from + ossobject + where + data_value > 14.8 and + data_value_unit = '%' or + Measure like '%18 Years' and + Category = 'Unhealthy Behaviors' or + high_confidence_limit > 70.0 ` + + selReq.InputSerializationSelect.CsvBodyInput.FileHeaderInfo = "Use" + ret,err := s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer ret.Close() + ts, err := ioutil.ReadAll(ret) + c.Assert(err, IsNil) + + str, err := readCsvComplicateCondition(localCsvFile) + c.Assert(err, IsNil) + c.Assert(string(ts), Equals, str) + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectCsvSuite) TestSelectCsvObjectInvalidSql(c *C) { + key := "sample_data.csv" + localCsvFile := "../sample/sample_data.csv" + err := s.bucket.PutObjectFromFile(key, localCsvFile) + c.Assert(err, IsNil) + selReq := SelectRequest{} + selReq.Expression = `select * from ossobject where avg(cast(year as int)) > 2016` + selReq.InputSerializationSelect.CsvBodyInput.FileHeaderInfo = "Use" + _, err = s.bucket.SelectObject(key, selReq) + c.Assert(err, NotNil) + + selReq.Expression = `` + _, err = s.bucket.SelectObject(key, selReq) + c.Assert(err, NotNil) + + selReq.Expression = `select year || CityName from ossobject` + _, err = s.bucket.SelectObject(key, selReq) + c.Assert(err, NotNil) + + selReq.Expression = `select * from ossobject group by CityName` + _, err = s.bucket.SelectObject(key, selReq) + c.Assert(err, NotNil) + + selReq.Expression = `select * from ossobject order by _1` + _, err = s.bucket.SelectObject(key, selReq) + c.Assert(err, NotNil) + + selReq.Expression = `select * from ossobject oss join s3object s3 on oss.CityName = s3.CityName` + _, err = s.bucket.SelectObject(key, selReq) + c.Assert(err, NotNil) + + selReq.Expression = `select _1 from ossobject` + ret, err := s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer ret.Close() + _, err = ioutil.ReadAll(ret) + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectCsvSuite) TestSelectCsvObjectWithOutputDelimiters(c *C) { + key := "sample_data.csv" + content := "abc,def\n" + err := s.bucket.PutObject(key, strings.NewReader(content)) + c.Assert(err, IsNil) + selReq := SelectRequest{} + selReq.Expression = `select _1, _2 from ossobject ` + selReq.OutputSerializationSelect.CsvBodyOutput.RecordDelimiter = "\r\n" + selReq.OutputSerializationSelect.CsvBodyOutput.FieldDelimiter = "|" + + ret,err := s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer ret.Close() + ts, err := ioutil.ReadAll(ret) + c.Assert(err, IsNil) + c.Assert(string(ts), Equals, "abc|def\r\n") + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectCsvSuite) TestSelectCsvObjectWithCrc(c *C) { + key := "sample_data.csv" + content := "abc,def\n" + err := s.bucket.PutObject(key, strings.NewReader(content)) + c.Assert(err, IsNil) + selReq := SelectRequest{} + selReq.Expression = `select * from ossobject` + bo := true + selReq.OutputSerializationSelect.EnablePayloadCrc = &bo + + ret,err := s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer ret.Close() + ts, err := ioutil.ReadAll(ret) + c.Assert(err, IsNil) + c.Assert(string(ts), Equals, content) + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectCsvSuite) TestSelectCsvObjectWithSkipPartialData(c *C) { + key := "sample_data.csv" + content := "abc,def\nefg\n" + err := s.bucket.PutObject(key, strings.NewReader(content)) + c.Assert(err, IsNil) + selReq := SelectRequest{} + selReq.Expression = `select _1, _2 from ossobject` + bo := true + selReq.SelectOptions.SkipPartialDataRecord = &bo + ret,err := s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer ret.Close() + ts, err := ioutil.ReadAll(ret) + c.Assert(err, IsNil) + c.Assert(string(ts), Equals, "abc,def\n") + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectCsvSuite) TestSelectCsvObjectWithOutputRaw(c *C) { + key := "sample_data.csv" + content := "abc,def\n" + err := s.bucket.PutObject(key, strings.NewReader(content)) + c.Assert(err, IsNil) + selReq := SelectRequest{} + selReq.Expression = `select _1 from ossobject` + bo := true + selReq.OutputSerializationSelect.OutputRawData = &bo + + ret,err := s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer ret.Close() + ts, err := ioutil.ReadAll(ret) + c.Assert(err, IsNil) + c.Assert(string(ts), Equals, "abc\n") + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectCsvSuite) TestSelectCsvObjectWithKeepColumns(c *C) { + key := "sample_data.csv" + content := "abc,def\n" + err := s.bucket.PutObject(key, strings.NewReader(content)) + c.Assert(err, IsNil) + selReq := SelectRequest{} + selReq.Expression = `select _1 from ossobject` + bo := true + selReq.OutputSerializationSelect.KeepAllColumns = &bo + + ret,err := s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer ret.Close() + ts, err := ioutil.ReadAll(ret) + c.Assert(err, IsNil) + c.Assert(string(ts), Equals, "abc,\n") + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectCsvSuite) TestSelectCsvObjectWithOutputHeader(c *C) { + key := "sample_data.csv" + content := "name,job\nabc,def\n" + err := s.bucket.PutObject(key, strings.NewReader(content)) + c.Assert(err, IsNil) + selReq := SelectRequest{} + selReq.Expression = `select name from ossobject` + bo := true + selReq.OutputSerializationSelect.OutputHeader = &bo + selReq.InputSerializationSelect.CsvBodyInput.FileHeaderInfo = "Use" + + ret,err := s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer ret.Close() + ts, err := ioutil.ReadAll(ret) + c.Assert(err, IsNil) + c.Assert(string(ts), Equals, "name\nabc\n") + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectCsvSuite) TestSelectCsvObjectRead(c *C) { + key := "sample_data.csv" + content := "name,job\nabc,def\n" + err := s.bucket.PutObject(key, strings.NewReader(content)) + c.Assert(err, IsNil) + selReq := SelectRequest{} + selReq.Expression = `select name from ossobject` + bo := true + selReq.OutputSerializationSelect.OutputHeader = &bo + selReq.InputSerializationSelect.CsvBodyInput.FileHeaderInfo = "Use" + selReq.OutputSerializationSelect.EnablePayloadCrc = &bo + + ret,err := s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer ret.Close() + + // case 1: read length > data length + p := make([]byte, 512) + n, err := ret.Read(p[:20]) + if err != nil && err != io.EOF { + c.Assert(err, IsNil) + } + c.Assert(string(p[:n]), Equals, "name\nabc\n") + ts, err := ioutil.ReadAll(ret) + c.Assert(err, IsNil) + c.Assert(string(ts), Equals, "") + + // case 2: read length = data length + ret,err = s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer ret.Close() + n, err = ret.Read(p[:9]) + if err != nil && err != io.EOF { + c.Assert(err, IsNil) + } + c.Assert(string(p[:n]), Equals, "name\nabc\n") + ts, err = ioutil.ReadAll(ret) + c.Assert(err, IsNil) + c.Assert(string(ts), Equals, "") + + // case 3: read length > one frame length and read length < two frame, (this data = 2 * frame length) + ret,err = s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer ret.Close() + n, err = ret.Read(p[:7]) + if err != nil && err != io.EOF { + c.Assert(err, IsNil) + } + c.Assert(string(p[:n]), Equals, "name\nab") + ts, err = ioutil.ReadAll(ret) + c.Assert(err, IsNil) + c.Assert(string(ts), Equals, "c\n") + + // case 4: read length = a frame length (this data = 2 * frame length) + ret,err = s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer ret.Close() + n, err = ret.Read(p[:5]) + if err != nil && err != io.EOF { + c.Assert(err, IsNil) + } + c.Assert(string(p[:n]), Equals, "name\n") + ts, err = ioutil.ReadAll(ret) + c.Assert(err, IsNil) + c.Assert(string(ts), Equals, "abc\n") + + // case 5: read length < a frame length (this data = 2 * frame length) + ret,err = s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer ret.Close() + n, err = ret.Read(p[:3]) + if err != nil && err != io.EOF { + c.Assert(err, IsNil) + } + c.Assert(string(p[:n]), Equals, "nam") + ts, err = ioutil.ReadAll(ret) + c.Assert(err, IsNil) + c.Assert(string(ts), Equals, "e\nabc\n") + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +// OssProgressListener is the progress listener +type OssSelectProgressListener struct { +} +// ProgressChanged handles progress event +func (listener *OssSelectProgressListener) ProgressChanged(event *ProgressEvent) { + switch event.EventType { + case TransferStartedEvent: + testLogger.Printf("Transfer Started.\n") + case TransferDataEvent: + testLogger.Printf("Transfer Data, This time consumedBytes: %d \n", event.ConsumedBytes) + case TransferCompletedEvent: + testLogger.Printf("Transfer Completed, This time consumedBytes: %d.\n", event.ConsumedBytes) + case TransferFailedEvent: + testLogger.Printf("Transfer Failed, This time consumedBytes: %d.\n", event.ConsumedBytes) + default: + } +} + +func(s *OssSelectCsvSuite) TestSelectCsvObjectConcatProgress(c *C) { + key := "sample_data.csv" + localCsvFile := "../sample/sample_data.csv" + err := s.bucket.PutObjectFromFile(key, localCsvFile) + c.Assert(err, IsNil) + selReq := SelectRequest{} + selReq.Expression = `select Year,StateAbbr, CityName, Short_Question_Text from ossobject where (data_value || data_value_unit) = '14.8%'` + selReq.InputSerializationSelect.CsvBodyInput.FileHeaderInfo = "Use" + ret,err := s.bucket.SelectObject(key, selReq, Progress(&OssSelectProgressListener{})) + c.Assert(err, IsNil) + defer ret.Close() + ts, err := ioutil.ReadAll(ret) + c.Assert(err, IsNil) + + str, err := readCsvConcat(localCsvFile) + c.Assert(err, IsNil) + c.Assert(string(ts), Equals, str) + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/select_json_object_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/select_json_object_test.go new file mode 100644 index 00000000..e4af98cf --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/select_json_object_test.go @@ -0,0 +1,488 @@ +package oss + +import ( + . "gopkg.in/check.v1" + "os" + "io/ioutil" + "strings" + "net/http" +) + +type OssSelectJsonSuite struct { + client *Client + bucket *Bucket +} + +var _ = Suite(&OssSelectJsonSuite{}) + +func (s *OssSelectJsonSuite) SetUpSuite(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + s.client = client + s.client.Config.LogLevel = Error // Debug + err = s.client.CreateBucket(bucketName) + c.Assert(err, IsNil) + bucket, err := s.client.Bucket(bucketName) + c.Assert(err, IsNil) + s.bucket = bucket + + testLogger.Println("test select json started") +} + +func(s *OssSelectJsonSuite) TearDownSuite(c *C){ + // Delete objects + marker := Marker("") + for { + lor, err := s.bucket.ListObjects(marker) + c.Assert(err, IsNil) + for _, object := range lor.Objects { + err = s.bucket.DeleteObject(object.Key) + c.Assert(err, IsNil) + } + marker = Marker(lor.NextMarker) + if !lor.IsTruncated { + break + } + } + + err := s.client.DeleteBucket(bucketName) + c.Assert(err, IsNil) + + testLogger.Println("test select json completed") +} + +func (s *OssSelectJsonSuite) SetUpTest(c *C) { + testLogger.Println("test func", c.TestName(), "start") +} + +func (s *OssSelectJsonSuite) TearDownTest(c *C) { + testLogger.Println("test func", c.TestName(), "succeed") +} + +func (s *OssSelectJsonSuite) TestCreateSelectJsonObjectMeta(c *C){ + key := "sample_json_lines.json" + err := s.bucket.PutObjectFromFile(key, "../sample/sample_json_lines.json") + c.Assert(err, IsNil) + jsonMeta := JsonMetaRequest{ + InputSerialization: InputSerialization { + JSON: JSON { + JSONType:"LINES", + }, + }, + } + res,err := s.bucket.CreateSelectJsonObjectMeta(key, jsonMeta) + c.Assert(err, IsNil) + c.Assert(res.RowsCount, Equals, int64(100)) + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectJsonSuite) TestSelectJsonDocument(c *C){ + key := "sample_json.json" + err := s.bucket.PutObjectFromFile(key, "../sample/sample_json.json") + c.Assert(err, IsNil) + selReq := SelectRequest{} + selReq.Expression = "select * from ossobject.objects[*] where party = 'Democrat'" + selReq.InputSerializationSelect.JsonBodyInput.JSONType = "DOCUMENT" + selReq.OutputSerializationSelect.JsonBodyOutput.RecordDelimiter = "," + + var responseHeader http.Header + body, err := s.bucket.SelectObject(key, selReq, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + defer body.Close() + requestId := GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + p := make([]byte, 512) + n, err := body.Read(p) + c.Assert(err, IsNil) + c.Assert(n, Equals, 512) + p1 := make([]byte, 3) + _, err = body.Read(p1) + c.Assert(err, IsNil) + rets, err := ioutil.ReadAll(body) + c.Assert(err,IsNil) + str,err := readJsonDocument("../sample/sample_json.json") + c.Assert(err, IsNil) + c.Assert(string(p) + string(p1) + string(rets), Equals, str) + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectJsonSuite) TestSelectJsonLines(c *C) { + key := "sample_json_lines.json" + err := s.bucket.PutObjectFromFile(key, "../sample/sample_json_lines.json") + c.Assert(err, IsNil) + selReq := SelectRequest{} + selReq.Expression = "select * from ossobject where party = 'Democrat'" + selReq.OutputSerializationSelect.JsonBodyOutput.RecordDelimiter = "," + selReq.InputSerializationSelect.JsonBodyInput.JSONType = "LINES" + + var responseHeader http.Header + body, err := s.bucket.SelectObject(key, selReq, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + defer body.Close() + + requestId := GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + rets, err := ioutil.ReadAll(body) + c.Assert(err,IsNil) + str,err := readJsonDocument("../sample/sample_json.json") + c.Assert(string(rets), Equals, str) + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectJsonSuite) TestSelectJsonLinesIntoFile(c *C) { + key := "sample_json_lines.json" + err := s.bucket.PutObjectFromFile(key, "../sample/sample_json_lines.json") + c.Assert(err, IsNil) + + jsonMeta := JsonMetaRequest{ + InputSerialization: InputSerialization { + JSON: JSON { + JSONType:"LINES", + }, + }, + } + res,err := s.bucket.CreateSelectJsonObjectMeta(key, jsonMeta) + c.Assert(err, IsNil) + c.Assert(res.RowsCount, Equals, int64(100)) + + selReq := SelectRequest{} + selReq.Expression = "select * from ossobject where party = 'Democrat'" + selReq.OutputSerializationSelect.JsonBodyOutput.RecordDelimiter = "," + selReq.InputSerializationSelect.JsonBodyInput.JSONType = "LINES" + + var responseHeader http.Header + outfile := "sample_json_out.json" + err = s.bucket.SelectObjectIntoFile(key, outfile, selReq, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + requestId := GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + _, err = os.Stat(outfile) + c.Assert(err,IsNil) + err = os.Remove(outfile) + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectJsonSuite) TestSelectJsonDocumentIntoFile(c *C) { + key := "sample_json_lines.json" + err := s.bucket.PutObjectFromFile(key, "../sample/sample_json.json") + c.Assert(err, IsNil) + + selReq := SelectRequest{} + selReq.Expression = "select * from ossobject.objects[*] where party = 'Democrat'" + selReq.OutputSerializationSelect.JsonBodyOutput.RecordDelimiter = "," + selReq.InputSerializationSelect.JsonBodyInput.JSONType = "DOCUMENT" + + var responseHeader http.Header + outfile := "sample_json_out.json" + err = s.bucket.SelectObjectIntoFile(key, outfile, selReq, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + requestId := GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + _, err = os.Stat(outfile) + c.Assert(err,IsNil) + err = os.Remove(outfile) + c.Assert(err, IsNil) + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectJsonSuite) TestSelectJsonLinesLike(c *C) { + key := "sample_json_lines.json" + err := s.bucket.PutObjectFromFile(key, "../sample/sample_json_lines.json") + c.Assert(err, IsNil) + selReq := SelectRequest{} + selReq.Expression = "select person.firstname, person.lastname from ossobject where person.birthday like '1959%'" + selReq.OutputSerializationSelect.JsonBodyOutput.RecordDelimiter = "," + selReq.InputSerializationSelect.JsonBodyInput.JSONType = "LINES" + + jsonMeta := JsonMetaRequest{ + InputSerialization: InputSerialization { + JSON: JSON { + JSONType:"LINES", + }, + }, + } + res,err := s.bucket.CreateSelectJsonObjectMeta(key, jsonMeta) + c.Assert(err, IsNil) + c.Assert(res.RowsCount, Equals, int64(100)) + + var responseHeader http.Header + body, err := s.bucket.SelectObject(key, selReq, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + defer body.Close() + + requestId := GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + rets, err := ioutil.ReadAll(body) + c.Assert(err,IsNil) + str,err := readJsonLinesLike("../sample/sample_json.json") + c.Assert(string(rets), Equals, str) + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectJsonSuite) TestSelectJsonLinesRange(c *C) { + key := "sample_json_lines.json" + err := s.bucket.PutObjectFromFile(key, "../sample/sample_json_lines.json") + c.Assert(err, IsNil) + jsonMeta := JsonMetaRequest{ + InputSerialization: InputSerialization { + JSON: JSON { + JSONType:"LINES", + }, + }, + } + res,err := s.bucket.CreateSelectJsonObjectMeta(key, jsonMeta) + c.Assert(err, IsNil) + c.Assert(res.RowsCount, Equals, int64(100)) + + selReq := SelectRequest{} + selReq.Expression = "select person.firstname as aaa as firstname, person.lastname, extra from ossobject'" + selReq.OutputSerializationSelect.JsonBodyOutput.RecordDelimiter = "," + selReq.InputSerializationSelect.JsonBodyInput.JSONType = "LINES" + selReq.InputSerializationSelect.JsonBodyInput.Range = "0-1" + + var responseHeader http.Header + body, err := s.bucket.SelectObject(key, selReq, GetResponseHeader(&responseHeader)) + c.Assert(err, IsNil) + defer body.Close() + + requestId := GetRequestId(responseHeader) + c.Assert(len(requestId) > 0, Equals, true) + + rets, err := ioutil.ReadAll(body) + c.Assert(err,IsNil) + str,err := readJsonLinesRange("../sample/sample_json.json", 0, 2) + c.Assert(string(rets), Equals, str) + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectJsonSuite) TestSelectJsonDocumentIntAggregation(c *C) { + key := "sample_json.json" + err := s.bucket.PutObjectFromFile(key, "../sample/sample_json.json") + c.Assert(err, IsNil) + + selReq := SelectRequest{} + selReq.Expression = ` + select + avg(cast(person.cspanid as int)), max(cast(person.cspanid as int)), + min(cast(person.cspanid as int)) + from + ossobject.objects[*] + where + person.cspanid = 1011723 + ` + selReq.OutputSerializationSelect.JsonBodyOutput.RecordDelimiter = "," + selReq.InputSerializationSelect.JsonBodyInput.JSONType = "Document" + + body, err := s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer body.Close() + + rets, err := ioutil.ReadAll(body) + c.Assert(err,IsNil) + c.Assert(string(rets), Equals, "{\"_1\":1011723,\"_2\":1011723,\"_3\":1011723},") + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectJsonSuite) TestSelectJsonDocumentFloatAggregation(c *C) { + key := "sample_json.json" + err := s.bucket.PutObjectFromFile(key, "../sample/sample_json.json") + c.Assert(err, IsNil) + + selReq := SelectRequest{} + selReq.Expression = ` + select + avg(cast(person.cspanid as double)), max(cast(person.cspanid as double)), + min(cast(person.cspanid as double)) + from + ossobject.objects[*] + ` + selReq.OutputSerializationSelect.JsonBodyOutput.RecordDelimiter = "," + selReq.InputSerializationSelect.JsonBodyInput.JSONType = "Document" + + body, err := s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer body.Close() + + rets, err := ioutil.ReadAll(body) + c.Assert(err,IsNil) + testLogger.Println(string(rets)) + // avg, max, min, err := readJsonFloatAggregation("../sample/sample_json.json") + // fmt.Println(string(rets), "\n", avg, max, min) + // retsArr := strings.Split(string(rets), ":") + // s1 := strconv.FormatFloat(avg, 'f', 6, 64) + "," + // s1 += strconv.FormatFloat(max, 'f', 6, 64) + "," + // s1 += strconv.FormatFloat(min, 'f', 6, 64) + "," + // retS := "" + // l := len(retsArr[1]) + // vv, err := strconv.ParseFloat(retsArr[1][:l-35], 64) + // c.Assert(err, IsNil) + // retS += strconv.FormatFloat(vv, 'f', 6, 64) + "," + // l = len(retsArr[2]) + // vv, err = strconv.ParseFloat(retsArr[2][:l-6], 64) + // c.Assert(err, IsNil) + // retS += strconv.FormatFloat(vv, 'f', 6, 64) + "," + // l = len(retsArr[3]) + // vv, err = strconv.ParseFloat(retsArr[3][:l-2], 64) + // c.Assert(err, IsNil) + // retS += strconv.FormatFloat(vv, 'f', 6, 64) + "," + // c.Assert(retS, Equals, s1) + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectJsonSuite) TestSelectJsonDocumentConcat(c *C) { + key := "sample_json.json" + err := s.bucket.PutObjectFromFile(key, "../sample/sample_json.json") + c.Assert(err, IsNil) + + selReq := SelectRequest{} + selReq.Expression = ` + select + person + from + ossobject.objects[*] + where + (person.firstname || person.lastname) = 'JohnKennedy' + ` + selReq.OutputSerializationSelect.JsonBodyOutput.RecordDelimiter = "," + selReq.InputSerializationSelect.JsonBodyInput.JSONType = "Document" + + body, err := s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer body.Close() + + rets, err := ioutil.ReadAll(body) + c.Assert(err,IsNil) + str, err := readJsonDocumentConcat("../sample/sample_json.json") + c.Assert(err,IsNil) + c.Assert(string(rets), Equals, str) + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectJsonSuite) TestSelectJsonComplicateConcat(c *C) { + key := "sample_json.json" + err := s.bucket.PutObjectFromFile(key, "../sample/sample_json_lines.json") + c.Assert(err, IsNil) + + selReq := SelectRequest{} + selReq.Expression = ` + select + person.firstname, person.lastname, congress_numbers + from + ossobject + where + startdate > '2017-01-01' and + senator_rank = 'junior' or + state = 'CA' and + party = 'Republican' + ` + selReq.OutputSerializationSelect.JsonBodyOutput.RecordDelimiter = "," + selReq.InputSerializationSelect.JsonBodyInput.JSONType = "LINES" + + body, err := s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer body.Close() + + rets, err := ioutil.ReadAll(body) + c.Assert(err,IsNil) + str, err := readJsonComplicateConcat("../sample/sample_json.json") + c.Assert(err,IsNil) + c.Assert(string(rets), Equals, str) + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectJsonSuite) TestSelectJsonLineInvalidSql(c *C) { + key := "sample_json.json" + err := s.bucket.PutObjectFromFile(key, "../sample/sample_json_lines.json") + c.Assert(err, IsNil) + + selReq := SelectRequest{} + selReq.OutputSerializationSelect.JsonBodyOutput.RecordDelimiter = "," + selReq.InputSerializationSelect.JsonBodyInput.JSONType = "LINES" + + selReq.Expression = `select * from ossobject where avg(cast(person.birthday as int)) > 2016` + _, err = s.bucket.SelectObject(key, selReq) + c.Assert(err, NotNil) + + selReq.Expression = `` + _, err = s.bucket.SelectObject(key, selReq) + c.Assert(err, NotNil) + + selReq.Expression = `select person.lastname || person.firstname from ossobject` + _, err = s.bucket.SelectObject(key, selReq) + c.Assert(err, NotNil) + + selReq.Expression = `select * from ossobject group by person.firstname` + _, err = s.bucket.SelectObject(key, selReq) + c.Assert(err, NotNil) + + selReq.Expression = `select * from ossobject order by _1` + _, err = s.bucket.SelectObject(key, selReq) + c.Assert(err, NotNil) + + selReq.Expression = `select * from ossobject oss join s3object s3 on oss.CityName = s3.CityName` + _, err = s.bucket.SelectObject(key, selReq) + c.Assert(err, NotNil) + + err = s.bucket.PutObjectFromFile(key, "../sample/sample_json.json") + c.Assert(err, IsNil) + selReq.InputSerializationSelect.JsonBodyInput.JSONType = "DOCUMENT" + selReq.Expression = `select _1 from ossobject.objects[*]` + body, err := s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer body.Close() + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} + +func (s *OssSelectJsonSuite) TestSelectJsonParseNumAsString(c *C) { + key := "sample_json.json" + content := "{\"a\":123456789.123456789}" + err := s.bucket.PutObject(key, strings.NewReader(content)) + c.Assert(err, IsNil) + + selReq := SelectRequest{} + selReq.Expression = `select a from ossobject where cast(a as decimal) = 123456789.1234567890` + bo := true + selReq.InputSerializationSelect.JsonBodyInput.ParseJSONNumberAsString = &bo + selReq.InputSerializationSelect.JsonBodyInput.JSONType = "DOCUMENT" + + body, err := s.bucket.SelectObject(key, selReq) + c.Assert(err, IsNil) + defer body.Close() + + rets, err := ioutil.ReadAll(body) + c.Assert(err,IsNil) + c.Assert(string(rets), Equals, "{\"a\":123456789.123456789}\n") + + err = s.bucket.DeleteObject(key) + c.Assert(err, IsNil) +} \ No newline at end of file diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/select_object.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/select_object.go new file mode 100644 index 00000000..2e0da463 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/select_object.go @@ -0,0 +1,197 @@ +package oss + +import ( + "bytes" + "encoding/xml" + "hash/crc32" + "io" + "io/ioutil" + "net/http" + "os" + "strings" +) + +// CreateSelectCsvObjectMeta is Creating csv object meta +// +// key the object key. +// csvMeta the csv file meta +// options the options for create csv Meta of the object. +// +// MetaEndFrameCSV the csv file meta info +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) CreateSelectCsvObjectMeta(key string, csvMeta CsvMetaRequest, options ...Option) (MetaEndFrameCSV, error) { + var endFrame MetaEndFrameCSV + params := map[string]interface{}{} + params["x-oss-process"] = "csv/meta" + + csvMeta.encodeBase64() + bs, err := xml.Marshal(csvMeta) + if err != nil { + return endFrame, err + } + buffer := new(bytes.Buffer) + buffer.Write(bs) + + resp, err := bucket.DoPostSelectObject(key, params, buffer, options...) + if err != nil { + return endFrame, err + } + defer resp.Body.Close() + + _, err = ioutil.ReadAll(resp) + + return resp.Frame.MetaEndFrameCSV, err +} + +// CreateSelectJsonObjectMeta is Creating json object meta +// +// key the object key. +// csvMeta the json file meta +// options the options for create json Meta of the object. +// +// MetaEndFrameJSON the json file meta info +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) CreateSelectJsonObjectMeta(key string, jsonMeta JsonMetaRequest, options ...Option) (MetaEndFrameJSON, error) { + var endFrame MetaEndFrameJSON + params := map[string]interface{}{} + params["x-oss-process"] = "json/meta" + + bs, err := xml.Marshal(jsonMeta) + if err != nil { + return endFrame, err + } + buffer := new(bytes.Buffer) + buffer.Write(bs) + + resp, err := bucket.DoPostSelectObject(key, params, buffer, options...) + if err != nil { + return endFrame, err + } + defer resp.Body.Close() + + _, err = ioutil.ReadAll(resp) + + return resp.Frame.MetaEndFrameJSON, err +} + +// SelectObject is the select object api, approve csv and json file. +// +// key the object key. +// selectReq the request data for select object +// options the options for select file of the object. +// +// o.ReadCloser reader instance for reading data from response. It must be called close() after the usage and only valid when error is nil. +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) SelectObject(key string, selectReq SelectRequest, options ...Option) (io.ReadCloser, error) { + params := map[string]interface{}{} + if selectReq.InputSerializationSelect.JsonBodyInput.JsonIsEmpty() { + params["x-oss-process"] = "csv/select" // default select csv file + } else { + params["x-oss-process"] = "json/select" + } + selectReq.encodeBase64() + bs, err := xml.Marshal(selectReq) + if err != nil { + return nil, err + } + buffer := new(bytes.Buffer) + buffer.Write(bs) + resp, err := bucket.DoPostSelectObject(key, params, buffer, options...) + if err != nil { + return nil, err + } + if selectReq.OutputSerializationSelect.EnablePayloadCrc != nil && *selectReq.OutputSerializationSelect.EnablePayloadCrc == true { + resp.Frame.EnablePayloadCrc = true + } + resp.Frame.OutputRawData = strings.ToUpper(resp.Headers.Get("x-oss-select-output-raw")) == "TRUE" + + return resp, err +} + +// DoPostSelectObject is the SelectObject/CreateMeta api, approve csv and json file. +// +// key the object key. +// params the resource of oss approve csv/meta, json/meta, csv/select, json/select. +// buf the request data trans to buffer. +// options the options for select file of the object. +// +// SelectObjectResponse the response of select object. +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) DoPostSelectObject(key string, params map[string]interface{}, buf *bytes.Buffer, options ...Option) (*SelectObjectResponse, error) { + resp, err := bucket.do("POST", key, params, options, buf, nil) + if err != nil { + return nil, err + } + + result := &SelectObjectResponse{ + Body: resp.Body, + StatusCode: resp.StatusCode, + Frame: SelectObjectResult{}, + } + result.Headers = resp.Headers + // result.Frame = SelectObjectResult{} + result.ReadTimeOut = bucket.GetConfig().Timeout + + // Progress + listener := GetProgressListener(options) + + // CRC32 + crcCalc := crc32.NewIEEE() + result.WriterForCheckCrc32 = crcCalc + result.Body = TeeReader(resp.Body, nil, 0, listener, nil) + + err = CheckRespCode(resp.StatusCode, []int{http.StatusPartialContent, http.StatusOK}) + + return result, err +} + +// SelectObjectIntoFile is the selectObject to file api +// +// key the object key. +// fileName saving file's name to localstation. +// selectReq the request data for select object +// options the options for select file of the object. +// +// error it's nil if no error, otherwise it's an error object. +// +func (bucket Bucket) SelectObjectIntoFile(key, fileName string, selectReq SelectRequest, options ...Option) error { + tempFilePath := fileName + TempFileSuffix + + params := map[string]interface{}{} + if selectReq.InputSerializationSelect.JsonBodyInput.JsonIsEmpty() { + params["x-oss-process"] = "csv/select" // default select csv file + } else { + params["x-oss-process"] = "json/select" + } + selectReq.encodeBase64() + bs, err := xml.Marshal(selectReq) + if err != nil { + return err + } + buffer := new(bytes.Buffer) + buffer.Write(bs) + resp, err := bucket.DoPostSelectObject(key, params, buffer, options...) + if err != nil { + return err + } + defer resp.Close() + + // If the local file does not exist, create a new one. If it exists, overwrite it. + fd, err := os.OpenFile(tempFilePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, FilePermMode) + if err != nil { + return err + } + + // Copy the data to the local file path. + _, err = io.Copy(fd, resp) + fd.Close() + if err != nil { + return err + } + + return os.Rename(tempFilePath, fileName) +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/select_object_read_file_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/select_object_read_file_test.go new file mode 100644 index 00000000..76290871 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/select_object_read_file_test.go @@ -0,0 +1,542 @@ +package oss + +import ( + "bufio" + "io" + "os" + "strings" + "strconv" + "regexp" + "encoding/json" + "encoding/csv" +) + +func handleError(err error) error { + if err == nil { + return nil + } + return err +} + +func readCsvLine(fileName string) (int, error) { + file, err := os.Open(fileName) + if err != nil { + return 0,err + } + defer file.Close() + rd := csv.NewReader(file) + rc, err := rd.ReadAll() + return len(rc), err +} +func readCsvIsEmpty(fileName string) (string, error) { + file, err := os.Open(fileName) + if err != nil { + return "",err + } + defer file.Close() + var out string + var i, index int + var indexYear, indexStateAbbr, indexCityName, indexPopulationCount int + + rd := bufio.NewReader(file) + for { + line, err := rd.ReadString('\n') // read a line + if io.EOF == err { + break + } + if err != nil { + return "",err + } + + sptLint := strings.Split(line, ",") + if i == 0 { + i = 1 + for _, val := range sptLint { + switch val { + case "Year": + indexYear = index + case "StateAbbr": + indexStateAbbr = index + case "CityName": + indexCityName = index + case "PopulationCount": + indexPopulationCount = index + } + index++ + } + } else { + if sptLint[indexCityName] != "" { + outLine := sptLint[indexYear] + "," + sptLint[indexStateAbbr] + "," + sptLint[indexCityName] + "," + sptLint[indexPopulationCount] + "\n" + out += outLine + } + } + } + + return out, nil +} + +func readCsvLike(fileName string) (string, error) { + file, err := os.Open(fileName) + if err != nil { + return "",err + } + defer file.Close() + var out string + var i, index int + var indexYear,indexStateAbbr,indexCityName,indexPopulationCount,indexMeasure int + + rd := bufio.NewReader(file) + for { + line, err := rd.ReadString('\n') // read a line + if io.EOF == err { + break + } + if err != nil { + return "",err + } + + //utf8Lint := ConvertToString(line,"gbk", "utf-8") + sptLint := strings.Split(line[:(len(line)-1)], ",") + if i == 0 { + i = 1 + for _, val := range sptLint { + switch val { + case "Year":indexYear = index + case "StateAbbr":indexStateAbbr = index + case "CityName":indexCityName = index + case "Short_Question_Text":indexPopulationCount = index + case "Measure":indexMeasure = index + } + index++ + } + } else { + if sptLint[indexMeasure] != "" { + reg := regexp.MustCompile("^.*blood pressure.*Years$") + res := reg.FindAllString(sptLint[indexMeasure], -1) + if len(res) > 0 { + outLine := sptLint[indexYear] + "," +sptLint[indexStateAbbr] + "," +sptLint[indexCityName] + "," + sptLint[indexPopulationCount] + "\n" + out += outLine + } + } + } + } + + return out, nil +} + +func readCsvRange(fileName string, l int, r int) (string, error) { + file, err := os.Open(fileName) + if err != nil { + return "", err + } + defer file.Close() + var out string + var i, index int + var indexYear,indexStateAbbr,indexCityName,indexPopulationCount int + + rd := bufio.NewReader(file) + for j := 0; j < r + 1; j++ { + if j < l { + continue + } + line, err := rd.ReadString('\n') // read a line + if io.EOF == err { + break + } + if err != nil { + return "", err + } + + sptLint := strings.Split(line[:(len(line)-1)], ",") + if i == 0 { + i = 1 + for _, val := range sptLint { + switch val { + case "Year":indexYear = index + case "StateAbbr":indexStateAbbr = index + case "CityName":indexCityName = index + case "Short_Question_Text":indexPopulationCount = index + } + index++ + } + } else { + outLine := sptLint[indexYear] + "," +sptLint[indexStateAbbr] + "," +sptLint[indexCityName] + "," + sptLint[indexPopulationCount] + "\n" + out += outLine + } + } + + return out, nil +} + +func readCsvFloatAgg(fileName string) (avg, max, sum float64, er error) { + file, err := os.Open(fileName) + if err != nil { + er = err + return + } + defer file.Close() + var i, index int + var indexDataValue int + + rd := csv.NewReader(file) + + for { + rc, err := rd.Read() + if io.EOF == err { + break + } + if err != nil { + er = err + return + } + if i == 0 { + i=1 + for index = 0; index < len(rc); index++ { + if rc[index] == "Data_Value" { + indexDataValue = index + } + } + } else { + if rc[indexDataValue] != "" { + s1, err := strconv.ParseFloat(rc[indexDataValue], 64) + if err != nil { + er = err + return + } + sum +=s1 + if s1 > max { + max = s1 + } + i++ + } + } + } + avg = sum / float64(i-1) + return +} +func readCsvConcat(fileName string) (string, error) { + var out string + file, err := os.Open(fileName) + if err != nil { + return out, err + } + defer file.Close() + var i int + var indexDataValue int + var indexYear,indexStateAbbr,indexCityName,indexShortQuestionText, indexDataValueUnit int + + rd := csv.NewReader(file) + + for { + rc, err := rd.Read() + if io.EOF == err { + break + } + if err != nil { + return out, err + } + if i == 0 { + for j, v := range rc { + switch v { + case "Year":indexYear = j + case "StateAbbr":indexStateAbbr = j + case "CityName":indexCityName = j + case "Short_Question_Text":indexShortQuestionText = j + case "Data_Value_Unit":indexDataValueUnit = j + case "Data_Value":indexDataValue = j + } + } + } else { + i++ + if rc[indexDataValue] != "" || rc[indexDataValueUnit] != "" { + reg := regexp.MustCompile("^14.8.*$") + reD := reg.FindAllString(rc[indexDataValue], -1) + reDU := reg.FindAllString(rc[indexDataValueUnit], -1) + if len(reD) > 0 || len(reDU) > 0 { + outLine := rc[indexYear] + "," +rc[indexStateAbbr] + "," +rc[indexCityName] + "," + rc[indexShortQuestionText] + "\n" + out += outLine + } + } + } + i++ + } + return out, nil +} +func readCsvComplicateCondition(fileName string)(string, error) { + var out string + file, err := os.Open(fileName) + if err != nil { + return out, err + } + defer file.Close() + var i int + var indexDataValue, indexCategory, indexHighConfidenceLimit, indexMeasure int + var indexYear,indexStateAbbr,indexCityName,indexShortQuestionText, indexDataValueUnit int + + rd := csv.NewReader(file) + + for { + rc, err := rd.Read() + if io.EOF == err { + break + } + if err != nil { + return out, err + } + if i == 0 { + for j, v := range rc { + switch v { + case "Year":indexYear = j + case "StateAbbr":indexStateAbbr = j + case "CityName":indexCityName = j + case "Short_Question_Text":indexShortQuestionText = j + case "Data_Value_Unit":indexDataValueUnit = j + case "Data_Value":indexDataValue = j + case "Measure":indexMeasure = j + case "Category":indexCategory = j + case "High_Confidence_Limit":indexHighConfidenceLimit = j + } + } + } else { + reg := regexp.MustCompile("^.*18 Years$") + reM := reg.FindAllString(rc[indexMeasure], -1) + var dataV, limitV float64 + if rc[indexDataValue] != "" { + dataV, err = strconv.ParseFloat(rc[indexDataValue], 64) + if err != nil { + return out, err + } + } + if rc[indexHighConfidenceLimit] != "" { + limitV, err = strconv.ParseFloat(rc[indexHighConfidenceLimit], 64) + if err != nil { + return out, err + } + } + if dataV > 14.8 && rc[indexDataValueUnit] == "%" || len(reM) > 0 && + rc[indexCategory] == "Unhealthy Behaviors" || limitV > 70.0 { + outLine := rc[indexYear] + "," +rc[indexStateAbbr] + "," +rc[indexCityName] + "," + rc[indexShortQuestionText] + "," + rc[indexDataValue] + "," + rc[indexDataValueUnit] + "," + rc[indexCategory] + "," + rc[indexHighConfidenceLimit] + "\n" + out += outLine + } + } + i++ + } + return out, nil +} + + +type Extra struct { + Address string `json:"address"` + ContactForm string `json:"contact_form"` + Fax string `json:"fax,omitempty"` + How string `json:"how,omitempty"` + Office string `json:"office"` + RssUrl string `json:"rss_url,omitempty"` +} + +type Person struct { + Bioguideid string `json:"bioguideid"` + Birthday string `json:"birthday"` + Cspanid int `json:"cspanid"` + Firstname string `json:"firstname"` + Gender string `json:"gender"` + GenderLabel string `json:"gender_label"` + Lastname string `json:"lastname"` + Link string `json:"link"` + Middlename string `json:"middlename"` + Name string `json:"name"` + Namemod string `json:"namemod"` + Nickname string `json:"nickname"` + Osid string `json:"osid"` + Pvsid *string `json:"pvsid"` + Sortname string `json:"sortname"` + Twitterid *string `json:"twitterid"` + Youtubeid *string `json:"youtubeid"` +} + +type JsonLineSt struct { + Caucus *string `json:"caucus"` + CongressNumbers []int `json:"congress_numbers"` + Current bool `json:"current"` + Description string `json:"description"` + District *string `json:"district"` + Enddate string `json:"enddate"` + Extra Extra `json:"extra"` + LeadershipTitle *string `json:"leadership_title"` + Party string `json:"party"` + Person Person `json:"person"` + Phone string `json:"phone"` + RoleType string `json:"role_type"` + RoleTypeLabel string `json:"role_type_label"` + SenatorClass string `json:"senator_class"` + SenatorClassLabel string `json:"senator_class_label"` + SenatorRank string `json:"senator_rank"` + SenatorRankLabel string `json:"senator_rank_label"` + Startdate string `json:"startdate"` + State string `json:"state"` + Title string `json:"title"` + TitleLong string `json:"title_long"` + Website string `json:"website"` +} +type Metast struct { + limit int + Offset int + TotalCount int +} + +type JsonSt struct { + Meta Metast + Objects []JsonLineSt `json:"objects"` +} + +func readJsonDocument(fileName string) (string, error){ + var out string + var data JsonSt + file, err := os.Open(fileName) + if err != nil { + return "",err + } + decoder := json.NewDecoder(file) + err = decoder.Decode(&data) + for _, v := range data.Objects { + if v.Party == "Democrat"{ + lint, err := json.Marshal(v) + if err != nil { + return "",err + } + lints := strings.Replace(string(lint), "\\u0026", "&", -1) + out += lints + "," + } + } + + return out, err +} +func readJsonLinesLike(fileName string) (string, error){ + var out string + var data JsonSt + file, err := os.Open(fileName) + if err != nil { + return "",err + } + decoder := json.NewDecoder(file) + err = decoder.Decode(&data) + reg := regexp.MustCompile("^1959.*") + for _, v := range data.Objects { + reB := reg.FindAllString(v.Person.Birthday, -1) + if len(reB) > 0 { + lints := "{\"firstname\":\"" + v.Person.Firstname + "\",\"lastname\":\"" + v.Person.Lastname + "\"}" + out += lints + "," + } + } + + return out, err +} + +func readJsonLinesRange(fileName string, l, r int) (string, error){ + var out string + var data JsonSt + var i int + file, err := os.Open(fileName) + if err != nil { + return "",err + } + decoder := json.NewDecoder(file) + err = decoder.Decode(&data) + for _, v := range data.Objects { + if i < l { + continue + } + if i >= r { + break + } + extrb, err := json.Marshal(v.Extra) + if err != nil { + return "",err + } + extr := strings.Replace(string(extrb), "\\u0026", "&", -1) + + lints := "{\"firstname\":\"" + v.Person.Firstname + "\",\"lastname\":\"" + v.Person.Lastname + + "\",\"extra\":" + extr + "}" + out += lints + "," + i++ + } + + return out, err +} + +func readJsonFloatAggregation(fileName string) (float64, float64, float64, error){ + var avg, max, min, sum float64 + var data JsonSt + var i int + file, err := os.Open(fileName) + if err != nil { + return avg, max, min, err + } + decoder := json.NewDecoder(file) + err = decoder.Decode(&data) + for _, v := range data.Objects { + if i == 0 { + min = float64(v.Person.Cspanid) + } + if max < float64(v.Person.Cspanid) { + max = float64(v.Person.Cspanid) + } + if min > float64(v.Person.Cspanid) { + min = float64(v.Person.Cspanid) + } + sum += float64(v.Person.Cspanid) + i++ + } + avg = sum / float64(i) + return avg, max, min, err +} + +func readJsonDocumentConcat(fileName string) (string, error){ + var out string + var data JsonSt + file, err := os.Open(fileName) + if err != nil { + return "",err + } + decoder := json.NewDecoder(file) + err = decoder.Decode(&data) + + for _, v := range data.Objects { + if v.Person.Firstname + v.Person.Lastname == "JohnKennedy" { + extrb, err := json.Marshal(v.Person) + if err != nil { + return "",err + } + extr := "{\"person\":" + strings.Replace(string(extrb), "\\u0026", "&", -1) + "}" + out += extr + "," + } + } + + return out, err +} + +func readJsonComplicateConcat(fileName string) (string, error){ + var out string + var data JsonSt + file, err := os.Open(fileName) + if err != nil { + return "",err + } + decoder := json.NewDecoder(file) + err = decoder.Decode(&data) + + for _, v := range data.Objects { + if v.Startdate > "2017-01-01" && v.SenatorRank == "junior" || + v.State == "CA" && v.Party == "Repulican" { + cn := "[" + for _,vv := range v.CongressNumbers { + cn += strconv.Itoa(vv) + "," + } + cn = cn[:len(cn)-1] + "]" + lints := "{\"firstname\":\"" + v.Person.Firstname + "\",\"lastname\":\"" + v.Person.Lastname + "\",\"congress_numbers\":" + cn + "}" + out += lints + "," + } + } + + return out, err +} \ No newline at end of file diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/select_object_type.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/select_object_type.go new file mode 100644 index 00000000..8b75782f --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/select_object_type.go @@ -0,0 +1,364 @@ +package oss + +import ( + "bytes" + "encoding/binary" + "fmt" + "hash" + "hash/crc32" + "io" + "net/http" + "time" +) + +// The adapter class for Select object's response. +// The response consists of frames. Each frame has the following format: + +// Type | Payload Length | Header Checksum | Payload | Payload Checksum + +// |<4-->| <--4 bytes------><---4 bytes-------><-n/a-----><--4 bytes---------> +// And we have three kind of frames. +// Data Frame: +// Type:8388609 +// Payload: Offset | Data +// <-8 bytes> + +// Continuous Frame +// Type:8388612 +// Payload: Offset (8-bytes) + +// End Frame +// Type:8388613 +// Payload: Offset | total scanned bytes | http status code | error message +// <-- 8bytes--><-----8 bytes--------><---4 bytes-------><---variabe---> + +// SelectObjectResponse defines HTTP response from OSS SelectObject +type SelectObjectResponse struct { + StatusCode int + Headers http.Header + Body io.ReadCloser + Frame SelectObjectResult + ReadTimeOut uint + ClientCRC32 uint32 + ServerCRC32 uint32 + WriterForCheckCrc32 hash.Hash32 + Finish bool +} + +func (sr *SelectObjectResponse) Read(p []byte) (n int, err error) { + n, err = sr.readFrames(p) + return +} + +// Close http reponse body +func (sr *SelectObjectResponse) Close() error { + return sr.Body.Close() +} + +// PostSelectResult is the request of SelectObject +type PostSelectResult struct { + Response *SelectObjectResponse +} + +// readFrames is read Frame +func (sr *SelectObjectResponse) readFrames(p []byte) (int, error) { + var nn int + var err error + var checkValid bool + if sr.Frame.OutputRawData == true { + nn, err = sr.Body.Read(p) + return nn, err + } + + if sr.Finish { + return 0, io.EOF + } + + for { + // if this Frame is Readed, then not reading Header + if sr.Frame.OpenLine != true { + err = sr.analysisHeader() + if err != nil { + return nn, err + } + } + + if sr.Frame.FrameType == DataFrameType { + n, err := sr.analysisData(p[nn:]) + if err != nil { + return nn, err + } + nn += n + + // if this Frame is readed all data, then empty the Frame to read it with next frame + if sr.Frame.ConsumedBytesLength == sr.Frame.PayloadLength-8 { + checkValid, err = sr.checkPayloadSum() + if err != nil || !checkValid { + return nn, fmt.Errorf("%s", err.Error()) + } + sr.emptyFrame() + } + + if nn == len(p) { + return nn, nil + } + } else if sr.Frame.FrameType == ContinuousFrameType { + checkValid, err = sr.checkPayloadSum() + if err != nil || !checkValid { + return nn, fmt.Errorf("%s", err.Error()) + } + } else if sr.Frame.FrameType == EndFrameType { + err = sr.analysisEndFrame() + if err != nil { + return nn, err + } + checkValid, err = sr.checkPayloadSum() + if checkValid { + sr.Finish = true + } + return nn, err + } else if sr.Frame.FrameType == MetaEndFrameCSVType { + err = sr.analysisMetaEndFrameCSV() + if err != nil { + return nn, err + } + checkValid, err = sr.checkPayloadSum() + if checkValid { + sr.Finish = true + } + return nn, err + } else if sr.Frame.FrameType == MetaEndFrameJSONType { + err = sr.analysisMetaEndFrameJSON() + if err != nil { + return nn, err + } + checkValid, err = sr.checkPayloadSum() + if checkValid { + sr.Finish = true + } + return nn, err + } + } + return nn, nil +} + +type chanReadIO struct { + readLen int + err error +} + +func (sr *SelectObjectResponse) readLen(p []byte, timeOut time.Duration) (int, error) { + r := sr.Body + ch := make(chan chanReadIO, 1) + defer close(ch) + go func(p []byte) { + var needReadLength int + readChan := chanReadIO{} + needReadLength = len(p) + for { + n, err := r.Read(p[readChan.readLen:needReadLength]) + readChan.readLen += n + if err != nil { + readChan.err = err + ch <- readChan + return + } + + if readChan.readLen == needReadLength { + break + } + } + ch <- readChan + }(p) + + select { + case <-time.After(time.Second * timeOut): + return 0, fmt.Errorf("requestId: %s, readLen timeout, timeout is %d(second),need read:%d", sr.Headers.Get(HTTPHeaderOssRequestID), timeOut, len(p)) + case result := <-ch: + return result.readLen, result.err + } +} + +// analysisHeader is reading selectObject response body's header +func (sr *SelectObjectResponse) analysisHeader() error { + headFrameByte := make([]byte, 20) + _, err := sr.readLen(headFrameByte, time.Duration(sr.ReadTimeOut)) + if err != nil { + return fmt.Errorf("requestId: %s, Read response frame header failure,err:%s", sr.Headers.Get(HTTPHeaderOssRequestID), err.Error()) + } + + frameTypeByte := headFrameByte[0:4] + sr.Frame.Version = frameTypeByte[0] + frameTypeByte[0] = 0 + bytesToInt(frameTypeByte, &sr.Frame.FrameType) + + if sr.Frame.FrameType != DataFrameType && sr.Frame.FrameType != ContinuousFrameType && + sr.Frame.FrameType != EndFrameType && sr.Frame.FrameType != MetaEndFrameCSVType && sr.Frame.FrameType != MetaEndFrameJSONType { + return fmt.Errorf("requestId: %s, Unexpected frame type: %d", sr.Headers.Get(HTTPHeaderOssRequestID), sr.Frame.FrameType) + } + + payloadLengthByte := headFrameByte[4:8] + bytesToInt(payloadLengthByte, &sr.Frame.PayloadLength) + headCheckSumByte := headFrameByte[8:12] + bytesToInt(headCheckSumByte, &sr.Frame.HeaderCheckSum) + byteOffset := headFrameByte[12:20] + bytesToInt(byteOffset, &sr.Frame.Offset) + sr.Frame.OpenLine = true + + err = sr.writerCheckCrc32(byteOffset) + return err +} + +// analysisData is reading the DataFrameType data of selectObject response body +func (sr *SelectObjectResponse) analysisData(p []byte) (int, error) { + var needReadLength int32 + lenP := int32(len(p)) + restByteLength := sr.Frame.PayloadLength - 8 - sr.Frame.ConsumedBytesLength + if lenP <= restByteLength { + needReadLength = lenP + } else { + needReadLength = restByteLength + } + n, err := sr.readLen(p[:needReadLength], time.Duration(sr.ReadTimeOut)) + if err != nil { + return n, fmt.Errorf("read frame data error,%s", err.Error()) + } + sr.Frame.ConsumedBytesLength += int32(n) + err = sr.writerCheckCrc32(p[:n]) + return n, err +} + +// analysisEndFrame is reading the EndFrameType data of selectObject response body +func (sr *SelectObjectResponse) analysisEndFrame() error { + var eF EndFrame + payLoadBytes := make([]byte, sr.Frame.PayloadLength-8) + _, err := sr.readLen(payLoadBytes, time.Duration(sr.ReadTimeOut)) + if err != nil { + return fmt.Errorf("read end frame error:%s", err.Error()) + } + bytesToInt(payLoadBytes[0:8], &eF.TotalScanned) + bytesToInt(payLoadBytes[8:12], &eF.HTTPStatusCode) + errMsgLength := sr.Frame.PayloadLength - 20 + eF.ErrorMsg = string(payLoadBytes[12 : errMsgLength+12]) + sr.Frame.EndFrame.TotalScanned = eF.TotalScanned + sr.Frame.EndFrame.HTTPStatusCode = eF.HTTPStatusCode + sr.Frame.EndFrame.ErrorMsg = eF.ErrorMsg + err = sr.writerCheckCrc32(payLoadBytes) + return err +} + +// analysisMetaEndFrameCSV is reading the MetaEndFrameCSVType data of selectObject response body +func (sr *SelectObjectResponse) analysisMetaEndFrameCSV() error { + var mCF MetaEndFrameCSV + payLoadBytes := make([]byte, sr.Frame.PayloadLength-8) + _, err := sr.readLen(payLoadBytes, time.Duration(sr.ReadTimeOut)) + if err != nil { + return fmt.Errorf("read meta end csv frame error:%s", err.Error()) + } + + bytesToInt(payLoadBytes[0:8], &mCF.TotalScanned) + bytesToInt(payLoadBytes[8:12], &mCF.Status) + bytesToInt(payLoadBytes[12:16], &mCF.SplitsCount) + bytesToInt(payLoadBytes[16:24], &mCF.RowsCount) + bytesToInt(payLoadBytes[24:28], &mCF.ColumnsCount) + errMsgLength := sr.Frame.PayloadLength - 36 + mCF.ErrorMsg = string(payLoadBytes[28 : errMsgLength+28]) + sr.Frame.MetaEndFrameCSV.ErrorMsg = mCF.ErrorMsg + sr.Frame.MetaEndFrameCSV.TotalScanned = mCF.TotalScanned + sr.Frame.MetaEndFrameCSV.Status = mCF.Status + sr.Frame.MetaEndFrameCSV.SplitsCount = mCF.SplitsCount + sr.Frame.MetaEndFrameCSV.RowsCount = mCF.RowsCount + sr.Frame.MetaEndFrameCSV.ColumnsCount = mCF.ColumnsCount + err = sr.writerCheckCrc32(payLoadBytes) + return err +} + +// analysisMetaEndFrameJSON is reading the MetaEndFrameJSONType data of selectObject response body +func (sr *SelectObjectResponse) analysisMetaEndFrameJSON() error { + var mJF MetaEndFrameJSON + payLoadBytes := make([]byte, sr.Frame.PayloadLength-8) + _, err := sr.readLen(payLoadBytes, time.Duration(sr.ReadTimeOut)) + if err != nil { + return fmt.Errorf("read meta end json frame error:%s", err.Error()) + } + + bytesToInt(payLoadBytes[0:8], &mJF.TotalScanned) + bytesToInt(payLoadBytes[8:12], &mJF.Status) + bytesToInt(payLoadBytes[12:16], &mJF.SplitsCount) + bytesToInt(payLoadBytes[16:24], &mJF.RowsCount) + errMsgLength := sr.Frame.PayloadLength - 32 + mJF.ErrorMsg = string(payLoadBytes[24 : errMsgLength+24]) + sr.Frame.MetaEndFrameJSON.ErrorMsg = mJF.ErrorMsg + sr.Frame.MetaEndFrameJSON.TotalScanned = mJF.TotalScanned + sr.Frame.MetaEndFrameJSON.Status = mJF.Status + sr.Frame.MetaEndFrameJSON.SplitsCount = mJF.SplitsCount + sr.Frame.MetaEndFrameJSON.RowsCount = mJF.RowsCount + + err = sr.writerCheckCrc32(payLoadBytes) + return err +} + +func (sr *SelectObjectResponse) checkPayloadSum() (bool, error) { + payLoadChecksumByte := make([]byte, 4) + n, err := sr.readLen(payLoadChecksumByte, time.Duration(sr.ReadTimeOut)) + if n == 4 { + bytesToInt(payLoadChecksumByte, &sr.Frame.PayloadChecksum) + sr.ServerCRC32 = sr.Frame.PayloadChecksum + sr.ClientCRC32 = sr.WriterForCheckCrc32.Sum32() + if sr.Frame.EnablePayloadCrc == true && sr.ServerCRC32 != 0 && sr.ServerCRC32 != sr.ClientCRC32 { + return false, fmt.Errorf("RequestId: %s, Unexpected frame type: %d, client %d but server %d", + sr.Headers.Get(HTTPHeaderOssRequestID), sr.Frame.FrameType, sr.ClientCRC32, sr.ServerCRC32) + } + return true, err + } + return false, fmt.Errorf("RequestId:%s, read checksum error:%s", sr.Headers.Get(HTTPHeaderOssRequestID), err.Error()) +} + +func (sr *SelectObjectResponse) writerCheckCrc32(p []byte) (err error) { + err = nil + if sr.Frame.EnablePayloadCrc == true { + _, err = sr.WriterForCheckCrc32.Write(p) + } + return err +} + +// emptyFrame is emptying SelectObjectResponse Frame information +func (sr *SelectObjectResponse) emptyFrame() { + crcCalc := crc32.NewIEEE() + sr.WriterForCheckCrc32 = crcCalc + sr.Finish = false + + sr.Frame.ConsumedBytesLength = 0 + sr.Frame.OpenLine = false + sr.Frame.Version = byte(0) + sr.Frame.FrameType = 0 + sr.Frame.PayloadLength = 0 + sr.Frame.HeaderCheckSum = 0 + sr.Frame.Offset = 0 + sr.Frame.Data = "" + + sr.Frame.EndFrame.TotalScanned = 0 + sr.Frame.EndFrame.HTTPStatusCode = 0 + sr.Frame.EndFrame.ErrorMsg = "" + + sr.Frame.MetaEndFrameCSV.TotalScanned = 0 + sr.Frame.MetaEndFrameCSV.Status = 0 + sr.Frame.MetaEndFrameCSV.SplitsCount = 0 + sr.Frame.MetaEndFrameCSV.RowsCount = 0 + sr.Frame.MetaEndFrameCSV.ColumnsCount = 0 + sr.Frame.MetaEndFrameCSV.ErrorMsg = "" + + sr.Frame.MetaEndFrameJSON.TotalScanned = 0 + sr.Frame.MetaEndFrameJSON.Status = 0 + sr.Frame.MetaEndFrameJSON.SplitsCount = 0 + sr.Frame.MetaEndFrameJSON.RowsCount = 0 + sr.Frame.MetaEndFrameJSON.ErrorMsg = "" + + sr.Frame.PayloadChecksum = 0 +} + +// bytesToInt byte's array trans to int +func bytesToInt(b []byte, ret interface{}) { + binBuf := bytes.NewBuffer(b) + binary.Read(binBuf, binary.BigEndian, ret) +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/transport_1_6.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/transport_1_6.go new file mode 100644 index 00000000..795ca8bb --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/transport_1_6.go @@ -0,0 +1,34 @@ +// +build !go1.7 + +package oss + +import ( + "net" + "net/http" + "time" +) + +func newTransport(conn *Conn, config *Config) *http.Transport { + httpTimeOut := conn.config.HTTPTimeout + httpMaxConns := conn.config.HTTPMaxConns + // New Transport + transport := &http.Transport{ + Dial: func(netw, addr string) (net.Conn, error) { + d := net.Dialer{ + Timeout: httpTimeOut.ConnectTimeout, + KeepAlive: 30 * time.Second, + } + if config.LocalAddr != nil { + d.LocalAddr = config.LocalAddr + } + conn, err := d.Dial(netw, addr) + if err != nil { + return nil, err + } + return newTimeoutConn(conn, httpTimeOut.ReadWriteTimeout, httpTimeOut.LongTimeout), nil + }, + MaxIdleConnsPerHost: httpMaxConns.MaxIdleConnsPerHost, + ResponseHeaderTimeout: httpTimeOut.HeaderTimeout, + } + return transport +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/transport_1_7.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/transport_1_7.go new file mode 100644 index 00000000..757543bc --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/transport_1_7.go @@ -0,0 +1,36 @@ +// +build go1.7 + +package oss + +import ( + "net" + "net/http" + "time" +) + +func newTransport(conn *Conn, config *Config) *http.Transport { + httpTimeOut := conn.config.HTTPTimeout + httpMaxConns := conn.config.HTTPMaxConns + // New Transport + transport := &http.Transport{ + Dial: func(netw, addr string) (net.Conn, error) { + d := net.Dialer{ + Timeout: httpTimeOut.ConnectTimeout, + KeepAlive: 30 * time.Second, + } + if config.LocalAddr != nil { + d.LocalAddr = config.LocalAddr + } + conn, err := d.Dial(netw, addr) + if err != nil { + return nil, err + } + return newTimeoutConn(conn, httpTimeOut.ReadWriteTimeout, httpTimeOut.LongTimeout), nil + }, + MaxIdleConns: httpMaxConns.MaxIdleConns, + MaxIdleConnsPerHost: httpMaxConns.MaxIdleConnsPerHost, + IdleConnTimeout: httpTimeOut.IdleConnTimeout, + ResponseHeaderTimeout: httpTimeOut.HeaderTimeout, + } + return transport +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/type.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/type.go new file mode 100644 index 00000000..718991ea --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/type.go @@ -0,0 +1,1247 @@ +package oss + +import ( + "encoding/base64" + "encoding/xml" + "fmt" + "net/url" + "time" +) + +// ListBucketsResult defines the result object from ListBuckets request +type ListBucketsResult struct { + XMLName xml.Name `xml:"ListAllMyBucketsResult"` + Prefix string `xml:"Prefix"` // The prefix in this query + Marker string `xml:"Marker"` // The marker filter + MaxKeys int `xml:"MaxKeys"` // The max entry count to return. This information is returned when IsTruncated is true. + IsTruncated bool `xml:"IsTruncated"` // Flag true means there's remaining buckets to return. + NextMarker string `xml:"NextMarker"` // The marker filter for the next list call + Owner Owner `xml:"Owner"` // The owner information + Buckets []BucketProperties `xml:"Buckets>Bucket"` // The bucket list +} + +// BucketProperties defines bucket properties +type BucketProperties struct { + XMLName xml.Name `xml:"Bucket"` + Name string `xml:"Name"` // Bucket name + Location string `xml:"Location"` // Bucket datacenter + CreationDate time.Time `xml:"CreationDate"` // Bucket create time + StorageClass string `xml:"StorageClass"` // Bucket storage class +} + +// GetBucketACLResult defines GetBucketACL request's result +type GetBucketACLResult struct { + XMLName xml.Name `xml:"AccessControlPolicy"` + ACL string `xml:"AccessControlList>Grant"` // Bucket ACL + Owner Owner `xml:"Owner"` // Bucket owner +} + +// LifecycleConfiguration is the Bucket Lifecycle configuration +type LifecycleConfiguration struct { + XMLName xml.Name `xml:"LifecycleConfiguration"` + Rules []LifecycleRule `xml:"Rule"` +} + +// LifecycleRule defines Lifecycle rules +type LifecycleRule struct { + XMLName xml.Name `xml:"Rule"` + ID string `xml:"ID,omitempty"` // The rule ID + Prefix string `xml:"Prefix"` // The object key prefix + Status string `xml:"Status"` // The rule status (enabled or not) + Tags []Tag `xml:"Tag,omitempty"` // the tags property + Expiration *LifecycleExpiration `xml:"Expiration,omitempty"` // The expiration property + Transitions []LifecycleTransition `xml:"Transition,omitempty"` // The transition property + AbortMultipartUpload *LifecycleAbortMultipartUpload `xml:"AbortMultipartUpload,omitempty"` // The AbortMultipartUpload property + NonVersionExpiration *LifecycleVersionExpiration `xml:"NoncurrentVersionExpiration,omitempty"` + // Deprecated: Use NonVersionTransitions instead. + NonVersionTransition *LifecycleVersionTransition `xml:"-"` // NonVersionTransition is not suggested to use + NonVersionTransitions []LifecycleVersionTransition `xml:"NoncurrentVersionTransition,omitempty"` +} + +// LifecycleExpiration defines the rule's expiration property +type LifecycleExpiration struct { + XMLName xml.Name `xml:"Expiration"` + Days int `xml:"Days,omitempty"` // Relative expiration time: The expiration time in days after the last modified time + Date string `xml:"Date,omitempty"` // Absolute expiration time: The expiration time in date, not recommended + CreatedBeforeDate string `xml:"CreatedBeforeDate,omitempty"` // objects created before the date will be expired + ExpiredObjectDeleteMarker *bool `xml:"ExpiredObjectDeleteMarker,omitempty"` // Specifies whether the expired delete tag is automatically deleted +} + +// LifecycleTransition defines the rule's transition propery +type LifecycleTransition struct { + XMLName xml.Name `xml:"Transition"` + Days int `xml:"Days,omitempty"` // Relative transition time: The transition time in days after the last modified time + CreatedBeforeDate string `xml:"CreatedBeforeDate,omitempty"` // objects created before the date will be expired + StorageClass StorageClassType `xml:"StorageClass,omitempty"` // Specifies the target storage type +} + +// LifecycleAbortMultipartUpload defines the rule's abort multipart upload propery +type LifecycleAbortMultipartUpload struct { + XMLName xml.Name `xml:"AbortMultipartUpload"` + Days int `xml:"Days,omitempty"` // Relative expiration time: The expiration time in days after the last modified time + CreatedBeforeDate string `xml:"CreatedBeforeDate,omitempty"` // objects created before the date will be expired +} + +// LifecycleVersionExpiration defines the rule's NoncurrentVersionExpiration propery +type LifecycleVersionExpiration struct { + XMLName xml.Name `xml:"NoncurrentVersionExpiration"` + NoncurrentDays int `xml:"NoncurrentDays,omitempty"` // How many days after the Object becomes a non-current version +} + +// LifecycleVersionTransition defines the rule's NoncurrentVersionTransition propery +type LifecycleVersionTransition struct { + XMLName xml.Name `xml:"NoncurrentVersionTransition"` + NoncurrentDays int `xml:"NoncurrentDays,omitempty"` // How many days after the Object becomes a non-current version + StorageClass StorageClassType `xml:"StorageClass,omitempty"` +} + +const iso8601DateFormat = "2006-01-02T15:04:05.000Z" + +// BuildLifecycleRuleByDays builds a lifecycle rule objects will expiration in days after the last modified time +func BuildLifecycleRuleByDays(id, prefix string, status bool, days int) LifecycleRule { + var statusStr = "Enabled" + if !status { + statusStr = "Disabled" + } + return LifecycleRule{ID: id, Prefix: prefix, Status: statusStr, + Expiration: &LifecycleExpiration{Days: days}} +} + +// BuildLifecycleRuleByDate builds a lifecycle rule objects will expiration in specified date +func BuildLifecycleRuleByDate(id, prefix string, status bool, year, month, day int) LifecycleRule { + var statusStr = "Enabled" + if !status { + statusStr = "Disabled" + } + date := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC).Format(iso8601DateFormat) + return LifecycleRule{ID: id, Prefix: prefix, Status: statusStr, + Expiration: &LifecycleExpiration{Date: date}} +} + +// ValidateLifecycleRule Determine if a lifecycle rule is valid, if it is invalid, it will return an error. +func verifyLifecycleRules(rules []LifecycleRule) error { + if len(rules) == 0 { + return fmt.Errorf("invalid rules, the length of rules is zero") + } + for k, rule := range rules { + if rule.Status != "Enabled" && rule.Status != "Disabled" { + return fmt.Errorf("invalid rule, the value of status must be Enabled or Disabled") + } + + abortMPU := rule.AbortMultipartUpload + if abortMPU != nil { + if (abortMPU.Days != 0 && abortMPU.CreatedBeforeDate != "") || (abortMPU.Days == 0 && abortMPU.CreatedBeforeDate == "") { + return fmt.Errorf("invalid abort multipart upload lifecycle, must be set one of CreatedBeforeDate and Days") + } + } + + transitions := rule.Transitions + if len(transitions) > 0 { + for _, transition := range transitions { + if (transition.Days != 0 && transition.CreatedBeforeDate != "") || (transition.Days == 0 && transition.CreatedBeforeDate == "") { + return fmt.Errorf("invalid transition lifecycle, must be set one of CreatedBeforeDate and Days") + } + } + } + + // NonVersionTransition is not suggested to use + // to keep compatible + if rule.NonVersionTransition != nil && len(rule.NonVersionTransitions) > 0 { + return fmt.Errorf("NonVersionTransition and NonVersionTransitions cannot both have values") + } else if rule.NonVersionTransition != nil { + rules[k].NonVersionTransitions = append(rules[k].NonVersionTransitions, *rule.NonVersionTransition) + } + } + + return nil +} + +// GetBucketLifecycleResult defines GetBucketLifecycle's result object +type GetBucketLifecycleResult LifecycleConfiguration + +// RefererXML defines Referer configuration +type RefererXML struct { + XMLName xml.Name `xml:"RefererConfiguration"` + AllowEmptyReferer bool `xml:"AllowEmptyReferer"` // Allow empty referrer + RefererList []string `xml:"RefererList>Referer"` // Referer whitelist +} + +// GetBucketRefererResult defines result object for GetBucketReferer request +type GetBucketRefererResult RefererXML + +// LoggingXML defines logging configuration +type LoggingXML struct { + XMLName xml.Name `xml:"BucketLoggingStatus"` + LoggingEnabled LoggingEnabled `xml:"LoggingEnabled"` // The logging configuration information +} + +type loggingXMLEmpty struct { + XMLName xml.Name `xml:"BucketLoggingStatus"` +} + +// LoggingEnabled defines the logging configuration information +type LoggingEnabled struct { + XMLName xml.Name `xml:"LoggingEnabled"` + TargetBucket string `xml:"TargetBucket"` // The bucket name for storing the log files + TargetPrefix string `xml:"TargetPrefix"` // The log file prefix +} + +// GetBucketLoggingResult defines the result from GetBucketLogging request +type GetBucketLoggingResult LoggingXML + +// WebsiteXML defines Website configuration +type WebsiteXML struct { + XMLName xml.Name `xml:"WebsiteConfiguration"` + IndexDocument IndexDocument `xml:"IndexDocument,omitempty"` // The index page + ErrorDocument ErrorDocument `xml:"ErrorDocument,omitempty"` // The error page + RoutingRules []RoutingRule `xml:"RoutingRules>RoutingRule,omitempty"` // The routing Rule list +} + +// IndexDocument defines the index page info +type IndexDocument struct { + XMLName xml.Name `xml:"IndexDocument"` + Suffix string `xml:"Suffix"` // The file name for the index page +} + +// ErrorDocument defines the 404 error page info +type ErrorDocument struct { + XMLName xml.Name `xml:"ErrorDocument"` + Key string `xml:"Key"` // 404 error file name +} + +// RoutingRule defines the routing rules +type RoutingRule struct { + XMLName xml.Name `xml:"RoutingRule"` + RuleNumber int `xml:"RuleNumber,omitempty"` // The routing number + Condition Condition `xml:"Condition,omitempty"` // The routing condition + Redirect Redirect `xml:"Redirect,omitempty"` // The routing redirect + +} + +// Condition defines codition in the RoutingRule +type Condition struct { + XMLName xml.Name `xml:"Condition"` + KeyPrefixEquals string `xml:"KeyPrefixEquals,omitempty"` // Matching objcet prefix + HTTPErrorCodeReturnedEquals int `xml:"HttpErrorCodeReturnedEquals,omitempty"` // The rule is for Accessing to the specified object + IncludeHeader []IncludeHeader `xml:"IncludeHeader"` // The rule is for request which include header +} + +// IncludeHeader defines includeHeader in the RoutingRule's Condition +type IncludeHeader struct { + XMLName xml.Name `xml:"IncludeHeader"` + Key string `xml:"Key,omitempty"` // The Include header key + Equals string `xml:"Equals,omitempty"` // The Include header value +} + +// Redirect defines redirect in the RoutingRule +type Redirect struct { + XMLName xml.Name `xml:"Redirect"` + RedirectType string `xml:"RedirectType,omitempty"` // The redirect type, it have Mirror,External,Internal,AliCDN + PassQueryString *bool `xml:"PassQueryString"` // Whether to send the specified request's parameters, true or false + MirrorURL string `xml:"MirrorURL,omitempty"` // Mirror of the website address back to the source. + MirrorPassQueryString *bool `xml:"MirrorPassQueryString"` // To Mirror of the website Whether to send the specified request's parameters, true or false + MirrorFollowRedirect *bool `xml:"MirrorFollowRedirect"` // Redirect the location, if the mirror return 3XX + MirrorCheckMd5 *bool `xml:"MirrorCheckMd5"` // Check the mirror is MD5. + MirrorHeaders MirrorHeaders `xml:"MirrorHeaders,omitempty"` // Mirror headers + Protocol string `xml:"Protocol,omitempty"` // The redirect Protocol + HostName string `xml:"HostName,omitempty"` // The redirect HostName + ReplaceKeyPrefixWith string `xml:"ReplaceKeyPrefixWith,omitempty"` // object name'Prefix replace the value + HttpRedirectCode int `xml:"HttpRedirectCode,omitempty"` // THe redirect http code + ReplaceKeyWith string `xml:"ReplaceKeyWith,omitempty"` // object name replace the value +} + +// MirrorHeaders defines MirrorHeaders in the Redirect +type MirrorHeaders struct { + XMLName xml.Name `xml:"MirrorHeaders"` + PassAll *bool `xml:"PassAll"` // Penetrating all of headers to source website. + Pass []string `xml:"Pass"` // Penetrating some of headers to source website. + Remove []string `xml:"Remove"` // Prohibit passthrough some of headers to source website + Set []MirrorHeaderSet `xml:"Set"` // Setting some of headers send to source website +} + +// MirrorHeaderSet defines Set for Redirect's MirrorHeaders +type MirrorHeaderSet struct { + XMLName xml.Name `xml:"Set"` + Key string `xml:"Key,omitempty"` // The mirror header key + Value string `xml:"Value,omitempty"` // The mirror header value +} + +// GetBucketWebsiteResult defines the result from GetBucketWebsite request. +type GetBucketWebsiteResult WebsiteXML + +// CORSXML defines CORS configuration +type CORSXML struct { + XMLName xml.Name `xml:"CORSConfiguration"` + CORSRules []CORSRule `xml:"CORSRule"` // CORS rules +} + +// CORSRule defines CORS rules +type CORSRule struct { + XMLName xml.Name `xml:"CORSRule"` + AllowedOrigin []string `xml:"AllowedOrigin"` // Allowed origins. By default it's wildcard '*' + AllowedMethod []string `xml:"AllowedMethod"` // Allowed methods + AllowedHeader []string `xml:"AllowedHeader"` // Allowed headers + ExposeHeader []string `xml:"ExposeHeader"` // Allowed response headers + MaxAgeSeconds int `xml:"MaxAgeSeconds"` // Max cache ages in seconds +} + +// GetBucketCORSResult defines the result from GetBucketCORS request. +type GetBucketCORSResult CORSXML + +// GetBucketInfoResult defines the result from GetBucketInfo request. +type GetBucketInfoResult struct { + XMLName xml.Name `xml:"BucketInfo"` + BucketInfo BucketInfo `xml:"Bucket"` +} + +// BucketInfo defines Bucket information +type BucketInfo struct { + XMLName xml.Name `xml:"Bucket"` + Name string `xml:"Name"` // Bucket name + Location string `xml:"Location"` // Bucket datacenter + CreationDate time.Time `xml:"CreationDate"` // Bucket creation time + ExtranetEndpoint string `xml:"ExtranetEndpoint"` // Bucket external endpoint + IntranetEndpoint string `xml:"IntranetEndpoint"` // Bucket internal endpoint + ACL string `xml:"AccessControlList>Grant"` // Bucket ACL + RedundancyType string `xml:"DataRedundancyType"` // Bucket DataRedundancyType + Owner Owner `xml:"Owner"` // Bucket owner + StorageClass string `xml:"StorageClass"` // Bucket storage class + SseRule SSERule `xml:"ServerSideEncryptionRule"` // Bucket ServerSideEncryptionRule + Versioning string `xml:"Versioning"` // Bucket Versioning +} + +type SSERule struct { + XMLName xml.Name `xml:"ServerSideEncryptionRule"` // Bucket ServerSideEncryptionRule + KMSMasterKeyID string `xml:"KMSMasterKeyID,omitempty"` // Bucket KMSMasterKeyID + SSEAlgorithm string `xml:"SSEAlgorithm,omitempty"` // Bucket SSEAlgorithm + KMSDataEncryption string `xml:"KMSDataEncryption,omitempty"` //Bucket KMSDataEncryption +} + +// ListObjectsResult defines the result from ListObjects request +type ListObjectsResult struct { + XMLName xml.Name `xml:"ListBucketResult"` + Prefix string `xml:"Prefix"` // The object prefix + Marker string `xml:"Marker"` // The marker filter. + MaxKeys int `xml:"MaxKeys"` // Max keys to return + Delimiter string `xml:"Delimiter"` // The delimiter for grouping objects' name + IsTruncated bool `xml:"IsTruncated"` // Flag indicates if all results are returned (when it's false) + NextMarker string `xml:"NextMarker"` // The start point of the next query + Objects []ObjectProperties `xml:"Contents"` // Object list + CommonPrefixes []string `xml:"CommonPrefixes>Prefix"` // You can think of commonprefixes as "folders" whose names end with the delimiter +} + +// ObjectProperties defines Objecct properties +type ObjectProperties struct { + XMLName xml.Name `xml:"Contents"` + Key string `xml:"Key"` // Object key + Type string `xml:"Type"` // Object type + Size int64 `xml:"Size"` // Object size + ETag string `xml:"ETag"` // Object ETag + Owner Owner `xml:"Owner"` // Object owner information + LastModified time.Time `xml:"LastModified"` // Object last modified time + StorageClass string `xml:"StorageClass"` // Object storage class (Standard, IA, Archive) +} + +// ListObjectsResultV2 defines the result from ListObjectsV2 request +type ListObjectsResultV2 struct { + XMLName xml.Name `xml:"ListBucketResult"` + Prefix string `xml:"Prefix"` // The object prefix + StartAfter string `xml:"StartAfter"` // the input StartAfter + ContinuationToken string `xml:"ContinuationToken"` // the input ContinuationToken + MaxKeys int `xml:"MaxKeys"` // Max keys to return + Delimiter string `xml:"Delimiter"` // The delimiter for grouping objects' name + IsTruncated bool `xml:"IsTruncated"` // Flag indicates if all results are returned (when it's false) + NextContinuationToken string `xml:"NextContinuationToken"` // The start point of the next NextContinuationToken + Objects []ObjectProperties `xml:"Contents"` // Object list + CommonPrefixes []string `xml:"CommonPrefixes>Prefix"` // You can think of commonprefixes as "folders" whose names end with the delimiter +} + +// ListObjectVersionsResult defines the result from ListObjectVersions request +type ListObjectVersionsResult struct { + XMLName xml.Name `xml:"ListVersionsResult"` + Name string `xml:"Name"` // The Bucket Name + Owner Owner `xml:"Owner"` // The owner of bucket + Prefix string `xml:"Prefix"` // The object prefix + KeyMarker string `xml:"KeyMarker"` // The start marker filter. + VersionIdMarker string `xml:"VersionIdMarker"` // The start VersionIdMarker filter. + MaxKeys int `xml:"MaxKeys"` // Max keys to return + Delimiter string `xml:"Delimiter"` // The delimiter for grouping objects' name + IsTruncated bool `xml:"IsTruncated"` // Flag indicates if all results are returned (when it's false) + NextKeyMarker string `xml:"NextKeyMarker"` // The start point of the next query + NextVersionIdMarker string `xml:"NextVersionIdMarker"` // The start point of the next query + CommonPrefixes []string `xml:"CommonPrefixes>Prefix"` // You can think of commonprefixes as "folders" whose names end with the delimiter + ObjectDeleteMarkers []ObjectDeleteMarkerProperties `xml:"DeleteMarker"` // DeleteMarker list + ObjectVersions []ObjectVersionProperties `xml:"Version"` // version list +} + +type ObjectDeleteMarkerProperties struct { + XMLName xml.Name `xml:"DeleteMarker"` + Key string `xml:"Key"` // The Object Key + VersionId string `xml:"VersionId"` // The Object VersionId + IsLatest bool `xml:"IsLatest"` // is current version or not + LastModified time.Time `xml:"LastModified"` // Object last modified time + Owner Owner `xml:"Owner"` // bucket owner element +} + +type ObjectVersionProperties struct { + XMLName xml.Name `xml:"Version"` + Key string `xml:"Key"` // The Object Key + VersionId string `xml:"VersionId"` // The Object VersionId + IsLatest bool `xml:"IsLatest"` // is latest version or not + LastModified time.Time `xml:"LastModified"` // Object last modified time + Type string `xml:"Type"` // Object type + Size int64 `xml:"Size"` // Object size + ETag string `xml:"ETag"` // Object ETag + StorageClass string `xml:"StorageClass"` // Object storage class (Standard, IA, Archive) + Owner Owner `xml:"Owner"` // bucket owner element +} + +// Owner defines Bucket/Object's owner +type Owner struct { + XMLName xml.Name `xml:"Owner"` + ID string `xml:"ID"` // Owner ID + DisplayName string `xml:"DisplayName"` // Owner's display name +} + +// CopyObjectResult defines result object of CopyObject +type CopyObjectResult struct { + XMLName xml.Name `xml:"CopyObjectResult"` + LastModified time.Time `xml:"LastModified"` // New object's last modified time. + ETag string `xml:"ETag"` // New object's ETag +} + +// GetObjectACLResult defines result of GetObjectACL request +type GetObjectACLResult GetBucketACLResult + +type deleteXML struct { + XMLName xml.Name `xml:"Delete"` + Objects []DeleteObject `xml:"Object"` // Objects to delete + Quiet bool `xml:"Quiet"` // Flag of quiet mode. +} + +// DeleteObject defines the struct for deleting object +type DeleteObject struct { + XMLName xml.Name `xml:"Object"` + Key string `xml:"Key"` // Object name + VersionId string `xml:"VersionId,omitempty"` // Object VersionId +} + +// DeleteObjectsResult defines result of DeleteObjects request +type DeleteObjectsResult struct { + XMLName xml.Name + DeletedObjects []string // Deleted object key list +} + +// DeleteObjectsResult_inner defines result of DeleteObjects request +type DeleteObjectVersionsResult struct { + XMLName xml.Name `xml:"DeleteResult"` + DeletedObjectsDetail []DeletedKeyInfo `xml:"Deleted"` // Deleted object detail info +} + +// DeleteKeyInfo defines object delete info +type DeletedKeyInfo struct { + XMLName xml.Name `xml:"Deleted"` + Key string `xml:"Key"` // Object key + VersionId string `xml:"VersionId"` // VersionId + DeleteMarker bool `xml:"DeleteMarker"` // Object DeleteMarker + DeleteMarkerVersionId string `xml:"DeleteMarkerVersionId"` // Object DeleteMarkerVersionId +} + +// InitiateMultipartUploadResult defines result of InitiateMultipartUpload request +type InitiateMultipartUploadResult struct { + XMLName xml.Name `xml:"InitiateMultipartUploadResult"` + Bucket string `xml:"Bucket"` // Bucket name + Key string `xml:"Key"` // Object name to upload + UploadID string `xml:"UploadId"` // Generated UploadId +} + +// UploadPart defines the upload/copy part +type UploadPart struct { + XMLName xml.Name `xml:"Part"` + PartNumber int `xml:"PartNumber"` // Part number + ETag string `xml:"ETag"` // ETag value of the part's data +} + +type UploadParts []UploadPart + +func (slice UploadParts) Len() int { + return len(slice) +} + +func (slice UploadParts) Less(i, j int) bool { + return slice[i].PartNumber < slice[j].PartNumber +} + +func (slice UploadParts) Swap(i, j int) { + slice[i], slice[j] = slice[j], slice[i] +} + +// UploadPartCopyResult defines result object of multipart copy request. +type UploadPartCopyResult struct { + XMLName xml.Name `xml:"CopyPartResult"` + LastModified time.Time `xml:"LastModified"` // Last modified time + ETag string `xml:"ETag"` // ETag +} + +type completeMultipartUploadXML struct { + XMLName xml.Name `xml:"CompleteMultipartUpload"` + Part []UploadPart `xml:"Part"` +} + +// CompleteMultipartUploadResult defines result object of CompleteMultipartUploadRequest +type CompleteMultipartUploadResult struct { + XMLName xml.Name `xml:"CompleteMultipartUploadResult"` + Location string `xml:"Location"` // Object URL + Bucket string `xml:"Bucket"` // Bucket name + ETag string `xml:"ETag"` // Object ETag + Key string `xml:"Key"` // Object name +} + +// ListUploadedPartsResult defines result object of ListUploadedParts +type ListUploadedPartsResult struct { + XMLName xml.Name `xml:"ListPartsResult"` + Bucket string `xml:"Bucket"` // Bucket name + Key string `xml:"Key"` // Object name + UploadID string `xml:"UploadId"` // Upload ID + NextPartNumberMarker string `xml:"NextPartNumberMarker"` // Next part number + MaxParts int `xml:"MaxParts"` // Max parts count + IsTruncated bool `xml:"IsTruncated"` // Flag indicates all entries returned.false: all entries returned. + UploadedParts []UploadedPart `xml:"Part"` // Uploaded parts +} + +// UploadedPart defines uploaded part +type UploadedPart struct { + XMLName xml.Name `xml:"Part"` + PartNumber int `xml:"PartNumber"` // Part number + LastModified time.Time `xml:"LastModified"` // Last modified time + ETag string `xml:"ETag"` // ETag cache + Size int `xml:"Size"` // Part size +} + +// ListMultipartUploadResult defines result object of ListMultipartUpload +type ListMultipartUploadResult struct { + XMLName xml.Name `xml:"ListMultipartUploadsResult"` + Bucket string `xml:"Bucket"` // Bucket name + Delimiter string `xml:"Delimiter"` // Delimiter for grouping object. + Prefix string `xml:"Prefix"` // Object prefix + KeyMarker string `xml:"KeyMarker"` // Object key marker + UploadIDMarker string `xml:"UploadIdMarker"` // UploadId marker + NextKeyMarker string `xml:"NextKeyMarker"` // Next key marker, if not all entries returned. + NextUploadIDMarker string `xml:"NextUploadIdMarker"` // Next uploadId marker, if not all entries returned. + MaxUploads int `xml:"MaxUploads"` // Max uploads to return + IsTruncated bool `xml:"IsTruncated"` // Flag indicates all entries are returned. + Uploads []UncompletedUpload `xml:"Upload"` // Ongoing uploads (not completed, not aborted) + CommonPrefixes []string `xml:"CommonPrefixes>Prefix"` // Common prefixes list. +} + +// UncompletedUpload structure wraps an uncompleted upload task +type UncompletedUpload struct { + XMLName xml.Name `xml:"Upload"` + Key string `xml:"Key"` // Object name + UploadID string `xml:"UploadId"` // The UploadId + Initiated time.Time `xml:"Initiated"` // Initialization time in the format such as 2012-02-23T04:18:23.000Z +} + +// ProcessObjectResult defines result object of ProcessObject +type ProcessObjectResult struct { + Bucket string `json:"bucket"` + FileSize int `json:"fileSize"` + Object string `json:"object"` + Status string `json:"status"` +} + +// decodeDeleteObjectsResult decodes deleting objects result in URL encoding +func decodeDeleteObjectsResult(result *DeleteObjectVersionsResult) error { + var err error + for i := 0; i < len(result.DeletedObjectsDetail); i++ { + result.DeletedObjectsDetail[i].Key, err = url.QueryUnescape(result.DeletedObjectsDetail[i].Key) + if err != nil { + return err + } + } + return nil +} + +// decodeListObjectsResult decodes list objects result in URL encoding +func decodeListObjectsResult(result *ListObjectsResult) error { + var err error + result.Prefix, err = url.QueryUnescape(result.Prefix) + if err != nil { + return err + } + result.Marker, err = url.QueryUnescape(result.Marker) + if err != nil { + return err + } + result.Delimiter, err = url.QueryUnescape(result.Delimiter) + if err != nil { + return err + } + result.NextMarker, err = url.QueryUnescape(result.NextMarker) + if err != nil { + return err + } + for i := 0; i < len(result.Objects); i++ { + result.Objects[i].Key, err = url.QueryUnescape(result.Objects[i].Key) + if err != nil { + return err + } + } + for i := 0; i < len(result.CommonPrefixes); i++ { + result.CommonPrefixes[i], err = url.QueryUnescape(result.CommonPrefixes[i]) + if err != nil { + return err + } + } + return nil +} + +// decodeListObjectsResult decodes list objects result in URL encoding +func decodeListObjectsResultV2(result *ListObjectsResultV2) error { + var err error + result.Prefix, err = url.QueryUnescape(result.Prefix) + if err != nil { + return err + } + result.StartAfter, err = url.QueryUnescape(result.StartAfter) + if err != nil { + return err + } + result.Delimiter, err = url.QueryUnescape(result.Delimiter) + if err != nil { + return err + } + result.NextContinuationToken, err = url.QueryUnescape(result.NextContinuationToken) + if err != nil { + return err + } + for i := 0; i < len(result.Objects); i++ { + result.Objects[i].Key, err = url.QueryUnescape(result.Objects[i].Key) + if err != nil { + return err + } + } + for i := 0; i < len(result.CommonPrefixes); i++ { + result.CommonPrefixes[i], err = url.QueryUnescape(result.CommonPrefixes[i]) + if err != nil { + return err + } + } + return nil +} + +// decodeListObjectVersionsResult decodes list version objects result in URL encoding +func decodeListObjectVersionsResult(result *ListObjectVersionsResult) error { + var err error + + // decode:Delimiter + result.Delimiter, err = url.QueryUnescape(result.Delimiter) + if err != nil { + return err + } + + // decode Prefix + result.Prefix, err = url.QueryUnescape(result.Prefix) + if err != nil { + return err + } + + // decode KeyMarker + result.KeyMarker, err = url.QueryUnescape(result.KeyMarker) + if err != nil { + return err + } + + // decode VersionIdMarker + result.VersionIdMarker, err = url.QueryUnescape(result.VersionIdMarker) + if err != nil { + return err + } + + // decode NextKeyMarker + result.NextKeyMarker, err = url.QueryUnescape(result.NextKeyMarker) + if err != nil { + return err + } + + // decode NextVersionIdMarker + result.NextVersionIdMarker, err = url.QueryUnescape(result.NextVersionIdMarker) + if err != nil { + return err + } + + // decode CommonPrefixes + for i := 0; i < len(result.CommonPrefixes); i++ { + result.CommonPrefixes[i], err = url.QueryUnescape(result.CommonPrefixes[i]) + if err != nil { + return err + } + } + + // decode deleteMarker + for i := 0; i < len(result.ObjectDeleteMarkers); i++ { + result.ObjectDeleteMarkers[i].Key, err = url.QueryUnescape(result.ObjectDeleteMarkers[i].Key) + if err != nil { + return err + } + } + + // decode ObjectVersions + for i := 0; i < len(result.ObjectVersions); i++ { + result.ObjectVersions[i].Key, err = url.QueryUnescape(result.ObjectVersions[i].Key) + if err != nil { + return err + } + } + + return nil +} + +// decodeListUploadedPartsResult decodes +func decodeListUploadedPartsResult(result *ListUploadedPartsResult) error { + var err error + result.Key, err = url.QueryUnescape(result.Key) + if err != nil { + return err + } + return nil +} + +// decodeListMultipartUploadResult decodes list multipart upload result in URL encoding +func decodeListMultipartUploadResult(result *ListMultipartUploadResult) error { + var err error + result.Prefix, err = url.QueryUnescape(result.Prefix) + if err != nil { + return err + } + result.Delimiter, err = url.QueryUnescape(result.Delimiter) + if err != nil { + return err + } + result.KeyMarker, err = url.QueryUnescape(result.KeyMarker) + if err != nil { + return err + } + result.NextKeyMarker, err = url.QueryUnescape(result.NextKeyMarker) + if err != nil { + return err + } + for i := 0; i < len(result.Uploads); i++ { + result.Uploads[i].Key, err = url.QueryUnescape(result.Uploads[i].Key) + if err != nil { + return err + } + } + for i := 0; i < len(result.CommonPrefixes); i++ { + result.CommonPrefixes[i], err = url.QueryUnescape(result.CommonPrefixes[i]) + if err != nil { + return err + } + } + return nil +} + +// createBucketConfiguration defines the configuration for creating a bucket. +type createBucketConfiguration struct { + XMLName xml.Name `xml:"CreateBucketConfiguration"` + StorageClass StorageClassType `xml:"StorageClass,omitempty"` + DataRedundancyType DataRedundancyType `xml:"DataRedundancyType,omitempty"` +} + +// LiveChannelConfiguration defines the configuration for live-channel +type LiveChannelConfiguration struct { + XMLName xml.Name `xml:"LiveChannelConfiguration"` + Description string `xml:"Description,omitempty"` //Description of live-channel, up to 128 bytes + Status string `xml:"Status,omitempty"` //Specify the status of livechannel + Target LiveChannelTarget `xml:"Target"` //target configuration of live-channel + // use point instead of struct to avoid omit empty snapshot + Snapshot *LiveChannelSnapshot `xml:"Snapshot,omitempty"` //snapshot configuration of live-channel +} + +// LiveChannelTarget target configuration of live-channel +type LiveChannelTarget struct { + XMLName xml.Name `xml:"Target"` + Type string `xml:"Type"` //the type of object, only supports HLS + FragDuration int `xml:"FragDuration,omitempty"` //the length of each ts object (in seconds), in the range [1,100] + FragCount int `xml:"FragCount,omitempty"` //the number of ts objects in the m3u8 object, in the range of [1,100] + PlaylistName string `xml:"PlaylistName,omitempty"` //the name of m3u8 object, which must end with ".m3u8" and the length range is [6,128] +} + +// LiveChannelSnapshot snapshot configuration of live-channel +type LiveChannelSnapshot struct { + XMLName xml.Name `xml:"Snapshot"` + RoleName string `xml:"RoleName,omitempty"` //The role of snapshot operations, it sholud has write permission of DestBucket and the permission to send messages to the NotifyTopic. + DestBucket string `xml:"DestBucket,omitempty"` //Bucket the snapshots will be written to. should be the same owner as the source bucket. + NotifyTopic string `xml:"NotifyTopic,omitempty"` //Topics of MNS for notifying users of high frequency screenshot operation results + Interval int `xml:"Interval,omitempty"` //interval of snapshots, threre is no snapshot if no I-frame during the interval time +} + +// CreateLiveChannelResult the result of crete live-channel +type CreateLiveChannelResult struct { + XMLName xml.Name `xml:"CreateLiveChannelResult"` + PublishUrls []string `xml:"PublishUrls>Url"` //push urls list + PlayUrls []string `xml:"PlayUrls>Url"` //play urls list +} + +// LiveChannelStat the result of get live-channel state +type LiveChannelStat struct { + XMLName xml.Name `xml:"LiveChannelStat"` + Status string `xml:"Status"` //Current push status of live-channel: Disabled,Live,Idle + ConnectedTime time.Time `xml:"ConnectedTime"` //The time when the client starts pushing, format: ISO8601 + RemoteAddr string `xml:"RemoteAddr"` //The ip address of the client + Video LiveChannelVideo `xml:"Video"` //Video stream information + Audio LiveChannelAudio `xml:"Audio"` //Audio stream information +} + +// LiveChannelVideo video stream information +type LiveChannelVideo struct { + XMLName xml.Name `xml:"Video"` + Width int `xml:"Width"` //Width (unit: pixels) + Height int `xml:"Height"` //Height (unit: pixels) + FrameRate int `xml:"FrameRate"` //FramRate + Bandwidth int `xml:"Bandwidth"` //Bandwidth (unit: B/s) +} + +// LiveChannelAudio audio stream information +type LiveChannelAudio struct { + XMLName xml.Name `xml:"Audio"` + SampleRate int `xml:"SampleRate"` //SampleRate + Bandwidth int `xml:"Bandwidth"` //Bandwidth (unit: B/s) + Codec string `xml:"Codec"` //Encoding forma +} + +// LiveChannelHistory the result of GetLiveChannelHistory, at most return up to lastest 10 push records +type LiveChannelHistory struct { + XMLName xml.Name `xml:"LiveChannelHistory"` + Record []LiveRecord `xml:"LiveRecord"` //push records list +} + +// LiveRecord push recode +type LiveRecord struct { + XMLName xml.Name `xml:"LiveRecord"` + StartTime time.Time `xml:"StartTime"` //StartTime, format: ISO8601 + EndTime time.Time `xml:"EndTime"` //EndTime, format: ISO8601 + RemoteAddr string `xml:"RemoteAddr"` //The ip address of remote client +} + +// ListLiveChannelResult the result of ListLiveChannel +type ListLiveChannelResult struct { + XMLName xml.Name `xml:"ListLiveChannelResult"` + Prefix string `xml:"Prefix"` //Filter by the name start with the value of "Prefix" + Marker string `xml:"Marker"` //cursor from which starting list + MaxKeys int `xml:"MaxKeys"` //The maximum count returned. the default value is 100. it cannot be greater than 1000. + IsTruncated bool `xml:"IsTruncated"` //Indicates whether all results have been returned, "true" indicates partial results returned while "false" indicates all results have been returned + NextMarker string `xml:"NextMarker"` //NextMarker indicate the Marker value of the next request + LiveChannel []LiveChannelInfo `xml:"LiveChannel"` //The infomation of live-channel +} + +// LiveChannelInfo the infomation of live-channel +type LiveChannelInfo struct { + XMLName xml.Name `xml:"LiveChannel"` + Name string `xml:"Name"` //The name of live-channel + Description string `xml:"Description"` //Description of live-channel + Status string `xml:"Status"` //Status: disabled or enabled + LastModified time.Time `xml:"LastModified"` //Last modification time, format: ISO8601 + PublishUrls []string `xml:"PublishUrls>Url"` //push urls list + PlayUrls []string `xml:"PlayUrls>Url"` //play urls list +} + +// Tag a tag for the object +type Tag struct { + XMLName xml.Name `xml:"Tag"` + Key string `xml:"Key"` + Value string `xml:"Value"` +} + +// Tagging tagset for the object +type Tagging struct { + XMLName xml.Name `xml:"Tagging"` + Tags []Tag `xml:"TagSet>Tag,omitempty"` +} + +// for GetObjectTagging return value +type GetObjectTaggingResult Tagging + +// VersioningConfig for the bucket +type VersioningConfig struct { + XMLName xml.Name `xml:"VersioningConfiguration"` + Status string `xml:"Status"` +} + +type GetBucketVersioningResult VersioningConfig + +// Server Encryption rule for the bucket +type ServerEncryptionRule struct { + XMLName xml.Name `xml:"ServerSideEncryptionRule"` + SSEDefault SSEDefaultRule `xml:"ApplyServerSideEncryptionByDefault"` +} + +// Server Encryption deafult rule for the bucket +type SSEDefaultRule struct { + XMLName xml.Name `xml:"ApplyServerSideEncryptionByDefault"` + SSEAlgorithm string `xml:"SSEAlgorithm,omitempty"` + KMSMasterKeyID string `xml:"KMSMasterKeyID,omitempty"` + KMSDataEncryption string `xml:"KMSDataEncryption,,omitempty"` +} + +type GetBucketEncryptionResult ServerEncryptionRule +type GetBucketTaggingResult Tagging + +type BucketStat struct { + XMLName xml.Name `xml:"BucketStat"` + Storage int64 `xml:"Storage"` + ObjectCount int64 `xml:"ObjectCount"` + MultipartUploadCount int64 `xml:"MultipartUploadCount"` +} +type GetBucketStatResult BucketStat + +// RequestPaymentConfiguration define the request payment configuration +type RequestPaymentConfiguration struct { + XMLName xml.Name `xml:"RequestPaymentConfiguration"` + Payer string `xml:"Payer,omitempty"` +} + +// BucketQoSConfiguration define QoS configuration +type BucketQoSConfiguration struct { + XMLName xml.Name `xml:"QoSConfiguration"` + TotalUploadBandwidth *int `xml:"TotalUploadBandwidth"` // Total upload bandwidth + IntranetUploadBandwidth *int `xml:"IntranetUploadBandwidth"` // Intranet upload bandwidth + ExtranetUploadBandwidth *int `xml:"ExtranetUploadBandwidth"` // Extranet upload bandwidth + TotalDownloadBandwidth *int `xml:"TotalDownloadBandwidth"` // Total download bandwidth + IntranetDownloadBandwidth *int `xml:"IntranetDownloadBandwidth"` // Intranet download bandwidth + ExtranetDownloadBandwidth *int `xml:"ExtranetDownloadBandwidth"` // Extranet download bandwidth + TotalQPS *int `xml:"TotalQps"` // Total Qps + IntranetQPS *int `xml:"IntranetQps"` // Intranet Qps + ExtranetQPS *int `xml:"ExtranetQps"` // Extranet Qps +} + +// UserQoSConfiguration define QoS and Range configuration +type UserQoSConfiguration struct { + XMLName xml.Name `xml:"QoSConfiguration"` + Region string `xml:"Region,omitempty"` // Effective area of Qos configuration + BucketQoSConfiguration +} + +////////////////////////////////////////////////////////////// +/////////////////// Select OBject //////////////////////////// +////////////////////////////////////////////////////////////// + +type CsvMetaRequest struct { + XMLName xml.Name `xml:"CsvMetaRequest"` + InputSerialization InputSerialization `xml:"InputSerialization"` + OverwriteIfExists *bool `xml:"OverwriteIfExists,omitempty"` +} + +// encodeBase64 encode base64 of the CreateSelectObjectMeta api request params +func (meta *CsvMetaRequest) encodeBase64() { + meta.InputSerialization.CSV.RecordDelimiter = + base64.StdEncoding.EncodeToString([]byte(meta.InputSerialization.CSV.RecordDelimiter)) + meta.InputSerialization.CSV.FieldDelimiter = + base64.StdEncoding.EncodeToString([]byte(meta.InputSerialization.CSV.FieldDelimiter)) + meta.InputSerialization.CSV.QuoteCharacter = + base64.StdEncoding.EncodeToString([]byte(meta.InputSerialization.CSV.QuoteCharacter)) +} + +type JsonMetaRequest struct { + XMLName xml.Name `xml:"JsonMetaRequest"` + InputSerialization InputSerialization `xml:"InputSerialization"` + OverwriteIfExists *bool `xml:"OverwriteIfExists,omitempty"` +} + +type InputSerialization struct { + XMLName xml.Name `xml:"InputSerialization"` + CSV CSV `xml:CSV,omitempty` + JSON JSON `xml:JSON,omitempty` + CompressionType string `xml:"CompressionType,omitempty"` +} +type CSV struct { + XMLName xml.Name `xml:"CSV"` + RecordDelimiter string `xml:"RecordDelimiter,omitempty"` + FieldDelimiter string `xml:"FieldDelimiter,omitempty"` + QuoteCharacter string `xml:"QuoteCharacter,omitempty"` +} + +type JSON struct { + XMLName xml.Name `xml:"JSON"` + JSONType string `xml:"Type,omitempty"` +} + +// SelectRequest is for the SelectObject request params of json file +type SelectRequest struct { + XMLName xml.Name `xml:"SelectRequest"` + Expression string `xml:"Expression"` + InputSerializationSelect InputSerializationSelect `xml:"InputSerialization"` + OutputSerializationSelect OutputSerializationSelect `xml:"OutputSerialization"` + SelectOptions SelectOptions `xml:"Options,omitempty"` +} +type InputSerializationSelect struct { + XMLName xml.Name `xml:"InputSerialization"` + CsvBodyInput CSVSelectInput `xml:CSV,omitempty` + JsonBodyInput JSONSelectInput `xml:JSON,omitempty` + CompressionType string `xml:"CompressionType,omitempty"` +} +type CSVSelectInput struct { + XMLName xml.Name `xml:"CSV"` + FileHeaderInfo string `xml:"FileHeaderInfo,omitempty"` + RecordDelimiter string `xml:"RecordDelimiter,omitempty"` + FieldDelimiter string `xml:"FieldDelimiter,omitempty"` + QuoteCharacter string `xml:"QuoteCharacter,omitempty"` + CommentCharacter string `xml:"CommentCharacter,omitempty"` + Range string `xml:"Range,omitempty"` + SplitRange string +} +type JSONSelectInput struct { + XMLName xml.Name `xml:"JSON"` + JSONType string `xml:"Type,omitempty"` + Range string `xml:"Range,omitempty"` + ParseJSONNumberAsString *bool `xml:"ParseJsonNumberAsString"` + SplitRange string +} + +func (jsonInput *JSONSelectInput) JsonIsEmpty() bool { + if jsonInput.JSONType != "" { + return false + } + return true +} + +type OutputSerializationSelect struct { + XMLName xml.Name `xml:"OutputSerialization"` + CsvBodyOutput CSVSelectOutput `xml:CSV,omitempty` + JsonBodyOutput JSONSelectOutput `xml:JSON,omitempty` + OutputRawData *bool `xml:"OutputRawData,omitempty"` + KeepAllColumns *bool `xml:"KeepAllColumns,omitempty"` + EnablePayloadCrc *bool `xml:"EnablePayloadCrc,omitempty"` + OutputHeader *bool `xml:"OutputHeader,omitempty"` +} +type CSVSelectOutput struct { + XMLName xml.Name `xml:"CSV"` + RecordDelimiter string `xml:"RecordDelimiter,omitempty"` + FieldDelimiter string `xml:"FieldDelimiter,omitempty"` +} +type JSONSelectOutput struct { + XMLName xml.Name `xml:"JSON"` + RecordDelimiter string `xml:"RecordDelimiter,omitempty"` +} + +func (selectReq *SelectRequest) encodeBase64() { + if selectReq.InputSerializationSelect.JsonBodyInput.JsonIsEmpty() { + selectReq.csvEncodeBase64() + } else { + selectReq.jsonEncodeBase64() + } +} + +// csvEncodeBase64 encode base64 of the SelectObject api request params +func (selectReq *SelectRequest) csvEncodeBase64() { + selectReq.Expression = base64.StdEncoding.EncodeToString([]byte(selectReq.Expression)) + selectReq.InputSerializationSelect.CsvBodyInput.RecordDelimiter = + base64.StdEncoding.EncodeToString([]byte(selectReq.InputSerializationSelect.CsvBodyInput.RecordDelimiter)) + selectReq.InputSerializationSelect.CsvBodyInput.FieldDelimiter = + base64.StdEncoding.EncodeToString([]byte(selectReq.InputSerializationSelect.CsvBodyInput.FieldDelimiter)) + selectReq.InputSerializationSelect.CsvBodyInput.QuoteCharacter = + base64.StdEncoding.EncodeToString([]byte(selectReq.InputSerializationSelect.CsvBodyInput.QuoteCharacter)) + selectReq.InputSerializationSelect.CsvBodyInput.CommentCharacter = + base64.StdEncoding.EncodeToString([]byte(selectReq.InputSerializationSelect.CsvBodyInput.CommentCharacter)) + selectReq.OutputSerializationSelect.CsvBodyOutput.FieldDelimiter = + base64.StdEncoding.EncodeToString([]byte(selectReq.OutputSerializationSelect.CsvBodyOutput.FieldDelimiter)) + selectReq.OutputSerializationSelect.CsvBodyOutput.RecordDelimiter = + base64.StdEncoding.EncodeToString([]byte(selectReq.OutputSerializationSelect.CsvBodyOutput.RecordDelimiter)) + + // handle Range + if selectReq.InputSerializationSelect.CsvBodyInput.Range != "" { + selectReq.InputSerializationSelect.CsvBodyInput.Range = "line-range=" + selectReq.InputSerializationSelect.CsvBodyInput.Range + } + + if selectReq.InputSerializationSelect.CsvBodyInput.SplitRange != "" { + selectReq.InputSerializationSelect.CsvBodyInput.Range = "split-range=" + selectReq.InputSerializationSelect.CsvBodyInput.SplitRange + } +} + +// jsonEncodeBase64 encode base64 of the SelectObject api request params +func (selectReq *SelectRequest) jsonEncodeBase64() { + selectReq.Expression = base64.StdEncoding.EncodeToString([]byte(selectReq.Expression)) + selectReq.OutputSerializationSelect.JsonBodyOutput.RecordDelimiter = + base64.StdEncoding.EncodeToString([]byte(selectReq.OutputSerializationSelect.JsonBodyOutput.RecordDelimiter)) + + // handle Range + if selectReq.InputSerializationSelect.JsonBodyInput.Range != "" { + selectReq.InputSerializationSelect.JsonBodyInput.Range = "line-range=" + selectReq.InputSerializationSelect.JsonBodyInput.Range + } + + if selectReq.InputSerializationSelect.JsonBodyInput.SplitRange != "" { + selectReq.InputSerializationSelect.JsonBodyInput.Range = "split-range=" + selectReq.InputSerializationSelect.JsonBodyInput.SplitRange + } +} + +// CsvOptions is a element in the SelectObject api request's params +type SelectOptions struct { + XMLName xml.Name `xml:"Options"` + SkipPartialDataRecord *bool `xml:"SkipPartialDataRecord,omitempty"` + MaxSkippedRecordsAllowed string `xml:"MaxSkippedRecordsAllowed,omitempty"` +} + +// SelectObjectResult is the SelectObject api's return +type SelectObjectResult struct { + Version byte + FrameType int32 + PayloadLength int32 + HeaderCheckSum uint32 + Offset uint64 + Data string // DataFrame + EndFrame EndFrame // EndFrame + MetaEndFrameCSV MetaEndFrameCSV // MetaEndFrameCSV + MetaEndFrameJSON MetaEndFrameJSON // MetaEndFrameJSON + PayloadChecksum uint32 + ReadFlagInfo +} + +// ReadFlagInfo if reading the frame data, recode the reading status +type ReadFlagInfo struct { + OpenLine bool + ConsumedBytesLength int32 + EnablePayloadCrc bool + OutputRawData bool +} + +// EndFrame is EndFrameType of SelectObject api +type EndFrame struct { + TotalScanned int64 + HTTPStatusCode int32 + ErrorMsg string +} + +// MetaEndFrameCSV is MetaEndFrameCSVType of CreateSelectObjectMeta +type MetaEndFrameCSV struct { + TotalScanned int64 + Status int32 + SplitsCount int32 + RowsCount int64 + ColumnsCount int32 + ErrorMsg string +} + +// MetaEndFrameJSON is MetaEndFrameJSON of CreateSelectObjectMeta +type MetaEndFrameJSON struct { + TotalScanned int64 + Status int32 + SplitsCount int32 + RowsCount int64 + ErrorMsg string +} + +// InventoryConfiguration is Inventory config +type InventoryConfiguration struct { + XMLName xml.Name `xml:"InventoryConfiguration"` + Id string `xml:"Id,omitempty"` + IsEnabled *bool `xml:"IsEnabled,omitempty"` + Prefix string `xml:"Filter>Prefix,omitempty"` + OSSBucketDestination OSSBucketDestination `xml:"Destination>OSSBucketDestination,omitempty"` + Frequency string `xml:"Schedule>Frequency,omitempty"` + IncludedObjectVersions string `xml:"IncludedObjectVersions,omitempty"` + OptionalFields OptionalFields `xml:OptionalFields,omitempty` +} + +type OptionalFields struct { + XMLName xml.Name `xml:"OptionalFields,omitempty` + Field []string `xml:"Field,omitempty` +} + +type OSSBucketDestination struct { + XMLName xml.Name `xml:"OSSBucketDestination"` + Format string `xml:"Format,omitempty"` + AccountId string `xml:"AccountId,omitempty"` + RoleArn string `xml:"RoleArn,omitempty"` + Bucket string `xml:"Bucket,omitempty"` + Prefix string `xml:"Prefix,omitempty"` + Encryption *InvEncryption `xml:"Encryption,omitempty"` +} + +type InvEncryption struct { + XMLName xml.Name `xml:"Encryption"` + SseOss *InvSseOss `xml:"SSE-OSS"` + SseKms *InvSseKms `xml:"SSE-KMS"` +} + +type InvSseOss struct { + XMLName xml.Name `xml:"SSE-OSS"` +} + +type InvSseKms struct { + XMLName xml.Name `xml:"SSE-KMS"` + KmsId string `xml:"KeyId,omitempty"` +} + +type ListInventoryConfigurationsResult struct { + XMLName xml.Name `xml:"ListInventoryConfigurationsResult"` + InventoryConfiguration []InventoryConfiguration `xml:"InventoryConfiguration,omitempty` + IsTruncated *bool `xml:"IsTruncated,omitempty"` + NextContinuationToken string `xml:"NextContinuationToken,omitempty"` +} + +// RestoreConfiguration for RestoreObject +type RestoreConfiguration struct { + XMLName xml.Name `xml:"RestoreRequest"` + Days int32 `xml:"Days,omitempty"` + Tier string `xml:"JobParameters>Tier,omitempty"` +} + +// AsyncFetchTaskConfiguration for SetBucketAsyncFetchTask +type AsyncFetchTaskConfiguration struct { + XMLName xml.Name `xml:"AsyncFetchTaskConfiguration"` + Url string `xml:"Url,omitempty"` + Object string `xml:"Object,omitempty"` + Host string `xml:"Host,omitempty"` + ContentMD5 string `xml:"ContentMD5,omitempty"` + Callback string `xml:"Callback,omitempty"` + StorageClass string `xml:"StorageClass,omitempty"` + IgnoreSameKey bool `xml:"IgnoreSameKey"` +} + +// AsyncFetchTaskResult for SetBucketAsyncFetchTask result +type AsyncFetchTaskResult struct { + XMLName xml.Name `xml:"AsyncFetchTaskResult"` + TaskId string `xml:"TaskId,omitempty"` +} + +// AsynFetchTaskInfo for GetBucketAsyncFetchTask result +type AsynFetchTaskInfo struct { + XMLName xml.Name `xml:"AsyncFetchTaskInfo"` + TaskId string `xml:"TaskId,omitempty"` + State string `xml:"State,omitempty"` + ErrorMsg string `xml:"ErrorMsg,omitempty"` + TaskInfo AsyncTaskInfo `xml:"TaskInfo,omitempty"` +} + +// AsyncTaskInfo for async task information +type AsyncTaskInfo struct { + XMLName xml.Name `xml:"TaskInfo"` + Url string `xml:"Url,omitempty"` + Object string `xml:"Object,omitempty"` + Host string `xml:"Host,omitempty"` + ContentMD5 string `xml:"ContentMD5,omitempty"` + Callback string `xml:"Callback,omitempty"` + StorageClass string `xml:"StorageClass,omitempty"` + IgnoreSameKey bool `xml:"IgnoreSameKey"` +} + +// InitiateWormConfiguration define InitiateBucketWorm configuration +type InitiateWormConfiguration struct { + XMLName xml.Name `xml:"InitiateWormConfiguration"` + RetentionPeriodInDays int `xml:"RetentionPeriodInDays"` // specify retention days +} + +// ExtendWormConfiguration define ExtendWormConfiguration configuration +type ExtendWormConfiguration struct { + XMLName xml.Name `xml:"ExtendWormConfiguration"` + RetentionPeriodInDays int `xml:"RetentionPeriodInDays"` // specify retention days +} + +// WormConfiguration define WormConfiguration +type WormConfiguration struct { + XMLName xml.Name `xml:"WormConfiguration"` + WormId string `xml:"WormId,omitempty"` + State string `xml:"State,omitempty"` + RetentionPeriodInDays int `xml:"RetentionPeriodInDays"` // specify retention days + CreationDate string `xml:"CreationDate,omitempty"` +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/type_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/type_test.go new file mode 100644 index 00000000..caeaa05a --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/type_test.go @@ -0,0 +1,391 @@ +package oss + +import ( + "net/url" + "sort" + + . "gopkg.in/check.v1" +) + +type OssTypeSuite struct{} + +var _ = Suite(&OssTypeSuite{}) + +var ( + goStr = "go go + go <> go" + chnStr = "试问闲情几许" + goURLStr = url.QueryEscape(goStr) + chnURLStr = url.QueryEscape(chnStr) +) + +func (s *OssTypeSuite) TestDecodeDeleteObjectsResult(c *C) { + var res DeleteObjectVersionsResult + err := decodeDeleteObjectsResult(&res) + c.Assert(err, IsNil) + + res.DeletedObjectsDetail = []DeletedKeyInfo{DeletedKeyInfo{Key: ""}} + err = decodeDeleteObjectsResult(&res) + c.Assert(err, IsNil) + c.Assert(res.DeletedObjectsDetail[0].Key, Equals, "") + + res.DeletedObjectsDetail = []DeletedKeyInfo{DeletedKeyInfo{Key: goURLStr}, DeletedKeyInfo{Key: chnURLStr}} + err = decodeDeleteObjectsResult(&res) + c.Assert(err, IsNil) + c.Assert(res.DeletedObjectsDetail[0].Key, Equals, goStr) + c.Assert(res.DeletedObjectsDetail[1].Key, Equals, chnStr) +} + +func (s *OssTypeSuite) TestDecodeListObjectsResult(c *C) { + var res ListObjectsResult + err := decodeListObjectsResult(&res) + c.Assert(err, IsNil) + + res = ListObjectsResult{} + err = decodeListObjectsResult(&res) + c.Assert(err, IsNil) + + res = ListObjectsResult{Prefix: goURLStr, Marker: goURLStr, + Delimiter: goURLStr, NextMarker: goURLStr, + Objects: []ObjectProperties{{Key: chnURLStr}}, + CommonPrefixes: []string{chnURLStr}} + + err = decodeListObjectsResult(&res) + c.Assert(err, IsNil) + + c.Assert(res.Prefix, Equals, goStr) + c.Assert(res.Marker, Equals, goStr) + c.Assert(res.Delimiter, Equals, goStr) + c.Assert(res.NextMarker, Equals, goStr) + c.Assert(res.Objects[0].Key, Equals, chnStr) + c.Assert(res.CommonPrefixes[0], Equals, chnStr) +} + +func (s *OssTypeSuite) TestDecodeListMultipartUploadResult(c *C) { + res := ListMultipartUploadResult{} + err := decodeListMultipartUploadResult(&res) + c.Assert(err, IsNil) + + res = ListMultipartUploadResult{Prefix: goURLStr, KeyMarker: goURLStr, + Delimiter: goURLStr, NextKeyMarker: goURLStr, + Uploads: []UncompletedUpload{{Key: chnURLStr}}} + + err = decodeListMultipartUploadResult(&res) + c.Assert(err, IsNil) + + c.Assert(res.Prefix, Equals, goStr) + c.Assert(res.KeyMarker, Equals, goStr) + c.Assert(res.Delimiter, Equals, goStr) + c.Assert(res.NextKeyMarker, Equals, goStr) + c.Assert(res.Uploads[0].Key, Equals, chnStr) +} + +func (s *OssTypeSuite) TestSortUploadPart(c *C) { + parts := []UploadPart{} + + sort.Sort(UploadParts(parts)) + c.Assert(len(parts), Equals, 0) + + parts = []UploadPart{ + {PartNumber: 5, ETag: "E5"}, + {PartNumber: 1, ETag: "E1"}, + {PartNumber: 4, ETag: "E4"}, + {PartNumber: 2, ETag: "E2"}, + {PartNumber: 3, ETag: "E3"}, + } + + sort.Sort(UploadParts(parts)) + + c.Assert(parts[0].PartNumber, Equals, 1) + c.Assert(parts[0].ETag, Equals, "E1") + c.Assert(parts[1].PartNumber, Equals, 2) + c.Assert(parts[1].ETag, Equals, "E2") + c.Assert(parts[2].PartNumber, Equals, 3) + c.Assert(parts[2].ETag, Equals, "E3") + c.Assert(parts[3].PartNumber, Equals, 4) + c.Assert(parts[3].ETag, Equals, "E4") + c.Assert(parts[4].PartNumber, Equals, 5) + c.Assert(parts[4].ETag, Equals, "E5") +} + +func (s *OssTypeSuite) TestValidateLifecleRules(c *C) { + expiration := LifecycleExpiration{ + Days: 30, + CreatedBeforeDate: "2015-11-11T00:00:00.000Z", + } + rule := LifecycleRule{ + ID: "ruleID", + Prefix: "prefix", + Status: "Enabled", + Expiration: &expiration, + } + rules := []LifecycleRule{rule} + err := verifyLifecycleRules(rules) + c.Assert(err, IsNil) + + expiration = LifecycleExpiration{ + Date: "2015-11-11T00:00:00.000Z", + CreatedBeforeDate: "2015-11-11T00:00:00.000Z", + } + rule = LifecycleRule{ + ID: "ruleID", + Prefix: "prefix", + Status: "Enabled", + Expiration: &expiration, + } + rules = []LifecycleRule{rule} + err = verifyLifecycleRules(rules) + c.Assert(err, IsNil) + + expiration = LifecycleExpiration{ + Days: 0, + CreatedBeforeDate: "", + Date: "", + } + rule = LifecycleRule{ + ID: "ruleID", + Prefix: "prefix", + Status: "Enabled", + Expiration: &expiration, + } + rules = []LifecycleRule{rule} + err = verifyLifecycleRules(rules) + c.Assert(err, IsNil) + + abortMPU := LifecycleAbortMultipartUpload{ + Days: 30, + CreatedBeforeDate: "2015-11-11T00:00:00.000Z", + } + rule = LifecycleRule{ + ID: "ruleID", + Prefix: "prefix", + Status: "Enabled", + AbortMultipartUpload: &abortMPU, + } + rules = []LifecycleRule{rule} + err = verifyLifecycleRules(rules) + c.Assert(err, NotNil) + + abortMPU = LifecycleAbortMultipartUpload{ + Days: 0, + CreatedBeforeDate: "", + } + rule = LifecycleRule{ + ID: "ruleID", + Prefix: "prefix", + Status: "Enabled", + AbortMultipartUpload: &abortMPU, + } + rules = []LifecycleRule{rule} + err = verifyLifecycleRules(rules) + c.Assert(err, NotNil) + + transition := LifecycleTransition{ + Days: 30, + CreatedBeforeDate: "2015-11-11T00:00:00.000Z", + StorageClass: StorageIA, + } + rule = LifecycleRule{ + ID: "ruleID", + Prefix: "prefix", + Status: "Enabled", + Transitions: []LifecycleTransition{transition}, + } + rules = []LifecycleRule{rule} + err = verifyLifecycleRules(rules) + c.Assert(err, NotNil) + + transition = LifecycleTransition{ + Days: 0, + CreatedBeforeDate: "", + StorageClass: StorageIA, + } + rule = LifecycleRule{ + ID: "ruleID", + Prefix: "prefix", + Status: "Enabled", + Transitions: []LifecycleTransition{transition}, + } + rules = []LifecycleRule{rule} + err = verifyLifecycleRules(rules) + c.Assert(err, NotNil) + + transition = LifecycleTransition{ + Days: 30, + StorageClass: StorageStandard, + } + rule = LifecycleRule{ + ID: "ruleID", + Prefix: "prefix", + Status: "Enabled", + Transitions: []LifecycleTransition{transition}, + } + rules = []LifecycleRule{rule} + err = verifyLifecycleRules(rules) + c.Assert(err, IsNil) + + transition = LifecycleTransition{ + CreatedBeforeDate: "2015-11-11T00:00:00.000Z", + StorageClass: StorageStandard, + } + rule = LifecycleRule{ + ID: "ruleID", + Prefix: "prefix", + Status: "Enabled", + Transitions: []LifecycleTransition{transition}, + } + rules = []LifecycleRule{rule} + err = verifyLifecycleRules(rules) + c.Assert(err, IsNil) + + transition1 := LifecycleTransition{ + Days: 30, + StorageClass: StorageIA, + } + transition2 := LifecycleTransition{ + Days: 60, + StorageClass: StorageArchive, + } + transition3 := LifecycleTransition{ + Days: 100, + StorageClass: StorageArchive, + } + rule = LifecycleRule{ + ID: "ruleID", + Prefix: "prefix", + Status: "Enabled", + Transitions: []LifecycleTransition{transition1, transition2, transition3}, + } + rules = []LifecycleRule{rule} + err = verifyLifecycleRules(rules) + c.Assert(err, IsNil) + + rule = LifecycleRule{ + ID: "ruleID", + Prefix: "prefix", + Status: "Enabled", + } + rules = []LifecycleRule{rule} + err = verifyLifecycleRules(rules) + c.Assert(err, IsNil) + + rules = []LifecycleRule{} + err1 := verifyLifecycleRules(rules) + c.Assert(err1, NotNil) + + expiration = LifecycleExpiration{ + Days: 30, + } + rule = LifecycleRule{ + ID: "ruleID", + Prefix: "prefix", + Status: "Enabled", + Expiration: &expiration, + } + rules = []LifecycleRule{rule} + err = verifyLifecycleRules(rules) + c.Assert(err, IsNil) + + expiration = LifecycleExpiration{ + CreatedBeforeDate: "2015-11-11T00:00:00.000Z", + } + rule = LifecycleRule{ + ID: "ruleID", + Prefix: "prefix", + Status: "Enabled", + Expiration: &expiration, + } + rules = []LifecycleRule{rule} + err = verifyLifecycleRules(rules) + c.Assert(err, IsNil) + + abortMPU = LifecycleAbortMultipartUpload{ + Days: 30, + } + rule = LifecycleRule{ + ID: "ruleID", + Prefix: "prefix", + Status: "Enabled", + AbortMultipartUpload: &abortMPU, + } + rules = []LifecycleRule{rule} + err = verifyLifecycleRules(rules) + c.Assert(err, IsNil) + + abortMPU = LifecycleAbortMultipartUpload{ + CreatedBeforeDate: "2015-11-11T00:00:00.000Z", + } + rule = LifecycleRule{ + ID: "ruleID", + Prefix: "prefix", + Status: "Enabled", + AbortMultipartUpload: &abortMPU, + } + rules = []LifecycleRule{rule} + err = verifyLifecycleRules(rules) + c.Assert(err, IsNil) + + expiration = LifecycleExpiration{ + Days: 30, + } + abortMPU = LifecycleAbortMultipartUpload{ + Days: 30, + } + rule = LifecycleRule{ + ID: "ruleID", + Prefix: "prefix", + Status: "Enabled", + Expiration: &expiration, + AbortMultipartUpload: &abortMPU, + } + rules = []LifecycleRule{rule} + err = verifyLifecycleRules(rules) + c.Assert(err, IsNil) + + expiration = LifecycleExpiration{ + CreatedBeforeDate: "2015-11-11T00:00:00.000Z", + } + abortMPU = LifecycleAbortMultipartUpload{ + Days: 30, + } + transition = LifecycleTransition{ + Days: 30, + StorageClass: StorageIA, + } + rule = LifecycleRule{ + ID: "ruleID", + Prefix: "prefix", + Status: "Enabled", + Expiration: &expiration, + AbortMultipartUpload: &abortMPU, + Transitions: []LifecycleTransition{transition}, + } + rules = []LifecycleRule{rule} + err = verifyLifecycleRules(rules) + c.Assert(err, IsNil) + + expiration = LifecycleExpiration{ + CreatedBeforeDate: "2015-11-11T00:00:00.000Z", + } + abortMPU = LifecycleAbortMultipartUpload{ + Days: 30, + } + transition1 = LifecycleTransition{ + Days: 30, + StorageClass: StorageIA, + } + transition2 = LifecycleTransition{ + Days: 60, + StorageClass: StorageArchive, + } + rule = LifecycleRule{ + ID: "ruleID", + Prefix: "prefix", + Status: "Enabled", + Expiration: &expiration, + AbortMultipartUpload: &abortMPU, + Transitions: []LifecycleTransition{transition1, transition2}, + } + rules = []LifecycleRule{rule} + err = verifyLifecycleRules(rules) + c.Assert(err, IsNil) +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/upload.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/upload.go new file mode 100644 index 00000000..8b3ea09d --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/upload.go @@ -0,0 +1,552 @@ +package oss + +import ( + "crypto/md5" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "time" +) + +// UploadFile is multipart file upload. +// +// objectKey the object name. +// filePath the local file path to upload. +// partSize the part size in byte. +// options the options for uploading object. +// +// error it's nil if the operation succeeds, otherwise it's an error object. +// +func (bucket Bucket) UploadFile(objectKey, filePath string, partSize int64, options ...Option) error { + if partSize < MinPartSize || partSize > MaxPartSize { + return errors.New("oss: part size invalid range (100KB, 5GB]") + } + + cpConf := getCpConfig(options) + routines := getRoutines(options) + + if cpConf != nil && cpConf.IsEnable { + cpFilePath := getUploadCpFilePath(cpConf, filePath, bucket.BucketName, objectKey) + if cpFilePath != "" { + return bucket.uploadFileWithCp(objectKey, filePath, partSize, options, cpFilePath, routines) + } + } + + return bucket.uploadFile(objectKey, filePath, partSize, options, routines) +} + +func getUploadCpFilePath(cpConf *cpConfig, srcFile, destBucket, destObject string) string { + if cpConf.FilePath == "" && cpConf.DirPath != "" { + dest := fmt.Sprintf("oss://%v/%v", destBucket, destObject) + absPath, _ := filepath.Abs(srcFile) + cpFileName := getCpFileName(absPath, dest, "") + cpConf.FilePath = cpConf.DirPath + string(os.PathSeparator) + cpFileName + } + return cpConf.FilePath +} + +// ----- concurrent upload without checkpoint ----- + +// getCpConfig gets checkpoint configuration +func getCpConfig(options []Option) *cpConfig { + cpcOpt, err := FindOption(options, checkpointConfig, nil) + if err != nil || cpcOpt == nil { + return nil + } + + return cpcOpt.(*cpConfig) +} + +// getCpFileName return the name of the checkpoint file +func getCpFileName(src, dest, versionId string) string { + md5Ctx := md5.New() + md5Ctx.Write([]byte(src)) + srcCheckSum := hex.EncodeToString(md5Ctx.Sum(nil)) + + md5Ctx.Reset() + md5Ctx.Write([]byte(dest)) + destCheckSum := hex.EncodeToString(md5Ctx.Sum(nil)) + + if versionId == "" { + return fmt.Sprintf("%v-%v.cp", srcCheckSum, destCheckSum) + } + + md5Ctx.Reset() + md5Ctx.Write([]byte(versionId)) + versionCheckSum := hex.EncodeToString(md5Ctx.Sum(nil)) + return fmt.Sprintf("%v-%v-%v.cp", srcCheckSum, destCheckSum, versionCheckSum) +} + +// getRoutines gets the routine count. by default it's 1. +func getRoutines(options []Option) int { + rtnOpt, err := FindOption(options, routineNum, nil) + if err != nil || rtnOpt == nil { + return 1 + } + + rs := rtnOpt.(int) + if rs < 1 { + rs = 1 + } else if rs > 100 { + rs = 100 + } + + return rs +} + +// getPayer return the payer of the request +func getPayer(options []Option) string { + payerOpt, err := FindOption(options, HTTPHeaderOssRequester, nil) + if err != nil || payerOpt == nil { + return "" + } + return payerOpt.(string) +} + +// GetProgressListener gets the progress callback +func GetProgressListener(options []Option) ProgressListener { + isSet, listener, _ := IsOptionSet(options, progressListener) + if !isSet { + return nil + } + return listener.(ProgressListener) +} + +// uploadPartHook is for testing usage +type uploadPartHook func(id int, chunk FileChunk) error + +var uploadPartHooker uploadPartHook = defaultUploadPart + +func defaultUploadPart(id int, chunk FileChunk) error { + return nil +} + +// workerArg defines worker argument structure +type workerArg struct { + bucket *Bucket + filePath string + imur InitiateMultipartUploadResult + options []Option + hook uploadPartHook +} + +// worker is the worker coroutine function +type defaultUploadProgressListener struct { +} + +// ProgressChanged no-ops +func (listener *defaultUploadProgressListener) ProgressChanged(event *ProgressEvent) { +} + +func worker(id int, arg workerArg, jobs <-chan FileChunk, results chan<- UploadPart, failed chan<- error, die <-chan bool) { + for chunk := range jobs { + if err := arg.hook(id, chunk); err != nil { + failed <- err + break + } + var respHeader http.Header + p := Progress(&defaultUploadProgressListener{}) + opts := make([]Option, len(arg.options)+2) + opts = append(opts, arg.options...) + + // use defaultUploadProgressListener + opts = append(opts, p, GetResponseHeader(&respHeader)) + + startT := time.Now().UnixNano() / 1000 / 1000 / 1000 + part, err := arg.bucket.UploadPartFromFile(arg.imur, arg.filePath, chunk.Offset, chunk.Size, chunk.Number, opts...) + endT := time.Now().UnixNano() / 1000 / 1000 / 1000 + if err != nil { + arg.bucket.Client.Config.WriteLog(Debug, "upload part error,cost:%d second,part number:%d,request id:%s,error:%s\n", endT-startT, chunk.Number, GetRequestId(respHeader), err.Error()) + failed <- err + break + } + select { + case <-die: + return + default: + } + results <- part + } +} + +// scheduler function +func scheduler(jobs chan FileChunk, chunks []FileChunk) { + for _, chunk := range chunks { + jobs <- chunk + } + close(jobs) +} + +func getTotalBytes(chunks []FileChunk) int64 { + var tb int64 + for _, chunk := range chunks { + tb += chunk.Size + } + return tb +} + +// uploadFile is a concurrent upload, without checkpoint +func (bucket Bucket) uploadFile(objectKey, filePath string, partSize int64, options []Option, routines int) error { + listener := GetProgressListener(options) + + chunks, err := SplitFileByPartSize(filePath, partSize) + if err != nil { + return err + } + + partOptions := ChoiceTransferPartOption(options) + completeOptions := ChoiceCompletePartOption(options) + abortOptions := ChoiceAbortPartOption(options) + + // Initialize the multipart upload + imur, err := bucket.InitiateMultipartUpload(objectKey, options...) + if err != nil { + return err + } + + jobs := make(chan FileChunk, len(chunks)) + results := make(chan UploadPart, len(chunks)) + failed := make(chan error) + die := make(chan bool) + + var completedBytes int64 + totalBytes := getTotalBytes(chunks) + event := newProgressEvent(TransferStartedEvent, 0, totalBytes, 0) + publishProgress(listener, event) + + // Start the worker coroutine + arg := workerArg{&bucket, filePath, imur, partOptions, uploadPartHooker} + for w := 1; w <= routines; w++ { + go worker(w, arg, jobs, results, failed, die) + } + + // Schedule the jobs + go scheduler(jobs, chunks) + + // Waiting for the upload finished + completed := 0 + parts := make([]UploadPart, len(chunks)) + for completed < len(chunks) { + select { + case part := <-results: + completed++ + parts[part.PartNumber-1] = part + completedBytes += chunks[part.PartNumber-1].Size + + // why RwBytes in ProgressEvent is 0 ? + // because read or write event has been notified in teeReader.Read() + event = newProgressEvent(TransferDataEvent, completedBytes, totalBytes, chunks[part.PartNumber-1].Size) + publishProgress(listener, event) + case err := <-failed: + close(die) + event = newProgressEvent(TransferFailedEvent, completedBytes, totalBytes, 0) + publishProgress(listener, event) + bucket.AbortMultipartUpload(imur, abortOptions...) + return err + } + + if completed >= len(chunks) { + break + } + } + + event = newProgressEvent(TransferStartedEvent, completedBytes, totalBytes, 0) + publishProgress(listener, event) + + // Complete the multpart upload + _, err = bucket.CompleteMultipartUpload(imur, parts, completeOptions...) + if err != nil { + bucket.AbortMultipartUpload(imur, abortOptions...) + return err + } + return nil +} + +// ----- concurrent upload with checkpoint ----- +const uploadCpMagic = "FE8BB4EA-B593-4FAC-AD7A-2459A36E2E62" + +type uploadCheckpoint struct { + Magic string // Magic + MD5 string // Checkpoint file content's MD5 + FilePath string // Local file path + FileStat cpStat // File state + ObjectKey string // Key + UploadID string // Upload ID + Parts []cpPart // All parts of the local file +} + +type cpStat struct { + Size int64 // File size + LastModified time.Time // File's last modified time + MD5 string // Local file's MD5 +} + +type cpPart struct { + Chunk FileChunk // File chunk + Part UploadPart // Uploaded part + IsCompleted bool // Upload complete flag +} + +// isValid checks if the uploaded data is valid---it's valid when the file is not updated and the checkpoint data is valid. +func (cp uploadCheckpoint) isValid(filePath string) (bool, error) { + // Compare the CP's magic number and MD5. + cpb := cp + cpb.MD5 = "" + js, _ := json.Marshal(cpb) + sum := md5.Sum(js) + b64 := base64.StdEncoding.EncodeToString(sum[:]) + + if cp.Magic != uploadCpMagic || b64 != cp.MD5 { + return false, nil + } + + // Make sure if the local file is updated. + fd, err := os.Open(filePath) + if err != nil { + return false, err + } + defer fd.Close() + + st, err := fd.Stat() + if err != nil { + return false, err + } + + md, err := calcFileMD5(filePath) + if err != nil { + return false, err + } + + // Compare the file size, file's last modified time and file's MD5 + if cp.FileStat.Size != st.Size() || + !cp.FileStat.LastModified.Equal(st.ModTime()) || + cp.FileStat.MD5 != md { + return false, nil + } + + return true, nil +} + +// load loads from the file +func (cp *uploadCheckpoint) load(filePath string) error { + contents, err := ioutil.ReadFile(filePath) + if err != nil { + return err + } + + err = json.Unmarshal(contents, cp) + return err +} + +// dump dumps to the local file +func (cp *uploadCheckpoint) dump(filePath string) error { + bcp := *cp + + // Calculate MD5 + bcp.MD5 = "" + js, err := json.Marshal(bcp) + if err != nil { + return err + } + sum := md5.Sum(js) + b64 := base64.StdEncoding.EncodeToString(sum[:]) + bcp.MD5 = b64 + + // Serialization + js, err = json.Marshal(bcp) + if err != nil { + return err + } + + // Dump + return ioutil.WriteFile(filePath, js, FilePermMode) +} + +// updatePart updates the part status +func (cp *uploadCheckpoint) updatePart(part UploadPart) { + cp.Parts[part.PartNumber-1].Part = part + cp.Parts[part.PartNumber-1].IsCompleted = true +} + +// todoParts returns unfinished parts +func (cp *uploadCheckpoint) todoParts() []FileChunk { + fcs := []FileChunk{} + for _, part := range cp.Parts { + if !part.IsCompleted { + fcs = append(fcs, part.Chunk) + } + } + return fcs +} + +// allParts returns all parts +func (cp *uploadCheckpoint) allParts() []UploadPart { + ps := []UploadPart{} + for _, part := range cp.Parts { + ps = append(ps, part.Part) + } + return ps +} + +// getCompletedBytes returns completed bytes count +func (cp *uploadCheckpoint) getCompletedBytes() int64 { + var completedBytes int64 + for _, part := range cp.Parts { + if part.IsCompleted { + completedBytes += part.Chunk.Size + } + } + return completedBytes +} + +// calcFileMD5 calculates the MD5 for the specified local file +func calcFileMD5(filePath string) (string, error) { + return "", nil +} + +// prepare initializes the multipart upload +func prepare(cp *uploadCheckpoint, objectKey, filePath string, partSize int64, bucket *Bucket, options []Option) error { + // CP + cp.Magic = uploadCpMagic + cp.FilePath = filePath + cp.ObjectKey = objectKey + + // Local file + fd, err := os.Open(filePath) + if err != nil { + return err + } + defer fd.Close() + + st, err := fd.Stat() + if err != nil { + return err + } + cp.FileStat.Size = st.Size() + cp.FileStat.LastModified = st.ModTime() + md, err := calcFileMD5(filePath) + if err != nil { + return err + } + cp.FileStat.MD5 = md + + // Chunks + parts, err := SplitFileByPartSize(filePath, partSize) + if err != nil { + return err + } + + cp.Parts = make([]cpPart, len(parts)) + for i, part := range parts { + cp.Parts[i].Chunk = part + cp.Parts[i].IsCompleted = false + } + + // Init load + imur, err := bucket.InitiateMultipartUpload(objectKey, options...) + if err != nil { + return err + } + cp.UploadID = imur.UploadID + + return nil +} + +// complete completes the multipart upload and deletes the local CP files +func complete(cp *uploadCheckpoint, bucket *Bucket, parts []UploadPart, cpFilePath string, options []Option) error { + imur := InitiateMultipartUploadResult{Bucket: bucket.BucketName, + Key: cp.ObjectKey, UploadID: cp.UploadID} + _, err := bucket.CompleteMultipartUpload(imur, parts, options...) + if err != nil { + return err + } + os.Remove(cpFilePath) + return err +} + +// uploadFileWithCp handles concurrent upload with checkpoint +func (bucket Bucket) uploadFileWithCp(objectKey, filePath string, partSize int64, options []Option, cpFilePath string, routines int) error { + listener := GetProgressListener(options) + + partOptions := ChoiceTransferPartOption(options) + completeOptions := ChoiceCompletePartOption(options) + + // Load CP data + ucp := uploadCheckpoint{} + err := ucp.load(cpFilePath) + if err != nil { + os.Remove(cpFilePath) + } + + // Load error or the CP data is invalid. + valid, err := ucp.isValid(filePath) + if err != nil || !valid { + if err = prepare(&ucp, objectKey, filePath, partSize, &bucket, options); err != nil { + return err + } + os.Remove(cpFilePath) + } + + chunks := ucp.todoParts() + imur := InitiateMultipartUploadResult{ + Bucket: bucket.BucketName, + Key: objectKey, + UploadID: ucp.UploadID} + + jobs := make(chan FileChunk, len(chunks)) + results := make(chan UploadPart, len(chunks)) + failed := make(chan error) + die := make(chan bool) + + completedBytes := ucp.getCompletedBytes() + + // why RwBytes in ProgressEvent is 0 ? + // because read or write event has been notified in teeReader.Read() + event := newProgressEvent(TransferStartedEvent, completedBytes, ucp.FileStat.Size, 0) + publishProgress(listener, event) + + // Start the workers + arg := workerArg{&bucket, filePath, imur, partOptions, uploadPartHooker} + for w := 1; w <= routines; w++ { + go worker(w, arg, jobs, results, failed, die) + } + + // Schedule jobs + go scheduler(jobs, chunks) + + // Waiting for the job finished + completed := 0 + for completed < len(chunks) { + select { + case part := <-results: + completed++ + ucp.updatePart(part) + ucp.dump(cpFilePath) + completedBytes += ucp.Parts[part.PartNumber-1].Chunk.Size + event = newProgressEvent(TransferDataEvent, completedBytes, ucp.FileStat.Size, ucp.Parts[part.PartNumber-1].Chunk.Size) + publishProgress(listener, event) + case err := <-failed: + close(die) + event = newProgressEvent(TransferFailedEvent, completedBytes, ucp.FileStat.Size, 0) + publishProgress(listener, event) + return err + } + + if completed >= len(chunks) { + break + } + } + + event = newProgressEvent(TransferCompletedEvent, completedBytes, ucp.FileStat.Size, 0) + publishProgress(listener, event) + + // Complete the multipart upload + err = complete(&ucp, &bucket, ucp.allParts(), cpFilePath, completeOptions) + return err +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/upload_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/upload_test.go new file mode 100644 index 00000000..7368949a --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/upload_test.go @@ -0,0 +1,741 @@ +package oss + +import ( + "fmt" + "io" + "net/http" + "os" + "time" + + . "gopkg.in/check.v1" +) + +type OssUploadSuite struct { + client *Client + bucket *Bucket +} + +var _ = Suite(&OssUploadSuite{}) + +// SetUpSuite runs once when the suite starts running +func (s *OssUploadSuite) SetUpSuite(c *C) { + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + s.client = client + + s.client.CreateBucket(bucketName) + + bucket, err := s.client.Bucket(bucketName) + c.Assert(err, IsNil) + s.bucket = bucket + + testLogger.Println("test upload started") +} + +// TearDownSuite runs before each test or benchmark starts running +func (s *OssUploadSuite) TearDownSuite(c *C) { + // Delete part + keyMarker := KeyMarker("") + uploadIDMarker := UploadIDMarker("") + for { + lmur, err := s.bucket.ListMultipartUploads(keyMarker, uploadIDMarker) + c.Assert(err, IsNil) + for _, upload := range lmur.Uploads { + var imur = InitiateMultipartUploadResult{Bucket: s.bucket.BucketName, + Key: upload.Key, UploadID: upload.UploadID} + err = s.bucket.AbortMultipartUpload(imur) + c.Assert(err, IsNil) + } + keyMarker = KeyMarker(lmur.NextKeyMarker) + uploadIDMarker = UploadIDMarker(lmur.NextUploadIDMarker) + if !lmur.IsTruncated { + break + } + } + + // Delete objects + marker := Marker("") + for { + lor, err := s.bucket.ListObjects(marker) + c.Assert(err, IsNil) + for _, object := range lor.Objects { + err = s.bucket.DeleteObject(object.Key) + c.Assert(err, IsNil) + } + marker = Marker(lor.NextMarker) + if !lor.IsTruncated { + break + } + } + + // Delete bucket + err := s.client.DeleteBucket(s.bucket.BucketName) + c.Assert(err, IsNil) + + testLogger.Println("test upload completed") +} + +// SetUpTest runs after each test or benchmark runs +func (s *OssUploadSuite) SetUpTest(c *C) { + err := removeTempFiles("../oss", ".jpg") + c.Assert(err, IsNil) +} + +// TearDownTest runs once after all tests or benchmarks have finished running +func (s *OssUploadSuite) TearDownTest(c *C) { + err := removeTempFiles("../oss", ".jpg") + c.Assert(err, IsNil) +} + +// TestUploadRoutineWithoutRecovery tests multiroutineed upload without checkpoint +func (s *OssUploadSuite) TestUploadRoutineWithoutRecovery(c *C) { + objectName := objectNamePrefix + RandStr(8) + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + newFile := RandStr(8) + ".jpg" + + // Routines is not specified, by default single routine + err := s.bucket.UploadFile(objectName, fileName, 100*1024) + c.Assert(err, IsNil) + + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err := compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Specify routine count as 1 + err = s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(1)) + c.Assert(err, IsNil) + + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Specify routine count as 3, which is smaller than parts count 5 + err = s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(3)) + c.Assert(err, IsNil) + + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Specify routine count as 5, which is same as the part count 5 + err = s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(5)) + c.Assert(err, IsNil) + + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Specify routine count as 10, which is bigger than the part count 5. + err = s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(10)) + c.Assert(err, IsNil) + + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Invalid routine count, it will use 1 automatically. + err = s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(0)) + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Invalid routine count, it will use 1 automatically + err = s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(-1)) + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Option + err = s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(3), Meta("myprop", "mypropval")) + + meta, err := s.bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + c.Assert(meta.Get("X-Oss-Meta-Myprop"), Equals, "mypropval") + + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +// ErrorHooker is a UploadPart hook---it will fail the 5th part's upload. +func ErrorHooker(id int, chunk FileChunk) error { + if chunk.Number == 5 { + time.Sleep(time.Second) + return fmt.Errorf("ErrorHooker") + } + return nil +} + +// TestUploadRoutineWithoutRecoveryNegative is multiroutineed upload without checkpoint +func (s *OssUploadSuite) TestUploadRoutineWithoutRecoveryNegative(c *C) { + objectName := objectNamePrefix + RandStr(8) + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + + uploadPartHooker = ErrorHooker + // Worker routine error + err := s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(2)) + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "ErrorHooker") + uploadPartHooker = defaultUploadPart + + // Local file does not exist + err = s.bucket.UploadFile(objectName, "NotExist", 100*1024, Routines(2)) + c.Assert(err, NotNil) + + // The part size is invalid + err = s.bucket.UploadFile(objectName, fileName, 1024, Routines(2)) + c.Assert(err, NotNil) + + err = s.bucket.UploadFile(objectName, fileName, 1024*1024*1024*100, Routines(2)) + c.Assert(err, NotNil) +} + +// TestUploadRoutineWithRecovery is multi-routine upload with resumable recovery +func (s *OssUploadSuite) TestUploadRoutineWithRecovery(c *C) { + objectName := objectNamePrefix + RandStr(8) + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + newFile := "upload-new-file-2.jpg" + + // Use default routines and default CP file path (fileName+.cp) + // First upload for 4 parts + uploadPartHooker = ErrorHooker + err := s.bucket.UploadFile(objectName, fileName, 100*1024, Checkpoint(true, fileName+".cp")) + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "ErrorHooker") + uploadPartHooker = defaultUploadPart + + // Check CP + ucp := uploadCheckpoint{} + err = ucp.load(fileName + ".cp") + c.Assert(err, IsNil) + c.Assert(ucp.Magic, Equals, uploadCpMagic) + c.Assert(len(ucp.MD5), Equals, len("LC34jZU5xK4hlxi3Qn3XGQ==")) + c.Assert(ucp.FilePath, Equals, fileName) + c.Assert(ucp.FileStat.Size, Equals, int64(482048)) + c.Assert(len(ucp.FileStat.LastModified.String()) > 0, Equals, true) + c.Assert(ucp.FileStat.MD5, Equals, "") + c.Assert(ucp.ObjectKey, Equals, objectName) + c.Assert(len(ucp.UploadID), Equals, len("3F79722737D1469980DACEDCA325BB52")) + c.Assert(len(ucp.Parts), Equals, 5) + c.Assert(len(ucp.todoParts()), Equals, 1) + c.Assert(len(ucp.allParts()), Equals, 5) + + // Second upload, finish the remaining part + err = s.bucket.UploadFile(objectName, fileName, 100*1024, Checkpoint(true, fileName+".cp")) + c.Assert(err, IsNil) + + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err := compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + err = ucp.load(fileName + ".cp") + c.Assert(err, NotNil) + + // Resumable upload with empty checkpoint path + uploadPartHooker = ErrorHooker + err = s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(3), CheckpointDir(true, "")) + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "ErrorHooker") + uploadPartHooker = defaultUploadPart + ucp = uploadCheckpoint{} + err = ucp.load(fileName + ".cp") + c.Assert(err, NotNil) + + // Resumable upload with checkpoint dir + uploadPartHooker = ErrorHooker + err = s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(3), CheckpointDir(true, "./")) + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "ErrorHooker") + uploadPartHooker = defaultUploadPart + + // Check CP + ucp = uploadCheckpoint{} + cpConf := cpConfig{IsEnable: true, DirPath: "./"} + cpFilePath := getUploadCpFilePath(&cpConf, fileName, s.bucket.BucketName, objectName) + err = ucp.load(cpFilePath) + c.Assert(err, IsNil) + c.Assert(ucp.Magic, Equals, uploadCpMagic) + c.Assert(len(ucp.MD5), Equals, len("LC34jZU5xK4hlxi3Qn3XGQ==")) + c.Assert(ucp.FilePath, Equals, fileName) + c.Assert(ucp.FileStat.Size, Equals, int64(482048)) + c.Assert(len(ucp.FileStat.LastModified.String()) > 0, Equals, true) + c.Assert(ucp.FileStat.MD5, Equals, "") + c.Assert(ucp.ObjectKey, Equals, objectName) + c.Assert(len(ucp.UploadID), Equals, len("3F79722737D1469980DACEDCA325BB52")) + c.Assert(len(ucp.Parts), Equals, 5) + c.Assert(len(ucp.todoParts()), Equals, 1) + c.Assert(len(ucp.allParts()), Equals, 5) + + err = s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(3), CheckpointDir(true, "./")) + c.Assert(err, IsNil) + + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + err = ucp.load(cpFilePath) + c.Assert(err, NotNil) + + // Upload all 5 parts without error + err = s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(3), Checkpoint(true, objectName+".cp")) + c.Assert(err, IsNil) + + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Upload all 5 parts with 10 routines without error + err = s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(10), Checkpoint(true, objectName+".cp")) + c.Assert(err, IsNil) + + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) + + // Option + err = s.bucket.UploadFile(objectName, fileName, 100*1024, Routines(3), Checkpoint(true, objectName+".cp"), Meta("myprop", "mypropval")) + + meta, err := s.bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + c.Assert(meta.Get("X-Oss-Meta-Myprop"), Equals, "mypropval") + + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err = compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +// TestUploadRoutineWithRecoveryNegative is multiroutineed upload without checkpoint +func (s *OssUploadSuite) TestUploadRoutineWithRecoveryNegative(c *C) { + objectName := objectNamePrefix + RandStr(8) + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + + // The local file does not exist + err := s.bucket.UploadFile(objectName, "NotExist", 100*1024, Checkpoint(true, "NotExist.cp")) + c.Assert(err, NotNil) + + err = s.bucket.UploadFile(objectName, "NotExist", 100*1024, Routines(2), Checkpoint(true, "NotExist.cp")) + c.Assert(err, NotNil) + + // Specified part size is invalid + err = s.bucket.UploadFile(objectName, fileName, 1024, Checkpoint(true, fileName+".cp")) + c.Assert(err, NotNil) + + err = s.bucket.UploadFile(objectName, fileName, 1024, Routines(2), Checkpoint(true, fileName+".cp")) + c.Assert(err, NotNil) + + err = s.bucket.UploadFile(objectName, fileName, 1024*1024*1024*100, Checkpoint(true, fileName+".cp")) + c.Assert(err, NotNil) + + err = s.bucket.UploadFile(objectName, fileName, 1024*1024*1024*100, Routines(2), Checkpoint(true, fileName+".cp")) + c.Assert(err, NotNil) +} + +// TestUploadLocalFileChange tests the file is updated while being uploaded +func (s *OssUploadSuite) TestUploadLocalFileChange(c *C) { + objectName := objectNamePrefix + RandStr(8) + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + localFile := RandStr(8) + ".jpg" + newFile := RandStr(8) + ".jpg" + + os.Remove(localFile) + err := copyFile(fileName, localFile) + c.Assert(err, IsNil) + + // First upload for 4 parts + uploadPartHooker = ErrorHooker + err = s.bucket.UploadFile(objectName, localFile, 100*1024, Checkpoint(true, localFile+".cp")) + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "ErrorHooker") + uploadPartHooker = defaultUploadPart + + os.Remove(localFile) + err = copyFile(fileName, localFile) + c.Assert(err, IsNil) + + // Updating the file. The second upload will re-upload all 5 parts. + err = s.bucket.UploadFile(objectName, localFile, 100*1024, Checkpoint(true, localFile+".cp")) + c.Assert(err, IsNil) + + os.Remove(newFile) + err = s.bucket.GetObjectToFile(objectName, newFile) + c.Assert(err, IsNil) + + eq, err := compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + err = s.bucket.DeleteObject(objectName) + c.Assert(err, IsNil) +} + +// TestUploadPartArchiveObject +func (s *OssUploadSuite) TestUploadPartArchiveObject(c *C) { + // create archive bucket + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName, StorageClass(StorageArchive)) + c.Assert(err, IsNil) + bucket, err := client.Bucket(bucketName) + objectName := objectNamePrefix + RandStr(8) + + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + fileInfo, err := os.Stat(fileName) + c.Assert(err, IsNil) + + // Updating the file,archive object + err = bucket.UploadFile(objectName, fileName, fileInfo.Size()/2, ObjectStorageClass(StorageArchive)) + c.Assert(err, IsNil) + + // Updating the file,archive object,checkpoint + err = bucket.UploadFile(objectName, fileName, fileInfo.Size()/2, ObjectStorageClass(StorageArchive), Checkpoint(true, fileName+".cp")) + c.Assert(err, IsNil) + ForceDeleteBucket(client, bucketName, c) +} + +func copyFile(src, dst string) error { + srcFile, err := os.Open(src) + if err != nil { + return err + } + defer srcFile.Close() + + dstFile, err := os.Create(dst) + if err != nil { + return err + } + defer dstFile.Close() + + _, err = io.Copy(dstFile, srcFile) + return err +} + +func (s *OssUploadSuite) TestVersioningUploadRoutineWithRecovery(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + + bucket, err := client.Bucket(bucketName) + + // put bucket version:enabled + var versioningConfig VersioningConfig + versioningConfig.Status = string(VersionEnabled) + err = client.SetBucketVersioning(bucketName, versioningConfig) + c.Assert(err, IsNil) + + // begin test + objectName := objectNamePrefix + RandStr(8) + fileName := "test-file-" + RandStr(8) + fileData := RandStr(500 * 1024) + CreateFile(fileName, fileData, c) + newFile := "test-file-" + RandStr(8) + + // Use default routines and default CP file path (fileName+.cp)Header + // First upload for 4 parts + var respHeader http.Header + uploadPartHooker = ErrorHooker + options := []Option{Checkpoint(true, fileName+".cp"), GetResponseHeader(&respHeader)} + err = bucket.UploadFile(objectName, fileName, 100*1024, options...) + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "ErrorHooker") + c.Assert(GetVersionId(respHeader), Equals, "") + + uploadPartHooker = defaultUploadPart + + // Second upload, finish the remaining part + options = []Option{Checkpoint(true, fileName+".cp"), GetResponseHeader(&respHeader)} + err = bucket.UploadFile(objectName, fileName, 100*1024, options...) + c.Assert(err, IsNil) + versionIdUp := GetVersionId(respHeader) + c.Assert(len(versionIdUp) > 0, Equals, true) + + os.Remove(newFile) + var respHeaderDown http.Header + err = bucket.GetObjectToFile(objectName, newFile, GetResponseHeader(&respHeaderDown)) + versionIdDown := GetVersionId(respHeaderDown) + c.Assert(err, IsNil) + c.Assert(versionIdUp, Equals, versionIdDown) + + eq, err := compareFiles(fileName, newFile) + c.Assert(err, IsNil) + c.Assert(eq, Equals, true) + + os.Remove(fileName) + os.Remove(newFile) + bucket.DeleteObject(objectName) + ForceDeleteBucket(client, bucketName, c) +} + +// TestUploadFileChoiceOptions +func (s *OssUploadSuite) TestUploadFileChoiceOptions(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + bucket, err := client.Bucket(bucketName) + + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + fileInfo, err := os.Stat(fileName) + c.Assert(err, IsNil) + + objectName := objectNamePrefix + RandStr(8) + + // UploadFile with properties + options := []Option{ + ObjectACL(ACLPublicRead), + RequestPayer(Requester), + TrafficLimitHeader(1024 * 1024 * 8), + ServerSideEncryption("AES256"), + ObjectStorageClass(StorageArchive), + } + + // Updating the file + err = bucket.UploadFile(objectName, fileName, fileInfo.Size()/2, options...) + c.Assert(err, IsNil) + + // GetMetaDetail + headerResp, err := bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + + c.Assert(headerResp.Get("X-Oss-Server-Side-Encryption"), Equals, "AES256") + aclResult, err := bucket.GetObjectACL(objectName) + c.Assert(aclResult.ACL, Equals, "public-read") + c.Assert(err, IsNil) + ForceDeleteBucket(client, bucketName, c) +} + +// TestUploadFileWithCpChoiceOptions +func (s *OssUploadSuite) TestUploadFileWithCpChoiceOptions(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + bucket, err := client.Bucket(bucketName) + + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + fileInfo, err := os.Stat(fileName) + c.Assert(err, IsNil) + + objectName := objectNamePrefix + RandStr(8) + + // UploadFile with properties + options := []Option{ + ObjectACL(ACLPublicRead), + RequestPayer(Requester), + TrafficLimitHeader(1024 * 1024 * 8), + ServerSideEncryption("AES256"), + ObjectStorageClass(StorageArchive), + Checkpoint(true, fileName+".cp"), // with checkpoint + } + + // Updating the file + err = bucket.UploadFile(objectName, fileName, fileInfo.Size()/2, options...) + c.Assert(err, IsNil) + + // GetMetaDetail + headerResp, err := bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + + c.Assert(headerResp.Get("X-Oss-Server-Side-Encryption"), Equals, "AES256") + c.Assert(headerResp.Get("X-Oss-Storage-Class"), Equals, "Archive") + + aclResult, err := bucket.GetObjectACL(objectName) + c.Assert(aclResult.ACL, Equals, "public-read") + c.Assert(err, IsNil) + + ForceDeleteBucket(client, bucketName, c) +} + +// TestUploadFileWithForbidOverWrite +func (s *OssUploadSuite) TestUploadFileWithForbidOverWrite(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + bucket, err := client.Bucket(bucketName) + + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + fileInfo, err := os.Stat(fileName) + c.Assert(err, IsNil) + + objectName := objectNamePrefix + RandStr(8) + + // UploadFile with properties + options := []Option{ + ObjectACL(ACLPublicRead), + RequestPayer(Requester), + TrafficLimitHeader(1024 * 1024 * 8), + ServerSideEncryption("AES256"), + ObjectStorageClass(StorageArchive), + ForbidOverWrite(true), + Checkpoint(true, fileName+".cp"), + } + + // Updating the file + err = bucket.UploadFile(objectName, fileName, fileInfo.Size()/2, options...) + c.Assert(err, IsNil) + + // Updating the file with ForbidOverWrite(true) + err = bucket.UploadFile(objectName, fileName, fileInfo.Size()/2, options...) + c.Assert(err, NotNil) + + // without Checkpoint + options = []Option{ + ObjectACL(ACLPublicRead), + RequestPayer(Requester), + TrafficLimitHeader(1024 * 1024 * 8), + ServerSideEncryption("AES256"), + ObjectStorageClass(StorageArchive), + ForbidOverWrite(true), + } + + // Updating the file with ForbidOverWrite(true) + err = bucket.UploadFile(objectName, fileName, fileInfo.Size()/2, options...) + c.Assert(err, NotNil) + + ForceDeleteBucket(client, bucketName, c) +} + +// TestUploadFileWithSequential +func (s *OssUploadSuite) TestUploadFileWithSequential(c *C) { + // create a bucket with default proprety + client, err := New(endpoint, accessID, accessKey) + c.Assert(err, IsNil) + + bucketName := bucketNamePrefix + RandLowStr(6) + err = client.CreateBucket(bucketName) + c.Assert(err, IsNil) + bucket, err := client.Bucket(bucketName) + + fileName := "../sample/BingWallpaper-2015-11-07.jpg" + fileInfo, err := os.Stat(fileName) + c.Assert(err, IsNil) + + objectName := objectNamePrefix + RandStr(8) + + var respHeader http.Header + + // UploadFile with properties + options := []Option{ + Sequential(), + GetResponseHeader(&respHeader), + Checkpoint(true, fileName+".cp"), + } + + // Updating the file + err = bucket.UploadFile(objectName, fileName, fileInfo.Size()/2, options...) + c.Assert(err, IsNil) + + respHeader, err = bucket.GetObjectDetailedMeta(objectName) + c.Assert(err, IsNil) + + strMD5 := respHeader.Get("Content-MD5") + c.Assert(len(strMD5) > 0, Equals, true) + + ForceDeleteBucket(client, bucketName, c) +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/utils.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/utils.go new file mode 100644 index 00000000..3dff335f --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/utils.go @@ -0,0 +1,512 @@ +package oss + +import ( + "bytes" + "errors" + "fmt" + "hash/crc32" + "hash/crc64" + "io" + "net/http" + "os" + "os/exec" + "runtime" + "strconv" + "strings" + "time" +) + +var sys_name string +var sys_release string +var sys_machine string + +func init() { + sys_name = runtime.GOOS + sys_release = "-" + sys_machine = runtime.GOARCH + + if out, err := exec.Command("uname", "-s").CombinedOutput(); err == nil { + sys_name = string(bytes.TrimSpace(out)) + } + if out, err := exec.Command("uname", "-r").CombinedOutput(); err == nil { + sys_release = string(bytes.TrimSpace(out)) + } + if out, err := exec.Command("uname", "-m").CombinedOutput(); err == nil { + sys_machine = string(bytes.TrimSpace(out)) + } +} + +// userAgent gets user agent +// It has the SDK version information, OS information and GO version +func userAgent() string { + sys := getSysInfo() + return fmt.Sprintf("aliyun-sdk-go/%s (%s/%s/%s;%s)", Version, sys.name, + sys.release, sys.machine, runtime.Version()) +} + +type sysInfo struct { + name string // OS name such as windows/Linux + release string // OS version 2.6.32-220.23.2.ali1089.el5.x86_64 etc + machine string // CPU type amd64/x86_64 +} + +// getSysInfo gets system info +// gets the OS information and CPU type +func getSysInfo() sysInfo { + return sysInfo{name: sys_name, release: sys_release, machine: sys_machine} +} + +// GetRangeConfig gets the download range from the options. +func GetRangeConfig(options []Option) (*UnpackedRange, error) { + rangeOpt, err := FindOption(options, HTTPHeaderRange, nil) + if err != nil || rangeOpt == nil { + return nil, err + } + return ParseRange(rangeOpt.(string)) +} + +// UnpackedRange +type UnpackedRange struct { + HasStart bool // Flag indicates if the start point is specified + HasEnd bool // Flag indicates if the end point is specified + Start int64 // Start point + End int64 // End point +} + +// InvalidRangeError returns invalid range error +func InvalidRangeError(r string) error { + return fmt.Errorf("InvalidRange %s", r) +} + +func GetRangeString(unpackRange UnpackedRange) string { + var strRange string + if unpackRange.HasStart && unpackRange.HasEnd { + strRange = fmt.Sprintf("%d-%d", unpackRange.Start, unpackRange.End) + } else if unpackRange.HasStart { + strRange = fmt.Sprintf("%d-", unpackRange.Start) + } else if unpackRange.HasEnd { + strRange = fmt.Sprintf("-%d", unpackRange.End) + } + return strRange +} + +// ParseRange parse various styles of range such as bytes=M-N +func ParseRange(normalizedRange string) (*UnpackedRange, error) { + var err error + hasStart := false + hasEnd := false + var start int64 + var end int64 + + // Bytes==M-N or ranges=M-N + nrSlice := strings.Split(normalizedRange, "=") + if len(nrSlice) != 2 || nrSlice[0] != "bytes" { + return nil, InvalidRangeError(normalizedRange) + } + + // Bytes=M-N,X-Y + rSlice := strings.Split(nrSlice[1], ",") + rStr := rSlice[0] + + if strings.HasSuffix(rStr, "-") { // M- + startStr := rStr[:len(rStr)-1] + start, err = strconv.ParseInt(startStr, 10, 64) + if err != nil { + return nil, InvalidRangeError(normalizedRange) + } + hasStart = true + } else if strings.HasPrefix(rStr, "-") { // -N + len := rStr[1:] + end, err = strconv.ParseInt(len, 10, 64) + if err != nil { + return nil, InvalidRangeError(normalizedRange) + } + if end == 0 { // -0 + return nil, InvalidRangeError(normalizedRange) + } + hasEnd = true + } else { // M-N + valSlice := strings.Split(rStr, "-") + if len(valSlice) != 2 { + return nil, InvalidRangeError(normalizedRange) + } + start, err = strconv.ParseInt(valSlice[0], 10, 64) + if err != nil { + return nil, InvalidRangeError(normalizedRange) + } + hasStart = true + end, err = strconv.ParseInt(valSlice[1], 10, 64) + if err != nil { + return nil, InvalidRangeError(normalizedRange) + } + hasEnd = true + } + + return &UnpackedRange{hasStart, hasEnd, start, end}, nil +} + +// AdjustRange returns adjusted range, adjust the range according to the length of the file +func AdjustRange(ur *UnpackedRange, size int64) (start, end int64) { + if ur == nil { + return 0, size + } + + if ur.HasStart && ur.HasEnd { + start = ur.Start + end = ur.End + 1 + if ur.Start < 0 || ur.Start >= size || ur.End > size || ur.Start > ur.End { + start = 0 + end = size + } + } else if ur.HasStart { + start = ur.Start + end = size + if ur.Start < 0 || ur.Start >= size { + start = 0 + } + } else if ur.HasEnd { + start = size - ur.End + end = size + if ur.End < 0 || ur.End > size { + start = 0 + end = size + } + } + return +} + +// GetNowSec returns Unix time, the number of seconds elapsed since January 1, 1970 UTC. +// gets the current time in Unix time, in seconds. +func GetNowSec() int64 { + return time.Now().Unix() +} + +// GetNowNanoSec returns t as a Unix time, the number of nanoseconds elapsed +// since January 1, 1970 UTC. The result is undefined if the Unix time +// in nanoseconds cannot be represented by an int64. Note that this +// means the result of calling UnixNano on the zero Time is undefined. +// gets the current time in Unix time, in nanoseconds. +func GetNowNanoSec() int64 { + return time.Now().UnixNano() +} + +// GetNowGMT gets the current time in GMT format. +func GetNowGMT() string { + return time.Now().UTC().Format(http.TimeFormat) +} + +// FileChunk is the file chunk definition +type FileChunk struct { + Number int // Chunk number + Offset int64 // Chunk offset + Size int64 // Chunk size. +} + +// SplitFileByPartNum splits big file into parts by the num of parts. +// Split the file with specified parts count, returns the split result when error is nil. +func SplitFileByPartNum(fileName string, chunkNum int) ([]FileChunk, error) { + if chunkNum <= 0 || chunkNum > 10000 { + return nil, errors.New("chunkNum invalid") + } + + file, err := os.Open(fileName) + if err != nil { + return nil, err + } + defer file.Close() + + stat, err := file.Stat() + if err != nil { + return nil, err + } + + if int64(chunkNum) > stat.Size() { + return nil, errors.New("oss: chunkNum invalid") + } + + var chunks []FileChunk + var chunk = FileChunk{} + var chunkN = (int64)(chunkNum) + for i := int64(0); i < chunkN; i++ { + chunk.Number = int(i + 1) + chunk.Offset = i * (stat.Size() / chunkN) + if i == chunkN-1 { + chunk.Size = stat.Size()/chunkN + stat.Size()%chunkN + } else { + chunk.Size = stat.Size() / chunkN + } + chunks = append(chunks, chunk) + } + + return chunks, nil +} + +// SplitFileByPartSize splits big file into parts by the size of parts. +// Splits the file by the part size. Returns the FileChunk when error is nil. +func SplitFileByPartSize(fileName string, chunkSize int64) ([]FileChunk, error) { + if chunkSize <= 0 { + return nil, errors.New("chunkSize invalid") + } + + file, err := os.Open(fileName) + if err != nil { + return nil, err + } + defer file.Close() + + stat, err := file.Stat() + if err != nil { + return nil, err + } + var chunkN = stat.Size() / chunkSize + if chunkN >= 10000 { + return nil, errors.New("Too many parts, please increase part size") + } + + var chunks []FileChunk + var chunk = FileChunk{} + for i := int64(0); i < chunkN; i++ { + chunk.Number = int(i + 1) + chunk.Offset = i * chunkSize + chunk.Size = chunkSize + chunks = append(chunks, chunk) + } + + if stat.Size()%chunkSize > 0 { + chunk.Number = len(chunks) + 1 + chunk.Offset = int64(len(chunks)) * chunkSize + chunk.Size = stat.Size() % chunkSize + chunks = append(chunks, chunk) + } + + return chunks, nil +} + +// GetPartEnd calculates the end position +func GetPartEnd(begin int64, total int64, per int64) int64 { + if begin+per > total { + return total - 1 + } + return begin + per - 1 +} + +// CrcTable returns the table constructed from the specified polynomial +var CrcTable = func() *crc64.Table { + return crc64.MakeTable(crc64.ECMA) +} + +// CrcTable returns the table constructed from the specified polynomial +var crc32Table = func() *crc32.Table { + return crc32.MakeTable(crc32.IEEE) +} + +// choiceTransferPartOption choices valid option supported by Uploadpart or DownloadPart +func ChoiceTransferPartOption(options []Option) []Option { + var outOption []Option + + listener, _ := FindOption(options, progressListener, nil) + if listener != nil { + outOption = append(outOption, Progress(listener.(ProgressListener))) + } + + payer, _ := FindOption(options, HTTPHeaderOssRequester, nil) + if payer != nil { + outOption = append(outOption, RequestPayer(PayerType(payer.(string)))) + } + + versionId, _ := FindOption(options, "versionId", nil) + if versionId != nil { + outOption = append(outOption, VersionId(versionId.(string))) + } + + trafficLimit, _ := FindOption(options, HTTPHeaderOssTrafficLimit, nil) + if trafficLimit != nil { + speed, _ := strconv.ParseInt(trafficLimit.(string), 10, 64) + outOption = append(outOption, TrafficLimitHeader(speed)) + } + + respHeader, _ := FindOption(options, responseHeader, nil) + if respHeader != nil { + outOption = append(outOption, GetResponseHeader(respHeader.(*http.Header))) + } + + return outOption +} + +// ChoiceCompletePartOption choices valid option supported by CompleteMulitiPart +func ChoiceCompletePartOption(options []Option) []Option { + var outOption []Option + + listener, _ := FindOption(options, progressListener, nil) + if listener != nil { + outOption = append(outOption, Progress(listener.(ProgressListener))) + } + + payer, _ := FindOption(options, HTTPHeaderOssRequester, nil) + if payer != nil { + outOption = append(outOption, RequestPayer(PayerType(payer.(string)))) + } + + acl, _ := FindOption(options, HTTPHeaderOssObjectACL, nil) + if acl != nil { + outOption = append(outOption, ObjectACL(ACLType(acl.(string)))) + } + + callback, _ := FindOption(options, HTTPHeaderOssCallback, nil) + if callback != nil { + outOption = append(outOption, Callback(callback.(string))) + } + + callbackVar, _ := FindOption(options, HTTPHeaderOssCallbackVar, nil) + if callbackVar != nil { + outOption = append(outOption, CallbackVar(callbackVar.(string))) + } + + respHeader, _ := FindOption(options, responseHeader, nil) + if respHeader != nil { + outOption = append(outOption, GetResponseHeader(respHeader.(*http.Header))) + } + + forbidOverWrite, _ := FindOption(options, HTTPHeaderOssForbidOverWrite, nil) + if forbidOverWrite != nil { + if forbidOverWrite.(string) == "true" { + outOption = append(outOption, ForbidOverWrite(true)) + } else { + outOption = append(outOption, ForbidOverWrite(false)) + } + } + + return outOption +} + +// ChoiceAbortPartOption choices valid option supported by AbortMultipartUpload +func ChoiceAbortPartOption(options []Option) []Option { + var outOption []Option + payer, _ := FindOption(options, HTTPHeaderOssRequester, nil) + if payer != nil { + outOption = append(outOption, RequestPayer(PayerType(payer.(string)))) + } + + respHeader, _ := FindOption(options, responseHeader, nil) + if respHeader != nil { + outOption = append(outOption, GetResponseHeader(respHeader.(*http.Header))) + } + + return outOption +} + +// ChoiceHeadObjectOption choices valid option supported by HeadObject +func ChoiceHeadObjectOption(options []Option) []Option { + var outOption []Option + + // not select HTTPHeaderRange to get whole object length + payer, _ := FindOption(options, HTTPHeaderOssRequester, nil) + if payer != nil { + outOption = append(outOption, RequestPayer(PayerType(payer.(string)))) + } + + versionId, _ := FindOption(options, "versionId", nil) + if versionId != nil { + outOption = append(outOption, VersionId(versionId.(string))) + } + + respHeader, _ := FindOption(options, responseHeader, nil) + if respHeader != nil { + outOption = append(outOption, GetResponseHeader(respHeader.(*http.Header))) + } + + return outOption +} + +func CheckBucketName(bucketName string) error { + nameLen := len(bucketName) + if nameLen < 3 || nameLen > 63 { + return fmt.Errorf("bucket name %s len is between [3-63],now is %d", bucketName, nameLen) + } + + for _, v := range bucketName { + if !(('a' <= v && v <= 'z') || ('0' <= v && v <= '9') || v == '-') { + return fmt.Errorf("bucket name %s can only include lowercase letters, numbers, and -", bucketName) + } + } + if bucketName[0] == '-' || bucketName[nameLen-1] == '-' { + return fmt.Errorf("bucket name %s must start and end with a lowercase letter or number", bucketName) + } + return nil +} + +func GetReaderLen(reader io.Reader) (int64, error) { + var contentLength int64 + var err error + switch v := reader.(type) { + case *bytes.Buffer: + contentLength = int64(v.Len()) + case *bytes.Reader: + contentLength = int64(v.Len()) + case *strings.Reader: + contentLength = int64(v.Len()) + case *os.File: + fInfo, fError := v.Stat() + if fError != nil { + err = fmt.Errorf("can't get reader content length,%s", fError.Error()) + } else { + contentLength = fInfo.Size() + } + case *io.LimitedReader: + contentLength = int64(v.N) + case *LimitedReadCloser: + contentLength = int64(v.N) + default: + err = fmt.Errorf("can't get reader content length,unkown reader type") + } + return contentLength, err +} + +func LimitReadCloser(r io.Reader, n int64) io.Reader { + var lc LimitedReadCloser + lc.R = r + lc.N = n + return &lc +} + +// LimitedRC support Close() +type LimitedReadCloser struct { + io.LimitedReader +} + +func (lc *LimitedReadCloser) Close() error { + if closer, ok := lc.R.(io.ReadCloser); ok { + return closer.Close() + } + return nil +} + +type DiscardReadCloser struct { + RC io.ReadCloser + Discard int +} + +func (drc *DiscardReadCloser) Read(b []byte) (int, error) { + n, err := drc.RC.Read(b) + if drc.Discard == 0 || n <= 0 { + return n, err + } + + if n <= drc.Discard { + drc.Discard -= n + return 0, err + } + + realLen := n - drc.Discard + copy(b[0:realLen], b[drc.Discard:n]) + drc.Discard = 0 + return realLen, err +} + +func (drc *DiscardReadCloser) Close() error { + closer, ok := drc.RC.(io.ReadCloser) + if ok { + return closer.Close() + } + return nil +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/utils_test.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/utils_test.go new file mode 100644 index 00000000..dde929aa --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/oss/utils_test.go @@ -0,0 +1,259 @@ +package oss + +import ( + "strings" + + . "gopkg.in/check.v1" +) + +type OssUtilsSuite struct{} + +var _ = Suite(&OssUtilsSuite{}) + +func (s *OssUtilsSuite) TestUtilsTime(c *C) { + c.Assert(GetNowSec() > 1448597674, Equals, true) + c.Assert(GetNowNanoSec() > 1448597674000000000, Equals, true) + c.Assert(len(GetNowGMT()), Equals, len("Fri, 27 Nov 2015 04:14:34 GMT")) +} + +func (s *OssUtilsSuite) TestUtilsSplitFile(c *C) { + localFile := "../sample/BingWallpaper-2015-11-07.jpg" + + // Num + parts, err := SplitFileByPartNum(localFile, 4) + c.Assert(err, IsNil) + c.Assert(len(parts), Equals, 4) + testLogger.Println("parts 4:", parts) + for i, part := range parts { + c.Assert(part.Number, Equals, i+1) + c.Assert(part.Offset, Equals, int64(i*120512)) + c.Assert(part.Size, Equals, int64(120512)) + } + + parts, err = SplitFileByPartNum(localFile, 5) + c.Assert(err, IsNil) + c.Assert(len(parts), Equals, 5) + testLogger.Println("parts 5:", parts) + for i, part := range parts { + c.Assert(part.Number, Equals, i+1) + c.Assert(part.Offset, Equals, int64(i*96409)) + } + + _, err = SplitFileByPartNum(localFile, 10001) + c.Assert(err, NotNil) + + _, err = SplitFileByPartNum(localFile, 0) + c.Assert(err, NotNil) + + _, err = SplitFileByPartNum(localFile, -1) + c.Assert(err, NotNil) + + _, err = SplitFileByPartNum("notexist", 1024) + c.Assert(err, NotNil) + + // Size + parts, err = SplitFileByPartSize(localFile, 120512) + c.Assert(err, IsNil) + c.Assert(len(parts), Equals, 4) + testLogger.Println("parts 4:", parts) + for i, part := range parts { + c.Assert(part.Number, Equals, i+1) + c.Assert(part.Offset, Equals, int64(i*120512)) + c.Assert(part.Size, Equals, int64(120512)) + } + + parts, err = SplitFileByPartSize(localFile, 96409) + c.Assert(err, IsNil) + c.Assert(len(parts), Equals, 6) + testLogger.Println("parts 6:", parts) + for i, part := range parts { + c.Assert(part.Number, Equals, i+1) + c.Assert(part.Offset, Equals, int64(i*96409)) + } + + _, err = SplitFileByPartSize(localFile, 0) + c.Assert(err, NotNil) + + _, err = SplitFileByPartSize(localFile, -1) + c.Assert(err, NotNil) + + _, err = SplitFileByPartSize(localFile, 10) + c.Assert(err, NotNil) + + _, err = SplitFileByPartSize("noexist", 120512) + c.Assert(err, NotNil) +} + +func (s *OssUtilsSuite) TestUtilsFileExt(c *C) { + c.Assert(strings.Contains(TypeByExtension("test.txt"), "text/plain"), Equals, true) + c.Assert(TypeByExtension("test.jpg"), Equals, "image/jpeg") + c.Assert(TypeByExtension("test.pdf"), Equals, "application/pdf") + c.Assert(TypeByExtension("test"), Equals, "") + c.Assert(strings.Contains(TypeByExtension("/root/dir/test.txt"), "text/plain"), Equals, true) + c.Assert(strings.Contains(TypeByExtension("root/dir/test.txt"), "text/plain"), Equals, true) + c.Assert(strings.Contains(TypeByExtension("root\\dir\\test.txt"), "text/plain"), Equals, true) + c.Assert(strings.Contains(TypeByExtension("D:\\work\\dir\\test.txt"), "text/plain"), Equals, true) +} + +func (s *OssUtilsSuite) TestGetPartEnd(c *C) { + end := GetPartEnd(3, 10, 3) + c.Assert(end, Equals, int64(5)) + + end = GetPartEnd(9, 10, 3) + c.Assert(end, Equals, int64(9)) + + end = GetPartEnd(7, 10, 3) + c.Assert(end, Equals, int64(9)) +} + +func (s *OssUtilsSuite) TestParseRange(c *C) { + // InvalidRange bytes==M-N + _, err := ParseRange("bytes==M-N") + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "InvalidRange bytes==M-N") + + // InvalidRange ranges=M-N + _, err = ParseRange("ranges=M-N") + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "InvalidRange ranges=M-N") + + // InvalidRange ranges=M-N + _, err = ParseRange("bytes=M-N") + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "InvalidRange bytes=M-N") + + // InvalidRange ranges=M- + _, err = ParseRange("bytes=M-") + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "InvalidRange bytes=M-") + + // InvalidRange ranges=-N + _, err = ParseRange("bytes=-N") + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "InvalidRange bytes=-N") + + // InvalidRange ranges=-0 + _, err = ParseRange("bytes=-0") + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "InvalidRange bytes=-0") + + // InvalidRange bytes=1-2-3 + _, err = ParseRange("bytes=1-2-3") + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "InvalidRange bytes=1-2-3") + + // InvalidRange bytes=1-N + _, err = ParseRange("bytes=1-N") + c.Assert(err, NotNil) + c.Assert(err.Error(), Equals, "InvalidRange bytes=1-N") + + // Ranges=M-N + ur, err := ParseRange("bytes=1024-4096") + c.Assert(err, IsNil) + c.Assert(ur.Start, Equals, (int64)(1024)) + c.Assert(ur.End, Equals, (int64)(4096)) + c.Assert(ur.HasStart, Equals, true) + c.Assert(ur.HasEnd, Equals, true) + + // Ranges=M-N,X-Y + ur, err = ParseRange("bytes=1024-4096,2048-4096") + c.Assert(err, IsNil) + c.Assert(ur.Start, Equals, (int64)(1024)) + c.Assert(ur.End, Equals, (int64)(4096)) + c.Assert(ur.HasStart, Equals, true) + c.Assert(ur.HasEnd, Equals, true) + + // Ranges=M- + ur, err = ParseRange("bytes=1024-") + c.Assert(err, IsNil) + c.Assert(ur.Start, Equals, (int64)(1024)) + c.Assert(ur.End, Equals, (int64)(0)) + c.Assert(ur.HasStart, Equals, true) + c.Assert(ur.HasEnd, Equals, false) + + // Ranges=-N + ur, err = ParseRange("bytes=-4096") + c.Assert(err, IsNil) + c.Assert(ur.Start, Equals, (int64)(0)) + c.Assert(ur.End, Equals, (int64)(4096)) + c.Assert(ur.HasStart, Equals, false) + c.Assert(ur.HasEnd, Equals, true) +} + +func (s *OssUtilsSuite) TestAdjustRange(c *C) { + // Nil + start, end := AdjustRange(nil, 8192) + c.Assert(start, Equals, (int64)(0)) + c.Assert(end, Equals, (int64)(8192)) + + // 1024-4096 + ur := &UnpackedRange{true, true, 1024, 4095} + start, end = AdjustRange(ur, 8192) + c.Assert(start, Equals, (int64)(1024)) + c.Assert(end, Equals, (int64)(4096)) + + // 1024- + ur = &UnpackedRange{true, false, 1024, 4096} + start, end = AdjustRange(ur, 8192) + c.Assert(start, Equals, (int64)(1024)) + c.Assert(end, Equals, (int64)(8192)) + + // -4096 + ur = &UnpackedRange{false, true, 1024, 4096} + start, end = AdjustRange(ur, 8192) + c.Assert(start, Equals, (int64)(4096)) + c.Assert(end, Equals, (int64)(8192)) + + // Invalid range 4096-1024 + ur = &UnpackedRange{true, true, 4096, 1024} + start, end = AdjustRange(ur, 8192) + c.Assert(start, Equals, (int64)(0)) + c.Assert(end, Equals, (int64)(8192)) + + // Invalid range -1- + ur = &UnpackedRange{true, false, -1, 0} + start, end = AdjustRange(ur, 8192) + c.Assert(start, Equals, (int64)(0)) + c.Assert(end, Equals, (int64)(8192)) + + // Invalid range -9999 + ur = &UnpackedRange{false, true, 0, 9999} + start, end = AdjustRange(ur, 8192) + c.Assert(start, Equals, (int64)(0)) + c.Assert(end, Equals, (int64)(8192)) +} + +func (s *OssUtilsSuite) TestUtilCheckBucketName(c *C) { + err := CheckBucketName("a") + c.Assert(err, NotNil) + + err = CheckBucketName("a11111111111111111111111111111nbbbbbbbbbbbbbbbbbbbbbbbbbbbqqqqqqqqqqqqqqqqqqqq") + c.Assert(err, NotNil) + + err = CheckBucketName("-abcd") + c.Assert(err, NotNil) + + err = CheckBucketName("abcd-") + c.Assert(err, NotNil) + + err = CheckBucketName("abcD") + c.Assert(err, NotNil) + + err = CheckBucketName("abc 1") + c.Assert(err, NotNil) + + err = CheckBucketName("abc&1") + c.Assert(err, NotNil) + + err = CheckBucketName("abc-1") + c.Assert(err, IsNil) + + err = CheckBucketName("1bc-1") + c.Assert(err, IsNil) + + err = CheckBucketName("111-1") + c.Assert(err, IsNil) + + err = CheckBucketName("abc123-def1") + c.Assert(err, IsNil) +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample.go new file mode 100644 index 00000000..072fe856 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample.go @@ -0,0 +1,59 @@ +// main of samples + +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/aliyun/aliyun-oss-go-sdk/sample" +) + +// sampleMap contains all samples +var sampleMap = map[string]interface{}{ + "CreateBucketSample": sample.CreateBucketSample, + "NewBucketSample": sample.NewBucketSample, + "ListBucketsSample": sample.ListBucketsSample, + "BucketACLSample": sample.BucketACLSample, + "BucketLifecycleSample": sample.BucketLifecycleSample, + "BucketRefererSample": sample.BucketRefererSample, + "BucketLoggingSample": sample.BucketLoggingSample, + "BucketWebsiteSample": sample.BucketWebsiteSample, + "BucketCORSSample": sample.BucketCORSSample, + "BucketPolicySample": sample.BucketPolicySample, + "BucketrRequestPaymentSample": sample.BucketrRequestPaymentSample, + "BucketQoSInfoSample": sample.BucketQoSInfoSample, + "BucketInventorySample": sample.BucketInventorySample, + "ObjectACLSample": sample.ObjectACLSample, + "ObjectMetaSample": sample.ObjectMetaSample, + "ListObjectsSample": sample.ListObjectsSample, + "DeleteObjectSample": sample.DeleteObjectSample, + "AppendObjectSample": sample.AppendObjectSample, + "CopyObjectSample": sample.CopyObjectSample, + "PutObjectSample": sample.PutObjectSample, + "GetObjectSample": sample.GetObjectSample, + "CnameSample": sample.CnameSample, + "SignURLSample": sample.SignURLSample, + "ArchiveSample": sample.ArchiveSample, + "ObjectTaggingSample": sample.ObjectTaggingSample, + "BucketEncryptionSample": sample.BucketEncryptionSample, + "SelectObjectSample": sample.SelectObjectSample, +} + +func main() { + var name string + flag.StringVar(&name, "name", "", "Waiting for a sample of execution") + flag.Parse() + + if len(name) <= 0 { + fmt.Println("please enter your sample's name. like '-name CreateBucketSample'") + os.Exit(-1) + } else { + if sampleMap[name] == nil { + fmt.Println("the " + name + "is not exist.") + os.Exit(-1) + } + sampleMap[name].(func())() + } +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/BingWallpaper-2015-11-07.jpg b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/BingWallpaper-2015-11-07.jpg new file mode 100644 index 0000000000000000000000000000000000000000..90960f0f2a0fad64b8c832315cd27966b218cee4 GIT binary patch literal 482048 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 *j-^X+IKt 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%0A&-m23b#{}M?RBT>5V(U6 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%0D19N=*E)+3h>;Ot;@);3ktJ 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

xN>kA{J8aGl{Mjoa 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νq+ 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 Z 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 E16yGfHUIzs literal 0 HcmV?d00001 diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/The Go Programming Language.html b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/The Go Programming Language.html new file mode 100644 index 00000000..ee91f073 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/The Go Programming Language.html @@ -0,0 +1,3663 @@ + + + + + + + + http - The Go Programming Language + + + + + + + + + + + +

+... +
+ +
+ + +
+
+
+
+ Run + Format + +
+
+ + +
+
+ + +

Package http

+ + + + + + + + + + + + + + +
+
+
import "net/http"
+
+
+
Overview
+
Index
+ +
Examples
+ + +
Subdirectories
+ +
+
+ +
+ +
+

Overview ▾

+

+Package http provides HTTP client and server implementations. +

+

+Get, Head, Post, and PostForm make HTTP (or HTTPS) requests: +

+
resp, err := http.Get("http://example.com/")
+...
+resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
+...
+resp, err := http.PostForm("http://example.com/form",
+	url.Values{"key": {"Value"}, "id": {"123"}})
+
+

+The client must close the response body when finished with it: +

+
resp, err := http.Get("http://example.com/")
+if err != nil {
+	// handle error
+}
+defer resp.Body.Close()
+body, err := ioutil.ReadAll(resp.Body)
+// ...
+
+

+For control over HTTP client headers, redirect policy, and other +settings, create a Client: +

+
client := &http.Client{
+	CheckRedirect: redirectPolicyFunc,
+}
+
+resp, err := client.Get("http://example.com")
+// ...
+
+req, err := http.NewRequest("GET", "http://example.com", nil)
+// ...
+req.Header.Add("If-None-Match", `W/"wyzzy"`)
+resp, err := client.Do(req)
+// ...
+
+

+For control over proxies, TLS configuration, keep-alives, +compression, and other settings, create a Transport: +

+
tr := &http.Transport{
+	TLSClientConfig:    &tls.Config{RootCAs: pool},
+	DisableCompression: true,
+}
+client := &http.Client{Transport: tr}
+resp, err := client.Get("https://example.com")
+
+

+Clients and Transports are safe for concurrent use by multiple +goroutines and for efficiency should only be created once and re-used. +

+

+ListenAndServe starts an HTTP server with a given address and handler. +The handler is usually nil, which means to use DefaultServeMux. +Handle and HandleFunc add handlers to DefaultServeMux: +

+
http.Handle("/foo", fooHandler)
+
+http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
+	fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
+})
+
+log.Fatal(http.ListenAndServe(":8080", nil))
+
+

+More control over the server's behavior is available by creating a +custom Server: +

+
s := &http.Server{
+	Addr:           ":8080",
+	Handler:        myHandler,
+	ReadTimeout:    10 * time.Second,
+	WriteTimeout:   10 * time.Second,
+	MaxHeaderBytes: 1 << 20,
+}
+log.Fatal(s.ListenAndServe())
+
+ +
+
+ + +
+ +
+

Index ▾

+ + +
+
+ +
Constants
+ + +
Variables
+ + + +
func CanonicalHeaderKey(s string) string
+ + +
func DetectContentType(data []byte) string
+ + +
func Error(w ResponseWriter, error string, code int)
+ + +
func Handle(pattern string, handler Handler)
+ + +
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
+ + +
func ListenAndServe(addr string, handler Handler) error
+ + +
func ListenAndServeTLS(addr string, certFile string, keyFile string, handler Handler) error
+ + +
func MaxBytesReader(w ResponseWriter, r io.ReadCloser, n int64) io.ReadCloser
+ + +
func NotFound(w ResponseWriter, r *Request)
+ + +
func ParseHTTPVersion(vers string) (major, minor int, ok bool)
+ + +
func ParseTime(text string) (t time.Time, err error)
+ + +
func ProxyFromEnvironment(req *Request) (*url.URL, error)
+ + +
func ProxyURL(fixedURL *url.URL) func(*Request) (*url.URL, error)
+ + +
func Redirect(w ResponseWriter, r *Request, urlStr string, code int)
+ + +
func Serve(l net.Listener, handler Handler) error
+ + +
func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker)
+ + +
func ServeFile(w ResponseWriter, r *Request, name string)
+ + +
func SetCookie(w ResponseWriter, cookie *Cookie)
+ + +
func StatusText(code int) string
+ + + +
type Client
+ + + +
    func (c *Client) Do(req *Request) (resp *Response, err error)
+ + +
    func (c *Client) Get(url string) (resp *Response, err error)
+ + +
    func (c *Client) Head(url string) (resp *Response, err error)
+ + +
    func (c *Client) Post(url string, bodyType string, body io.Reader) (resp *Response, err error)
+ + +
    func (c *Client) PostForm(url string, data url.Values) (resp *Response, err error)
+ + + +
type CloseNotifier
+ + + + +
type ConnState
+ + + +
    func (c ConnState) String() string
+ + + +
type Cookie
+ + + +
    func (c *Cookie) String() string
+ + + +
type CookieJar
+ + + + +
type Dir
+ + + +
    func (d Dir) Open(name string) (File, error)
+ + + +
type File
+ + + + +
type FileSystem
+ + + + +
type Flusher
+ + + + +
type Handler
+ + +
    func FileServer(root FileSystem) Handler
+ + +
    func NotFoundHandler() Handler
+ + +
    func RedirectHandler(url string, code int) Handler
+ + +
    func StripPrefix(prefix string, h Handler) Handler
+ + +
    func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler
+ + + + +
type HandlerFunc
+ + + +
    func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request)
+ + + +
type Header
+ + + +
    func (h Header) Add(key, value string)
+ + +
    func (h Header) Del(key string)
+ + +
    func (h Header) Get(key string) string
+ + +
    func (h Header) Set(key, value string)
+ + +
    func (h Header) Write(w io.Writer) error
+ + +
    func (h Header) WriteSubset(w io.Writer, exclude map[string]bool) error
+ + + +
type Hijacker
+ + + + +
type ProtocolError
+ + + +
    func (err *ProtocolError) Error() string
+ + + +
type Request
+ + +
    func NewRequest(method, urlStr string, body io.Reader) (*Request, error)
+ + +
    func ReadRequest(b *bufio.Reader) (req *Request, err error)
+ + + +
    func (r *Request) AddCookie(c *Cookie)
+ + +
    func (r *Request) BasicAuth() (username, password string, ok bool)
+ + +
    func (r *Request) Cookie(name string) (*Cookie, error)
+ + +
    func (r *Request) Cookies() []*Cookie
+ + +
    func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error)
+ + +
    func (r *Request) FormValue(key string) string
+ + +
    func (r *Request) MultipartReader() (*multipart.Reader, error)
+ + +
    func (r *Request) ParseForm() error
+ + +
    func (r *Request) ParseMultipartForm(maxMemory int64) error
+ + +
    func (r *Request) PostFormValue(key string) string
+ + +
    func (r *Request) ProtoAtLeast(major, minor int) bool
+ + +
    func (r *Request) Referer() string
+ + +
    func (r *Request) SetBasicAuth(username, password string)
+ + +
    func (r *Request) UserAgent() string
+ + +
    func (r *Request) Write(w io.Writer) error
+ + +
    func (r *Request) WriteProxy(w io.Writer) error
+ + + +
type Response
+ + +
    func Get(url string) (resp *Response, err error)
+ + +
    func Head(url string) (resp *Response, err error)
+ + +
    func Post(url string, bodyType string, body io.Reader) (resp *Response, err error)
+ + +
    func PostForm(url string, data url.Values) (resp *Response, err error)
+ + +
    func ReadResponse(r *bufio.Reader, req *Request) (*Response, error)
+ + + +
    func (r *Response) Cookies() []*Cookie
+ + +
    func (r *Response) Location() (*url.URL, error)
+ + +
    func (r *Response) ProtoAtLeast(major, minor int) bool
+ + +
    func (r *Response) Write(w io.Writer) error
+ + + +
type ResponseWriter
+ + + + +
type RoundTripper
+ + +
    func NewFileTransport(fs FileSystem) RoundTripper
+ + + + +
type ServeMux
+ + +
    func NewServeMux() *ServeMux
+ + + +
    func (mux *ServeMux) Handle(pattern string, handler Handler)
+ + +
    func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))
+ + +
    func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string)
+ + +
    func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)
+ + + +
type Server
+ + + +
    func (srv *Server) ListenAndServe() error
+ + +
    func (srv *Server) ListenAndServeTLS(certFile, keyFile string) error
+ + +
    func (srv *Server) Serve(l net.Listener) error
+ + +
    func (srv *Server) SetKeepAlivesEnabled(v bool)
+ + + +
type Transport
+ + + +
    func (t *Transport) CancelRequest(req *Request)
+ + +
    func (t *Transport) CloseIdleConnections()
+ + +
    func (t *Transport) RegisterProtocol(scheme string, rt RoundTripper)
+ + +
    func (t *Transport) RoundTrip(req *Request) (resp *Response, err error)
+ + + +
+
+ + + + + + +

Package files

+

+ + + client.go + + cookie.go + + doc.go + + filetransport.go + + fs.go + + header.go + + jar.go + + lex.go + + request.go + + response.go + + server.go + + sniff.go + + status.go + + transfer.go + + transport.go + + +

+ +
+
+ + + + +

Constants

+ +
const (
+        StatusContinue           = 100
+        StatusSwitchingProtocols = 101
+
+        StatusOK                   = 200
+        StatusCreated              = 201
+        StatusAccepted             = 202
+        StatusNonAuthoritativeInfo = 203
+        StatusNoContent            = 204
+        StatusResetContent         = 205
+        StatusPartialContent       = 206
+
+        StatusMultipleChoices   = 300
+        StatusMovedPermanently  = 301
+        StatusFound             = 302
+        StatusSeeOther          = 303
+        StatusNotModified       = 304
+        StatusUseProxy          = 305
+        StatusTemporaryRedirect = 307
+
+        StatusBadRequest                   = 400
+        StatusUnauthorized                 = 401
+        StatusPaymentRequired              = 402
+        StatusForbidden                    = 403
+        StatusNotFound                     = 404
+        StatusMethodNotAllowed             = 405
+        StatusNotAcceptable                = 406
+        StatusProxyAuthRequired            = 407
+        StatusRequestTimeout               = 408
+        StatusConflict                     = 409
+        StatusGone                         = 410
+        StatusLengthRequired               = 411
+        StatusPreconditionFailed           = 412
+        StatusRequestEntityTooLarge        = 413
+        StatusRequestURITooLong            = 414
+        StatusUnsupportedMediaType         = 415
+        StatusRequestedRangeNotSatisfiable = 416
+        StatusExpectationFailed            = 417
+        StatusTeapot                       = 418
+
+        StatusInternalServerError     = 500
+        StatusNotImplemented          = 501
+        StatusBadGateway              = 502
+        StatusServiceUnavailable      = 503
+        StatusGatewayTimeout          = 504
+        StatusHTTPVersionNotSupported = 505
+)
+

+HTTP status codes, defined in RFC 2616. +

+ + +
const DefaultMaxHeaderBytes = 1 << 20 // 1 MB
+
+

+DefaultMaxHeaderBytes is the maximum permitted size of the headers +in an HTTP request. +This can be overridden by setting Server.MaxHeaderBytes. +

+ + +
const DefaultMaxIdleConnsPerHost = 2
+

+DefaultMaxIdleConnsPerHost is the default value of Transport's +MaxIdleConnsPerHost. +

+ + +
const TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
+

+TimeFormat is the time format to use with +time.Parse and time.Time.Format when parsing +or generating times in HTTP headers. +It is like time.RFC1123 but hard codes GMT as the time zone. +

+ + + + +

Variables

+ +
var (
+        ErrHeaderTooLong        = &ProtocolError{"header too long"}
+        ErrShortBody            = &ProtocolError{"entity body too short"}
+        ErrNotSupported         = &ProtocolError{"feature not supported"}
+        ErrUnexpectedTrailer    = &ProtocolError{"trailer header without chunked transfer encoding"}
+        ErrMissingContentLength = &ProtocolError{"missing ContentLength in HEAD response"}
+        ErrNotMultipart         = &ProtocolError{"request Content-Type isn't multipart/form-data"}
+        ErrMissingBoundary      = &ProtocolError{"no multipart boundary param in Content-Type"}
+)
+ + +
var (
+        ErrWriteAfterFlush = errors.New("Conn.Write called after Flush")
+        ErrBodyNotAllowed  = errors.New("http: request method or response status code does not allow body")
+        ErrHijacked        = errors.New("Conn has been hijacked")
+        ErrContentLength   = errors.New("Conn.Write wrote more than the declared Content-Length")
+)
+

+Errors introduced by the HTTP server. +

+ + +
var DefaultClient = &Client{}
+

+DefaultClient is the default Client and is used by Get, Head, and Post. +

+ + +
var DefaultServeMux = NewServeMux()
+

+DefaultServeMux is the default ServeMux used by Serve. +

+ + +
var ErrBodyReadAfterClose = errors.New("http: invalid Read on closed Body")
+

+ErrBodyReadAfterClose is returned when reading a Request or Response +Body after the body has been closed. This typically happens when the body is +read after an HTTP Handler calls WriteHeader or Write on its +ResponseWriter. +

+ + +
var ErrHandlerTimeout = errors.New("http: Handler timeout")
+

+ErrHandlerTimeout is returned on ResponseWriter Write calls +in handlers which have timed out. +

+ + +
var ErrLineTooLong = internal.ErrLineTooLong
+

+ErrLineTooLong is returned when reading request or response bodies +with malformed chunked encoding. +

+ + +
var ErrMissingFile = errors.New("http: no such file")
+

+ErrMissingFile is returned by FormFile when the provided file field name +is either not present in the request or not a file field. +

+ + +
var ErrNoCookie = errors.New("http: named cookie not present")
+

+ErrNoCookie is returned by Request's Cookie method when a cookie is not found. +

+ + +
var ErrNoLocation = errors.New("http: no Location header in response")
+

+ErrNoLocation is returned by Response's Location method +when no Location header is present. +

+ + + + + + +

func CanonicalHeaderKey

+
func CanonicalHeaderKey(s string) string
+

+CanonicalHeaderKey returns the canonical format of the +header key s. The canonicalization converts the first +letter and any letter following a hyphen to upper case; +the rest are converted to lowercase. For example, the +canonical key for "accept-encoding" is "Accept-Encoding". +If s contains a space or invalid header field bytes, it is +returned without modifications. +

+ + + + + + + +

func DetectContentType

+
func DetectContentType(data []byte) string
+

+DetectContentType implements the algorithm described +at http://mimesniff.spec.whatwg.org/ to determine the +Content-Type of the given data. It considers at most the +first 512 bytes of data. DetectContentType always returns +a valid MIME type: if it cannot determine a more specific one, it +returns "application/octet-stream". +

+ + + + + + + +

func Error

+
func Error(w ResponseWriter, error string, code int)
+

+Error replies to the request with the specified error message and HTTP code. +The error message should be plain text. +

+ + + + + + + +

func Handle

+
func Handle(pattern string, handler Handler)
+

+Handle registers the handler for the given pattern +in the DefaultServeMux. +The documentation for ServeMux explains how patterns are matched. +

+ + + + + + + +

func HandleFunc

+
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
+

+HandleFunc registers the handler function for the given pattern +in the DefaultServeMux. +The documentation for ServeMux explains how patterns are matched. +

+ + + + + + + +

func ListenAndServe

+
func ListenAndServe(addr string, handler Handler) error
+

+ListenAndServe listens on the TCP network address addr +and then calls Serve with handler to handle requests +on incoming connections. Handler is typically nil, +in which case the DefaultServeMux is used. +

+

+A trivial example server is: +

+
package main
+
+import (
+	"io"
+	"net/http"
+	"log"
+)
+
+// hello world, the web server
+func HelloServer(w http.ResponseWriter, req *http.Request) {
+	io.WriteString(w, "hello, world!\n")
+}
+
+func main() {
+	http.HandleFunc("/hello", HelloServer)
+	err := http.ListenAndServe(":12345", nil)
+	if err != nil {
+		log.Fatal("ListenAndServe: ", err)
+	}
+}
+
+ + + + + + + +

func ListenAndServeTLS

+
func ListenAndServeTLS(addr string, certFile string, keyFile string, handler Handler) error
+

+ListenAndServeTLS acts identically to ListenAndServe, except that it +expects HTTPS connections. Additionally, files containing a certificate and +matching private key for the server must be provided. If the certificate +is signed by a certificate authority, the certFile should be the concatenation +of the server's certificate, any intermediates, and the CA's certificate. +

+

+A trivial example server is: +

+
import (
+	"log"
+	"net/http"
+)
+
+func handler(w http.ResponseWriter, req *http.Request) {
+	w.Header().Set("Content-Type", "text/plain")
+	w.Write([]byte("This is an example server.\n"))
+}
+
+func main() {
+	http.HandleFunc("/", handler)
+	log.Printf("About to listen on 10443. Go to https://127.0.0.1:10443/")
+	err := http.ListenAndServeTLS(":10443", "cert.pem", "key.pem", nil)
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+
+

+One can use generate_cert.go in crypto/tls to generate cert.pem and key.pem. +

+ + + + + + + +

func MaxBytesReader

+
func MaxBytesReader(w ResponseWriter, r io.ReadCloser, n int64) io.ReadCloser
+

+MaxBytesReader is similar to io.LimitReader but is intended for +limiting the size of incoming request bodies. In contrast to +io.LimitReader, MaxBytesReader's result is a ReadCloser, returns a +non-EOF error for a Read beyond the limit, and closes the +underlying reader when its Close method is called. +

+

+MaxBytesReader prevents clients from accidentally or maliciously +sending a large request and wasting server resources. +

+ + + + + + + +

func NotFound

+
func NotFound(w ResponseWriter, r *Request)
+

+NotFound replies to the request with an HTTP 404 not found error. +

+ + + + + + + +

func ParseHTTPVersion

+
func ParseHTTPVersion(vers string) (major, minor int, ok bool)
+

+ParseHTTPVersion parses a HTTP version string. +"HTTP/1.0" returns (1, 0, true). +

+ + + + + + + +

func ParseTime

+
func ParseTime(text string) (t time.Time, err error)
+

+ParseTime parses a time header (such as the Date: header), +trying each of the three formats allowed by HTTP/1.1: +TimeFormat, time.RFC850, and time.ANSIC. +

+ + + + + + + +

func ProxyFromEnvironment

+
func ProxyFromEnvironment(req *Request) (*url.URL, error)
+

+ProxyFromEnvironment returns the URL of the proxy to use for a +given request, as indicated by the environment variables +HTTP_PROXY, HTTPS_PROXY and NO_PROXY (or the lowercase versions +thereof). HTTPS_PROXY takes precedence over HTTP_PROXY for https +requests. +

+

+The environment values may be either a complete URL or a +"host[:port]", in which case the "http" scheme is assumed. +An error is returned if the value is a different form. +

+

+A nil URL and nil error are returned if no proxy is defined in the +environment, or a proxy should not be used for the given request, +as defined by NO_PROXY. +

+

+As a special case, if req.URL.Host is "localhost" (with or without +a port number), then a nil URL and nil error will be returned. +

+ + + + + + + +

func ProxyURL

+
func ProxyURL(fixedURL *url.URL) func(*Request) (*url.URL, error)
+

+ProxyURL returns a proxy function (for use in a Transport) +that always returns the same URL. +

+ + + + + + + +

func Redirect

+
func Redirect(w ResponseWriter, r *Request, urlStr string, code int)
+

+Redirect replies to the request with a redirect to url, +which may be a path relative to the request path. +

+ + + + + + + +

func Serve

+
func Serve(l net.Listener, handler Handler) error
+

+Serve accepts incoming HTTP connections on the listener l, +creating a new service goroutine for each. The service goroutines +read requests and then call handler to reply to them. +Handler is typically nil, in which case the DefaultServeMux is used. +

+ + + + + + + +

func ServeContent

+
func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker)
+

+ServeContent replies to the request using the content in the +provided ReadSeeker. The main benefit of ServeContent over io.Copy +is that it handles Range requests properly, sets the MIME type, and +handles If-Modified-Since requests. +

+

+If the response's Content-Type header is not set, ServeContent +first tries to deduce the type from name's file extension and, +if that fails, falls back to reading the first block of the content +and passing it to DetectContentType. +The name is otherwise unused; in particular it can be empty and is +never sent in the response. +

+

+If modtime is not the zero time or Unix epoch, ServeContent +includes it in a Last-Modified header in the response. If the +request includes an If-Modified-Since header, ServeContent uses +modtime to decide whether the content needs to be sent at all. +

+

+The content's Seek method must work: ServeContent uses +a seek to the end of the content to determine its size. +

+

+If the caller has set w's ETag header, ServeContent uses it to +handle requests using If-Range and If-None-Match. +

+

+Note that *os.File implements the io.ReadSeeker interface. +

+ + + + + + + +

func ServeFile

+
func ServeFile(w ResponseWriter, r *Request, name string)
+

+ServeFile replies to the request with the contents of the named +file or directory. +

+

+As a special case, ServeFile redirects any request where r.URL.Path +ends in "/index.html" to the same path, without the final +"index.html". To avoid such redirects either modify the path or +use ServeContent. +

+ + + + + + + +

func SetCookie

+
func SetCookie(w ResponseWriter, cookie *Cookie)
+

+SetCookie adds a Set-Cookie header to the provided ResponseWriter's headers. +The provided cookie must have a valid Name. Invalid cookies may be +silently dropped. +

+ + + + + + + +

func StatusText

+
func StatusText(code int) string
+

+StatusText returns a text for the HTTP status code. It returns the empty +string if the code is unknown. +

+ + + + + + + + +

type Client

+
type Client struct {
+        // Transport specifies the mechanism by which individual
+        // HTTP requests are made.
+        // If nil, DefaultTransport is used.
+        Transport RoundTripper
+
+        // CheckRedirect specifies the policy for handling redirects.
+        // If CheckRedirect is not nil, the client calls it before
+        // following an HTTP redirect. The arguments req and via are
+        // the upcoming request and the requests made already, oldest
+        // first. If CheckRedirect returns an error, the Client's Get
+        // method returns both the previous Response and
+        // CheckRedirect's error (wrapped in a url.Error) instead of
+        // issuing the Request req.
+        //
+        // If CheckRedirect is nil, the Client uses its default policy,
+        // which is to stop after 10 consecutive requests.
+        CheckRedirect func(req *Request, via []*Request) error
+
+        // Jar specifies the cookie jar.
+        // If Jar is nil, cookies are not sent in requests and ignored
+        // in responses.
+        Jar CookieJar
+
+        // Timeout specifies a time limit for requests made by this
+        // Client. The timeout includes connection time, any
+        // redirects, and reading the response body. The timer remains
+        // running after Get, Head, Post, or Do return and will
+        // interrupt reading of the Response.Body.
+        //
+        // A Timeout of zero means no timeout.
+        //
+        // The Client's Transport must support the CancelRequest
+        // method or Client will return errors when attempting to make
+        // a request with Get, Head, Post, or Do. Client's default
+        // Transport (DefaultTransport) supports CancelRequest.
+        Timeout time.Duration
+}
+

+A Client is an HTTP client. Its zero value (DefaultClient) is a +usable client that uses DefaultTransport. +

+

+The Client's Transport typically has internal state (cached TCP +connections), so Clients should be reused instead of created as +needed. Clients are safe for concurrent use by multiple goroutines. +

+

+A Client is higher-level than a RoundTripper (such as Transport) +and additionally handles HTTP details such as cookies and +redirects. +

+ + + + + + + + + + + + + + +

func (*Client) Do

+
func (c *Client) Do(req *Request) (resp *Response, err error)
+

+Do sends an HTTP request and returns an HTTP response, following +policy (e.g. redirects, cookies, auth) as configured on the client. +

+

+An error is returned if caused by client policy (such as +CheckRedirect), or if there was an HTTP protocol error. +A non-2xx response doesn't cause an error. +

+

+When err is nil, resp always contains a non-nil resp.Body. +

+

+Callers should close resp.Body when done reading from it. If +resp.Body is not closed, the Client's underlying RoundTripper +(typically Transport) may not be able to re-use a persistent TCP +connection to the server for a subsequent "keep-alive" request. +

+

+The request Body, if non-nil, will be closed by the underlying +Transport, even on errors. +

+

+Generally Get, Post, or PostForm will be used instead of Do. +

+ + + + + + +

func (*Client) Get

+
func (c *Client) Get(url string) (resp *Response, err error)
+

+Get issues a GET to the specified URL. If the response is one of the +following redirect codes, Get follows the redirect after calling the +Client's CheckRedirect function: +

+
301 (Moved Permanently)
+302 (Found)
+303 (See Other)
+307 (Temporary Redirect)
+
+

+An error is returned if the Client's CheckRedirect function fails +or if there was an HTTP protocol error. A non-2xx response doesn't +cause an error. +

+

+When err is nil, resp always contains a non-nil resp.Body. +Caller should close resp.Body when done reading from it. +

+

+To make a request with custom headers, use NewRequest and Client.Do. +

+ + + + + + +

func (*Client) Head

+
func (c *Client) Head(url string) (resp *Response, err error)
+

+Head issues a HEAD to the specified URL. If the response is one of the +following redirect codes, Head follows the redirect after calling the +Client's CheckRedirect function: +

+
301 (Moved Permanently)
+302 (Found)
+303 (See Other)
+307 (Temporary Redirect)
+
+ + + + + + +

func (*Client) Post

+
func (c *Client) Post(url string, bodyType string, body io.Reader) (resp *Response, err error)
+

+Post issues a POST to the specified URL. +

+

+Caller should close resp.Body when done reading from it. +

+

+If the provided body is an io.Closer, it is closed after the +request. +

+

+To set custom headers, use NewRequest and Client.Do. +

+ + + + + + +

func (*Client) PostForm

+
func (c *Client) PostForm(url string, data url.Values) (resp *Response, err error)
+

+PostForm issues a POST to the specified URL, +with data's keys and values URL-encoded as the request body. +

+

+The Content-Type header is set to application/x-www-form-urlencoded. +To set other headers, use NewRequest and DefaultClient.Do. +

+

+When err is nil, resp always contains a non-nil resp.Body. +Caller should close resp.Body when done reading from it. +

+ + + + + + + + +

type CloseNotifier

+
type CloseNotifier interface {
+        // CloseNotify returns a channel that receives a single value
+        // when the client connection has gone away.
+        CloseNotify() <-chan bool
+}
+

+The CloseNotifier interface is implemented by ResponseWriters which +allow detecting when the underlying connection has gone away. +

+

+This mechanism can be used to cancel long operations on the server +if the client has disconnected before the response is ready. +

+ + + + + + + + + + + + + + + + +

type ConnState

+
type ConnState int
+

+A ConnState represents the state of a client connection to a server. +It's used by the optional Server.ConnState hook. +

+ + + +
const (
+        // StateNew represents a new connection that is expected to
+        // send a request immediately. Connections begin at this
+        // state and then transition to either StateActive or
+        // StateClosed.
+        StateNew ConnState = iota
+
+        // StateActive represents a connection that has read 1 or more
+        // bytes of a request. The Server.ConnState hook for
+        // StateActive fires before the request has entered a handler
+        // and doesn't fire again until the request has been
+        // handled. After the request is handled, the state
+        // transitions to StateClosed, StateHijacked, or StateIdle.
+        StateActive
+
+        // StateIdle represents a connection that has finished
+        // handling a request and is in the keep-alive state, waiting
+        // for a new request. Connections transition from StateIdle
+        // to either StateActive or StateClosed.
+        StateIdle
+
+        // StateHijacked represents a hijacked connection.
+        // This is a terminal state. It does not transition to StateClosed.
+        StateHijacked
+
+        // StateClosed represents a closed connection.
+        // This is a terminal state. Hijacked connections do not
+        // transition to StateClosed.
+        StateClosed
+)
+ + + + + + + + + + + + + +

func (ConnState) String

+
func (c ConnState) String() string
+ + + + + + + + + +
type Cookie struct {
+        Name  string
+        Value string
+
+        Path       string    // optional
+        Domain     string    // optional
+        Expires    time.Time // optional
+        RawExpires string    // for reading cookies only
+
+        // MaxAge=0 means no 'Max-Age' attribute specified.
+        // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
+        // MaxAge>0 means Max-Age attribute present and given in seconds
+        MaxAge   int
+        Secure   bool
+        HttpOnly bool
+        Raw      string
+        Unparsed []string // Raw text of unparsed attribute-value pairs
+}
+

+A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an +HTTP response or the Cookie header of an HTTP request. +

+

+See http://tools.ietf.org/html/rfc6265 for details. +

+ + + + + + + + + + + + + + +

func (*Cookie) String

+
func (c *Cookie) String() string
+

+String returns the serialization of the cookie for use in a Cookie +header (if only Name and Value are set) or a Set-Cookie response +header (if other fields are set). +If c is nil or c.Name is invalid, the empty string is returned. +

+ + + + + + + + +

type CookieJar

+
type CookieJar interface {
+        // SetCookies handles the receipt of the cookies in a reply for the
+        // given URL.  It may or may not choose to save the cookies, depending
+        // on the jar's policy and implementation.
+        SetCookies(u *url.URL, cookies []*Cookie)
+
+        // Cookies returns the cookies to send in a request for the given URL.
+        // It is up to the implementation to honor the standard cookie use
+        // restrictions such as in RFC 6265.
+        Cookies(u *url.URL) []*Cookie
+}
+

+A CookieJar manages storage and use of cookies in HTTP requests. +

+

+Implementations of CookieJar must be safe for concurrent use by multiple +goroutines. +

+

+The net/http/cookiejar package provides a CookieJar implementation. +

+ + + + + + + + + + + + + + + + +

type Dir

+
type Dir string
+

+A Dir implements FileSystem using the native file system restricted to a +specific directory tree. +

+

+While the FileSystem.Open method takes '/'-separated paths, a Dir's string +value is a filename on the native file system, not a URL, so it is separated +by filepath.Separator, which isn't necessarily '/'. +

+

+An empty Dir is treated as ".". +

+ + + + + + + + + + + + + + +

func (Dir) Open

+
func (d Dir) Open(name string) (File, error)
+ + + + + + + + +

type File

+
type File interface {
+        io.Closer
+        io.Reader
+        Readdir(count int) ([]os.FileInfo, error)
+        Seek(offset int64, whence int) (int64, error)
+        Stat() (os.FileInfo, error)
+}
+

+A File is returned by a FileSystem's Open method and can be +served by the FileServer implementation. +

+

+The methods should behave the same as those on an *os.File. +

+ + + + + + + + + + + + + + + + +

type FileSystem

+
type FileSystem interface {
+        Open(name string) (File, error)
+}
+

+A FileSystem implements access to a collection of named files. +The elements in a file path are separated by slash ('/', U+002F) +characters, regardless of host operating system convention. +

+ + + + + + + + + + + + + + + + +

type Flusher

+
type Flusher interface {
+        // Flush sends any buffered data to the client.
+        Flush()
+}
+

+The Flusher interface is implemented by ResponseWriters that allow +an HTTP handler to flush buffered data to the client. +

+

+Note that even for ResponseWriters that support Flush, +if the client is connected through an HTTP proxy, +the buffered data may not reach the client until the response +completes. +

+ + + + + + + + + + + + + + + + +

type Handler

+
type Handler interface {
+        ServeHTTP(ResponseWriter, *Request)
+}
+

+Objects implementing the Handler interface can be +registered to serve a particular path or subtree +in the HTTP server. +

+

+ServeHTTP should write reply headers and data to the ResponseWriter +and then return. Returning signals that the request is finished +and that the HTTP server can move on to the next request on +the connection. +

+

+If ServeHTTP panics, the server (the caller of ServeHTTP) assumes +that the effect of the panic was isolated to the active request. +It recovers the panic, logs a stack trace to the server error log, +and hangs up the connection. +

+ + + + + + + + + + + + +

func FileServer

+
func FileServer(root FileSystem) Handler
+

+FileServer returns a handler that serves HTTP requests +with the contents of the file system rooted at root. +

+

+To use the operating system's file system implementation, +use http.Dir: +

+
http.Handle("/", http.FileServer(http.Dir("/tmp")))
+
+

+As a special case, the returned file server redirects any request +ending in "/index.html" to the same path, without the final +"index.html". +

+ +
+ +
+

▾ Example

+ + + +
+
+
+
+ Run + Format + +
+
+ +
+
+
+ +
+

▾ Example (StripPrefix)

+ + + +
+
+
+
+ Run + Format + +
+
+ +
+
+ + + + +

func NotFoundHandler

+
func NotFoundHandler() Handler
+

+NotFoundHandler returns a simple request handler +that replies to each request with a “404 page not found” reply. +

+ + + + + +

func RedirectHandler

+
func RedirectHandler(url string, code int) Handler
+

+RedirectHandler returns a request handler that redirects +each request it receives to the given url using the given +status code. +

+ + + + + +

func StripPrefix

+
func StripPrefix(prefix string, h Handler) Handler
+

+StripPrefix returns a handler that serves HTTP requests +by removing the given prefix from the request URL's Path +and invoking the handler h. StripPrefix handles a +request for a path that doesn't begin with prefix by +replying with an HTTP 404 not found error. +

+ +
+ +
+

▾ Example

+ + + +
+
+
+
+ Run + Format + +
+
+ +
+
+ + + + +

func TimeoutHandler

+
func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler
+

+TimeoutHandler returns a Handler that runs h with the given time limit. +

+

+The new Handler calls h.ServeHTTP to handle each request, but if a +call runs for longer than its time limit, the handler responds with +a 503 Service Unavailable error and the given message in its body. +(If msg is empty, a suitable default message will be sent.) +After such a timeout, writes by h to its ResponseWriter will return +ErrHandlerTimeout. +

+ + + + + + + + + +

type HandlerFunc

+
type HandlerFunc func(ResponseWriter, *Request)
+

+The HandlerFunc type is an adapter to allow the use of +ordinary functions as HTTP handlers. If f is a function +with the appropriate signature, HandlerFunc(f) is a +Handler object that calls f. +

+ + + + + + + + + + + + + + +

func (HandlerFunc) ServeHTTP

+
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request)
+

+ServeHTTP calls f(w, r). +

+ + + + + + + + + +
type Header map[string][]string
+

+A Header represents the key-value pairs in an HTTP header. +

+ + + + + + + + + + + + + + +

func (Header) Add

+
func (h Header) Add(key, value string)
+

+Add adds the key, value pair to the header. +It appends to any existing values associated with key. +

+ + + + + + +

func (Header) Del

+
func (h Header) Del(key string)
+

+Del deletes the values associated with key. +

+ + + + + + +

func (Header) Get

+
func (h Header) Get(key string) string
+

+Get gets the first value associated with the given key. +If there are no values associated with the key, Get returns "". +To access multiple values of a key, access the map directly +with CanonicalHeaderKey. +

+ + + + + + +

func (Header) Set

+
func (h Header) Set(key, value string)
+

+Set sets the header entries associated with key to +the single element value. It replaces any existing +values associated with key. +

+ + + + + + +

func (Header) Write

+
func (h Header) Write(w io.Writer) error
+

+Write writes a header in wire format. +

+ + + + + + +

func (Header) WriteSubset

+
func (h Header) WriteSubset(w io.Writer, exclude map[string]bool) error
+

+WriteSubset writes a header in wire format. +If exclude is not nil, keys where exclude[key] == true are not written. +

+ + + + + + + + +

type Hijacker

+
type Hijacker interface {
+        // Hijack lets the caller take over the connection.
+        // After a call to Hijack(), the HTTP server library
+        // will not do anything else with the connection.
+        //
+        // It becomes the caller's responsibility to manage
+        // and close the connection.
+        //
+        // The returned net.Conn may have read or write deadlines
+        // already set, depending on the configuration of the
+        // Server. It is the caller's responsibility to set
+        // or clear those deadlines as needed.
+        Hijack() (net.Conn, *bufio.ReadWriter, error)
+}
+

+The Hijacker interface is implemented by ResponseWriters that allow +an HTTP handler to take over the connection. +

+ + + + + + +
+ +
+

▾ Example

+ + + +
+
+
+
+ Run + Format + +
+
+ +
+
+ + + + + + + + + + +

type ProtocolError

+
type ProtocolError struct {
+        ErrorString string
+}
+

+HTTP request parsing errors. +

+ + + + + + + + + + + + + + +

func (*ProtocolError) Error

+
func (err *ProtocolError) Error() string
+ + + + + + + + +

type Request

+
type Request struct {
+        // Method specifies the HTTP method (GET, POST, PUT, etc.).
+        // For client requests an empty string means GET.
+        Method string
+
+        // URL specifies either the URI being requested (for server
+        // requests) or the URL to access (for client requests).
+        //
+        // For server requests the URL is parsed from the URI
+        // supplied on the Request-Line as stored in RequestURI.  For
+        // most requests, fields other than Path and RawQuery will be
+        // empty. (See RFC 2616, Section 5.1.2)
+        //
+        // For client requests, the URL's Host specifies the server to
+        // connect to, while the Request's Host field optionally
+        // specifies the Host header value to send in the HTTP
+        // request.
+        URL *url.URL
+
+        // The protocol version for incoming requests.
+        // Client requests always use HTTP/1.1.
+        Proto      string // "HTTP/1.0"
+        ProtoMajor int    // 1
+        ProtoMinor int    // 0
+
+        // A header maps request lines to their values.
+        // If the header says
+        //
+        //	accept-encoding: gzip, deflate
+        //	Accept-Language: en-us
+        //	Connection: keep-alive
+        //
+        // then
+        //
+        //	Header = map[string][]string{
+        //		"Accept-Encoding": {"gzip, deflate"},
+        //		"Accept-Language": {"en-us"},
+        //		"Connection": {"keep-alive"},
+        //	}
+        //
+        // HTTP defines that header names are case-insensitive.
+        // The request parser implements this by canonicalizing the
+        // name, making the first character and any characters
+        // following a hyphen uppercase and the rest lowercase.
+        //
+        // For client requests certain headers are automatically
+        // added and may override values in Header.
+        //
+        // See the documentation for the Request.Write method.
+        Header Header
+
+        // Body is the request's body.
+        //
+        // For client requests a nil body means the request has no
+        // body, such as a GET request. The HTTP Client's Transport
+        // is responsible for calling the Close method.
+        //
+        // For server requests the Request Body is always non-nil
+        // but will return EOF immediately when no body is present.
+        // The Server will close the request body. The ServeHTTP
+        // Handler does not need to.
+        Body io.ReadCloser
+
+        // ContentLength records the length of the associated content.
+        // The value -1 indicates that the length is unknown.
+        // Values >= 0 indicate that the given number of bytes may
+        // be read from Body.
+        // For client requests, a value of 0 means unknown if Body is not nil.
+        ContentLength int64
+
+        // TransferEncoding lists the transfer encodings from outermost to
+        // innermost. An empty list denotes the "identity" encoding.
+        // TransferEncoding can usually be ignored; chunked encoding is
+        // automatically added and removed as necessary when sending and
+        // receiving requests.
+        TransferEncoding []string
+
+        // Close indicates whether to close the connection after
+        // replying to this request (for servers) or after sending
+        // the request (for clients).
+        Close bool
+
+        // For server requests Host specifies the host on which the
+        // URL is sought. Per RFC 2616, this is either the value of
+        // the "Host" header or the host name given in the URL itself.
+        // It may be of the form "host:port".
+        //
+        // For client requests Host optionally overrides the Host
+        // header to send. If empty, the Request.Write method uses
+        // the value of URL.Host.
+        Host string
+
+        // Form contains the parsed form data, including both the URL
+        // field's query parameters and the POST or PUT form data.
+        // This field is only available after ParseForm is called.
+        // The HTTP client ignores Form and uses Body instead.
+        Form url.Values
+
+        // PostForm contains the parsed form data from POST, PATCH,
+        // or PUT body parameters.
+        //
+        // This field is only available after ParseForm is called.
+        // The HTTP client ignores PostForm and uses Body instead.
+        PostForm url.Values
+
+        // MultipartForm is the parsed multipart form, including file uploads.
+        // This field is only available after ParseMultipartForm is called.
+        // The HTTP client ignores MultipartForm and uses Body instead.
+        MultipartForm *multipart.Form
+
+        // Trailer specifies additional headers that are sent after the request
+        // body.
+        //
+        // For server requests the Trailer map initially contains only the
+        // trailer keys, with nil values. (The client declares which trailers it
+        // will later send.)  While the handler is reading from Body, it must
+        // not reference Trailer. After reading from Body returns EOF, Trailer
+        // can be read again and will contain non-nil values, if they were sent
+        // by the client.
+        //
+        // For client requests Trailer must be initialized to a map containing
+        // the trailer keys to later send. The values may be nil or their final
+        // values. The ContentLength must be 0 or -1, to send a chunked request.
+        // After the HTTP request is sent the map values can be updated while
+        // the request body is read. Once the body returns EOF, the caller must
+        // not mutate Trailer.
+        //
+        // Few HTTP clients, servers, or proxies support HTTP trailers.
+        Trailer Header
+
+        // RemoteAddr allows HTTP servers and other software to record
+        // the network address that sent the request, usually for
+        // logging. This field is not filled in by ReadRequest and
+        // has no defined format. The HTTP server in this package
+        // sets RemoteAddr to an "IP:port" address before invoking a
+        // handler.
+        // This field is ignored by the HTTP client.
+        RemoteAddr string
+
+        // RequestURI is the unmodified Request-URI of the
+        // Request-Line (RFC 2616, Section 5.1) as sent by the client
+        // to a server. Usually the URL field should be used instead.
+        // It is an error to set this field in an HTTP client request.
+        RequestURI string
+
+        // TLS allows HTTP servers and other software to record
+        // information about the TLS connection on which the request
+        // was received. This field is not filled in by ReadRequest.
+        // The HTTP server in this package sets the field for
+        // TLS-enabled connections before invoking a handler;
+        // otherwise it leaves the field nil.
+        // This field is ignored by the HTTP client.
+        TLS *tls.ConnectionState
+
+        // Cancel is an optional channel whose closure indicates that the client
+        // request should be regarded as canceled. Not all implementations of
+        // RoundTripper may support Cancel.
+        //
+        // For server requests, this field is not applicable.
+        Cancel <-chan struct{}
+}
+

+A Request represents an HTTP request received by a server +or to be sent by a client. +

+

+The field semantics differ slightly between client and server +usage. In addition to the notes on the fields below, see the +documentation for Request.Write and RoundTripper. +

+ + + + + + + + + + + + +

func NewRequest

+
func NewRequest(method, urlStr string, body io.Reader) (*Request, error)
+

+NewRequest returns a new Request given a method, URL, and optional body. +

+

+If the provided body is also an io.Closer, the returned +Request.Body is set to body and will be closed by the Client +methods Do, Post, and PostForm, and Transport.RoundTrip. +

+

+NewRequest returns a Request suitable for use with Client.Do or +Transport.RoundTrip. +To create a request for use with testing a Server Handler use either +ReadRequest or manually update the Request fields. See the Request +type's documentation for the difference between inbound and outbound +request fields. +

+ + + + + +

func ReadRequest

+
func ReadRequest(b *bufio.Reader) (req *Request, err error)
+

+ReadRequest reads and parses an incoming request from b. +

+ + + + + + + +

func (*Request) AddCookie

+
func (r *Request) AddCookie(c *Cookie)
+

+AddCookie adds a cookie to the request. Per RFC 6265 section 5.4, +AddCookie does not attach more than one Cookie header field. That +means all cookies, if any, are written into the same line, +separated by semicolon. +

+ + + + + + +

func (*Request) BasicAuth

+
func (r *Request) BasicAuth() (username, password string, ok bool)
+

+BasicAuth returns the username and password provided in the request's +Authorization header, if the request uses HTTP Basic Authentication. +See RFC 2617, Section 2. +

+ + + + + + +

func (*Request) Cookie

+
func (r *Request) Cookie(name string) (*Cookie, error)
+

+Cookie returns the named cookie provided in the request or +ErrNoCookie if not found. +

+ + + + + + +

func (*Request) Cookies

+
func (r *Request) Cookies() []*Cookie
+

+Cookies parses and returns the HTTP cookies sent with the request. +

+ + + + + + +

func (*Request) FormFile

+
func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error)
+

+FormFile returns the first file for the provided form key. +FormFile calls ParseMultipartForm and ParseForm if necessary. +

+ + + + + + +

func (*Request) FormValue

+
func (r *Request) FormValue(key string) string
+

+FormValue returns the first value for the named component of the query. +POST and PUT body parameters take precedence over URL query string values. +FormValue calls ParseMultipartForm and ParseForm if necessary and ignores +any errors returned by these functions. +If key is not present, FormValue returns the empty string. +To access multiple values of the same key, call ParseForm and +then inspect Request.Form directly. +

+ + + + + + +

func (*Request) MultipartReader

+
func (r *Request) MultipartReader() (*multipart.Reader, error)
+

+MultipartReader returns a MIME multipart reader if this is a +multipart/form-data POST request, else returns nil and an error. +Use this function instead of ParseMultipartForm to +process the request body as a stream. +

+ + + + + + +

func (*Request) ParseForm

+
func (r *Request) ParseForm() error
+

+ParseForm parses the raw query from the URL and updates r.Form. +

+

+For POST or PUT requests, it also parses the request body as a form and +put the results into both r.PostForm and r.Form. +POST and PUT body parameters take precedence over URL query string values +in r.Form. +

+

+If the request Body's size has not already been limited by MaxBytesReader, +the size is capped at 10MB. +

+

+ParseMultipartForm calls ParseForm automatically. +It is idempotent. +

+ + + + + + +

func (*Request) ParseMultipartForm

+
func (r *Request) ParseMultipartForm(maxMemory int64) error
+

+ParseMultipartForm parses a request body as multipart/form-data. +The whole request body is parsed and up to a total of maxMemory bytes of +its file parts are stored in memory, with the remainder stored on +disk in temporary files. +ParseMultipartForm calls ParseForm if necessary. +After one call to ParseMultipartForm, subsequent calls have no effect. +

+ + + + + + +

func (*Request) PostFormValue

+
func (r *Request) PostFormValue(key string) string
+

+PostFormValue returns the first value for the named component of the POST +or PUT request body. URL query parameters are ignored. +PostFormValue calls ParseMultipartForm and ParseForm if necessary and ignores +any errors returned by these functions. +If key is not present, PostFormValue returns the empty string. +

+ + + + + + +

func (*Request) ProtoAtLeast

+
func (r *Request) ProtoAtLeast(major, minor int) bool
+

+ProtoAtLeast reports whether the HTTP protocol used +in the request is at least major.minor. +

+ + + + + + +

func (*Request) Referer

+
func (r *Request) Referer() string
+

+Referer returns the referring URL, if sent in the request. +

+

+Referer is misspelled as in the request itself, a mistake from the +earliest days of HTTP. This value can also be fetched from the +Header map as Header["Referer"]; the benefit of making it available +as a method is that the compiler can diagnose programs that use the +alternate (correct English) spelling req.Referrer() but cannot +diagnose programs that use Header["Referrer"]. +

+ + + + + + +

func (*Request) SetBasicAuth

+
func (r *Request) SetBasicAuth(username, password string)
+

+SetBasicAuth sets the request's Authorization header to use HTTP +Basic Authentication with the provided username and password. +

+

+With HTTP Basic Authentication the provided username and password +are not encrypted. +

+ + + + + + +

func (*Request) UserAgent

+
func (r *Request) UserAgent() string
+

+UserAgent returns the client's User-Agent, if sent in the request. +

+ + + + + + +

func (*Request) Write

+
func (r *Request) Write(w io.Writer) error
+

+Write writes an HTTP/1.1 request, which is the header and body, in wire format. +This method consults the following fields of the request: +

+
Host
+URL
+Method (defaults to "GET")
+Header
+ContentLength
+TransferEncoding
+Body
+
+

+If Body is present, Content-Length is <= 0 and TransferEncoding +hasn't been set to "identity", Write adds "Transfer-Encoding: +chunked" to the header. Body is closed after it is sent. +

+ + + + + + +

func (*Request) WriteProxy

+
func (r *Request) WriteProxy(w io.Writer) error
+

+WriteProxy is like Write but writes the request in the form +expected by an HTTP proxy. In particular, WriteProxy writes the +initial Request-URI line of the request with an absolute URI, per +section 5.1.2 of RFC 2616, including the scheme and host. +In either case, WriteProxy also writes a Host header, using +either r.Host or r.URL.Host. +

+ + + + + + + + +

type Response

+
type Response struct {
+        Status     string // e.g. "200 OK"
+        StatusCode int    // e.g. 200
+        Proto      string // e.g. "HTTP/1.0"
+        ProtoMajor int    // e.g. 1
+        ProtoMinor int    // e.g. 0
+
+        // Header maps header keys to values.  If the response had multiple
+        // headers with the same key, they may be concatenated, with comma
+        // delimiters.  (Section 4.2 of RFC 2616 requires that multiple headers
+        // be semantically equivalent to a comma-delimited sequence.) Values
+        // duplicated by other fields in this struct (e.g., ContentLength) are
+        // omitted from Header.
+        //
+        // Keys in the map are canonicalized (see CanonicalHeaderKey).
+        Header Header
+
+        // Body represents the response body.
+        //
+        // The http Client and Transport guarantee that Body is always
+        // non-nil, even on responses without a body or responses with
+        // a zero-length body. It is the caller's responsibility to
+        // close Body. The default HTTP client's Transport does not
+        // attempt to reuse HTTP/1.0 or HTTP/1.1 TCP connections
+        // ("keep-alive") unless the Body is read to completion and is
+        // closed.
+        //
+        // The Body is automatically dechunked if the server replied
+        // with a "chunked" Transfer-Encoding.
+        Body io.ReadCloser
+
+        // ContentLength records the length of the associated content.  The
+        // value -1 indicates that the length is unknown.  Unless Request.Method
+        // is "HEAD", values >= 0 indicate that the given number of bytes may
+        // be read from Body.
+        ContentLength int64
+
+        // Contains transfer encodings from outer-most to inner-most. Value is
+        // nil, means that "identity" encoding is used.
+        TransferEncoding []string
+
+        // Close records whether the header directed that the connection be
+        // closed after reading Body.  The value is advice for clients: neither
+        // ReadResponse nor Response.Write ever closes a connection.
+        Close bool
+
+        // Trailer maps trailer keys to values, in the same
+        // format as the header.
+        Trailer Header
+
+        // The Request that was sent to obtain this Response.
+        // Request's Body is nil (having already been consumed).
+        // This is only populated for Client requests.
+        Request *Request
+
+        // TLS contains information about the TLS connection on which the
+        // response was received. It is nil for unencrypted responses.
+        // The pointer is shared between responses and should not be
+        // modified.
+        TLS *tls.ConnectionState
+}
+

+Response represents the response from an HTTP request. +

+ + + + + + + + + + + + +

func Get

+
func Get(url string) (resp *Response, err error)
+

+Get issues a GET to the specified URL. If the response is one of +the following redirect codes, Get follows the redirect, up to a +maximum of 10 redirects: +

+
301 (Moved Permanently)
+302 (Found)
+303 (See Other)
+307 (Temporary Redirect)
+
+

+An error is returned if there were too many redirects or if there +was an HTTP protocol error. A non-2xx response doesn't cause an +error. +

+

+When err is nil, resp always contains a non-nil resp.Body. +Caller should close resp.Body when done reading from it. +

+

+Get is a wrapper around DefaultClient.Get. +

+

+To make a request with custom headers, use NewRequest and +DefaultClient.Do. +

+ +
+ +
+

▾ Example

+ + + +
+
+
+
+ Run + Format + +
+
+ +
+
+ + + + + +
func Head(url string) (resp *Response, err error)
+

+Head issues a HEAD to the specified URL. If the response is one of +the following redirect codes, Head follows the redirect, up to a +maximum of 10 redirects: +

+
301 (Moved Permanently)
+302 (Found)
+303 (See Other)
+307 (Temporary Redirect)
+
+

+Head is a wrapper around DefaultClient.Head +

+ + + + + +

func Post

+
func Post(url string, bodyType string, body io.Reader) (resp *Response, err error)
+

+Post issues a POST to the specified URL. +

+

+Caller should close resp.Body when done reading from it. +

+

+If the provided body is an io.Closer, it is closed after the +request. +

+

+Post is a wrapper around DefaultClient.Post. +

+

+To set custom headers, use NewRequest and DefaultClient.Do. +

+ + + + + +

func PostForm

+
func PostForm(url string, data url.Values) (resp *Response, err error)
+

+PostForm issues a POST to the specified URL, with data's keys and +values URL-encoded as the request body. +

+

+The Content-Type header is set to application/x-www-form-urlencoded. +To set other headers, use NewRequest and DefaultClient.Do. +

+

+When err is nil, resp always contains a non-nil resp.Body. +Caller should close resp.Body when done reading from it. +

+

+PostForm is a wrapper around DefaultClient.PostForm. +

+ + + + + +

func ReadResponse

+
func ReadResponse(r *bufio.Reader, req *Request) (*Response, error)
+

+ReadResponse reads and returns an HTTP response from r. +The req parameter optionally specifies the Request that corresponds +to this Response. If nil, a GET request is assumed. +Clients must call resp.Body.Close when finished reading resp.Body. +After that call, clients can inspect resp.Trailer to find key/value +pairs included in the response trailer. +

+ + + + + + + +

func (*Response) Cookies

+
func (r *Response) Cookies() []*Cookie
+

+Cookies parses and returns the cookies set in the Set-Cookie headers. +

+ + + + + + +

func (*Response) Location

+
func (r *Response) Location() (*url.URL, error)
+

+Location returns the URL of the response's "Location" header, +if present. Relative redirects are resolved relative to +the Response's Request. ErrNoLocation is returned if no +Location header is present. +

+ + + + + + +

func (*Response) ProtoAtLeast

+
func (r *Response) ProtoAtLeast(major, minor int) bool
+

+ProtoAtLeast reports whether the HTTP protocol used +in the response is at least major.minor. +

+ + + + + + +

func (*Response) Write

+
func (r *Response) Write(w io.Writer) error
+

+Write writes r to w in the HTTP/1.n server response format, +including the status line, headers, body, and optional trailer. +

+

+This method consults the following fields of the response r: +

+
StatusCode
+ProtoMajor
+ProtoMinor
+Request.Method
+TransferEncoding
+Trailer
+Body
+ContentLength
+Header, values for non-canonical keys will have unpredictable behavior
+
+

+The Response Body is closed after it is sent. +

+ + + + + + + + +

type ResponseWriter

+
type ResponseWriter interface {
+        // Header returns the header map that will be sent by
+        // WriteHeader. Changing the header after a call to
+        // WriteHeader (or Write) has no effect unless the modified
+        // headers were declared as trailers by setting the
+        // "Trailer" header before the call to WriteHeader (see example).
+        // To suppress implicit response headers, set their value to nil.
+        Header() Header
+
+        // Write writes the data to the connection as part of an HTTP reply.
+        // If WriteHeader has not yet been called, Write calls WriteHeader(http.StatusOK)
+        // before writing the data.  If the Header does not contain a
+        // Content-Type line, Write adds a Content-Type set to the result of passing
+        // the initial 512 bytes of written data to DetectContentType.
+        Write([]byte) (int, error)
+
+        // WriteHeader sends an HTTP response header with status code.
+        // If WriteHeader is not called explicitly, the first call to Write
+        // will trigger an implicit WriteHeader(http.StatusOK).
+        // Thus explicit calls to WriteHeader are mainly used to
+        // send error codes.
+        WriteHeader(int)
+}
+

+A ResponseWriter interface is used by an HTTP handler to +construct an HTTP response. +

+ + + + + + +
+ +
+

▾ Example (Trailers)

+

HTTP Trailers are a set of key/value pairs like headers that come +after the HTTP response, instead of before. +

+ + +
+
+
+
+ Run + Format + +
+
+ +
+
+ + + + + + + + + + +

type RoundTripper

+
type RoundTripper interface {
+        // RoundTrip executes a single HTTP transaction, returning
+        // the Response for the request req.  RoundTrip should not
+        // attempt to interpret the response.  In particular,
+        // RoundTrip must return err == nil if it obtained a response,
+        // regardless of the response's HTTP status code.  A non-nil
+        // err should be reserved for failure to obtain a response.
+        // Similarly, RoundTrip should not attempt to handle
+        // higher-level protocol details such as redirects,
+        // authentication, or cookies.
+        //
+        // RoundTrip should not modify the request, except for
+        // consuming and closing the Body, including on errors. The
+        // request's URL and Header fields are guaranteed to be
+        // initialized.
+        RoundTrip(*Request) (*Response, error)
+}
+

+RoundTripper is an interface representing the ability to execute a +single HTTP transaction, obtaining the Response for a given Request. +

+

+A RoundTripper must be safe for concurrent use by multiple +goroutines. +

+ + + + + +
var DefaultTransport RoundTripper = &Transport{
+        Proxy: ProxyFromEnvironment,
+        Dial: (&net.Dialer{
+                Timeout:   30 * time.Second,
+                KeepAlive: 30 * time.Second,
+        }).Dial,
+        TLSHandshakeTimeout: 10 * time.Second,
+}
+

+DefaultTransport is the default implementation of Transport and is +used by DefaultClient. It establishes network connections as needed +and caches them for reuse by subsequent calls. It uses HTTP proxies +as directed by the $HTTP_PROXY and $NO_PROXY (or $http_proxy and +$no_proxy) environment variables. +

+ + + + + + + + + +

func NewFileTransport

+
func NewFileTransport(fs FileSystem) RoundTripper
+

+NewFileTransport returns a new RoundTripper, serving the provided +FileSystem. The returned RoundTripper ignores the URL host in its +incoming requests, as well as most other properties of the +request. +

+

+The typical use case for NewFileTransport is to register the "file" +protocol with a Transport, as in: +

+
t := &http.Transport{}
+t.RegisterProtocol("file", http.NewFileTransport(http.Dir("/")))
+c := &http.Client{Transport: t}
+res, err := c.Get("file:///etc/passwd")
+...
+
+ + + + + + + + + +

type ServeMux

+
type ServeMux struct {
+        // contains filtered or unexported fields
+}
+

+ServeMux is an HTTP request multiplexer. +It matches the URL of each incoming request against a list of registered +patterns and calls the handler for the pattern that +most closely matches the URL. +

+

+Patterns name fixed, rooted paths, like "/favicon.ico", +or rooted subtrees, like "/images/" (note the trailing slash). +Longer patterns take precedence over shorter ones, so that +if there are handlers registered for both "/images/" +and "/images/thumbnails/", the latter handler will be +called for paths beginning "/images/thumbnails/" and the +former will receive requests for any other paths in the +"/images/" subtree. +

+

+Note that since a pattern ending in a slash names a rooted subtree, +the pattern "/" matches all paths not matched by other registered +patterns, not just the URL with Path == "/". +

+

+Patterns may optionally begin with a host name, restricting matches to +URLs on that host only. Host-specific patterns take precedence over +general patterns, so that a handler might register for the two patterns +"/codesearch" and "codesearch.google.com/" without also taking over +requests for "http://www.google.com/". +

+

+ServeMux also takes care of sanitizing the URL request path, +redirecting any request containing . or .. elements to an +equivalent .- and ..-free URL. +

+ + + + + + + + + + + + +

func NewServeMux

+
func NewServeMux() *ServeMux
+

+NewServeMux allocates and returns a new ServeMux. +

+ + + + + + + +

func (*ServeMux) Handle

+
func (mux *ServeMux) Handle(pattern string, handler Handler)
+

+Handle registers the handler for the given pattern. +If a handler already exists for pattern, Handle panics. +

+ + +
+ +
+

▾ Example

+ + + +

Code:

+
    mux := http.NewServeMux()
+    mux.Handle("/api/", apiHandler{})
+    mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
+            // The "/" pattern matches everything, so we need to check
+            // that we're at the root here.
+            if req.URL.Path != "/" {
+                    http.NotFound(w, req)
+                    return
+            }
+            fmt.Fprintf(w, "Welcome to the home page!")
+    })
+
+ + +
+
+ + + + +

func (*ServeMux) HandleFunc

+
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))
+

+HandleFunc registers the handler function for the given pattern. +

+ + + + + + +

func (*ServeMux) Handler

+
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string)
+

+Handler returns the handler to use for the given request, +consulting r.Method, r.Host, and r.URL.Path. It always returns +a non-nil handler. If the path is not in its canonical form, the +handler will be an internally-generated handler that redirects +to the canonical path. +

+

+Handler also returns the registered pattern that matches the +request or, in the case of internally-generated redirects, +the pattern that will match after following the redirect. +

+

+If there is no registered handler that applies to the request, +Handler returns a “page not found” handler and an empty pattern. +

+ + + + + + +

func (*ServeMux) ServeHTTP

+
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)
+

+ServeHTTP dispatches the request to the handler whose +pattern most closely matches the request URL. +

+ + + + + + + + +

type Server

+
type Server struct {
+        Addr           string        // TCP address to listen on, ":http" if empty
+        Handler        Handler       // handler to invoke, http.DefaultServeMux if nil
+        ReadTimeout    time.Duration // maximum duration before timing out read of the request
+        WriteTimeout   time.Duration // maximum duration before timing out write of the response
+        MaxHeaderBytes int           // maximum size of request headers, DefaultMaxHeaderBytes if 0
+        TLSConfig      *tls.Config   // optional TLS config, used by ListenAndServeTLS
+
+        // TLSNextProto optionally specifies a function to take over
+        // ownership of the provided TLS connection when an NPN
+        // protocol upgrade has occurred.  The map key is the protocol
+        // name negotiated. The Handler argument should be used to
+        // handle HTTP requests and will initialize the Request's TLS
+        // and RemoteAddr if not already set.  The connection is
+        // automatically closed when the function returns.
+        TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
+
+        // ConnState specifies an optional callback function that is
+        // called when a client connection changes state. See the
+        // ConnState type and associated constants for details.
+        ConnState func(net.Conn, ConnState)
+
+        // ErrorLog specifies an optional logger for errors accepting
+        // connections and unexpected behavior from handlers.
+        // If nil, logging goes to os.Stderr via the log package's
+        // standard logger.
+        ErrorLog *log.Logger
+        // contains filtered or unexported fields
+}
+

+A Server defines parameters for running an HTTP server. +The zero value for Server is a valid configuration. +

+ + + + + + + + + + + + + + +

func (*Server) ListenAndServe

+
func (srv *Server) ListenAndServe() error
+

+ListenAndServe listens on the TCP network address srv.Addr and then +calls Serve to handle requests on incoming connections. If +srv.Addr is blank, ":http" is used. +

+ + + + + + +

func (*Server) ListenAndServeTLS

+
func (srv *Server) ListenAndServeTLS(certFile, keyFile string) error
+

+ListenAndServeTLS listens on the TCP network address srv.Addr and +then calls Serve to handle requests on incoming TLS connections. +

+

+Filenames containing a certificate and matching private key for the +server must be provided if the Server's TLSConfig.Certificates is +not populated. If the certificate is signed by a certificate +authority, the certFile should be the concatenation of the server's +certificate, any intermediates, and the CA's certificate. +

+

+If srv.Addr is blank, ":https" is used. +

+ + + + + + +

func (*Server) Serve

+
func (srv *Server) Serve(l net.Listener) error
+

+Serve accepts incoming connections on the Listener l, creating a +new service goroutine for each. The service goroutines read requests and +then call srv.Handler to reply to them. +

+ + + + + + +

func (*Server) SetKeepAlivesEnabled

+
func (srv *Server) SetKeepAlivesEnabled(v bool)
+

+SetKeepAlivesEnabled controls whether HTTP keep-alives are enabled. +By default, keep-alives are always enabled. Only very +resource-constrained environments or servers in the process of +shutting down should disable them. +

+ + + + + + + + +

type Transport

+
type Transport struct {
+
+        // Proxy specifies a function to return a proxy for a given
+        // Request. If the function returns a non-nil error, the
+        // request is aborted with the provided error.
+        // If Proxy is nil or returns a nil *URL, no proxy is used.
+        Proxy func(*Request) (*url.URL, error)
+
+        // Dial specifies the dial function for creating unencrypted
+        // TCP connections.
+        // If Dial is nil, net.Dial is used.
+        Dial func(network, addr string) (net.Conn, error)
+
+        // DialTLS specifies an optional dial function for creating
+        // TLS connections for non-proxied HTTPS requests.
+        //
+        // If DialTLS is nil, Dial and TLSClientConfig are used.
+        //
+        // If DialTLS is set, the Dial hook is not used for HTTPS
+        // requests and the TLSClientConfig and TLSHandshakeTimeout
+        // are ignored. The returned net.Conn is assumed to already be
+        // past the TLS handshake.
+        DialTLS func(network, addr string) (net.Conn, error)
+
+        // TLSClientConfig specifies the TLS configuration to use with
+        // tls.Client. If nil, the default configuration is used.
+        TLSClientConfig *tls.Config
+
+        // TLSHandshakeTimeout specifies the maximum amount of time waiting to
+        // wait for a TLS handshake. Zero means no timeout.
+        TLSHandshakeTimeout time.Duration
+
+        // DisableKeepAlives, if true, prevents re-use of TCP connections
+        // between different HTTP requests.
+        DisableKeepAlives bool
+
+        // DisableCompression, if true, prevents the Transport from
+        // requesting compression with an "Accept-Encoding: gzip"
+        // request header when the Request contains no existing
+        // Accept-Encoding value. If the Transport requests gzip on
+        // its own and gets a gzipped response, it's transparently
+        // decoded in the Response.Body. However, if the user
+        // explicitly requested gzip it is not automatically
+        // uncompressed.
+        DisableCompression bool
+
+        // MaxIdleConnsPerHost, if non-zero, controls the maximum idle
+        // (keep-alive) to keep per-host.  If zero,
+        // DefaultMaxIdleConnsPerHost is used.
+        MaxIdleConnsPerHost int
+
+        // ResponseHeaderTimeout, if non-zero, specifies the amount of
+        // time to wait for a server's response headers after fully
+        // writing the request (including its body, if any). This
+        // time does not include the time to read the response body.
+        ResponseHeaderTimeout time.Duration
+        // contains filtered or unexported fields
+}
+

+Transport is an implementation of RoundTripper that supports HTTP, +HTTPS, and HTTP proxies (for either HTTP or HTTPS with CONNECT). +Transport can also cache connections for future re-use. +

+ + + + + + + + + + + + + + +

func (*Transport) CancelRequest

+
func (t *Transport) CancelRequest(req *Request)
+

+CancelRequest cancels an in-flight request by closing its connection. +CancelRequest should only be called after RoundTrip has returned. +

+ + + + + + +

func (*Transport) CloseIdleConnections

+
func (t *Transport) CloseIdleConnections()
+

+CloseIdleConnections closes any connections which were previously +connected from previous requests but are now sitting idle in +a "keep-alive" state. It does not interrupt any connections currently +in use. +

+ + + + + + +

func (*Transport) RegisterProtocol

+
func (t *Transport) RegisterProtocol(scheme string, rt RoundTripper)
+

+RegisterProtocol registers a new protocol with scheme. +The Transport will pass requests using the given scheme to rt. +It is rt's responsibility to simulate HTTP request semantics. +

+

+RegisterProtocol can be used by other packages to provide +implementations of protocol schemes like "ftp" or "file". +

+ + + + + + +

func (*Transport) RoundTrip

+
func (t *Transport) RoundTrip(req *Request) (resp *Response, err error)
+

+RoundTrip implements the RoundTripper interface. +

+

+For higher-level HTTP client support (such as handling of cookies +and redirects), see Get, Post, and the Client type. +

+ + + + + + + + + + + + + + + + +

Subdirectories

+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameSynopsis
..
+ cgi + + Package cgi implements CGI (Common Gateway Interface) as specified in RFC 3875. +
+ cookiejar + + Package cookiejar implements an in-memory RFC 6265-compliant http.CookieJar. +
+ fcgi + + Package fcgi implements the FastCGI protocol. +
+ httptest + + Package httptest provides utilities for HTTP testing. +
+ httputil + + Package httputil provides HTTP utility functions, complementing the more common ones in the net/http package. +
+ pprof + + Package pprof serves via its HTTP server runtime profiling data in the format expected by the pprof visualization tool. +
+
+ + + + + + + + +
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/append_object.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/append_object.go new file mode 100644 index 00000000..42212f49 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/append_object.go @@ -0,0 +1,164 @@ +// Package sample examples +package sample + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "strconv" + "strings" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// AppendObjectSample shows the append file's usage +func AppendObjectSample() { + // Create bucket + bucket, err := GetTestBucket(bucketName) + if err != nil { + HandleError(err) + } + + err = bucket.DeleteObject(objectKey) + + var str = "弃我去者,昨日之日不可留。 乱我心者,今日之日多烦忧!" + var nextPos int64 + + // Case 1: Append a string to the object + // The first append position is 0 and the return value is for the next append's position. + nextPos, err = bucket.AppendObject(objectKey, strings.NewReader(str), nextPos) + if err != nil { + HandleError(err) + } + + // Second append + nextPos, err = bucket.AppendObject(objectKey, strings.NewReader(str), nextPos) + if err != nil { + HandleError(err) + } + + // Download + body, err := bucket.GetObject(objectKey) + if err != nil { + HandleError(err) + } + data, err := ioutil.ReadAll(body) + body.Close() + if err != nil { + HandleError(err) + } + fmt.Println(objectKey, ":", string(data)) + + err = bucket.DeleteObject(objectKey) + if err != nil { + HandleError(err) + } + + // Case 2: Append byte array to the object + nextPos = 0 + // The first append position is 0, and the return value is for the next append's position. + nextPos, err = bucket.AppendObject(objectKey, bytes.NewReader([]byte(str)), nextPos) + if err != nil { + HandleError(err) + } + + // Second append + nextPos, err = bucket.AppendObject(objectKey, bytes.NewReader([]byte(str)), nextPos) + if err != nil { + HandleError(err) + } + + // Download + body, err = bucket.GetObject(objectKey) + if err != nil { + HandleError(err) + } + data, err = ioutil.ReadAll(body) + body.Close() + if err != nil { + HandleError(err) + } + fmt.Println(objectKey, ":", string(data)) + + err = bucket.DeleteObject(objectKey) + if err != nil { + HandleError(err) + } + + // Case 3: Append a local file to the object + fd, err := os.Open(localFile) + if err != nil { + HandleError(err) + } + defer fd.Close() + + nextPos = 0 + nextPos, err = bucket.AppendObject(objectKey, fd, nextPos) + if err != nil { + HandleError(err) + } + + // Case 4: Get the next append position by GetObjectDetailedMeta + props, err := bucket.GetObjectDetailedMeta(objectKey) + nextPos, err = strconv.ParseInt(props.Get(oss.HTTPHeaderOssNextAppendPosition), 10, 64) + if err != nil { + HandleError(err) + } + + nextPos, err = bucket.AppendObject(objectKey, strings.NewReader(str), nextPos) + if err != nil { + HandleError(err) + } + + err = bucket.DeleteObject(objectKey) + if err != nil { + HandleError(err) + } + + // Case 5: Specify the object properties for the first append, including the "x-oss-meta"'s custom metadata. + options := []oss.Option{ + oss.Expires(futureDate), + oss.ObjectACL(oss.ACLPublicRead), + oss.Meta("myprop", "mypropval")} + nextPos = 0 + fd.Seek(0, os.SEEK_SET) + nextPos, err = bucket.AppendObject(objectKey, strings.NewReader(str), nextPos, options...) + if err != nil { + HandleError(err) + } + // Second append + fd.Seek(0, os.SEEK_SET) + nextPos, err = bucket.AppendObject(objectKey, strings.NewReader(str), nextPos) + if err != nil { + HandleError(err) + } + + props, err = bucket.GetObjectDetailedMeta(objectKey) + if err != nil { + HandleError(err) + } + fmt.Println("myprop:", props.Get("x-oss-meta-myprop")) + + goar, err := bucket.GetObjectACL(objectKey) + if err != nil { + HandleError(err) + } + fmt.Println("Object ACL:", goar.ACL) + + // Case 6: Set the storage classes.OSS provides three storage classes: Standard, Infrequent Access, and Archive. + // Upload a strings, and you can append some strings in the behind of object. but the object is 'Archive' storange class. + // An object created with the AppendObject operation is an appendable object. set the object storange-class to 'Archive'. + nextPos, err = bucket.AppendObject(appendObjectKey, strings.NewReader("昨夜雨疏风骤,浓睡不消残酒。试问卷帘人,"), nextPos, oss.ObjectStorageClass("Archive")) + if err != nil { + HandleError(err) + } + + // Delete the object and bucket + err = DeleteTestBucketAndObject(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("AppendObjectSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/archive.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/archive.go new file mode 100644 index 00000000..1002ef5f --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/archive.go @@ -0,0 +1,74 @@ +package sample + +import ( + "fmt" + "strings" + "time" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// ArchiveSample archives sample +func ArchiveSample() { + // Create archive bucket + client, err := oss.New(endpoint, accessID, accessKey) + if err != nil { + HandleError(err) + } + + err = client.CreateBucket(bucketName, oss.StorageClass(oss.StorageArchive)) + if err != nil { + HandleError(err) + } + + archiveBucket, err := client.Bucket(bucketName) + if err != nil { + HandleError(err) + } + + // Put archive object + var val = "花间一壶酒,独酌无相亲。 举杯邀明月,对影成三人。" + err = archiveBucket.PutObject(objectKey, strings.NewReader(val)) + if err != nil { + HandleError(err) + } + + // Check whether the object is archive class + meta, err := archiveBucket.GetObjectDetailedMeta(objectKey) + if err != nil { + HandleError(err) + } + + if meta.Get("X-Oss-Storage-Class") == string(oss.StorageArchive) { + // Restore object + err = archiveBucket.RestoreObject(objectKey) + if err != nil { + HandleError(err) + } + + // Wait for restore completed + meta, err = archiveBucket.GetObjectDetailedMeta(objectKey) + for meta.Get("X-Oss-Restore") == "ongoing-request=\"true\"" { + fmt.Println("x-oss-restore:" + meta.Get("X-Oss-Restore")) + time.Sleep(1000 * time.Second) + meta, err = archiveBucket.GetObjectDetailedMeta(objectKey) + } + } + + // Get restored object + err = archiveBucket.GetObjectToFile(objectKey, localFile) + if err != nil { + HandleError(err) + } + + // Restore repeatedly + err = archiveBucket.RestoreObject(objectKey) + + // Delete object and bucket + err = DeleteTestBucketAndObject(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("ArchiveSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_acl.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_acl.go new file mode 100644 index 00000000..92c78c75 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_acl.go @@ -0,0 +1,43 @@ +package sample + +import ( + "fmt" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// BucketACLSample shows how to get and set the bucket ACL +func BucketACLSample() { + // New client + client, err := oss.New(endpoint, accessID, accessKey) + if err != nil { + HandleError(err) + } + + // Create a bucket with default parameters + err = client.CreateBucket(bucketName) + if err != nil { + HandleError(err) + } + + // Set bucket ACL. The valid ACLs are ACLPrivate、ACLPublicRead、ACLPublicReadWrite + err = client.SetBucketACL(bucketName, oss.ACLPublicRead) + if err != nil { + HandleError(err) + } + + // Get bucket ACL + gbar, err := client.GetBucketACL(bucketName) + if err != nil { + HandleError(err) + } + fmt.Println("Bucket ACL:", gbar.ACL) + + // Delete the bucket + err = client.DeleteBucket(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("BucketACLSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_cors.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_cors.go new file mode 100644 index 00000000..7af95e6d --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_cors.go @@ -0,0 +1,71 @@ +package sample + +import ( + "fmt" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// BucketCORSSample shows how to get or set the bucket CORS. +func BucketCORSSample() { + // New client + client, err := oss.New(endpoint, accessID, accessKey) + if err != nil { + HandleError(err) + } + + // Create the bucket with default parameters + err = client.CreateBucket(bucketName) + if err != nil { + HandleError(err) + } + + rule1 := oss.CORSRule{ + AllowedOrigin: []string{"*"}, + AllowedMethod: []string{"PUT", "GET", "POST"}, + AllowedHeader: []string{}, + ExposeHeader: []string{}, + MaxAgeSeconds: 100, + } + + rule2 := oss.CORSRule{ + AllowedOrigin: []string{"http://www.a.com", "http://www.b.com"}, + AllowedMethod: []string{"GET"}, + AllowedHeader: []string{"Authorization"}, + ExposeHeader: []string{"x-oss-test", "x-oss-test1"}, + MaxAgeSeconds: 100, + } + + // Case 1: Set the bucket CORS rules + err = client.SetBucketCORS(bucketName, []oss.CORSRule{rule1}) + if err != nil { + HandleError(err) + } + + // Case 2: Set the bucket CORS rules. if CORS rules exist, they will be overwritten. + err = client.SetBucketCORS(bucketName, []oss.CORSRule{rule1, rule2}) + if err != nil { + HandleError(err) + } + + // Get the bucket's CORS + gbl, err := client.GetBucketCORS(bucketName) + if err != nil { + HandleError(err) + } + fmt.Println("Bucket CORS:", gbl.CORSRules) + + // Delete bucket's CORS + err = client.DeleteBucketCORS(bucketName) + if err != nil { + HandleError(err) + } + + // Delete bucket + err = client.DeleteBucket(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("BucketCORSSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_encryption.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_encryption.go new file mode 100644 index 00000000..38c0ec94 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_encryption.go @@ -0,0 +1,51 @@ +package sample + +import ( + "fmt" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// BucketEncryptionSample shows how to get and set the bucket encryption Algorithm +func BucketEncryptionSample() { + // New client + client, err := oss.New(endpoint, accessID, accessKey) + if err != nil { + HandleError(err) + } + + // Create a bucket with default parameters + err = client.CreateBucket(bucketName) + if err != nil { + HandleError(err) + } + + // SetBucketEncryption:AES256 ,"123" + encryptionRule := oss.ServerEncryptionRule{} + encryptionRule.SSEDefault.SSEAlgorithm = string(oss.AESAlgorithm) + err = client.SetBucketEncryption(bucketName, encryptionRule) + if err != nil { + HandleError(err) + } + + // Get bucket encryption + encryptionResult, err := client.GetBucketEncryption(bucketName) + if err != nil { + HandleError(err) + } + fmt.Println("Bucket Encryption:", encryptionResult) + + // Delete the bucket + err = client.DeleteBucketEncryption(bucketName) + if err != nil { + HandleError(err) + } + + // Delete the object and bucket + err = DeleteTestBucketAndObject(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("BucketEncryptionSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_inventory.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_inventory.go new file mode 100644 index 00000000..16218553 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_inventory.go @@ -0,0 +1,102 @@ +package sample + +import ( + "fmt" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// BucketInventorySample shows how to set, get, list and delete the bucket inventory configuration +func BucketInventorySample() { + // New client + client, err := oss.New(endpoint, accessID, accessKey) + if err != nil { + HandleError(err) + } + + // Create the bucket with default parameters + err = client.CreateBucket(bucketName) + if err != nil { + HandleError(err) + } + + // the inventory configuration,not use any encryption + bl := true + invConfig := oss.InventoryConfiguration{ + Id: "report1", + IsEnabled: &bl, + Prefix: "filterPrefix/", + OSSBucketDestination: oss.OSSBucketDestination{ + Format: "CSV", + AccountId: accountID, + RoleArn: stsARN, + Bucket: "acs:oss:::" + bucketName, + Prefix: "prefix1", + }, + Frequency: "Daily", + IncludedObjectVersions: "All", + OptionalFields: oss.OptionalFields{ + Field: []string{ + "Size", "LastModifiedDate", "ETag", "StorageClass", "IsMultipartUploaded", "EncryptionStatus", + }, + }, + } + + // case 1: Set inventory + err = client.SetBucketInventory(bucketName, invConfig) + if err != nil { + HandleError(err) + } + + // case 2: Get Bucket inventory + ret, err := client.GetBucketInventory(bucketName, invConfig.Id) + if err != nil { + HandleError(err) + } + fmt.Println("Bucket inventory:", ret) + + // case 3: List Bucket inventory + invConfig2 := oss.InventoryConfiguration{ + Id: "report2", + IsEnabled: &bl, + Prefix: "filterPrefix/", + OSSBucketDestination: oss.OSSBucketDestination{ + Format: "CSV", + AccountId: accountID, + RoleArn: stsARN, + Bucket: "acs:oss:::" + bucketName, + Prefix: "prefix1", + }, + Frequency: "Daily", + IncludedObjectVersions: "All", + OptionalFields: oss.OptionalFields{ + Field: []string{ + "Size", "LastModifiedDate", "ETag", "StorageClass", "IsMultipartUploaded", "EncryptionStatus", + }, + }, + } + err = client.SetBucketInventory(bucketName, invConfig2) + if err != nil { + HandleError(err) + } + NextContinuationToken := "" + listInvConf, err := client.ListBucketInventory(bucketName, NextContinuationToken) + if err != nil { + HandleError(err) + } + fmt.Println("Bucket inventory list:", listInvConf) + + // case 4: Delete Bucket inventory + err = client.DeleteBucketInventory(bucketName, invConfig.Id) + if err != nil { + HandleError(err) + } + + // Delete bucket + err = client.DeleteBucket(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("BucketInventorySample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_lifecycle.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_lifecycle.go new file mode 100644 index 00000000..29e83caf --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_lifecycle.go @@ -0,0 +1,121 @@ +package sample + +import ( + "fmt" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// BucketLifecycleSample shows how to set, get and delete bucket's lifecycle. +func BucketLifecycleSample() { + // New client + client, err := oss.New(endpoint, accessID, accessKey) + if err != nil { + HandleError(err) + } + + // Create the bucket with default parameters + err = client.CreateBucket(bucketName) + if err != nil { + HandleError(err) + } + + // Case 1: Set the lifecycle. The rule ID is rule1 and the applied objects' prefix is one and the last modified Date is before 2015/11/11 + expriation := oss.LifecycleExpiration{ + CreatedBeforeDate: "2015-11-11T00:00:00.000Z", + } + rule1 := oss.LifecycleRule{ + ID: "rule1", + Prefix: "one", + Status: "Enabled", + Expiration: &expriation, + } + var rules = []oss.LifecycleRule{rule1} + err = client.SetBucketLifecycle(bucketName, rules) + if err != nil { + HandleError(err) + } + + // Case 2: Get the bucket's lifecycle + lc, err := client.GetBucketLifecycle(bucketName) + if err != nil { + HandleError(err) + } + fmt.Printf("Bucket Lifecycle:%v, %v\n", lc.Rules, *lc.Rules[0].Expiration) + + // Case 3: Set the lifecycle, The rule ID is rule2 and the applied objects' prefix is two. The object start with the prefix will be transited to IA storage Type 3 days latter, and to archive storage type 30 days latter + transitionIA := oss.LifecycleTransition{ + Days: 3, + StorageClass: oss.StorageIA, + } + transitionArch := oss.LifecycleTransition{ + Days: 30, + StorageClass: oss.StorageArchive, + } + rule2 := oss.LifecycleRule{ + ID: "rule2", + Prefix: "two", + Status: "Enabled", + Transitions: []oss.LifecycleTransition{transitionIA, transitionArch}, + } + rules = []oss.LifecycleRule{rule2} + err = client.SetBucketLifecycle(bucketName, rules) + if err != nil { + HandleError(err) + } + + // Case 4: Set the lifecycle, The rule ID is rule3 and the applied objects' prefix is three. The object start with the prefix will be transited to IA storage Type 3 days latter, and to archive storage type 30 days latter, the uncompleted multipart upload will be abort 3 days latter. + abortMPU := oss.LifecycleAbortMultipartUpload{ + Days: 3, + } + rule3 := oss.LifecycleRule{ + ID: "rule3", + Prefix: "three", + Status: "Enabled", + AbortMultipartUpload: &abortMPU, + } + rules = append(lc.Rules, rule3) + err = client.SetBucketLifecycle(bucketName, rules) + if err != nil { + HandleError(err) + } + + // Case 5: Set the lifecycle. The rule ID is rule4 and the applied objects' has the tagging which prefix is four and the last modified Date is before 2015/11/11 + expriation = oss.LifecycleExpiration{ + CreatedBeforeDate: "2015-11-11T00:00:00.000Z", + } + tag1 := oss.Tag{ + Key: "key1", + Value: "value1", + } + tag2 := oss.Tag{ + Key: "key2", + Value: "value2", + } + rule4 := oss.LifecycleRule{ + ID: "rule4", + Prefix: "four", + Status: "Enabled", + Tags: []oss.Tag{tag1, tag2}, + Expiration: &expriation, + } + rules = []oss.LifecycleRule{rule4} + err = client.SetBucketLifecycle(bucketName, rules) + if err != nil { + HandleError(err) + } + + // Case 6: Delete bucket's Lifecycle + err = client.DeleteBucketLifecycle(bucketName) + if err != nil { + HandleError(err) + } + + // Delete bucket + err = client.DeleteBucket(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("BucketLifecycleSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_logging.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_logging.go new file mode 100644 index 00000000..3a7ad1bd --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_logging.go @@ -0,0 +1,90 @@ +package sample + +import ( + "fmt" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// BucketLoggingSample shows how to set, get and delete the bucket logging configuration +func BucketLoggingSample() { + // New client + client, err := oss.New(endpoint, accessID, accessKey) + if err != nil { + HandleError(err) + } + + // Create the bucket with default parameters + err = client.CreateBucket(bucketName) + if err != nil { + HandleError(err) + } + // Create target bucket to store the logging files. + var targetBucketName = "target-bucket" + err = client.CreateBucket(targetBucketName) + if err != nil { + HandleError(err) + } + + // Case 1: Set the logging for the object prefixed with "prefix-1" and save their access logs to the target bucket + err = client.SetBucketLogging(bucketName, targetBucketName, "prefix-1", true) + if err != nil { + HandleError(err) + } + + // Case 2: Set the logging for the object prefixed with "prefix-2" and save their logs to the same bucket + // Note: the rule will overwrite other rules if they have same bucket and prefix + err = client.SetBucketLogging(bucketName, bucketName, "prefix-2", true) + if err != nil { + HandleError(err) + } + + // Delete the bucket's logging configuration + err = client.DeleteBucketLogging(bucketName) + if err != nil { + HandleError(err) + } + + // Case 3: Set the logging without enabling it + err = client.SetBucketLogging(bucketName, targetBucketName, "prefix-3", false) + if err != nil { + HandleError(err) + } + + // Get the bucket's logging configuration + gbl, err := client.GetBucketLogging(bucketName) + if err != nil { + HandleError(err) + } + fmt.Println("Bucket Logging:", gbl.LoggingEnabled) + + err = client.SetBucketLogging(bucketName, bucketName, "prefix2", true) + if err != nil { + HandleError(err) + } + + // Get the bucket's logging configuration + gbl, err = client.GetBucketLogging(bucketName) + if err != nil { + HandleError(err) + } + fmt.Println("Bucket Logging:", gbl.LoggingEnabled) + + // Delete the bucket's logging configuration + err = client.DeleteBucketLogging(bucketName) + if err != nil { + HandleError(err) + } + + // Delete bucket + err = client.DeleteBucket(bucketName) + if err != nil { + HandleError(err) + } + err = client.DeleteBucket(targetBucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("BucketLoggingSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_policy.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_policy.go new file mode 100644 index 00000000..54349bb0 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_policy.go @@ -0,0 +1,67 @@ +package sample + +import ( + "fmt" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// BucketPolicySample shows how to set, get and delete the bucket policy configuration +func BucketPolicySample() { + // New client + client, err := oss.New(endpoint, accessID, accessKey) + if err != nil { + HandleError(err) + } + + // Create the bucket with default parameters + err = client.CreateBucket(bucketName) + if err != nil { + HandleError(err) + } + + // the policy string + var policyInfo string + policyInfo = ` + { + "Version":"1", + "Statement":[ + { + "Action":[ + "oss:GetObject", + "oss:PutObject" + ], + "Effect":"Deny", + "Principal":"[123456790]", + "Resource":["acs:oss:*:1234567890:*/*"] + } + ] + }` + + // Set policy + err = client.SetBucketPolicy(bucketName, policyInfo) + if err != nil { + HandleError(err) + } + + // Get Bucket policy + ret, err := client.GetBucketPolicy(bucketName) + if err != nil { + HandleError(err) + } + fmt.Println("Bucket policy:", ret) + + // Delete Bucket policy + err = client.DeleteBucketPolicy(bucketName) + if err != nil { + HandleError(err) + } + + // Delete bucket + err = client.DeleteBucket(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("BucketPolicySample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_qosInfo.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_qosInfo.go new file mode 100644 index 00000000..16a6df2d --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_qosInfo.go @@ -0,0 +1,65 @@ +package sample + +import ( + "fmt" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// BucketQoSInfoSample shows how to set, get and delete the bucket QoS configuration +func BucketQoSInfoSample() { + // New client + client, err := oss.New(endpoint, accessID, accessKey) + if err != nil { + HandleError(err) + } + + // Create the bucket with default parameters + err = client.CreateBucket(bucketName) + if err != nil { + HandleError(err) + } + // Initial QoS Configuration + five := 5 + four := 4 + totalQps := 200 + qosConf := oss.BucketQoSConfiguration{ + TotalUploadBandwidth: &five, + IntranetUploadBandwidth: &four, + ExtranetUploadBandwidth: &four, + TotalDownloadBandwidth: &four, + IntranetDownloadBandwidth: &four, + ExtranetDownloadBandwidth: &four, + TotalQPS: &totalQps, + IntranetQPS: &totalQps, + ExtranetQPS: &totalQps, + } + + // Set Qos Info + err = client.SetBucketQoSInfo(bucketName, qosConf) + if err != nil { + HandleError(err) + } + + // Get Qos Info + ret, err := client.GetBucketQosInfo(bucketName) + if err != nil { + HandleError(err) + } + fmt.Printf("Bucket QoSInfo\n TotalUploadBandwidth: %d\n IntranetUploadBandwidth: %d\n ExtranetUploadBandwidth: %d\n", + *ret.TotalUploadBandwidth, *ret.IntranetUploadBandwidth, *ret.ExtranetUploadBandwidth) + + // Delete QosInfo + err = client.DeleteBucketQosInfo(bucketName) + if err != nil { + HandleError(err) + } + + // Delete bucket + err = client.DeleteBucket(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("BucketPolicySample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_referer.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_referer.go new file mode 100644 index 00000000..7d1e4db1 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_referer.go @@ -0,0 +1,57 @@ +package sample + +import ( + "fmt" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// BucketRefererSample shows how to set, get and delete the bucket referer. +func BucketRefererSample() { + // New client + client, err := oss.New(endpoint, accessID, accessKey) + if err != nil { + HandleError(err) + } + + // Create the bucket with default parameters + err = client.CreateBucket(bucketName) + if err != nil { + HandleError(err) + } + + var referers = []string{ + "http://www.aliyun.com", + "http://www.???.aliyuncs.com", + "http://www.*.com", + } + + // Case 1: Set referers. The referers are with wildcards ? and * which could represent one and zero to multiple characters + err = client.SetBucketReferer(bucketName, referers, false) + if err != nil { + HandleError(err) + } + + // Case 2: Clear referers + referers = []string{} + err = client.SetBucketReferer(bucketName, referers, true) + if err != nil { + HandleError(err) + } + + // Get bucket referer configuration + gbr, err := client.GetBucketReferer(bucketName) + if err != nil { + HandleError(err) + } + fmt.Println("Bucket Referers:", gbr.RefererList, + "AllowEmptyReferer:", gbr.AllowEmptyReferer) + + // Delete bucket + err = client.DeleteBucket(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("BucketRefererSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_requestpayment.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_requestpayment.go new file mode 100644 index 00000000..c7e045ff --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_requestpayment.go @@ -0,0 +1,117 @@ +package sample + +import ( + "fmt" + "strings" + "io/ioutil" + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// BucketrRequestPaymentSample shows how to set, get the bucket request payment. +func BucketrRequestPaymentSample() { + // New client + client, err := oss.New(endpoint, accessID, accessKey) + if err != nil { + HandleError(err) + } + + // Create the bucket with default parameters + err = client.CreateBucket(bucketName) + if err != nil { + HandleError(err) + } + + reqPayConf := oss.RequestPaymentConfiguration{ + Payer: string(oss.Requester), + } + + // Case 1: Set bucket request payment. + err = client.SetBucketRequestPayment(bucketName, reqPayConf) + if err != nil { + HandleError(err) + } + + // Get bucket request payment configuration + ret, err := client.GetBucketRequestPayment(bucketName) + if err != nil { + HandleError(err) + } + fmt.Println("Bucket request payer:", ret.Payer) + + if credentialUID == "" { + fmt.Println("Please enter a credential User ID, if you want to test credential user.") + clearData(client, bucketName) + return + } + // Credential other User + policyInfo := ` + { + "Version":"1", + "Statement":[ + { + "Action":[ + "oss:*" + ], + "Effect":"Allow", + "Principal":["` + credentialUID + `"], + "Resource":["acs:oss:*:*:` + bucketName + `", "acs:oss:*:*:` + bucketName + `/*"] + } + ] + }` + + err = client.SetBucketPolicy(bucketName, policyInfo) + if err != nil { + HandleError(err) + } + + // New a Credential client + creClient, err := oss.New(endpoint, credentialAccessID, credentialAccessKey) + if err != nil { + HandleError(err) + } + + // Get credential bucket + creBucket, err := creClient.Bucket(bucketName) + if err != nil { + HandleError(err) + } + + // Put object by credential User + key := "testCredentialObject" + objectValue := "this is a test string." + // Put object + err = creBucket.PutObject(key, strings.NewReader(objectValue), oss.RequestPayer(oss.Requester)) + if err != nil { + HandleError(err) + } + // Get object + body, err := creBucket.GetObject(key, oss.RequestPayer(oss.Requester)) + if err != nil { + HandleError(err) + } + defer body.Close() + + data, err := ioutil.ReadAll(body) + if err != nil { + HandleError(err) + } + fmt.Println(string(data)) + + // Delete object + err = creBucket.DeleteObject(key, oss.RequestPayer(oss.Requester)) + if err != nil { + HandleError(err) + } + + clearData(client, bucketName) +} + +func clearData(client *oss.Client, bucketName string) { + // Delete bucket + err := client.DeleteBucket(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("BucketrRequestPaymentSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_website.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_website.go new file mode 100644 index 00000000..e58801e3 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/bucket_website.go @@ -0,0 +1,104 @@ +package sample + +import ( + "fmt" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// BucketWebsiteSample shows how to set, get and delete the bucket website. +func BucketWebsiteSample() { + // New client + client, err := oss.New(endpoint, accessID, accessKey) + if err != nil { + HandleError(err) + } + + // Create the bucket with default parameters + err = client.CreateBucket(bucketName) + if err != nil { + HandleError(err) + } + + //Define bucket website indexWebsite or errorWebsite + var indexWebsite = "myindex.html" + var errorWebsite = "myerror.html" + + // Set bucket website indexWebsite or errorWebsite + err = client.SetBucketWebsite(bucketName, indexWebsite, errorWebsite) + if err != nil { + HandleError(err) + } + + bEnable := true + bDisable := false + + // Define one website detail + ruleOk := oss.RoutingRule{ + RuleNumber: 1, + Condition: oss.Condition{ + KeyPrefixEquals: "abc", + HTTPErrorCodeReturnedEquals: 404, + IncludeHeader: []oss.IncludeHeader{ + oss.IncludeHeader{ + Key: "host", + Equals: "test.oss-cn-beijing-internal.aliyuncs.com", + }, + }, + }, + Redirect: oss.Redirect{ + RedirectType: "Mirror", + PassQueryString: &bDisable, + MirrorURL: "http://www.test.com/", + MirrorPassQueryString: &bEnable, + MirrorFollowRedirect: &bEnable, + MirrorCheckMd5: &bDisable, + MirrorHeaders: oss.MirrorHeaders{ + PassAll: &bEnable, + Pass: []string{"key1", "key2"}, + Remove: []string{"remove1", "remove2"}, + Set: []oss.MirrorHeaderSet{ + oss.MirrorHeaderSet{ + Key: "setKey1", + Value: "setValue1", + }, + }, + }, + }, + } + wxmlDetail := oss.WebsiteXML{} + wxmlDetail.RoutingRules = append(wxmlDetail.RoutingRules, ruleOk) + + // Get website + res, err := client.GetBucketWebsite(bucketName) + if err != nil { + HandleError(err) + } + fmt.Println("Website IndexDocument:", res.IndexDocument.Suffix) + + // Set bucket website detail + err = client.SetBucketWebsiteDetail(bucketName, wxmlDetail) + if err != nil { + HandleError(err) + } + // Get website Detail + res, err = client.GetBucketWebsite(bucketName) + if err != nil { + HandleError(err) + } + fmt.Println("Website Redirect type:", res.RoutingRules[0].Redirect.RedirectType) + + // Delete Website + err = client.DeleteBucketWebsite(bucketName) + if err != nil { + HandleError(err) + } + + // Delete bucket + err = client.DeleteBucket(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("BucketWebsiteSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/cname_sample.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/cname_sample.go new file mode 100644 index 00000000..5e0b503f --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/cname_sample.go @@ -0,0 +1,95 @@ +package sample + +import ( + "fmt" + "io/ioutil" + "strings" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// CnameSample shows the cname usage +func CnameSample() { + // New client + client, err := oss.New(endpoint4Cname, accessID, accessKey, oss.UseCname(true)) + if err != nil { + HandleError(err) + } + + // Create bucket + err = client.CreateBucket(bucketName) + if err != nil { + HandleError(err) + } + + // Set bucket ACL + err = client.SetBucketACL(bucketName, oss.ACLPrivate) + if err != nil { + HandleError(err) + } + + // Look up bucket ACL + gbar, err := client.GetBucketACL(bucketName) + if err != nil { + HandleError(err) + } + fmt.Println("Bucket ACL:", gbar.ACL) + + // List buckets, the list operation could not be done by cname's endpoint + _, err = client.ListBuckets() + if err == nil { + HandleError(err) + } + + bucket, err := client.Bucket(bucketName) + if err != nil { + HandleError(err) + } + + objectValue := "长忆观潮, 满郭人争江上望。来疑沧海尽成空, 万面鼓声中。弄潮儿向涛头立, 手把红旗旗不湿。别来几向梦中看, 梦觉尚心寒。" + + // Put object + err = bucket.PutObject(objectKey, strings.NewReader(objectValue)) + if err != nil { + HandleError(err) + } + + // Get object + body, err := bucket.GetObject(objectKey) + if err != nil { + HandleError(err) + } + data, err := ioutil.ReadAll(body) + body.Close() + if err != nil { + HandleError(err) + } + fmt.Println(objectKey, ":", string(data)) + + // Put object from file + err = bucket.PutObjectFromFile(objectKey, localFile) + if err != nil { + HandleError(err) + } + + // Get object to file + err = bucket.GetObjectToFile(objectKey, localFile) + if err != nil { + HandleError(err) + } + + // List objects + lor, err := bucket.ListObjects() + if err != nil { + HandleError(err) + } + fmt.Println("objects:", lor.Objects) + + // Delete object + err = bucket.DeleteObject(objectKey) + if err != nil { + HandleError(err) + } + + fmt.Println("CnameSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/comm.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/comm.go new file mode 100644 index 00000000..91ba23c0 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/comm.go @@ -0,0 +1,181 @@ +package sample + +import ( + "fmt" + "os" + "strings" + "time" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +var ( + pastDate = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) + futureDate = time.Date(2049, time.January, 10, 23, 0, 0, 0, time.UTC) +) + +// HandleError is the error handling method in the sample code +func HandleError(err error) { + fmt.Println("occurred error:", err) + os.Exit(-1) +} + +// GetTestBucket creates the test bucket +func GetTestBucket(bucketName string) (*oss.Bucket, error) { + // New client + client, err := oss.New(endpoint, accessID, accessKey) + if err != nil { + return nil, err + } + + // Create bucket + err = client.CreateBucket(bucketName) + if err != nil { + return nil, err + } + + // Get bucket + bucket, err := client.Bucket(bucketName) + if err != nil { + return nil, err + } + + return bucket, nil +} + +// DeleteTestBucketAndLiveChannel 删除sample的channelname和bucket,该函数为了简化sample,让sample代码更明了 +func DeleteTestBucketAndLiveChannel(bucketName string) error { + // New Client + client, err := oss.New(endpoint, accessID, accessKey) + if err != nil { + return err + } + + // Get Bucket + bucket, err := client.Bucket(bucketName) + if err != nil { + return err + } + + marker := "" + for { + result, err := bucket.ListLiveChannel(oss.Marker(marker)) + if err != nil { + HandleError(err) + } + + for _, channel := range result.LiveChannel { + err := bucket.DeleteLiveChannel(channel.Name) + if err != nil { + HandleError(err) + } + } + + if result.IsTruncated { + marker = result.NextMarker + } else { + break + } + } + + // Delete Bucket + err = client.DeleteBucket(bucketName) + if err != nil { + return err + } + + return nil +} + +// DeleteTestBucketAndObject deletes the test bucket and its objects +func DeleteTestBucketAndObject(bucketName string) error { + // New client + client, err := oss.New(endpoint, accessID, accessKey) + if err != nil { + return err + } + + // Get bucket + bucket, err := client.Bucket(bucketName) + if err != nil { + return err + } + + // Delete part + keyMarker := oss.KeyMarker("") + uploadIDMarker := oss.UploadIDMarker("") + for { + lmur, err := bucket.ListMultipartUploads(keyMarker, uploadIDMarker) + if err != nil { + return err + } + for _, upload := range lmur.Uploads { + var imur = oss.InitiateMultipartUploadResult{Bucket: bucket.BucketName, + Key: upload.Key, UploadID: upload.UploadID} + err = bucket.AbortMultipartUpload(imur) + if err != nil { + return err + } + } + keyMarker = oss.KeyMarker(lmur.NextKeyMarker) + uploadIDMarker = oss.UploadIDMarker(lmur.NextUploadIDMarker) + if !lmur.IsTruncated { + break + } + } + + // Delete objects + marker := oss.Marker("") + for { + lor, err := bucket.ListObjects(marker) + if err != nil { + return err + } + for _, object := range lor.Objects { + err = bucket.DeleteObject(object.Key) + if err != nil { + return err + } + } + marker = oss.Marker(lor.NextMarker) + if !lor.IsTruncated { + break + } + } + + // Delete bucket + err = client.DeleteBucket(bucketName) + if err != nil { + return err + } + + return nil +} + +// Object defines pair of key and value +type Object struct { + Key string + Value string +} + +// CreateObjects creates some objects +func CreateObjects(bucket *oss.Bucket, objects []Object) error { + for _, object := range objects { + err := bucket.PutObject(object.Key, strings.NewReader(object.Value)) + if err != nil { + return err + } + } + return nil +} + +// DeleteObjects deletes some objects. +func DeleteObjects(bucket *oss.Bucket, objects []Object) error { + for _, object := range objects { + err := bucket.DeleteObject(object.Key) + if err != nil { + return err + } + } + return nil +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/config.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/config.go new file mode 100644 index 00000000..2e146e16 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/config.go @@ -0,0 +1,36 @@ +package sample + +import "os" + +var ( + // Sample code's env configuration. You need to specify them with the actual configuration if you want to run sample code + endpoint = os.Getenv("OSS_TEST_ENDPOINT") + accessID = os.Getenv("OSS_TEST_ACCESS_KEY_ID") + accessKey = os.Getenv("OSS_TEST_ACCESS_KEY_SECRET") + bucketName = os.Getenv("OSS_TEST_BUCKET") + kmsID = os.Getenv("OSS_TEST_KMS_ID") + accountID = os.Getenv("OSS_TEST_ACCOUNT_ID") + stsARN = os.Getenv("OSS_TEST_STS_ARN") + + // Credential + credentialAccessID = os.Getenv("OSS_CREDENTIAL_KEY_ID") + credentialAccessKey = os.Getenv("OSS_CREDENTIAL_KEY_SECRET") + credentialUID = os.Getenv("OSS_CREDENTIAL_UID") + + // The cname endpoint + endpoint4Cname = os.Getenv("OSS_TEST_CNAME_ENDPOINT") +) + +const ( + + // The object name in the sample code + objectKey string = "my-object" + appendObjectKey string = "my-object-append" + + // The local files to run sample code. + localFile string = "sample/BingWallpaper-2015-11-07.jpg" + localCsvFile string = "sample/sample_data.csv" + localJSONFile string = "sample/sample_json.json" + localJSONLinesFile string = "sample/sample_json_lines.json" + htmlLocalFile string = "sample/The Go Programming Language.html" +) diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/copy_object.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/copy_object.go new file mode 100644 index 00000000..c25dcd50 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/copy_object.go @@ -0,0 +1,138 @@ +package sample + +import ( + "fmt" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// CopyObjectSample shows the copy files usage +func CopyObjectSample() { + // Create a bucket + bucket, err := GetTestBucket(bucketName) + if err != nil { + HandleError(err) + } + + // Create an object + err = bucket.PutObjectFromFile(objectKey, localFile) + if err != nil { + HandleError(err) + } + + // Case 1: Copy an existing object + var descObjectKey = "descobject" + _, err = bucket.CopyObject(objectKey, descObjectKey) + if err != nil { + HandleError(err) + } + + // Case 2: Copy an existing object to another existing object + _, err = bucket.CopyObject(objectKey, descObjectKey) + if err != nil { + HandleError(err) + } + + err = bucket.DeleteObject(descObjectKey) + if err != nil { + HandleError(err) + } + + // Case 3: Copy file with constraints. When the constraints are met, the copy executes. otherwise the copy does not execute. + // constraints are not met, copy does not execute + _, err = bucket.CopyObject(objectKey, descObjectKey, oss.CopySourceIfModifiedSince(futureDate)) + if err == nil { + HandleError(err) + } + fmt.Println("CopyObjectError:", err) + // Constraints are met, the copy executes + _, err = bucket.CopyObject(objectKey, descObjectKey, oss.CopySourceIfUnmodifiedSince(futureDate)) + if err != nil { + HandleError(err) + } + + // Case 4: Specify the properties when copying. The MetadataDirective needs to be MetaReplace + options := []oss.Option{ + oss.Expires(futureDate), + oss.Meta("myprop", "mypropval"), + oss.MetadataDirective(oss.MetaReplace)} + _, err = bucket.CopyObject(objectKey, descObjectKey, options...) + if err != nil { + HandleError(err) + } + + meta, err := bucket.GetObjectDetailedMeta(descObjectKey) + if err != nil { + HandleError(err) + } + fmt.Println("meta:", meta) + + // Case 5: When the source file is the same as the target file, the copy could be used to update metadata + options = []oss.Option{ + oss.Expires(futureDate), + oss.Meta("myprop", "mypropval"), + oss.MetadataDirective(oss.MetaReplace)} + + _, err = bucket.CopyObject(objectKey, objectKey, options...) + if err != nil { + HandleError(err) + } + fmt.Println("meta:", meta) + + // Case 6: Big file's multipart copy. It supports concurrent copy with resumable upload + // copy file with multipart. The part size is 100K. By default one routine is used without resumable upload + err = bucket.CopyFile(bucketName, objectKey, descObjectKey, 100*1024) + if err != nil { + HandleError(err) + } + + // Part size is 100K and three coroutines for the concurrent copy + err = bucket.CopyFile(bucketName, objectKey, descObjectKey, 100*1024, oss.Routines(3)) + if err != nil { + HandleError(err) + } + + // Part size is 100K and three coroutines for the concurrent copy with resumable upload + err = bucket.CopyFile(bucketName, objectKey, descObjectKey, 100*1024, oss.Routines(3), oss.Checkpoint(true, "")) + if err != nil { + HandleError(err) + } + + // Specify the checkpoint file path. If the checkpoint file path is not specified, the current folder is used. + err = bucket.CopyFile(bucketName, objectKey, descObjectKey, 100*1024, oss.Checkpoint(true, localFile+".cp")) + if err != nil { + HandleError(err) + } + + // Case 7: Set the storage classes.OSS provides three storage classes: Standard, Infrequent Access, and Archive. + // Copy a object in the same bucket, and set object's storage-class to Archive. + _, err = bucket.CopyObject(objectKey, objectKey+"DestArchive", oss.ObjectStorageClass("Archive")) + if err != nil { + HandleError(err) + } + + // Case 8: Copy object with tagging, the value of tagging directive is REPLACE + tag1 := oss.Tag{ + Key: "key1", + Value: "value1", + } + tag2 := oss.Tag{ + Key: "key2", + Value: "value2", + } + tagging := oss.Tagging{ + Tags: []oss.Tag{tag1, tag2}, + } + _, err = bucket.CopyObject(objectKey, objectKey+"WithTagging", oss.SetTagging(tagging), oss.TaggingDirective(oss.TaggingReplace)) + if err != nil { + HandleError(err) + } + + // Delete object and bucket + err = DeleteTestBucketAndObject(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("CopyObjectSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/create_bucket.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/create_bucket.go new file mode 100644 index 00000000..976e5c5f --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/create_bucket.go @@ -0,0 +1,50 @@ +package sample + +import ( + "fmt" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// CreateBucketSample shows how to create bucket +func CreateBucketSample() { + // New client + client, err := oss.New(endpoint, accessID, accessKey) + if err != nil { + HandleError(err) + } + + DeleteTestBucketAndObject(bucketName) + + // Case 1: Create a bucket with default parameters + err = client.CreateBucket(bucketName) + if err != nil { + HandleError(err) + } + + // Delete bucket + err = client.DeleteBucket(bucketName) + if err != nil { + HandleError(err) + } + + // Case 2: Create the bucket with ACL + err = client.CreateBucket(bucketName, oss.ACL(oss.ACLPublicRead)) + if err != nil { + HandleError(err) + } + + // Case 3: Repeat the same bucket. OSS will not return error, but just no op. The ACL is not updated. + err = client.CreateBucket(bucketName, oss.ACL(oss.ACLPublicReadWrite)) + if err != nil { + HandleError(err) + } + + // Delete bucket + err = client.DeleteBucket(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("CreateBucketSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/delete_object.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/delete_object.go new file mode 100644 index 00000000..d4d898e0 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/delete_object.go @@ -0,0 +1,108 @@ +package sample + +import ( + "fmt" + "strings" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// DeleteObjectSample shows how to delete single file or multiple files +func DeleteObjectSample() { + // Create a bucket + bucket, err := GetTestBucket(bucketName) + if err != nil { + HandleError(err) + } + + var val = "抽刀断水水更流,举杯销愁愁更愁。 人生在世不称意,明朝散发弄扁舟。" + + // Case 1: Delete an object + err = bucket.PutObject(objectKey, strings.NewReader(val)) + if err != nil { + HandleError(err) + } + + err = bucket.DeleteObject(objectKey) + if err != nil { + HandleError(err) + } + + // Case 2: Delete multiple Objects + err = bucket.PutObject(objectKey+"1", strings.NewReader(val)) + if err != nil { + HandleError(err) + } + + err = bucket.PutObject(objectKey+"2", strings.NewReader(val)) + if err != nil { + HandleError(err) + } + + delRes, err := bucket.DeleteObjects([]string{objectKey + "1", objectKey + "2"}) + if err != nil { + HandleError(err) + } + fmt.Println("Del Res:", delRes) + + lsRes, err := bucket.ListObjects() + if err != nil { + HandleError(err) + } + fmt.Println("Objects:", getObjectsFormResponse(lsRes)) + + // Case 3: Delete multiple objects and it will return deleted objects in detail mode which is by default. + err = bucket.PutObject(objectKey+"1", strings.NewReader(val)) + if err != nil { + HandleError(err) + } + + err = bucket.PutObject(objectKey+"2", strings.NewReader(val)) + if err != nil { + HandleError(err) + } + + delRes, err = bucket.DeleteObjects([]string{objectKey + "1", objectKey + "2"}, + oss.DeleteObjectsQuiet(false)) + if err != nil { + HandleError(err) + } + fmt.Println("Detail Del Res:", delRes) + + lsRes, err = bucket.ListObjects() + if err != nil { + HandleError(err) + } + fmt.Println("Objects:", getObjectsFormResponse(lsRes)) + + // Case 4: Delete multiple objects and returns undeleted objects in quiet mode + err = bucket.PutObject(objectKey+"1", strings.NewReader(val)) + if err != nil { + HandleError(err) + } + + err = bucket.PutObject(objectKey+"2", strings.NewReader(val)) + if err != nil { + HandleError(err) + } + + delRes, err = bucket.DeleteObjects([]string{objectKey + "1", objectKey + "2"}, oss.DeleteObjectsQuiet(true)) + if err != nil { + HandleError(err) + } + fmt.Println("Sample Del Res:", delRes) + + lsRes, err = bucket.ListObjects() + if err != nil { + HandleError(err) + } + fmt.Println("Objects:", getObjectsFormResponse(lsRes)) + + // Delete object and bucket + err = DeleteTestBucketAndObject(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("DeleteObjectSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/get_object.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/get_object.go new file mode 100644 index 00000000..019e86b6 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/get_object.go @@ -0,0 +1,156 @@ +package sample + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// GetObjectSample shows the streaming download, range download and resumable download. +func GetObjectSample() { + // Create bucket + bucket, err := GetTestBucket(bucketName) + if err != nil { + HandleError(err) + } + + // Upload the object + err = bucket.PutObjectFromFile(objectKey, localFile) + if err != nil { + HandleError(err) + } + + // Case 1: Download the object into ReadCloser(). The body needs to be closed + body, err := bucket.GetObject(objectKey) + if err != nil { + HandleError(err) + } + + data, err := ioutil.ReadAll(body) + body.Close() + if err != nil { + HandleError(err) + } + fmt.Println("size of data is: ", len(data)) + + // Case 2: Download in the range of object. + body, err = bucket.GetObject(objectKey, oss.Range(15, 19)) + if err != nil { + HandleError(err) + } + data, err = ioutil.ReadAll(body) + body.Close() + fmt.Println("the range of data is: ", string(data)) + + // Case 3: Download the object to byte array. This is for small object. + buf := new(bytes.Buffer) + body, err = bucket.GetObject(objectKey) + if err != nil { + HandleError(err) + } + io.Copy(buf, body) + body.Close() + + // Case 4: Download the object to local file. The file handle needs to be specified + fd, err := os.OpenFile("mynewfile-1.jpg", os.O_WRONLY|os.O_CREATE, 0660) + if err != nil { + HandleError(err) + } + defer fd.Close() + + body, err = bucket.GetObject(objectKey) + if err != nil { + HandleError(err) + } + io.Copy(fd, body) + body.Close() + + // Case 5: Download the object to local file with file name specified + err = bucket.GetObjectToFile(objectKey, "mynewfile-2.jpg") + if err != nil { + HandleError(err) + } + + // Case 6: Get the object with contraints. When contraints are met, download the file. Otherwise return precondition error + // last modified time constraint is met, download the file + body, err = bucket.GetObject(objectKey, oss.IfModifiedSince(pastDate)) + if err != nil { + HandleError(err) + } + body.Close() + + // Last modified time contraint is not met, do not download the file + _, err = bucket.GetObject(objectKey, oss.IfUnmodifiedSince(pastDate)) + if err == nil { + HandleError(fmt.Errorf("This result is not the expected result")) + } + // body.Close() + + meta, err := bucket.GetObjectDetailedMeta(objectKey) + if err != nil { + HandleError(err) + } + etag := meta.Get(oss.HTTPHeaderEtag) + // Check the content, etag contraint is met, download the file + body, err = bucket.GetObject(objectKey, oss.IfMatch(etag)) + if err != nil { + HandleError(err) + } + body.Close() + + // Check the content, etag contraint is not met, do not download the file + _, err = bucket.GetObject(objectKey, oss.IfNoneMatch(etag)) + if err == nil { + HandleError(fmt.Errorf("This result is not the expected result")) + } + // body.Close() + + // Case 7: Big file's multipart download, concurrent and resumable download is supported. + // multipart download with part size 100KB. By default single coroutine is used and no checkpoint + err = bucket.DownloadFile(objectKey, "mynewfile-3.jpg", 100*1024) + if err != nil { + HandleError(err) + } + + // Part size is 100K and 3 coroutines are used + err = bucket.DownloadFile(objectKey, "mynewfile-3.jpg", 100*1024, oss.Routines(3)) + if err != nil { + HandleError(err) + } + + // Part size is 100K and 3 coroutines with checkpoint + err = bucket.DownloadFile(objectKey, "mynewfile-3.jpg", 100*1024, oss.Routines(3), oss.Checkpoint(true, "")) + if err != nil { + HandleError(err) + } + + // Specify the checkpoint file path to record which parts have been downloaded. + // This file path can be specified by the 2nd parameter of Checkpoint, it will be the download directory if the file path is empty. + err = bucket.DownloadFile(objectKey, "mynewfile-3.jpg", 100*1024, oss.Checkpoint(true, "mynewfile.cp")) + if err != nil { + HandleError(err) + } + + // Case 8: Use GZIP encoding for downloading the file, GetObject/GetObjectToFile are the same. + err = bucket.PutObjectFromFile(objectKey, htmlLocalFile) + if err != nil { + HandleError(err) + } + + err = bucket.GetObjectToFile(objectKey, "myhtml.gzip", oss.AcceptEncoding("gzip")) + if err != nil { + HandleError(err) + } + + // Delete the object and bucket + err = DeleteTestBucketAndObject(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("GetObjectSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/list_buckets.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/list_buckets.go new file mode 100644 index 00000000..2d50826f --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/list_buckets.go @@ -0,0 +1,129 @@ +package sample + +import ( + "fmt" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// ListBucketsSample shows the list bucket, including default and specified parameters. +func ListBucketsSample() { + var myBuckets = []string{ + "my-bucket-1", + "my-bucket-11", + "my-bucket-2", + "my-bucket-21", + "my-bucket-22", + "my-bucket-3", + "my-bucket-31", + "my-bucket-32"} + + // New client + client, err := oss.New(endpoint, accessID, accessKey) + if err != nil { + HandleError(err) + } + + // Remove other bucket + lbr, err := client.ListBuckets() + if err != nil { + HandleError(err) + } + + for _, bucket := range lbr.Buckets { + err = client.DeleteBucket(bucket.Name) + if err != nil { + //HandleError(err) + } + } + + // Create bucket + for _, bucketName := range myBuckets { + err = client.CreateBucket(bucketName) + if err != nil { + HandleError(err) + } + } + + // Case 1: Use default parameter + lbr, err = client.ListBuckets() + if err != nil { + HandleError(err) + } + fmt.Println("my buckets:", lbr.Buckets) + + // Case 2: Specify the max keys : 3 + lbr, err = client.ListBuckets(oss.MaxKeys(3)) + if err != nil { + HandleError(err) + } + fmt.Println("my buckets max num:", lbr.Buckets) + + // Case 3: Specify the prefix of buckets. + lbr, err = client.ListBuckets(oss.Prefix("my-bucket-2")) + if err != nil { + HandleError(err) + } + fmt.Println("my buckets prefix :", lbr.Buckets) + + // Case 4: Specify the marker to return from a certain one + lbr, err = client.ListBuckets(oss.Marker("my-bucket-22")) + if err != nil { + HandleError(err) + } + fmt.Println("my buckets marker :", lbr.Buckets) + + // Case 5: Specify max key and list all buckets with paging, return 3 items each time. + marker := oss.Marker("") + for { + lbr, err = client.ListBuckets(oss.MaxKeys(3), marker) + if err != nil { + HandleError(err) + } + marker = oss.Marker(lbr.NextMarker) + fmt.Println("my buckets page :", lbr.Buckets) + if !lbr.IsTruncated { + break + } + } + + // Case 6: List bucket with marker and max key; return 3 items each time. + marker = oss.Marker("my-bucket-22") + for { + lbr, err = client.ListBuckets(oss.MaxKeys(3), marker) + if err != nil { + HandleError(err) + } + marker = oss.Marker(lbr.NextMarker) + fmt.Println("my buckets marker&page :", lbr.Buckets) + if !lbr.IsTruncated { + break + } + } + + // Case 7: List bucket with prefix and max key, return 3 items each time. + pre := oss.Prefix("my-bucket-2") + marker = oss.Marker("") + for { + lbr, err = client.ListBuckets(oss.MaxKeys(3), pre, marker) + if err != nil { + HandleError(err) + } + pre = oss.Prefix(lbr.Prefix) + marker = oss.Marker(lbr.NextMarker) + fmt.Println("my buckets prefix&page :", lbr.Buckets) + if !lbr.IsTruncated { + break + } + } + + // Delete bucket + for _, bucketName := range myBuckets { + err = client.DeleteBucket(bucketName) + if err != nil { + HandleError(err) + } + } + + fmt.Println("ListsBucketSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/list_objects.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/list_objects.go new file mode 100644 index 00000000..d0b08360 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/list_objects.go @@ -0,0 +1,148 @@ +package sample + +import ( + "fmt" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// ListObjectsSample shows the file list, including default and specified parameters. +func ListObjectsSample() { + var myObjects = []Object{ + {"my-object-1", ""}, + {"my-object-11", ""}, + {"my-object-2", ""}, + {"my-object-21", ""}, + {"my-object-22", ""}, + {"my-object-3", ""}, + {"my-object-31", ""}, + {"my-object-32", ""}} + + // Create bucket + bucket, err := GetTestBucket(bucketName) + if err != nil { + HandleError(err) + } + + // Create objects + err = CreateObjects(bucket, myObjects) + if err != nil { + HandleError(err) + } + + // Case 1: Use default parameters + lor, err := bucket.ListObjects() + if err != nil { + HandleError(err) + } + fmt.Println("my objects:", getObjectsFormResponse(lor)) + + // Case 2: Specify max keys + lor, err = bucket.ListObjects(oss.MaxKeys(3)) + if err != nil { + HandleError(err) + } + fmt.Println("my objects max num:", getObjectsFormResponse(lor)) + + // Case 3: Specify prefix of objects + lor, err = bucket.ListObjects(oss.Prefix("my-object-2")) + if err != nil { + HandleError(err) + } + fmt.Println("my objects prefix :", getObjectsFormResponse(lor)) + + // Case 4: Specify the marker + lor, err = bucket.ListObjects(oss.Marker("my-object-22")) + if err != nil { + HandleError(err) + } + fmt.Println("my objects marker :", getObjectsFormResponse(lor)) + + // Case 5: List object with paging. each page has 3 objects + marker := oss.Marker("") + for { + lor, err = bucket.ListObjects(oss.MaxKeys(3), marker) + if err != nil { + HandleError(err) + } + marker = oss.Marker(lor.NextMarker) + fmt.Println("my objects page :", getObjectsFormResponse(lor)) + if !lor.IsTruncated { + break + } + } + + // Case 6: List object with paging , marker and max keys; return 3 items each time. + marker = oss.Marker("my-object-22") + for { + lor, err = bucket.ListObjects(oss.MaxKeys(3), marker) + if err != nil { + HandleError(err) + } + marker = oss.Marker(lor.NextMarker) + fmt.Println("my objects marker&page :", getObjectsFormResponse(lor)) + if !lor.IsTruncated { + break + } + } + + // Case 7: List object with paging , with prefix and max keys; return 2 items each time. + pre := oss.Prefix("my-object-2") + marker = oss.Marker("") + for { + lor, err = bucket.ListObjects(oss.MaxKeys(2), marker, pre) + if err != nil { + HandleError(err) + } + pre = oss.Prefix(lor.Prefix) + marker = oss.Marker(lor.NextMarker) + fmt.Println("my objects prefix&page :", getObjectsFormResponse(lor)) + if !lor.IsTruncated { + break + } + } + + err = DeleteObjects(bucket, myObjects) + if err != nil { + HandleError(err) + } + + // Case 8: Combine the prefix and delimiter for grouping. ListObjectsResponse.Objects is the objects returned. + // ListObjectsResponse.CommonPrefixes is the common prefixes returned. + myObjects = []Object{ + {"fun/test.txt", ""}, + {"fun/test.jpg", ""}, + {"fun/movie/001.avi", ""}, + {"fun/movie/007.avi", ""}, + {"fun/music/001.mp3", ""}, + {"fun/music/001.mp3", ""}} + + // Create object + err = CreateObjects(bucket, myObjects) + if err != nil { + HandleError(err) + } + + lor, err = bucket.ListObjects(oss.Prefix("fun/"), oss.Delimiter("/")) + if err != nil { + HandleError(err) + } + fmt.Println("my objects prefix :", getObjectsFormResponse(lor), + "common prefixes:", lor.CommonPrefixes) + + // Delete object and bucket + err = DeleteTestBucketAndObject(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("ListObjectsSample completed") +} + +func getObjectsFormResponse(lor oss.ListObjectsResult) string { + var output string + for _, object := range lor.Objects { + output += object.Key + " " + } + return output +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/livechannel.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/livechannel.go new file mode 100644 index 00000000..d258be3f --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/livechannel.go @@ -0,0 +1,445 @@ +package sample + +import ( + "fmt" + "io/ioutil" + "time" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// CreateLiveChannelSample Samples for create a live-channel +func CreateLiveChannelSample() { + channelName := "create-livechannel" + //create bucket + bucket, err := GetTestBucket(bucketName) + if err != nil { + HandleError(err) + } + + // Case 1 - Create live-channel with Completely configure + config := oss.LiveChannelConfiguration{ + Description: "sample-for-livechannel", //description information, up to 128 bytes + Status: "enabled", //enabled or disabled + Target: oss.LiveChannelTarget{ + Type: "HLS", //the type of object, only supports HLS, required + FragDuration: 10, //the length of each ts object (in seconds), in the range [1,100], default: 5 + FragCount: 4, //the number of ts objects in the m3u8 object, in the range of [1,100], default: 3 + PlaylistName: "test-get-channel-status.m3u8", //the name of m3u8 object, which must end with ".m3u8" and the length range is [6,128],default: playlist.m3u8 + }, + } + + result, err := bucket.CreateLiveChannel(channelName, config) + if err != nil { + HandleError(err) + } + + playURL := result.PlayUrls[0] + publishURL := result.PublishUrls[0] + fmt.Printf("create livechannel:%s with config respones: playURL:%s, publishURL: %s\n", channelName, playURL, publishURL) + + // Case 2 - Create live-channel only specified type of target which is required + simpleCfg := oss.LiveChannelConfiguration{ + Target: oss.LiveChannelTarget{ + Type: "HLS", + }, + } + result, err = bucket.CreateLiveChannel(channelName, simpleCfg) + if err != nil { + HandleError(err) + } + playURL = result.PlayUrls[0] + publishURL = result.PublishUrls[0] + fmt.Printf("create livechannel:%s with simple config respones: playURL:%s, publishURL: %s\n", channelName, playURL, publishURL) + + err = DeleteTestBucketAndLiveChannel(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("PutObjectSample completed") +} + +// PutLiveChannelStatusSample Set the status of the live-channel sample: enabled/disabled +func PutLiveChannelStatusSample() { + channelName := "put-livechannel-status" + bucket, err := GetTestBucket(bucketName) + if err != nil { + HandleError(err) + } + + config := oss.LiveChannelConfiguration{ + Target: oss.LiveChannelTarget{ + Type: "HLS", //the type of object, only supports HLS, required + }, + } + + _, err = bucket.CreateLiveChannel(channelName, config) + if err != nil { + HandleError(err) + } + + // Case 1 - Set the status of live-channel to disabled + err = bucket.PutLiveChannelStatus(channelName, "disabled") + if err != nil { + HandleError(err) + } + + // Case 2 - Set the status of live-channel to enabled + err = bucket.PutLiveChannelStatus(channelName, "enabled") + if err != nil { + HandleError(err) + } + + err = DeleteTestBucketAndLiveChannel(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("PutLiveChannelStatusSample completed") +} + +// PostVodPlayListSample Sample for generate playlist +func PostVodPlayListSample() { + channelName := "post-vod-playlist" + playlistName := "playlist.m3u8" + bucket, err := GetTestBucket(bucketName) + if err != nil { + HandleError(err) + } + + config := oss.LiveChannelConfiguration{ + Target: oss.LiveChannelTarget{ + Type: "HLS", //the type of object, only supports HLS, required + PlaylistName: "playlist.m3u8", + }, + } + + _, err = bucket.CreateLiveChannel(channelName, config) + if err != nil { + HandleError(err) + } + + //This stage you can push live stream, and after that you could generator playlist + + endTime := time.Now().Add(-1 * time.Minute) + startTime := endTime.Add(-60 * time.Minute) + err = bucket.PostVodPlaylist(channelName, playlistName, startTime, endTime) + if err != nil { + HandleError(err) + } + + err = DeleteTestBucketAndLiveChannel(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("PostVodPlayListSampleSample completed") +} + +// GetVodPlayListSample Sample for generate playlist and return the content of the playlist +func GetVodPlayListSample() { + channelName := "get-vod-playlist" + bucket, err := GetTestBucket(bucketName) + if err != nil { + HandleError(err) + } + + config := oss.LiveChannelConfiguration{ + Target: oss.LiveChannelTarget{ + Type: "HLS", //the type of object, only supports HLS, required + PlaylistName: "playlist.m3u8", + }, + } + + _, err = bucket.CreateLiveChannel(channelName, config) + if err != nil { + HandleError(err) + } + + //This stage you can push live stream, and after that you could generator playlist + + endTime := time.Now().Add(-1 * time.Minute) + startTime := endTime.Add(-60 * time.Minute) + body, err := bucket.GetVodPlaylist(channelName, startTime, endTime) + if err != nil { + HandleError(err) + } + defer body.Close() + + data, err := ioutil.ReadAll(body) + if err != nil { + HandleError(err) + } + fmt.Printf("content of playlist is:%v\n", string(data)) + + err = DeleteTestBucketAndLiveChannel(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("PostVodPlayListSampleSample completed") +} + +// GetLiveChannelStatSample Sample for get the state of live-channel +func GetLiveChannelStatSample() { + channelName := "get-livechannel-stat" + bucket, err := GetTestBucket(bucketName) + if err != nil { + HandleError(err) + } + + config := oss.LiveChannelConfiguration{ + Target: oss.LiveChannelTarget{ + Type: "HLS", //the type of object, only supports HLS, required + }, + } + + _, err = bucket.CreateLiveChannel(channelName, config) + if err != nil { + HandleError(err) + } + + stat, err := bucket.GetLiveChannelStat(channelName) + if err != nil { + HandleError(err) + } + + status := stat.Status + connectedTime := stat.ConnectedTime + remoteAddr := stat.RemoteAddr + + audioBW := stat.Audio.Bandwidth + audioCodec := stat.Audio.Codec + audioSampleRate := stat.Audio.SampleRate + + videoBW := stat.Video.Bandwidth + videoFrameRate := stat.Video.FrameRate + videoHeight := stat.Video.Height + videoWidth := stat.Video.Width + + fmt.Printf("get channel stat:(%v, %v,%v, %v), audio(%v, %v, %v), video(%v, %v, %v, %v)\n", channelName, status, connectedTime, remoteAddr, audioBW, audioCodec, audioSampleRate, videoBW, videoFrameRate, videoHeight, videoWidth) + + err = DeleteTestBucketAndLiveChannel(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("GetLiveChannelStatSample completed") +} + +// GetLiveChannelInfoSample Sample for get the configuration infomation of live-channel +func GetLiveChannelInfoSample() { + channelName := "get-livechannel-info" + bucket, err := GetTestBucket(bucketName) + if err != nil { + HandleError(err) + } + + config := oss.LiveChannelConfiguration{ + Target: oss.LiveChannelTarget{ + Type: "HLS", //the type of object, only supports HLS, required + }, + } + + _, err = bucket.CreateLiveChannel(channelName, config) + if err != nil { + HandleError(err) + } + + info, err := bucket.GetLiveChannelInfo(channelName) + if err != nil { + HandleError(err) + } + + desc := info.Description + status := info.Status + fragCount := info.Target.FragCount + fragDuation := info.Target.FragDuration + playlistName := info.Target.PlaylistName + targetType := info.Target.Type + + fmt.Printf("get channel stat:(%v,%v, %v), target(%v, %v, %v, %v)\n", channelName, desc, status, fragCount, fragDuation, playlistName, targetType) + + err = DeleteTestBucketAndLiveChannel(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("GetLiveChannelInfoSample completed") +} + +// GetLiveChannelHistorySample Sample for get push records of live-channel +func GetLiveChannelHistorySample() { + channelName := "get-livechannel-info" + bucket, err := GetTestBucket(bucketName) + if err != nil { + HandleError(err) + } + + config := oss.LiveChannelConfiguration{ + Target: oss.LiveChannelTarget{ + Type: "HLS", //the type of object, only supports HLS, required + }, + } + + _, err = bucket.CreateLiveChannel(channelName, config) + if err != nil { + HandleError(err) + } + + //at most return up to lastest 10 push records + history, err := bucket.GetLiveChannelHistory(channelName) + for _, record := range history.Record { + remoteAddr := record.RemoteAddr + startTime := record.StartTime + endTime := record.EndTime + fmt.Printf("get channel:%s history:(%v, %v, %v)\n", channelName, remoteAddr, startTime, endTime) + } + + err = DeleteTestBucketAndLiveChannel(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("GetLiveChannelHistorySample completed") +} + +// ListLiveChannelSample Samples for list live-channels with specified bucket name +func ListLiveChannelSample() { + channelName := "list-livechannel" + bucket, err := GetTestBucket(bucketName) + if err != nil { + HandleError(err) + } + + config := oss.LiveChannelConfiguration{ + Target: oss.LiveChannelTarget{ + Type: "HLS", //the type of object, only supports HLS, required + }, + } + + _, err = bucket.CreateLiveChannel(channelName, config) + if err != nil { + HandleError(err) + } + + // Case 1: list all the live-channels + marker := "" + for { + // Set the marker value, the first time is "", the value of NextMarker that returned should as the marker in the next time + // At most return up to lastest 100 live-channels if "max-keys" is not specified + result, err := bucket.ListLiveChannel(oss.Marker(marker)) + if err != nil { + HandleError(err) + } + + for _, channel := range result.LiveChannel { + fmt.Printf("list livechannel: (%v, %v, %v, %v, %v, %v)\n", channel.Name, channel.Status, channel.Description, channel.LastModified, channel.PlayUrls[0], channel.PublishUrls[0]) + } + + if result.IsTruncated { + marker = result.NextMarker + } else { + break + } + } + + // Case 2: Use the parameter "max-keys" to specify the maximum number of records returned, the value of max-keys cannot exceed 1000 + // if "max-keys" the default value is 100 + result, err := bucket.ListLiveChannel(oss.MaxKeys(10)) + if err != nil { + HandleError(err) + } + for _, channel := range result.LiveChannel { + fmt.Printf("list livechannel: (%v, %v, %v, %v, %v, %v)\n", channel.Name, channel.Status, channel.Description, channel.LastModified, channel.PlayUrls[0], channel.PublishUrls[0]) + } + + // Case 3: Only list the live-channels with the value of parameter "prefix" as prefix + // max-keys, prefix, maker parameters can be combined + result, err = bucket.ListLiveChannel(oss.MaxKeys(10), oss.Prefix("list-")) + if err != nil { + HandleError(err) + } + for _, channel := range result.LiveChannel { + fmt.Printf("list livechannel: (%v, %v, %v, %v, %v, %v)\n", channel.Name, channel.Status, channel.Description, channel.LastModified, channel.PlayUrls[0], channel.PublishUrls[0]) + } + + err = DeleteTestBucketAndLiveChannel(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("ListLiveChannelSample completed") +} + +// DeleteLiveChannelSample Sample for delete live-channel +func DeleteLiveChannelSample() { + channelName := "delete-livechannel" + bucket, err := GetTestBucket(bucketName) + if err != nil { + HandleError(err) + } + + config := oss.LiveChannelConfiguration{ + Target: oss.LiveChannelTarget{ + Type: "HLS", //the type of object, only supports HLS, required + }, + } + + _, err = bucket.CreateLiveChannel(channelName, config) + if err != nil { + HandleError(err) + } + + err = bucket.DeleteLiveChannel(channelName) + if err != nil { + HandleError(err) + } + + err = DeleteTestBucketAndLiveChannel(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("DeleteLiveChannelSample completed") +} + +// SignRtmpURLSample Sample for generate a RTMP push-stream signature URL for the trusted user to push the RTMP stream to the live channel. +func SignRtmpURLSample() { + channelName := "sign-rtmp-url" + playlistName := "playlist.m3u8" + bucket, err := GetTestBucket(bucketName) + if err != nil { + HandleError(err) + } + + config := oss.LiveChannelConfiguration{ + Target: oss.LiveChannelTarget{ + Type: "HLS", //the type of object, only supports HLS, required + PlaylistName: "playlist.m3u8", + }, + } + + result, err := bucket.CreateLiveChannel(channelName, config) + if err != nil { + HandleError(err) + } + + playURL := result.PlayUrls[0] + publishURL := result.PublishUrls[0] + fmt.Printf("livechannel:%s, playURL:%s, publishURL: %s\n", channelName, playURL, publishURL) + + signedRtmpURL, err := bucket.SignRtmpURL(channelName, playlistName, 3600) + if err != nil { + HandleError(err) + } + fmt.Printf("livechannel:%s, sinedRtmpURL: %s\n", channelName, signedRtmpURL) + + err = DeleteTestBucketAndLiveChannel(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("SignRtmpURLSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/new_bucket.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/new_bucket.go new file mode 100644 index 00000000..2a375c1c --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/new_bucket.go @@ -0,0 +1,50 @@ +package sample + +import ( + "fmt" + "strings" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// NewBucketSample shows how to initialize client and bucket +func NewBucketSample() { + // New client + client, err := oss.New(endpoint, accessID, accessKey) + if err != nil { + HandleError(err) + } + + // Create bucket + err = client.CreateBucket(bucketName) + if err != nil { + HandleError(err) + } + + // New bucket + bucket, err := client.Bucket(bucketName) + if err != nil { + HandleError(err) + } + + // Put object, uploads an object + var objectName = "myobject" + err = bucket.PutObject(objectName, strings.NewReader("MyObjectValue")) + if err != nil { + HandleError(err) + } + + // Delete object, deletes an object + err = bucket.DeleteObject(objectName) + if err != nil { + HandleError(err) + } + + // Delete bucket + err = client.DeleteBucket(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("NewBucketSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/object_acl.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/object_acl.go new file mode 100755 index 00000000..167de79e --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/object_acl.go @@ -0,0 +1,44 @@ +package sample + +import ( + "fmt" + "strings" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// ObjectACLSample shows how to set and get object ACL +func ObjectACLSample() { + // Create bucket + bucket, err := GetTestBucket(bucketName) + if err != nil { + HandleError(err) + } + + // Create object + err = bucket.PutObject(objectKey, strings.NewReader("YoursObjectValue")) + if err != nil { + HandleError(err) + } + + // Case 1: Set bucket ACL, valid ACLs are ACLPrivate、ACLPublicRead、ACLPublicReadWrite + err = bucket.SetObjectACL(objectKey, oss.ACLPrivate) + if err != nil { + HandleError(err) + } + + // Get object ACL, returns one of the three values: private、public-read、public-read-write + goar, err := bucket.GetObjectACL(objectKey) + if err != nil { + HandleError(err) + } + fmt.Println("Object ACL:", goar.ACL) + + // Delete object and bucket + err = DeleteTestBucketAndObject(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("ObjectACLSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/object_meta.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/object_meta.go new file mode 100755 index 00000000..6150d6bd --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/object_meta.go @@ -0,0 +1,73 @@ +package sample + +import ( + "fmt" + "strings" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// ObjectMetaSample shows how to get and set the object metadata +func ObjectMetaSample() { + // Create bucket + bucket, err := GetTestBucket(bucketName) + if err != nil { + HandleError(err) + } + + // Delete object + err = bucket.PutObject(objectKey, strings.NewReader("YoursObjectValue")) + if err != nil { + HandleError(err) + } + + // Case 0: Set bucket meta. one or more properties could be set + // Note: Meta is case insensitive + options := []oss.Option{ + oss.Expires(futureDate), + oss.Meta("myprop", "mypropval")} + err = bucket.SetObjectMeta(objectKey, options...) + if err != nil { + HandleError(err) + } + + // Case 1: Get the object metadata. Only return basic meta information includes ETag, size and last modified. + props, err := bucket.GetObjectMeta(objectKey) + if err != nil { + HandleError(err) + } + fmt.Println("Object Meta:", props) + + // Case 2: Get all the detailed object meta including custom meta + props, err = bucket.GetObjectDetailedMeta(objectKey) + if err != nil { + HandleError(err) + } + fmt.Println("Expires:", props.Get("Expires")) + + // Case 3: Get the object's all metadata with contraints. When constraints are met, return the metadata. + props, err = bucket.GetObjectDetailedMeta(objectKey, oss.IfUnmodifiedSince(futureDate)) + if err != nil { + HandleError(err) + } + fmt.Println("MyProp:", props.Get("X-Oss-Meta-Myprop")) + + _, err = bucket.GetObjectDetailedMeta(objectKey, oss.IfModifiedSince(futureDate)) + if err == nil { + HandleError(err) + } + + goar, err := bucket.GetObjectACL(objectKey) + if err != nil { + HandleError(err) + } + fmt.Println("Object ACL:", goar.ACL) + + // Delete object and bucket + err = DeleteTestBucketAndObject(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("ObjectMetaSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/object_tagging.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/object_tagging.go new file mode 100644 index 00000000..67271e90 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/object_tagging.go @@ -0,0 +1,75 @@ +package sample + +import ( + "fmt" + "strings" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// ObjectTaggingSample shows how to set and get object Tagging +func ObjectTaggingSample() { + // Create bucket + bucket, err := GetTestBucket(bucketName) + if err != nil { + HandleError(err) + } + + // Create object + err = bucket.PutObject(objectKey, strings.NewReader("ObjectTaggingSample")) + if err != nil { + HandleError(err) + } + + // Case 1: Set Tagging of object + tag1 := oss.Tag{ + Key: "key1", + Value: "value1", + } + tag2 := oss.Tag{ + Key: "key2", + Value: "value2", + } + tagging := oss.Tagging{ + Tags: []oss.Tag{tag1, tag2}, + } + err = bucket.PutObjectTagging(objectKey, tagging) + if err != nil { + HandleError(err) + } + + // Case 2: Get Tagging of object + taggingResult, err := bucket.GetObjectTagging(objectKey) + if err != nil { + HandleError(err) + } + fmt.Printf("Object Tagging: %v\n", taggingResult) + + tag3 := oss.Tag{ + Key: "key3", + Value: "value3", + } + + // Case 3: Put object with tagging + tagging = oss.Tagging{ + Tags: []oss.Tag{tag1, tag2, tag3}, + } + err = bucket.PutObject(objectKey, strings.NewReader("ObjectTaggingSample"), oss.SetTagging(tagging)) + if err != nil { + HandleError(err) + } + + // Case 4: Delete Tagging of object + err = bucket.DeleteObjectTagging(objectKey) + if err != nil { + HandleError(err) + } + + // Delete object and bucket + err = DeleteTestBucketAndObject(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("ObjectACLSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/put_object.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/put_object.go new file mode 100755 index 00000000..667eb2d7 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/put_object.go @@ -0,0 +1,145 @@ +package sample + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// PutObjectSample illustrates two methods for uploading a file: simple upload and multipart upload. +func PutObjectSample() { + // Create bucket + bucket, err := GetTestBucket(bucketName) + if err != nil { + HandleError(err) + } + + var val = "花间一壶酒,独酌无相亲。 举杯邀明月,对影成三人。" + + // Case 1: Upload an object from a string + err = bucket.PutObject(objectKey, strings.NewReader(val)) + if err != nil { + HandleError(err) + } + + // Case 2: Upload an object whose value is a byte[] + err = bucket.PutObject(objectKey, bytes.NewReader([]byte(val))) + if err != nil { + HandleError(err) + } + + // Case 3: Upload the local file with file handle, user should open the file at first. + fd, err := os.Open(localFile) + if err != nil { + HandleError(err) + } + defer fd.Close() + + err = bucket.PutObject(objectKey, fd) + if err != nil { + HandleError(err) + } + + // Case 4: Upload an object with local file name, user need not open the file. + err = bucket.PutObjectFromFile(objectKey, localFile) + if err != nil { + HandleError(err) + } + + // Case 5: Upload an object with specified properties, PutObject/PutObjectFromFile/UploadFile also support this feature. + options := []oss.Option{ + oss.Expires(futureDate), + oss.ObjectACL(oss.ACLPublicRead), + oss.Meta("myprop", "mypropval"), + } + err = bucket.PutObject(objectKey, strings.NewReader(val), options...) + if err != nil { + HandleError(err) + } + + props, err := bucket.GetObjectDetailedMeta(objectKey) + if err != nil { + HandleError(err) + } + fmt.Println("Object Meta:", props) + + // Case 6: Upload an object with sever side encrpytion kms and kms id specified + err = bucket.PutObject(objectKey, strings.NewReader(val), oss.ServerSideEncryption("KMS"), oss.ServerSideEncryptionKeyID(kmsID)) + if err != nil { + HandleError(err) + } + + // Case 7: Upload an object with callback + callbackMap := map[string]string{} + callbackMap["callbackUrl"] = "http://oss-demo.aliyuncs.com:23450" + callbackMap["callbackHost"] = "oss-cn-hangzhou.aliyuncs.com" + callbackMap["callbackBody"] = "filename=${object}&size=${size}&mimeType=${mimeType}" + callbackMap["callbackBodyType"] = "application/x-www-form-urlencoded" + + callbackBuffer := bytes.NewBuffer([]byte{}) + callbackEncoder := json.NewEncoder(callbackBuffer) + //do not encode '&' to "\u0026" + callbackEncoder.SetEscapeHTML(false) + err = callbackEncoder.Encode(callbackMap) + if err != nil { + HandleError(err) + } + + callbackVal := base64.StdEncoding.EncodeToString(callbackBuffer.Bytes()) + err = bucket.PutObject(objectKey, strings.NewReader(val), oss.Callback(callbackVal)) + if err != nil { + HandleError(err) + } + + // Case 8: Big file's multipart upload. It supports concurrent upload with resumable upload. + // multipart upload with 100K as part size. By default 1 coroutine is used and no checkpoint is used. + err = bucket.UploadFile(objectKey, localFile, 100*1024) + if err != nil { + HandleError(err) + } + + // Part size is 100K and 3 coroutines are used + err = bucket.UploadFile(objectKey, localFile, 100*1024, oss.Routines(3)) + if err != nil { + HandleError(err) + } + + // Part size is 100K and 3 coroutines with checkpoint + err = bucket.UploadFile(objectKey, localFile, 100*1024, oss.Routines(3), oss.Checkpoint(true, "")) + if err != nil { + HandleError(err) + } + + // Specify the local file path for checkpoint files. + // the 2nd parameter of Checkpoint can specify the file path, when the file path is empty, it will upload the directory. + err = bucket.UploadFile(objectKey, localFile, 100*1024, oss.Checkpoint(true, localFile+".cp")) + if err != nil { + HandleError(err) + } + + // Case 9: Set the storage classes.OSS provides three storage classes: Standard, Infrequent Access, and Archive. + // Supported APIs: PutObject, CopyObject, UploadFile, AppendObject... + err = bucket.PutObject(objectKey, strings.NewReader(val), oss.ObjectStorageClass("IA")) + if err != nil { + HandleError(err) + } + + // Upload a local file, and set the object's storage-class to 'Archive'. + err = bucket.UploadFile(objectKey, localFile, 100*1024, oss.ObjectStorageClass("Archive")) + if err != nil { + HandleError(err) + } + + // Delete object and bucket + err = DeleteTestBucketAndObject(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("PutObjectSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/sample_data.csv b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/sample_data.csv new file mode 100644 index 00000000..0c3f3a98 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/sample_data.csv @@ -0,0 +1,304 @@ +Year,StateAbbr,StateDesc,CityName,GeographicLevel,DataSource,Category,UniqueID,Measure,Data_Value_Unit,DataValueTypeID,Data_Value_Type,Data_Value,Low_Confidence_Limit,High_Confidence_Limit,Data_Value_Footnote_Symbol,Data_Value_Footnote,PopulationCount,GeoLocation,CategoryID,MeasureId,CityFIPS,TractFIPS,Short_Question_Text +2015,US,United States,,US,BRFSS,Prevention,59,Current lack of health insurance among adults aged 18–64 Years,%,AgeAdjPrv,Age-adjusted prevalence,15.4,15.1,15.7,,,308745538,,PREVENT,ACCESS2,,,Health Insurance +2015,US,United States,,US,BRFSS,Prevention,59,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,14.8,14.5,15.0,,,308745538,,PREVENT,ACCESS2,,,Health Insurance +2015,US,United States,,US,BRFSS,Health Outcomes,59,Arthritis among adults aged >=18 Years,%,AgeAdjPrv,Age-adjusted prevalence,22.5,22.3,22.7,,,308745538,,HLTHOUT,ARTHRITIS,,,Arthritis +2015,US,United States,,US,BRFSS,Health Outcomes,59,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,24.7,24.5,24.9,,,308745538,,HLTHOUT,ARTHRITIS,,,Arthritis +2015,US,United States,,US,BRFSS,Unhealthy Behaviors,59,Binge drinking among adults aged >=18 Years,%,AgeAdjPrv,Age-adjusted prevalence,17.2,16.9,17.4,,,308745538,,UNHBEH,BINGE,,,Binge Drinking +2015,US,United States,,US,BRFSS,Unhealthy Behaviors,59,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,16.3,16.1,16.5,,,308745538,,UNHBEH,BINGE,,,Binge Drinking +2015,US,United States,,US,BRFSS,Health Outcomes,59,High blood pressure among adults aged >=18 Years,%,AgeAdjPrv,Age-adjusted prevalence,29.4,29.2,29.7,,,308745538,,HLTHOUT,BPHIGH,,,High Blood Pressure +2015,US,United States,,US,BRFSS,Health Outcomes,59,High blood pressure among adults aged >=18 Years,%,CrdPrv,Crude prevalence,31.9,31.6,32.2,,,308745538,,HLTHOUT,BPHIGH,,,High Blood Pressure +2015,US,United States,,US,BRFSS,Prevention,59,Taking medicine for high blood pressure control among adults aged >=18 Years with high blood pressure,%,AgeAdjPrv,Age-adjusted prevalence,57.7,57.1,58.4,,,308745538,,PREVENT,BPMED,,,Taking BP Medication +2015,US,United States,,US,BRFSS,Prevention,59,Taking medicine for high blood pressure control among adults aged >=18 Years with high blood pressure,%,CrdPrv,Crude prevalence,77.2,76.8,77.7,,,308745538,,PREVENT,BPMED,,,Taking BP Medication +2015,US,United States,,US,BRFSS,Health Outcomes,59,Cancer (excluding skin cancer) among adults aged >=18 Years,%,AgeAdjPrv,Age-adjusted prevalence,6.0,5.9,6.1,,,308745538,,HLTHOUT,CANCER,,,Cancer (except skin) +2015,US,United States,,US,BRFSS,Health Outcomes,59,Cancer (excluding skin cancer) among adults aged >=18 Years,%,CrdPrv,Crude prevalence,6.6,6.5,6.8,,,308745538,,HLTHOUT,CANCER,,,Cancer (except skin) +2015,US,United States,,US,BRFSS,Health Outcomes,59,Current asthma among adults aged >=18 Years,%,AgeAdjPrv,Age-adjusted prevalence,8.7,8.6,8.9,,,308745538,,HLTHOUT,CASTHMA,,,Current Asthma +2015,US,United States,,US,BRFSS,Health Outcomes,59,Current asthma among adults aged >=18 Years,%,CrdPrv,Crude prevalence,8.8,8.6,9.0,,,308745538,,HLTHOUT,CASTHMA,,,Current Asthma +2015,US,United States,,US,BRFSS,Health Outcomes,59,Coronary heart disease among adults aged >=18 Years,%,AgeAdjPrv,Age-adjusted prevalence,5.6,5.5,5.8,,,308745538,,HLTHOUT,CHD,,,Coronary Heart Disease +2015,US,United States,,US,BRFSS,Health Outcomes,59,Coronary heart disease among adults aged >=18 Years,%,CrdPrv,Crude prevalence,6.3,6.2,6.5,,,308745538,,HLTHOUT,CHD,,,Coronary Heart Disease +2015,US,United States,,US,BRFSS,Prevention,59,Visits to doctor for routine checkup within the past Year among adults aged >=18 Years,%,AgeAdjPrv,Age-adjusted prevalence,68.6,68.3,68.9,,,308745538,,PREVENT,CHECKUP,,,Annual Checkup +2015,US,United States,,US,BRFSS,Prevention,59,Visits to doctor for routine checkup within the past Year among adults aged >=18 Years,%,CrdPrv,Crude prevalence,70.0,69.7,70.3,,,308745538,,PREVENT,CHECKUP,,,Annual Checkup +2015,US,United States,,US,BRFSS,Prevention,59,Cholesterol screening among adults aged >=18 Years,%,AgeAdjPrv,Age-adjusted prevalence,75.2,74.9,75.5,,,308745538,,PREVENT,CHOLSCREEN,,,Cholesterol Screening +2015,US,United States,,US,BRFSS,Prevention,59,Cholesterol screening among adults aged >=18 Years,%,CrdPrv,Crude prevalence,77.0,76.7,77.3,,,308745538,,PREVENT,CHOLSCREEN,,,Cholesterol Screening +2014,US,United States,,US,BRFSS,Prevention,59,"Fecal occult blood test, sigmoidoscopy, or colonoscopy among adults aged 50–75 Years",%,AgeAdjPrv,Age-adjusted prevalence,64.0,63.5,64.5,,,308745538,,PREVENT,COLON_SCREEN,,,Colorectal Cancer Screening +2014,US,United States,,US,BRFSS,Prevention,59,"Fecal occult blood test, sigmoidoscopy, or colonoscopy among adults aged 50–75 Years",%,CrdPrv,Crude prevalence,63.7,63.3,64.1,,,308745538,,PREVENT,COLON_SCREEN,,,Colorectal Cancer Screening +2015,US,United States,,US,BRFSS,Health Outcomes,59,Chronic obstructive pulmonary disease among adults aged >=18 Years,%,AgeAdjPrv,Age-adjusted prevalence,5.7,5.6,5.9,,,308745538,,HLTHOUT,COPD,,,COPD +2015,US,United States,,US,BRFSS,Health Outcomes,59,Chronic obstructive pulmonary disease among adults aged >=18 Years,%,CrdPrv,Crude prevalence,6.3,6.2,6.4,,,308745538,,HLTHOUT,COPD,,,COPD +2015,US,United States,,US,BRFSS,Health Outcomes,59,Physical health not good for >=14 days among adults aged >=18 Years,%,AgeAdjPrv,Age-adjusted prevalence,11.5,11.3,11.7,,,308745538,,HLTHOUT,PHLTH,,,Physical Health +2014,US,United States,,US,BRFSS,Prevention,59,"Older adult men aged >=65 Years who are up to date on a core set of clinical preventive services: Flu shot past Year, PPV shot ever, Colorectal cancer screening",%,AgeAdjPrv,Age-adjusted prevalence,32.9,32.1,33.6,,,308745538,,PREVENT,COREM,,,Core preventive services for older men +2014,US,United States,,US,BRFSS,Prevention,59,"Older adult men aged >=65 Years who are up to date on a core set of clinical preventive services: Flu shot past Year, PPV shot ever, Colorectal cancer screening",%,CrdPrv,Crude prevalence,32.3,31.5,33.0,,,308745538,,PREVENT,COREM,,,Core preventive services for older men +2014,US,United States,,US,BRFSS,Prevention,59,"Older adult women aged >=65 Years who are up to date on a core set of clinical preventive services: Flu shot past Year, PPV shot ever, Colorectal cancer screening, and Mammogram past 2 Years",%,AgeAdjPrv,Age-adjusted prevalence,30.7,30.2,31.4,,,308745538,,PREVENT,COREW,,,Core preventive services for older women +2014,US,United States,,US,BRFSS,Prevention,59,"Older adult women aged >=65 Years who are up to date on a core set of clinical preventive services: Flu shot past Year, PPV shot ever, Colorectal cancer screening, and Mammogram past 2 Years",%,CrdPrv,Crude prevalence,30.7,30.1,31.3,,,308745538,,PREVENT,COREW,,,Core preventive services for older women +2015,US,United States,,US,BRFSS,Unhealthy Behaviors,59,Current smoking among adults aged >=18 Years,%,AgeAdjPrv,Age-adjusted prevalence,17.1,16.8,17.3,,,308745538,,UNHBEH,CSMOKING,,,Current Smoking +2015,US,United States,,US,BRFSS,Unhealthy Behaviors,59,Current smoking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,16.8,16.6,17.0,,,308745538,,UNHBEH,CSMOKING,,,Current Smoking +2014,US,United States,,US,BRFSS,Prevention,59,Visits to dentist or dental clinic among adults aged >=18 Years,%,AgeAdjPrv,Age-adjusted prevalence,64.1,63.8,64.4,,,308745538,,PREVENT,DENTAL,,,Dental Visit +2014,US,United States,,US,BRFSS,Prevention,59,Visits to dentist or dental clinic among adults aged >=18 Years,%,CrdPrv,Crude prevalence,64.4,64.1,64.7,,,308745538,,PREVENT,DENTAL,,,Dental Visit +2015,US,United States,,US,BRFSS,Health Outcomes,59,Diagnosed diabetes among adults aged >=18 Years,%,AgeAdjPrv,Age-adjusted prevalence,9.3,9.2,9.5,,,308745538,,HLTHOUT,DIABETES,,,Diabetes +2015,US,United States,,US,BRFSS,Health Outcomes,59,Diagnosed diabetes among adults aged >=18 Years,%,CrdPrv,Crude prevalence,10.4,10.3,10.6,,,308745538,,HLTHOUT,DIABETES,,,Diabetes +2015,US,United States,,US,BRFSS,Health Outcomes,59,High cholesterol among adults aged >=18 Years who have been screened in the past 5 Years,%,AgeAdjPrv,Age-adjusted prevalence,31.1,30.8,31.4,,,308745538,,HLTHOUT,HIGHCHOL,,,High Cholesterol +2015,US,United States,,US,BRFSS,Health Outcomes,59,High cholesterol among adults aged >=18 Years who have been screened in the past 5 Years,%,CrdPrv,Crude prevalence,37.1,36.8,37.4,,,308745538,,HLTHOUT,HIGHCHOL,,,High Cholesterol +2015,US,United States,,US,BRFSS,Health Outcomes,59,Chronic kidney disease among adults aged >=18 Years,%,AgeAdjPrv,Age-adjusted prevalence,2.5,2.4,2.6,,,308745538,,HLTHOUT,KIDNEY,,,Chronic Kidney Disease +2015,US,United States,,US,BRFSS,Health Outcomes,59,Chronic kidney disease among adults aged >=18 Years,%,CrdPrv,Crude prevalence,2.7,2.6,2.8,,,308745538,,HLTHOUT,KIDNEY,,,Chronic Kidney Disease +2015,US,United States,,US,BRFSS,Unhealthy Behaviors,59,No leisure-time physical activity among adults aged >=18 Years,%,AgeAdjPrv,Age-adjusted prevalence,25.5,25.2,25.8,,,308745538,,UNHBEH,LPA,,,Physical Inactivity +2015,US,United States,,US,BRFSS,Unhealthy Behaviors,59,No leisure-time physical activity among adults aged >=18 Years,%,CrdPrv,Crude prevalence,25.9,25.6,26.1,,,308745538,,UNHBEH,LPA,,,Physical Inactivity +2014,US,United States,,US,BRFSS,Prevention,59,Mammography use among women aged 50–74 Years,%,AgeAdjPrv,Age-adjusted prevalence,75.5,75.1,75.9,,,308745538,,PREVENT,MAMMOUSE,,,Mammography +2014,US,United States,,US,BRFSS,Prevention,59,Mammography use among women aged 50–74 Years,%,CrdPrv,Crude prevalence,75.8,75.4,76.2,,,308745538,,PREVENT,MAMMOUSE,,,Mammography +2015,US,United States,,US,BRFSS,Health Outcomes,59,Mental health not good for >=14 days among adults aged >=18 Years,%,AgeAdjPrv,Age-adjusted prevalence,11.6,11.4,11.8,,,308745538,,HLTHOUT,MHLTH,,,Mental Health +2015,US,United States,,US,BRFSS,Health Outcomes,59,Mental health not good for >=14 days among adults aged >=18 Years,%,CrdPrv,Crude prevalence,11.4,11.3,11.6,,,308745538,,HLTHOUT,MHLTH,,,Mental Health +2015,US,United States,,US,BRFSS,Unhealthy Behaviors,59,Obesity among adults aged >=18 Years,%,AgeAdjPrv,Age-adjusted prevalence,28.7,28.4,29.0,,,308745538,,UNHBEH,OBESITY,,,Obesity +2015,US,United States,,US,BRFSS,Unhealthy Behaviors,59,Obesity among adults aged >=18 Years,%,CrdPrv,Crude prevalence,28.8,28.6,29.1,,,308745538,,UNHBEH,OBESITY,,,Obesity +2014,US,United States,,US,BRFSS,Prevention,59,Papanicolaou smear use among adult women aged 21–65 Years,%,AgeAdjPrv,Age-adjusted prevalence,81.1,80.6,81.6,,,308745538,,PREVENT,PAPTEST,,,Pap Smear Test +2014,US,United States,,US,BRFSS,Prevention,59,Papanicolaou smear use among adult women aged 21–65 Years,%,CrdPrv,Crude prevalence,81.8,81.3,82.2,,,308745538,,PREVENT,PAPTEST,,,Pap Smear Test +2015,US,United States,,US,BRFSS,Health Outcomes,59,Physical health not good for >=14 days among adults aged >=18 Years,%,CrdPrv,Crude prevalence,12.0,11.8,12.2,,,308745538,,HLTHOUT,PHLTH,,,Physical Health +2014,US,United States,,US,BRFSS,Unhealthy Behaviors,59,Sleeping less than 7 hours among adults aged >=18 Years,%,AgeAdjPrv,Age-adjusted prevalence,35.1,34.8,35.5,,,308745538,,UNHBEH,SLEEP,,,Sleep < 7 hours +2014,US,United States,,US,BRFSS,Unhealthy Behaviors,59,Sleeping less than 7 hours among adults aged >=18 Years,%,CrdPrv,Crude prevalence,34.8,34.5,35.1,,,308745538,,UNHBEH,SLEEP,,,Sleep < 7 hours +2015,US,United States,,US,BRFSS,Health Outcomes,59,Stroke among adults aged >=18 Years,%,AgeAdjPrv,Age-adjusted prevalence,2.8,2.7,2.8,,,308745538,,HLTHOUT,STROKE,,,Stroke +2015,US,United States,,US,BRFSS,Health Outcomes,59,Stroke among adults aged >=18 Years,%,CrdPrv,Crude prevalence,3.0,3.0,3.1,,,308745538,,HLTHOUT,STROKE,,,Stroke +2014,US,United States,,US,BRFSS,Health Outcomes,59,All teeth lost among adults aged >=65 Years,%,AgeAdjPrv,Age-adjusted prevalence,15.4,15.0,15.8,,,308745538,,HLTHOUT,TEETHLOST,,,Teeth Loss +2014,US,United States,,US,BRFSS,Health Outcomes,59,All teeth lost among adults aged >=65 Years,%,CrdPrv,Crude prevalence,14.9,14.6,15.3,,,308745538,,HLTHOUT,TEETHLOST,,,Teeth Loss +2015,AL,Alabama,Birmingham,City,BRFSS,Prevention,0107000,Current lack of health insurance among adults aged 18–64 Years,%,AgeAdjPrv,Age-adjusted prevalence,19.8,19.5,20.2,,,212237,"(33.5275663773, -86.7988174678)",PREVENT,ACCESS2,0107000,,Health Insurance +2015,AL,Alabama,Birmingham,City,BRFSS,Prevention,0107000,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,19.6,19.2,20.0,,,212237,"(33.5275663773, -86.7988174678)",PREVENT,ACCESS2,0107000,,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073000100,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,23.9,21.2,27.2,,,3042,"(33.5794328326, -86.7228323926)",PREVENT,ACCESS2,0107000,01073000100,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073000300,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,28.8,25.4,32.4,,,2735,"(33.5428208686, -86.752433978)",PREVENT,ACCESS2,0107000,01073000300,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073000400,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,26.1,22.6,29.9,,,3338,"(33.5632449633, -86.7640474064)",PREVENT,ACCESS2,0107000,01073000400,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073000500,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,28.1,24.6,32.0,,,2864,"(33.5442404594, -86.7749130719)",PREVENT,ACCESS2,0107000,01073000500,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073000700,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,31.8,27.0,36.7,,,2577,"(33.5525406139, -86.8016893706)",PREVENT,ACCESS2,0107000,01073000700,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073000800,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,22.4,19.1,26.1,,,3859,"(33.549697789, -86.8330944744)",PREVENT,ACCESS2,0107000,01073000800,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073001100,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,16.8,13.7,20.5,,,5354,"(33.5429143325, -86.8756782852)",PREVENT,ACCESS2,0107000,01073001100,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073001200,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,24.6,22.2,27.2,,,2876,"(33.5278767706, -86.8604161686)",PREVENT,ACCESS2,0107000,01073001200,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073001400,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,22.0,18.4,25.7,,,2181,"(33.5261497258, -86.835146606)",PREVENT,ACCESS2,0107000,01073001400,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073001500,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,26.3,23.1,29.4,,,3189,"(33.5298727342, -86.8197191685)",PREVENT,ACCESS2,0107000,01073001500,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073001600,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,26.8,22.9,30.8,,,3390,"(33.5372993423, -86.8036590482)",PREVENT,ACCESS2,0107000,01073001600,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073001902,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,26.9,24.1,29.8,,,1894,"(33.5532050997, -86.7429801603)",PREVENT,ACCESS2,0107000,01073001902,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073002000,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,24.4,20.8,28.2,,,3885,"(33.5541574106, -86.7167229915)",PREVENT,ACCESS2,0107000,01073002000,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073002100,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,21.5,18.1,25.0,,,3186,"(33.5650015942, -86.7101024766)",PREVENT,ACCESS2,0107000,01073002100,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073002200,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,21.7,18.7,25.0,,,2630,"(33.5521301205, -86.7276759508)",PREVENT,ACCESS2,0107000,01073002200,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073002303,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,27.0,23.7,30.7,,,2936,"(33.5383153207, -86.7270445428)",PREVENT,ACCESS2,0107000,01073002303,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073002305,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,9.9,8.2,12.1,,,2952,"(33.5333415976, -86.7479566084)",PREVENT,ACCESS2,0107000,01073002305,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073002306,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,9.5,8.1,11.2,,,3257,"(33.5213873564, -86.7490031289)",PREVENT,ACCESS2,0107000,01073002306,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073002400,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,25.1,22.4,27.8,,,3629,"(33.5260748309, -86.7830315488)",PREVENT,ACCESS2,0107000,01073002400,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073002700,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,20.7,16.8,24.9,,,3992,"(33.5176008419, -86.8106887452)",PREVENT,ACCESS2,0107000,01073002700,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073002900,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,25.7,21.5,29.9,,,2064,"(33.5132498864, -86.83004749)",PREVENT,ACCESS2,0107000,01073002900,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073003001,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,18.4,15.3,22.4,,,3779,"(33.5125158094, -86.8577164946)",PREVENT,ACCESS2,0107000,01073003001,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073003002,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,26.3,21.7,31.3,,,2203,"(33.512258109, -86.8441439907)",PREVENT,ACCESS2,0107000,01073003002,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073003100,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,23.2,20.2,26.7,,,3637,"(33.5059655756, -86.8745506086)",PREVENT,ACCESS2,0107000,01073003100,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073003200,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,28.7,25.0,32.8,,,931,"(33.5094018502, -86.8859081961)",PREVENT,ACCESS2,0107000,01073003200,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073003300,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,22.2,19.1,25.7,,,947,"(33.5171261108, -86.8913819749)",PREVENT,ACCESS2,0107000,01073003300,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073003400,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,26.8,23.0,30.9,,,2477,"(33.5052229234, -86.9014844656)",PREVENT,ACCESS2,0107000,01073003400,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073003500,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,22.3,19.1,25.7,,,2780,"(33.5065714011, -86.9195910063)",PREVENT,ACCESS2,0107000,01073003500,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073003600,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,17.0,14.5,19.6,,,4683,"(33.48476397, -86.8981392947)",PREVENT,ACCESS2,0107000,01073003600,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073003700,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,21.6,19.2,23.9,,,5063,"(33.4969018589, -86.8907729426)",PREVENT,ACCESS2,0107000,01073003700,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073003802,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,19.0,16.7,21.6,,,5409,"(33.4785707794, -86.890000907)",PREVENT,ACCESS2,0107000,01073003802,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073003803,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,20.9,17.7,24.2,,,4199,"(33.485945214, -86.869692186)",PREVENT,ACCESS2,0107000,01073003803,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073003900,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,29.9,26.9,33.0,,,1783,"(33.4989959327, -86.8647600038)",PREVENT,ACCESS2,0107000,01073003900,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073004000,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,25.8,21.4,30.3,,,3772,"(33.4953246015, -86.8516232073)",PREVENT,ACCESS2,0107000,01073004000,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073004200,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,23.4,20.8,26.1,,,2341,"(33.5007439361, -86.8270720379)",PREVENT,ACCESS2,0107000,01073004200,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073004500,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,22.7,19.3,27.4,,,5003,"(33.5041857556, -86.8033798346)",PREVENT,ACCESS2,0107000,01073004500,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073004701,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,10.6,8.6,13.4,,,3480,"(33.5075242148, -86.7836675838)",PREVENT,ACCESS2,0107000,01073004701,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073004702,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,7.1,6.1,8.7,,,2944,"(33.5119902661, -86.7694550989)",PREVENT,ACCESS2,0107000,01073004702,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073004800,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,8.7,7.3,10.3,,,1861,"(33.4989064008, -86.78269914)",PREVENT,ACCESS2,0107000,01073004800,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073004901,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,11.0,9.1,13.2,,,1167,"(33.4971595645, -86.7917440668)",PREVENT,ACCESS2,0107000,01073004901,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073004902,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,15.2,12.5,18.5,,,3146,"(33.4935824043, -86.8009294603)",PREVENT,ACCESS2,0107000,01073004902,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073005000,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,17.9,15.4,20.7,,,3482,"(33.4866689795, -86.8173262831)",PREVENT,ACCESS2,0107000,01073005000,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073005101,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,30.5,26.5,34.4,,,1507,"(33.4945909008, -86.834763936)",PREVENT,ACCESS2,0107000,01073005101,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073005103,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,17.5,14.4,20.7,,,2587,"(33.485714885, -86.8327817467)",PREVENT,ACCESS2,0107000,01073005103,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073005104,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,24.9,21.3,28.9,,,2881,"(33.4749941816, -86.8335421747)",PREVENT,ACCESS2,0107000,01073005104,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073005200,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,20.2,16.4,24.2,,,3740,"(33.4806708775, -86.8508671514)",PREVENT,ACCESS2,0107000,01073005200,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073005302,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,12.8,10.9,15.1,,,3463,"(33.5766456449, -86.6965590316)",PREVENT,ACCESS2,0107000,01073005302,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073005500,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,26.5,22.9,30.7,,,1824,"(33.5670293898, -86.8005567213)",PREVENT,ACCESS2,0107000,01073005500,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073005600,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,12.3,10.0,15.0,,,4367,"(33.5200981234, -86.7272063198)",PREVENT,ACCESS2,0107000,01073005600,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073005701,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,18.9,15.7,22.3,,,2372,"(33.4629543329, -86.8898406628)",PREVENT,ACCESS2,0107000,01073005701,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073005702,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,19.8,16.4,23.3,,,3413,"(33.4698691385, -86.874290602)",PREVENT,ACCESS2,0107000,01073005702,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073005800,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,19.3,16.4,22.3,,,4216,"(33.479062325, -86.8128063089)",PREVENT,ACCESS2,0107000,01073005800,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073005903,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,15.9,13.9,18.0,,,4933,"(33.597162309, -86.6766736351)",PREVENT,ACCESS2,0107000,01073005903,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073005905,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,21.4,18.2,24.7,,,5039,"(33.603988456, -86.7008123418)",PREVENT,ACCESS2,0107000,01073005905,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073005907,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,12.5,10.3,14.9,,,1975,"(33.6142861501, -86.6691996417)",PREVENT,ACCESS2,0107000,01073005907,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073005908,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,20.9,18.1,23.7,,,1621,"(33.6182924432, -86.6801483084)",PREVENT,ACCESS2,0107000,01073005908,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073005909,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,14.4,11.8,17.5,,,2524,"(33.611811773, -86.7214221514)",PREVENT,ACCESS2,0107000,01073005909,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073005910,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,14.7,12.6,17.1,,,4612,"(33.6299017499, -86.7194311229)",PREVENT,ACCESS2,0107000,01073005910,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073010500,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,21.8,18.2,25.9,,,114,"(33.4363786806, -86.9128923072)",PREVENT,ACCESS2,0107000,01073010500,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073010701,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,18.5,14.4,23.6,,,74,"(33.473886155, -86.8146487762)",PREVENT,ACCESS2,0107000,01073010701,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073010706,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,13.5,11.0,16.3,,,1528,"(33.4443709442, -86.8405352645)",PREVENT,ACCESS2,0107000,01073010706,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073010801,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,5.2,4.4,6.3,,,168,"(33.514097853, -86.7466971362)",PREVENT,ACCESS2,0107000,01073010801,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073010802,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,5.0,3.9,6.5,,,172,"(33.4885493477, -86.780843024)",PREVENT,ACCESS2,0107000,01073010802,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073010803,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,11.3,9.0,14.0,,,514,"(33.5229093892, -86.7102618642)",PREVENT,ACCESS2,0107000,01073010803,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073010805,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,7.3,5.5,9.9,,,86,"(33.4952792472, -86.6987184974)",PREVENT,ACCESS2,0107000,01073010805,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073011104,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,13.6,11.4,16.1,,,1688,"(33.6159436433, -86.6557892507)",PREVENT,ACCESS2,0107000,01073011104,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073011107,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,,,,*,Estimates suppressed for population less than 50,42,"(33.5804845249, -86.6301110961)",PREVENT,ACCESS2,0107000,01073011107,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073011108,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,,,,*,Estimates suppressed for population less than 50,9,"(33.6050742596, -86.6316729386)",PREVENT,ACCESS2,0107000,01073011108,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073011207,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,17.7,15.1,20.6,,,815,"(33.6718848706, -86.6772510465)",PREVENT,ACCESS2,0107000,01073011207,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073011209,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,20.2,17.2,24.2,,,1062,"(33.6557189992, -86.7050698349)",PREVENT,ACCESS2,0107000,01073011209,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073011210,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,21.5,17.0,26.7,,,1385,"(33.6641893755, -86.6956170686)",PREVENT,ACCESS2,0107000,01073011210,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073011803,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,18.1,15.4,21.3,,,928,"(33.6252575173, -86.6998610409)",PREVENT,ACCESS2,0107000,01073011803,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073011804,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,18.3,15.2,21.9,,,1157,"(33.6474916175, -86.7042974424)",PREVENT,ACCESS2,0107000,01073011804,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073011901,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,,,,*,Estimates suppressed for population less than 50,6,"(33.6355414646, -86.736946691)",PREVENT,ACCESS2,0107000,01073011901,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073011904,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,14.4,12.1,16.8,,,1915,"(33.593140185, -86.7357930541)",PREVENT,ACCESS2,0107000,01073011904,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073012001,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,14.5,12.5,16.7,,,304,"(33.5919486112, -86.864384068)",PREVENT,ACCESS2,0107000,01073012001,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073012002,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,,,,*,Estimates suppressed for population less than 50,44,"(33.5859234197, -86.8357007188)",PREVENT,ACCESS2,0107000,01073012002,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073012200,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,,,,*,Estimates suppressed for population less than 50,23,"(33.5967441904, -87.0879396857)",PREVENT,ACCESS2,0107000,01073012200,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073012302,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,12.7,10.6,15.2,,,144,"(33.5542816352, -87.0544691416)",PREVENT,ACCESS2,0107000,01073012302,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073012305,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,12.5,10.2,15.1,,,403,"(33.4695358064, -86.96831739)",PREVENT,ACCESS2,0107000,01073012305,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073012401,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,11.4,8.9,14.2,,,1066,"(33.5571951048, -86.8777935049)",PREVENT,ACCESS2,0107000,01073012401,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073012402,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,16.7,14.6,19.0,,,418,"(33.549984172, -86.8994543327)",PREVENT,ACCESS2,0107000,01073012402,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073012500,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,21.0,17.6,24.4,,,410,"(33.529160486, -86.9346476445)",PREVENT,ACCESS2,0107000,01073012500,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073012602,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,17.5,15.7,19.6,,,371,"(33.570164674, -86.666430582)",PREVENT,ACCESS2,0107000,01073012602,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073012701,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,,,,*,Estimates suppressed for population less than 50,44,"(33.5484078071, -86.6323773455)",PREVENT,ACCESS2,0107000,01073012701,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073012703,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,11.3,8.6,14.9,,,498,"(33.4681180943, -86.6671888213)",PREVENT,ACCESS2,0107000,01073012703,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073012704,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,8.9,6.8,11.5,,,113,"(33.5034195908, -86.6180983403)",PREVENT,ACCESS2,0107000,01073012704,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073012803,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,8.6,6.7,11.2,,,1261,"(33.4439425865, -86.7212936938)",PREVENT,ACCESS2,0107000,01073012803,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073012910,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,,,,*,Estimates suppressed for population less than 50,9,"(33.4345805042, -86.7263292059)",PREVENT,ACCESS2,0107000,01073012910,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073013002,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,21.3,17.1,25.6,,,1514,"(33.46604181, -86.8567287797)",PREVENT,ACCESS2,0107000,01073013002,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073013100,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,21.8,18.8,24.9,,,4424,"(33.44880214, -86.8878401579)",PREVENT,ACCESS2,0107000,01073013100,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073013300,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,23.5,19.5,27.5,,,1782,"(33.4396443569, -86.9248768665)",PREVENT,ACCESS2,0107000,01073013300,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073013901,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,18.0,14.8,21.7,,,952,"(33.4729384906, -86.9547337648)",PREVENT,ACCESS2,0107000,01073013901,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073014302,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,10.0,8.3,12.1,,,2778,"(33.4244658829, -86.8841474217)",PREVENT,ACCESS2,0107000,01073014302,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01073014413,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,7.6,5.9,9.8,,,397,"(33.4226593117, -86.8508620751)",PREVENT,ACCESS2,0107000,01073014413,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01117030213,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,9.3,7.7,11.4,,,644,"(33.4395975193, -86.6735959359)",PREVENT,ACCESS2,0107000,01117030213,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01117030217,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,,,,*,Estimates suppressed for population less than 50,16,"(33.4556995763, -86.6520208639)",PREVENT,ACCESS2,0107000,01117030217,Health Insurance +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Prevention,0107000-01117030303,Current lack of health insurance among adults aged 18–64 Years,%,CrdPrv,Crude prevalence,9.9,8.1,12.0,,,968,"(33.4258661239, -86.713819356)",PREVENT,ACCESS2,0107000,01117030303,Health Insurance +2015,AL,Alabama,Birmingham,City,BRFSS,Health Outcomes,0107000,Arthritis among adults aged >=18 Years,%,AgeAdjPrv,Age-adjusted prevalence,31.0,30.8,31.1,,,212237,"(33.5275663773, -86.7988174678)",HLTHOUT,ARTHRITIS,0107000,,Arthritis +2015,AL,Alabama,Birmingham,City,BRFSS,Health Outcomes,0107000,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,30.9,30.8,31.1,,,212237,"(33.5275663773, -86.7988174678)",HLTHOUT,ARTHRITIS,0107000,,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073000100,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,32.5,31.5,33.6,,,3042,"(33.5794328326, -86.7228323926)",HLTHOUT,ARTHRITIS,0107000,01073000100,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073000300,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,31.3,30.0,32.4,,,2735,"(33.5428208686, -86.752433978)",HLTHOUT,ARTHRITIS,0107000,01073000300,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073000400,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,34.6,33.2,35.9,,,3338,"(33.5632449633, -86.7640474064)",HLTHOUT,ARTHRITIS,0107000,01073000400,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073000500,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,37.8,36.3,39.2,,,2864,"(33.5442404594, -86.7749130719)",HLTHOUT,ARTHRITIS,0107000,01073000500,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073000700,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,38.5,37.1,39.9,,,2577,"(33.5525406139, -86.8016893706)",HLTHOUT,ARTHRITIS,0107000,01073000700,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073000800,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,38.0,36.5,39.3,,,3859,"(33.549697789, -86.8330944744)",HLTHOUT,ARTHRITIS,0107000,01073000800,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073001100,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,34.0,32.4,35.5,,,5354,"(33.5429143325, -86.8756782852)",HLTHOUT,ARTHRITIS,0107000,01073001100,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073001200,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,36.5,35.4,37.6,,,2876,"(33.5278767706, -86.8604161686)",HLTHOUT,ARTHRITIS,0107000,01073001200,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073001400,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,37.1,35.6,38.6,,,2181,"(33.5261497258, -86.835146606)",HLTHOUT,ARTHRITIS,0107000,01073001400,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073001500,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,35.2,34.0,36.4,,,3189,"(33.5298727342, -86.8197191685)",HLTHOUT,ARTHRITIS,0107000,01073001500,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073001600,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,39.9,38.4,41.3,,,3390,"(33.5372993423, -86.8036590482)",HLTHOUT,ARTHRITIS,0107000,01073001600,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073001902,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,35.1,34.0,36.1,,,1894,"(33.5532050997, -86.7429801603)",HLTHOUT,ARTHRITIS,0107000,01073001902,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073002000,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,36.3,34.9,37.7,,,3885,"(33.5541574106, -86.7167229915)",HLTHOUT,ARTHRITIS,0107000,01073002000,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073002100,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,33.1,31.8,34.3,,,3186,"(33.5650015942, -86.7101024766)",HLTHOUT,ARTHRITIS,0107000,01073002100,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073002200,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,32.4,31.2,33.6,,,2630,"(33.5521301205, -86.7276759508)",HLTHOUT,ARTHRITIS,0107000,01073002200,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073002303,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,33.4,32.2,34.6,,,2936,"(33.5383153207, -86.7270445428)",HLTHOUT,ARTHRITIS,0107000,01073002303,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073002305,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,22.3,21.3,23.3,,,2952,"(33.5333415976, -86.7479566084)",HLTHOUT,ARTHRITIS,0107000,01073002305,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073002306,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,26.9,25.9,27.9,,,3257,"(33.5213873564, -86.7490031289)",HLTHOUT,ARTHRITIS,0107000,01073002306,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073002400,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,31.0,30.0,31.9,,,3629,"(33.5260748309, -86.7830315488)",HLTHOUT,ARTHRITIS,0107000,01073002400,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073002700,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,27.2,25.9,28.5,,,3992,"(33.5176008419, -86.8106887452)",HLTHOUT,ARTHRITIS,0107000,01073002700,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073002900,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,38.7,37.0,40.3,,,2064,"(33.5132498864, -86.83004749)",HLTHOUT,ARTHRITIS,0107000,01073002900,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073003001,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,24.3,23.6,25.0,,,3779,"(33.5125158094, -86.8577164946)",HLTHOUT,ARTHRITIS,0107000,01073003001,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073003002,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,40.9,39.1,42.7,,,2203,"(33.512258109, -86.8441439907)",HLTHOUT,ARTHRITIS,0107000,01073003002,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073003100,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,34.1,32.7,35.4,,,3637,"(33.5059655756, -86.8745506086)",HLTHOUT,ARTHRITIS,0107000,01073003100,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073003200,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,39.0,37.6,40.3,,,931,"(33.5094018502, -86.8859081961)",HLTHOUT,ARTHRITIS,0107000,01073003200,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073003300,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,38.6,37.3,39.9,,,947,"(33.5171261108, -86.8913819749)",HLTHOUT,ARTHRITIS,0107000,01073003300,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073003400,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,36.3,34.9,37.7,,,2477,"(33.5052229234, -86.9014844656)",HLTHOUT,ARTHRITIS,0107000,01073003400,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073003500,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,32.8,31.5,34.0,,,2780,"(33.5065714011, -86.9195910063)",HLTHOUT,ARTHRITIS,0107000,01073003500,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073003600,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,33.4,32.1,34.7,,,4683,"(33.48476397, -86.8981392947)",HLTHOUT,ARTHRITIS,0107000,01073003600,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073003700,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,31.2,30.1,32.1,,,5063,"(33.4969018589, -86.8907729426)",HLTHOUT,ARTHRITIS,0107000,01073003700,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073003802,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,32.1,31.0,33.2,,,5409,"(33.4785707794, -86.890000907)",HLTHOUT,ARTHRITIS,0107000,01073003802,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073003803,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,36.0,34.5,37.3,,,4199,"(33.485945214, -86.869692186)",HLTHOUT,ARTHRITIS,0107000,01073003803,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073003900,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,32.9,31.8,33.9,,,1783,"(33.4989959327, -86.8647600038)",HLTHOUT,ARTHRITIS,0107000,01073003900,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073004000,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,37.8,36.4,39.3,,,3772,"(33.4953246015, -86.8516232073)",HLTHOUT,ARTHRITIS,0107000,01073004000,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073004200,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,36.6,35.5,37.7,,,2341,"(33.5007439361, -86.8270720379)",HLTHOUT,ARTHRITIS,0107000,01073004200,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073004500,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,15.1,14.4,15.8,,,5003,"(33.5041857556, -86.8033798346)",HLTHOUT,ARTHRITIS,0107000,01073004500,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073004701,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,23.7,22.5,24.9,,,3480,"(33.5075242148, -86.7836675838)",HLTHOUT,ARTHRITIS,0107000,01073004701,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073004702,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,27.0,25.9,28.1,,,2944,"(33.5119902661, -86.7694550989)",HLTHOUT,ARTHRITIS,0107000,01073004702,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073004800,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,30.1,29.0,31.2,,,1861,"(33.4989064008, -86.78269914)",HLTHOUT,ARTHRITIS,0107000,01073004800,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073004901,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,22.1,21.2,23.0,,,1167,"(33.4971595645, -86.7917440668)",HLTHOUT,ARTHRITIS,0107000,01073004901,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073004902,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,18.4,17.5,19.4,,,3146,"(33.4935824043, -86.8009294603)",HLTHOUT,ARTHRITIS,0107000,01073004902,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073005000,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,19.6,18.7,20.4,,,3482,"(33.4866689795, -86.8173262831)",HLTHOUT,ARTHRITIS,0107000,01073005000,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073005101,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,36.6,35.3,37.8,,,1507,"(33.4945909008, -86.834763936)",HLTHOUT,ARTHRITIS,0107000,01073005101,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073005103,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,40.1,38.6,41.7,,,2587,"(33.485714885, -86.8327817467)",HLTHOUT,ARTHRITIS,0107000,01073005103,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073005104,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,18.9,18.0,19.8,,,2881,"(33.4749941816, -86.8335421747)",HLTHOUT,ARTHRITIS,0107000,01073005104,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073005200,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,37.9,36.1,39.7,,,3740,"(33.4806708775, -86.8508671514)",HLTHOUT,ARTHRITIS,0107000,01073005200,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073005302,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,33.2,31.8,34.5,,,3463,"(33.5766456449, -86.6965590316)",HLTHOUT,ARTHRITIS,0107000,01073005302,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073005500,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,36.7,35.4,37.9,,,1824,"(33.5670293898, -86.8005567213)",HLTHOUT,ARTHRITIS,0107000,01073005500,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073005600,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,31.8,30.3,33.2,,,4367,"(33.5200981234, -86.7272063198)",HLTHOUT,ARTHRITIS,0107000,01073005600,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073005701,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,35.8,34.1,37.3,,,2372,"(33.4629543329, -86.8898406628)",HLTHOUT,ARTHRITIS,0107000,01073005701,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073005702,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,37.8,36.0,39.3,,,3413,"(33.4698691385, -86.874290602)",HLTHOUT,ARTHRITIS,0107000,01073005702,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073005800,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,18.4,17.7,19.1,,,4216,"(33.479062325, -86.8128063089)",HLTHOUT,ARTHRITIS,0107000,01073005800,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073005903,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,32.2,31.1,33.3,,,4933,"(33.597162309, -86.6766736351)",HLTHOUT,ARTHRITIS,0107000,01073005903,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073005905,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,34.2,32.9,35.4,,,5039,"(33.603988456, -86.7008123418)",HLTHOUT,ARTHRITIS,0107000,01073005905,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073005907,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,30.1,28.9,31.3,,,1975,"(33.6142861501, -86.6691996417)",HLTHOUT,ARTHRITIS,0107000,01073005907,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073005908,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,32.5,31.5,33.5,,,1621,"(33.6182924432, -86.6801483084)",HLTHOUT,ARTHRITIS,0107000,01073005908,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073005909,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,28.0,26.5,29.4,,,2524,"(33.611811773, -86.7214221514)",HLTHOUT,ARTHRITIS,0107000,01073005909,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073005910,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,28.0,26.8,29.1,,,4612,"(33.6299017499, -86.7194311229)",HLTHOUT,ARTHRITIS,0107000,01073005910,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073010500,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,41.3,39.4,43.1,,,114,"(33.4363786806, -86.9128923072)",HLTHOUT,ARTHRITIS,0107000,01073010500,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073010701,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,15.1,14.0,16.3,,,74,"(33.473886155, -86.8146487762)",HLTHOUT,ARTHRITIS,0107000,01073010701,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073010706,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,15.3,14.6,16.0,,,1528,"(33.4443709442, -86.8405352645)",HLTHOUT,ARTHRITIS,0107000,01073010706,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073010801,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,24.9,23.9,26.2,,,168,"(33.514097853, -86.7466971362)",HLTHOUT,ARTHRITIS,0107000,01073010801,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073010802,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,33.2,31.7,35.0,,,172,"(33.4885493477, -86.780843024)",HLTHOUT,ARTHRITIS,0107000,01073010802,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073010803,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,23.8,22.6,25.0,,,514,"(33.5229093892, -86.7102618642)",HLTHOUT,ARTHRITIS,0107000,01073010803,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073010805,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,32.0,30.1,34.0,,,86,"(33.4952792472, -86.6987184974)",HLTHOUT,ARTHRITIS,0107000,01073010805,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073011104,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,29.8,28.5,31.1,,,1688,"(33.6159436433, -86.6557892507)",HLTHOUT,ARTHRITIS,0107000,01073011104,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073011107,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,,,,*,Estimates suppressed for population less than 50,42,"(33.5804845249, -86.6301110961)",HLTHOUT,ARTHRITIS,0107000,01073011107,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073011108,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,,,,*,Estimates suppressed for population less than 50,9,"(33.6050742596, -86.6316729386)",HLTHOUT,ARTHRITIS,0107000,01073011108,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073011207,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,25.3,24.3,26.2,,,815,"(33.6718848706, -86.6772510465)",HLTHOUT,ARTHRITIS,0107000,01073011207,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073011209,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,27.2,26.1,28.3,,,1062,"(33.6557189992, -86.7050698349)",HLTHOUT,ARTHRITIS,0107000,01073011209,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073011210,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,23.8,22.3,25.3,,,1385,"(33.6641893755, -86.6956170686)",HLTHOUT,ARTHRITIS,0107000,01073011210,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073011803,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,28.2,27.0,29.4,,,928,"(33.6252575173, -86.6998610409)",HLTHOUT,ARTHRITIS,0107000,01073011803,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073011804,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,25.8,24.6,27.0,,,1157,"(33.6474916175, -86.7042974424)",HLTHOUT,ARTHRITIS,0107000,01073011804,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073011901,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,,,,*,Estimates suppressed for population less than 50,6,"(33.6355414646, -86.736946691)",HLTHOUT,ARTHRITIS,0107000,01073011901,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073011904,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,34.8,33.3,36.3,,,1915,"(33.593140185, -86.7357930541)",HLTHOUT,ARTHRITIS,0107000,01073011904,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073012001,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,44.8,42.9,46.6,,,304,"(33.5919486112, -86.864384068)",HLTHOUT,ARTHRITIS,0107000,01073012001,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073012002,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,,,,*,Estimates suppressed for population less than 50,44,"(33.5859234197, -86.8357007188)",HLTHOUT,ARTHRITIS,0107000,01073012002,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073012200,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,,,,*,Estimates suppressed for population less than 50,23,"(33.5967441904, -87.0879396857)",HLTHOUT,ARTHRITIS,0107000,01073012200,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073012302,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,26.3,25.2,27.5,,,144,"(33.5542816352, -87.0544691416)",HLTHOUT,ARTHRITIS,0107000,01073012302,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073012305,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,30.5,28.6,32.2,,,403,"(33.4695358064, -86.96831739)",HLTHOUT,ARTHRITIS,0107000,01073012305,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073012401,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,27.4,25.6,29.4,,,1066,"(33.5571951048, -86.8777935049)",HLTHOUT,ARTHRITIS,0107000,01073012401,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073012402,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,31.9,30.8,33.0,,,418,"(33.549984172, -86.8994543327)",HLTHOUT,ARTHRITIS,0107000,01073012402,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073012500,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,37.9,36.5,39.4,,,410,"(33.529160486, -86.9346476445)",HLTHOUT,ARTHRITIS,0107000,01073012500,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073012602,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,35.4,34.5,36.3,,,371,"(33.570164674, -86.666430582)",HLTHOUT,ARTHRITIS,0107000,01073012602,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073012701,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,,,,*,Estimates suppressed for population less than 50,44,"(33.5484078071, -86.6323773455)",HLTHOUT,ARTHRITIS,0107000,01073012701,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073012703,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,14.8,13.8,15.9,,,498,"(33.4681180943, -86.6671888213)",HLTHOUT,ARTHRITIS,0107000,01073012703,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073012704,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,27.0,25.2,29.0,,,113,"(33.5034195908, -86.6180983403)",HLTHOUT,ARTHRITIS,0107000,01073012704,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073012803,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,11.9,11.1,12.7,,,1261,"(33.4439425865, -86.7212936938)",HLTHOUT,ARTHRITIS,0107000,01073012803,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073012910,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,,,,*,Estimates suppressed for population less than 50,9,"(33.4345805042, -86.7263292059)",HLTHOUT,ARTHRITIS,0107000,01073012910,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073013002,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,41.0,39.1,42.9,,,1514,"(33.46604181, -86.8567287797)",HLTHOUT,ARTHRITIS,0107000,01073013002,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073013100,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,38.7,37.5,39.9,,,4424,"(33.44880214, -86.8878401579)",HLTHOUT,ARTHRITIS,0107000,01073013100,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073013300,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,39.2,37.6,40.7,,,1782,"(33.4396443569, -86.9248768665)",HLTHOUT,ARTHRITIS,0107000,01073013300,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073013901,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,35.0,33.4,36.6,,,952,"(33.4729384906, -86.9547337648)",HLTHOUT,ARTHRITIS,0107000,01073013901,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073014302,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,19.0,18.1,19.8,,,2778,"(33.4244658829, -86.8841474217)",HLTHOUT,ARTHRITIS,0107000,01073014302,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01073014413,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,17.7,16.9,18.6,,,397,"(33.4226593117, -86.8508620751)",HLTHOUT,ARTHRITIS,0107000,01073014413,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01117030213,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,20.3,19.1,21.5,,,644,"(33.4395975193, -86.6735959359)",HLTHOUT,ARTHRITIS,0107000,01117030213,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01117030217,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,,,,*,Estimates suppressed for population less than 50,16,"(33.4556995763, -86.6520208639)",HLTHOUT,ARTHRITIS,0107000,01117030217,Arthritis +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Health Outcomes,0107000-01117030303,Arthritis among adults aged >=18 Years,%,CrdPrv,Crude prevalence,12.8,12.1,13.6,,,968,"(33.4258661239, -86.713819356)",HLTHOUT,ARTHRITIS,0107000,01117030303,Arthritis +2015,AL,Alabama,Birmingham,City,BRFSS,Unhealthy Behaviors,0107000,Binge drinking among adults aged >=18 Years,%,AgeAdjPrv,Age-adjusted prevalence,11.2,11.1,11.3,,,212237,"(33.5275663773, -86.7988174678)",UNHBEH,BINGE,0107000,,Binge Drinking +2015,AL,Alabama,Birmingham,City,BRFSS,Unhealthy Behaviors,0107000,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,11.3,11.3,11.4,,,212237,"(33.5275663773, -86.7988174678)",UNHBEH,BINGE,0107000,,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073000100,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,10.1,9.7,10.5,,,3042,"(33.5794328326, -86.7228323926)",UNHBEH,BINGE,0107000,01073000100,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073000300,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,10.8,10.3,11.2,,,2735,"(33.5428208686, -86.752433978)",UNHBEH,BINGE,0107000,01073000300,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073000400,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,9.5,9.0,10.0,,,3338,"(33.5632449633, -86.7640474064)",UNHBEH,BINGE,0107000,01073000400,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073000500,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,8.6,8.1,9.0,,,2864,"(33.5442404594, -86.7749130719)",UNHBEH,BINGE,0107000,01073000500,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073000700,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,7.4,6.9,7.9,,,2577,"(33.5525406139, -86.8016893706)",UNHBEH,BINGE,0107000,01073000700,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073000800,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,8.9,8.5,9.3,,,3859,"(33.549697789, -86.8330944744)",UNHBEH,BINGE,0107000,01073000800,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073001100,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,9.6,9.2,10.1,,,5354,"(33.5429143325, -86.8756782852)",UNHBEH,BINGE,0107000,01073001100,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073001200,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,9.4,9.1,9.7,,,2876,"(33.5278767706, -86.8604161686)",UNHBEH,BINGE,0107000,01073001200,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073001400,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,8.9,8.5,9.3,,,2181,"(33.5261497258, -86.835146606)",UNHBEH,BINGE,0107000,01073001400,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073001500,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,9.4,9.1,9.8,,,3189,"(33.5298727342, -86.8197191685)",UNHBEH,BINGE,0107000,01073001500,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073001600,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,8.2,7.7,8.6,,,3390,"(33.5372993423, -86.8036590482)",UNHBEH,BINGE,0107000,01073001600,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073001902,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,9.3,8.9,9.8,,,1894,"(33.5532050997, -86.7429801603)",UNHBEH,BINGE,0107000,01073001902,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073002000,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,9.1,8.7,9.6,,,3885,"(33.5541574106, -86.7167229915)",UNHBEH,BINGE,0107000,01073002000,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073002100,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,10.0,9.6,10.6,,,3186,"(33.5650015942, -86.7101024766)",UNHBEH,BINGE,0107000,01073002100,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073002200,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,10.1,9.7,10.6,,,2630,"(33.5521301205, -86.7276759508)",UNHBEH,BINGE,0107000,01073002200,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073002303,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,8.9,8.5,9.4,,,2936,"(33.5383153207, -86.7270445428)",UNHBEH,BINGE,0107000,01073002303,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073002305,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,16.3,15.8,16.7,,,2952,"(33.5333415976, -86.7479566084)",UNHBEH,BINGE,0107000,01073002305,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073002306,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,15.6,15.2,16.1,,,3257,"(33.5213873564, -86.7490031289)",UNHBEH,BINGE,0107000,01073002306,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073002400,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,11.1,10.8,11.5,,,3629,"(33.5260748309, -86.7830315488)",UNHBEH,BINGE,0107000,01073002400,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073002700,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,13.6,13.0,14.2,,,3992,"(33.5176008419, -86.8106887452)",UNHBEH,BINGE,0107000,01073002700,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073002900,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,8.3,7.8,8.8,,,2064,"(33.5132498864, -86.83004749)",UNHBEH,BINGE,0107000,01073002900,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073003001,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,14.3,13.3,15.3,,,3779,"(33.5125158094, -86.8577164946)",UNHBEH,BINGE,0107000,01073003001,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073003002,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,7.4,7.0,7.8,,,2203,"(33.512258109, -86.8441439907)",UNHBEH,BINGE,0107000,01073003002,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073003100,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,9.5,9.1,10.0,,,3637,"(33.5059655756, -86.8745506086)",UNHBEH,BINGE,0107000,01073003100,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073003200,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,7.6,7.3,8.0,,,931,"(33.5094018502, -86.8859081961)",UNHBEH,BINGE,0107000,01073003200,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073003300,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,8.4,8.0,8.9,,,947,"(33.5171261108, -86.8913819749)",UNHBEH,BINGE,0107000,01073003300,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073002305,Obesity among adults aged >=18 Years,%,CrdPrv,Crude prevalence,31.7,30.4,33.0,,,2952,"(33.5333415976, -86.7479566084)",UNHBEH,OBESITY,0107000,01073002305,Obesity +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073003400,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,8.8,8.3,9.3,,,2477,"(33.5052229234, -86.9014844656)",UNHBEH,BINGE,0107000,01073003400,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073003500,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,10.0,9.5,10.5,,,2780,"(33.5065714011, -86.9195910063)",UNHBEH,BINGE,0107000,01073003500,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073003600,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,9.9,9.4,10.3,,,4683,"(33.48476397, -86.8981392947)",UNHBEH,BINGE,0107000,01073003600,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073003700,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,10.3,9.9,10.7,,,5063,"(33.4969018589, -86.8907729426)",UNHBEH,BINGE,0107000,01073003700,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073003802,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,10.3,9.9,10.7,,,5409,"(33.4785707794, -86.890000907)",UNHBEH,BINGE,0107000,01073003802,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073003803,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,9.2,8.7,9.7,,,4199,"(33.485945214, -86.869692186)",UNHBEH,BINGE,0107000,01073003803,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073003900,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,9.5,9.1,9.8,,,1783,"(33.4989959327, -86.8647600038)",UNHBEH,BINGE,0107000,01073003900,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073004000,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,8.5,8.0,9.0,,,3772,"(33.4953246015, -86.8516232073)",UNHBEH,BINGE,0107000,01073004000,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073004200,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,9.1,8.8,9.4,,,2341,"(33.5007439361, -86.8270720379)",UNHBEH,BINGE,0107000,01073004200,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073004500,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,13.8,12.9,14.8,,,5003,"(33.5041857556, -86.8033798346)",UNHBEH,BINGE,0107000,01073004500,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073004701,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,16.4,15.7,17.2,,,3480,"(33.5075242148, -86.7836675838)",UNHBEH,BINGE,0107000,01073004701,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073004702,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,15.3,14.9,15.7,,,2944,"(33.5119902661, -86.7694550989)",UNHBEH,BINGE,0107000,01073004702,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073004800,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,14.1,13.7,14.5,,,1861,"(33.4989064008, -86.78269914)",UNHBEH,BINGE,0107000,01073004800,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073004901,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,17.0,16.4,17.7,,,1167,"(33.4971595645, -86.7917440668)",UNHBEH,BINGE,0107000,01073004901,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073004902,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,16.3,15.6,17.1,,,3146,"(33.4935824043, -86.8009294603)",UNHBEH,BINGE,0107000,01073004902,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073005000,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,16.1,15.6,16.7,,,3482,"(33.4866689795, -86.8173262831)",UNHBEH,BINGE,0107000,01073005000,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073005101,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,7.9,7.5,8.3,,,1507,"(33.4945909008, -86.834763936)",UNHBEH,BINGE,0107000,01073005101,Binge Drinking +2015,AL,Alabama,Birmingham,Census Tract,BRFSS,Unhealthy Behaviors,0107000-01073005103,Binge drinking among adults aged >=18 Years,%,CrdPrv,Crude prevalence,7.7,7.4,8.0,,,2587,"(33.485714885, -86.8327817467)",UNHBEH,BINGE,0107000,01073005103,Binge Drinking diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/sample_json.json b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/sample_json.json new file mode 100644 index 00000000..0a1eab9a --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/sample_json.json @@ -0,0 +1,5154 @@ +{ + "meta": { + "limit": 100, + "offset": 0, + "total_count": 100 + }, + "objects": [ + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for Wisconsin", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "709 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.baldwin.senate.gov/feedback", + "fax": "202-225-6942", + "office": "709 Hart Senate Office Building", + "rss_url": "http://www.baldwin.senate.gov/rss/feeds/?type=all" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "B001230", + "birthday": "1962-02-11", + "cspanid": 57884, + "firstname": "Tammy", + "gender": "female", + "gender_label": "Female", + "lastname": "Baldwin", + "link": "https://www.govtrack.us/congress/members/tammy_baldwin/400013", + "middlename": "", + "name": "Sen. Tammy Baldwin [D-WI]", + "namemod": "", + "nickname": "", + "osid": "N00004367", + "pvsid": "3470", + "sortname": "Baldwin, Tammy (Sen.) [D-WI]", + "twitterid": "SenatorBaldwin", + "youtubeid": "witammybaldwin" + }, + "phone": "202-224-5653", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "WI", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.baldwin.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Ohio", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "713 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.brown.senate.gov/contact/", + "fax": "202-228-6321", + "office": "713 Hart Senate Office Building", + "rss_url": "http://www.brown.senate.gov/rss/feeds/?type=all&" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "B000944", + "birthday": "1952-11-09", + "cspanid": 5051, + "firstname": "Sherrod", + "gender": "male", + "gender_label": "Male", + "lastname": "Brown", + "link": "https://www.govtrack.us/congress/members/sherrod_brown/400050", + "middlename": "", + "name": "Sen. Sherrod Brown [D-OH]", + "namemod": "", + "nickname": "", + "osid": "N00003535", + "pvsid": "27018", + "sortname": "Brown, Sherrod (Sen.) [D-OH]", + "twitterid": "SenSherrodBrown", + "youtubeid": "SherrodBrownOhio" + }, + "phone": "202-224-2315", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "OH", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.brown.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Maryland", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "509 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.cardin.senate.gov/contact/", + "fax": "202-224-1651", + "office": "509 Hart Senate Office Building", + "rss_url": "http://www.cardin.senate.gov/rss/feeds/?type=all" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "C000141", + "birthday": "1943-10-05", + "cspanid": 4004, + "firstname": "Benjamin", + "gender": "male", + "gender_label": "Male", + "lastname": "Cardin", + "link": "https://www.govtrack.us/congress/members/benjamin_cardin/400064", + "middlename": "L.", + "name": "Sen. Benjamin Cardin [D-MD]", + "namemod": "", + "nickname": "", + "osid": "N00001955", + "pvsid": "26888", + "sortname": "Cardin, Benjamin (Sen.) [D-MD]", + "twitterid": "SenatorCardin", + "youtubeid": "senatorcardin" + }, + "phone": "202-224-4524", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "MD", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.cardin.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Arizona", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "413 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.flake.senate.gov/public/index.cfm/contact-jeff", + "fax": "202-226-4386", + "office": "413 Russell Senate Office Building", + "rss_url": "http://www.flake.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "F000444", + "birthday": "1962-12-31", + "cspanid": 87582, + "firstname": "Jeff", + "gender": "male", + "gender_label": "Male", + "lastname": "Flake", + "link": "https://www.govtrack.us/congress/members/jeff_flake/400134", + "middlename": "", + "name": "Sen. Jeff Flake [R-AZ]", + "namemod": "", + "nickname": "", + "osid": "N00009573", + "pvsid": "28128", + "sortname": "Flake, Jeff (Sen.) [R-AZ]", + "twitterid": "JeffFlake", + "youtubeid": "flakeoffice" + }, + "phone": "202-224-4521", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "AZ", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.flake.senate.gov/public" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for New Jersey", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "528 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.menendez.senate.gov/contact", + "fax": "202-228-2197", + "office": "528 Hart Senate Office Building", + "rss_url": "http://www.menendez.senate.gov/rss/feeds/index.cfm?type=news" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "M000639", + "birthday": "1954-01-01", + "cspanid": 29608, + "firstname": "Robert", + "gender": "male", + "gender_label": "Male", + "lastname": "Menéndez", + "link": "https://www.govtrack.us/congress/members/robert_menendez/400272", + "middlename": "", + "name": "Sen. Robert “Bob” Menéndez [D-NJ]", + "namemod": "", + "nickname": "Bob", + "osid": "N00000699", + "pvsid": "26961", + "sortname": "Menéndez, Robert “Bob” (Sen.) [D-NJ]", + "twitterid": "SenatorMenendez", + "youtubeid": "SenatorMenendezNJ" + }, + "phone": "202-224-4744", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "NJ", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.menendez.senate.gov" + }, + { + "caucus": "Democrat", + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for Vermont", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "332 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "http://www.sanders.senate.gov/contact/", + "fax": "202-228-0776", + "office": "332 Dirksen Senate Office Building", + "rss_url": "http://www.sanders.senate.gov/rss/" + }, + "leadership_title": null, + "party": "Independent", + "person": { + "bioguideid": "S000033", + "birthday": "1941-09-08", + "cspanid": 994, + "firstname": "Bernard", + "gender": "male", + "gender_label": "Male", + "lastname": "Sanders", + "link": "https://www.govtrack.us/congress/members/bernard_sanders/400357", + "middlename": "", + "name": "Sen. Bernard “Bernie” Sanders [I-VT]", + "namemod": "", + "nickname": "Bernie", + "osid": "N00000528", + "pvsid": "27110", + "sortname": "Sanders, Bernard “Bernie” (Sen.) [I-VT]", + "twitterid": "SenSanders", + "youtubeid": "senatorsanders" + }, + "phone": "202-224-5141", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "VT", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.sanders.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for Washington", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "511 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.cantwell.senate.gov/public/index.cfm/email-maria", + "fax": "202-228-0514", + "office": "511 Hart Senate Office Building", + "rss_url": "http://www.cantwell.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "C000127", + "birthday": "1958-10-13", + "cspanid": 26137, + "firstname": "Maria", + "gender": "female", + "gender_label": "Female", + "lastname": "Cantwell", + "link": "https://www.govtrack.us/congress/members/maria_cantwell/300018", + "middlename": "", + "name": "Sen. Maria Cantwell [D-WA]", + "namemod": "", + "nickname": "", + "osid": "N00007836", + "pvsid": "27122", + "sortname": "Cantwell, Maria (Sen.) [D-WA]", + "twitterid": "SenatorCantwell", + "youtubeid": "SenatorCantwell" + }, + "phone": "202-224-3441", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "WA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.cantwell.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Delaware", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "513 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.carper.senate.gov/public/index.cfm/email-senator-carper", + "fax": "202-228-2190", + "office": "513 Hart Senate Office Building", + "rss_url": "http://www.carper.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "C000174", + "birthday": "1947-01-23", + "cspanid": 663, + "firstname": "Thomas", + "gender": "male", + "gender_label": "Male", + "lastname": "Carper", + "link": "https://www.govtrack.us/congress/members/thomas_carper/300019", + "middlename": "Richard", + "name": "Sen. Thomas Carper [D-DE]", + "namemod": "", + "nickname": "", + "osid": "N00012508", + "pvsid": "22421", + "sortname": "Carper, Thomas (Sen.) [D-DE]", + "twitterid": "SenatorCarper", + "youtubeid": "senatorcarper" + }, + "phone": "202-224-2441", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "DE", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.carper.senate.gov/public" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for California", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "331 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.feinstein.senate.gov/public/index.cfm/e-mail-me", + "fax": "202-228-3954", + "office": "331 Hart Senate Office Building", + "rss_url": "http://www.feinstein.senate.gov/public/?a=rss.feed" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "F000062", + "birthday": "1933-06-22", + "cspanid": 13061, + "firstname": "Dianne", + "gender": "female", + "gender_label": "Female", + "lastname": "Feinstein", + "link": "https://www.govtrack.us/congress/members/dianne_feinstein/300043", + "middlename": "", + "name": "Sen. Dianne Feinstein [D-CA]", + "namemod": "", + "nickname": "", + "osid": "N00007364", + "pvsid": "53273", + "sortname": "Feinstein, Dianne (Sen.) [D-CA]", + "twitterid": "SenFeinstein", + "youtubeid": "SenatorFeinstein" + }, + "phone": "202-224-3841", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "CA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.feinstein.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Utah", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "104 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.hatch.senate.gov/public/index.cfm/contact?p=Email-Orrin", + "fax": "202-224-6331", + "office": "104 Hart Senate Office Building", + "rss_url": "http://www.hatch.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "H000338", + "birthday": "1934-03-22", + "cspanid": 189, + "firstname": "Orrin", + "gender": "male", + "gender_label": "Male", + "lastname": "Hatch", + "link": "https://www.govtrack.us/congress/members/orrin_hatch/300052", + "middlename": "G.", + "name": "Sen. Orrin Hatch [R-UT]", + "namemod": "", + "nickname": "", + "osid": "N00009869", + "pvsid": "53352", + "sortname": "Hatch, Orrin (Sen.) [R-UT]", + "twitterid": "SenOrrinHatch", + "youtubeid": "SenatorOrrinHatch" + }, + "phone": "202-224-5251", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "UT", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.hatch.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Florida", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "716 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.billnelson.senate.gov/contact-bill", + "fax": "202-228-2183", + "office": "716 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "N000032", + "birthday": "1942-09-29", + "cspanid": 1931, + "firstname": "Bill", + "gender": "male", + "gender_label": "Male", + "lastname": "Nelson", + "link": "https://www.govtrack.us/congress/members/bill_nelson/300078", + "middlename": "", + "name": "Sen. Bill Nelson [D-FL]", + "namemod": "", + "nickname": "", + "osid": "N00009926", + "pvsid": "1606", + "sortname": "Nelson, Bill (Sen.) [D-FL]", + "twitterid": "SenBillNelson", + "youtubeid": "senbillnelson" + }, + "phone": "202-224-5274", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "FL", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.billnelson.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Michigan", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "731 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.stabenow.senate.gov/contact", + "fax": "202-228-0325", + "office": "731 Hart Senate Office Building", + "rss_url": "http://stabenow.senate.gov/rss/?p=news" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "S000770", + "birthday": "1950-04-29", + "cspanid": 45451, + "firstname": "Debbie", + "gender": "female", + "gender_label": "Female", + "lastname": "Stabenow", + "link": "https://www.govtrack.us/congress/members/debbie_stabenow/300093", + "middlename": "Ann", + "name": "Sen. Debbie Stabenow [D-MI]", + "namemod": "", + "nickname": "", + "osid": "N00004118", + "pvsid": "515", + "sortname": "Stabenow, Debbie (Sen.) [D-MI]", + "twitterid": "SenStabenow", + "youtubeid": "senatorstabenow" + }, + "phone": "202-224-4822", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "MI", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.stabenow.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for Connecticut", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "136 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.murphy.senate.gov/contact", + "fax": "202-225-5933", + "office": "136 Hart Senate Office Building", + "rss_url": "http://www.theday.com/article/20121216/nws12/312169935/1069/rss" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "M001169", + "birthday": "1973-08-03", + "cspanid": 1021270, + "firstname": "Christopher", + "gender": "male", + "gender_label": "Male", + "lastname": "Murphy", + "link": "https://www.govtrack.us/congress/members/christopher_murphy/412194", + "middlename": "S.", + "name": "Sen. Christopher Murphy [D-CT]", + "namemod": "", + "nickname": "", + "osid": "N00027566", + "pvsid": "17189", + "sortname": "Murphy, Christopher (Sen.) [D-CT]", + "twitterid": "senmurphyoffice", + "youtubeid": "senchrismurphy" + }, + "phone": "202-224-4041", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "CT", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.murphy.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for Hawaii", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "730 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.hirono.senate.gov/contact", + "fax": "202-225-4987", + "office": "730 Hart Senate Office Building", + "rss_url": "http://www.hirono.senate.gov/rss/feeds/?type=all" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "H001042", + "birthday": "1947-11-03", + "cspanid": 91216, + "firstname": "Mazie", + "gender": "female", + "gender_label": "Female", + "lastname": "Hirono", + "link": "https://www.govtrack.us/congress/members/mazie_hirono/412200", + "middlename": "K.", + "name": "Sen. Mazie Hirono [D-HI]", + "namemod": "", + "nickname": "", + "osid": "N00028139", + "pvsid": "1677", + "sortname": "Hirono, Mazie (Sen.) [D-HI]", + "twitterid": "MazieHirono", + "youtubeid": "CongresswomanHirono" + }, + "phone": "202-224-6361", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "HI", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.hirono.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Indiana", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "720 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.donnelly.senate.gov/contact/email-joe", + "fax": "202-225-6798", + "office": "720 Hart Senate Office Building", + "rss_url": "http://www.donnelly.senate.gov/rss/feeds/?type=all" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "D000607", + "birthday": "1955-09-28", + "cspanid": 1012000, + "firstname": "Joe", + "gender": "male", + "gender_label": "Male", + "lastname": "Donnelly", + "link": "https://www.govtrack.us/congress/members/joe_donnelly/412205", + "middlename": "", + "name": "Sen. Joe Donnelly [D-IN]", + "namemod": "", + "nickname": "", + "osid": "N00026586", + "pvsid": "34212", + "sortname": "Donnelly, Joe (Sen.) [D-IN]", + "twitterid": "SenDonnelly", + "youtubeid": "sendonnelly" + }, + "phone": "202-224-4814", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "IN", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.donnelly.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Nevada", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "324 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.heller.senate.gov/public/index.cfm/contact-form", + "fax": "202-228-6753", + "office": "324 Hart Senate Office Building", + "rss_url": "http://www.heller.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "H001041", + "birthday": "1960-05-10", + "cspanid": 1012368, + "firstname": "Dean", + "gender": "male", + "gender_label": "Male", + "lastname": "Heller", + "link": "https://www.govtrack.us/congress/members/dean_heller/412218", + "middlename": "", + "name": "Sen. Dean Heller [R-NV]", + "namemod": "", + "nickname": "", + "osid": "N00027522", + "pvsid": "2291", + "sortname": "Heller, Dean (Sen.) [R-NV]", + "twitterid": "SenDeanHeller", + "youtubeid": "SenDeanHeller" + }, + "phone": "202-224-6244", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "NV", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.heller.senate.gov/public" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for New York", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "478 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.gillibrand.senate.gov/contact/email-me", + "fax": "202-228-0282", + "office": "478 Russell Senate Office Building", + "rss_url": "http://www.gillibrand.senate.gov/rss/" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "G000555", + "birthday": "1966-12-09", + "cspanid": 1022862, + "firstname": "Kirsten", + "gender": "female", + "gender_label": "Female", + "lastname": "Gillibrand", + "link": "https://www.govtrack.us/congress/members/kirsten_gillibrand/412223", + "middlename": "E.", + "name": "Sen. Kirsten Gillibrand [D-NY]", + "namemod": "", + "nickname": "", + "osid": "N00027658", + "pvsid": "65147", + "sortname": "Gillibrand, Kirsten (Sen.) [D-NY]", + "twitterid": "SenGillibrand", + "youtubeid": "KirstenEGillibrand" + }, + "phone": "202-224-4451", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "NY", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.gillibrand.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Minnesota", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "302 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.klobuchar.senate.gov/public/index.cfm/contact", + "fax": "202-228-2186", + "office": "302 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "K000367", + "birthday": "1960-05-25", + "cspanid": 83701, + "firstname": "Amy", + "gender": "female", + "gender_label": "Female", + "lastname": "Klobuchar", + "link": "https://www.govtrack.us/congress/members/amy_klobuchar/412242", + "middlename": "Jean", + "name": "Sen. Amy Klobuchar [D-MN]", + "namemod": "", + "nickname": "", + "osid": "N00027500", + "pvsid": "65092", + "sortname": "Klobuchar, Amy (Sen.) [D-MN]", + "twitterid": "SenAmyKlobuchar", + "youtubeid": "senatorklobuchar" + }, + "phone": "202-224-3244", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "MN", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.klobuchar.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Missouri", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "503 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.mccaskill.senate.gov/contact", + "fax": "202-228-6326", + "office": "503 Hart Senate Office Building", + "rss_url": "http://mccaskill.senate.gov/rss/?p=news" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "M001170", + "birthday": "1953-07-24", + "cspanid": 44501, + "firstname": "Claire", + "gender": "female", + "gender_label": "Female", + "lastname": "McCaskill", + "link": "https://www.govtrack.us/congress/members/claire_mccaskill/412243", + "middlename": "", + "name": "Sen. Claire McCaskill [D-MO]", + "namemod": "", + "nickname": "", + "osid": "N00027694", + "pvsid": "2109", + "sortname": "McCaskill, Claire (Sen.) [D-MO]", + "twitterid": "McCaskillOffice", + "youtubeid": "SenatorMcCaskill" + }, + "phone": "202-224-6154", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "MO", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.mccaskill.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Montana", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "311 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.tester.senate.gov/?p=email_senator", + "fax": "202-224-8594", + "office": "311 Hart Senate Office Building", + "rss_url": "http://www.tester.senate.gov/rss/?p=hot_topic" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "T000464", + "birthday": "1956-08-21", + "cspanid": 1020176, + "firstname": "Jon", + "gender": "male", + "gender_label": "Male", + "lastname": "Tester", + "link": "https://www.govtrack.us/congress/members/jon_tester/412244", + "middlename": "", + "name": "Sen. Jon Tester [D-MT]", + "namemod": "", + "nickname": "", + "osid": "N00027605", + "pvsid": "20928", + "sortname": "Tester, Jon (Sen.) [D-MT]", + "twitterid": "SenatorTester", + "youtubeid": "senatorjontester" + }, + "phone": "202-224-2644", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "MT", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.tester.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Pennsylvania", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "393 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.casey.senate.gov/contact/", + "fax": "202-228-0604", + "office": "393 Russell Senate Office Building", + "rss_url": "http://www.casey.senate.gov/rss/feeds/?all" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "C001070", + "birthday": "1960-04-13", + "cspanid": 47036, + "firstname": "Robert", + "gender": "male", + "gender_label": "Male", + "lastname": "Casey", + "link": "https://www.govtrack.us/congress/members/robert_casey/412246", + "middlename": "P.", + "name": "Sen. Robert “Bob” Casey [D-PA]", + "namemod": "Jr.", + "nickname": "Bob", + "osid": "N00027503", + "pvsid": "2541", + "sortname": "Casey, Robert “Bob” (Sen.) [D-PA]", + "twitterid": "SenBobCasey", + "youtubeid": "SenatorBobCasey" + }, + "phone": "202-224-6324", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "PA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.casey.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for Rhode Island", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "530 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.whitehouse.senate.gov/contact/email-sheldon", + "fax": "202-228-6362", + "office": "530 Hart Senate Office Building", + "rss_url": "http://www.whitehouse.senate.gov/rss/feeds/?type=all&cachebuster=1" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "W000802", + "birthday": "1955-10-20", + "cspanid": 92235, + "firstname": "Sheldon", + "gender": "male", + "gender_label": "Male", + "lastname": "Whitehouse", + "link": "https://www.govtrack.us/congress/members/sheldon_whitehouse/412247", + "middlename": "", + "name": "Sen. Sheldon Whitehouse [D-RI]", + "namemod": "", + "nickname": "", + "osid": "N00027533", + "pvsid": "2572", + "sortname": "Whitehouse, Sheldon (Sen.) [D-RI]", + "twitterid": "SenWhitehouse", + "youtubeid": "SenatorWhitehouse" + }, + "phone": "202-224-2921", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "RI", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.whitehouse.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for Tennessee", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "425 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "https://www.corker.senate.gov/public/index.cfm/emailme", + "fax": "202-228-0566", + "office": "425 Dirksen Senate Office Building", + "rss_url": "http://www.corker.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "C001071", + "birthday": "1952-08-24", + "cspanid": 1021114, + "firstname": "Bob", + "gender": "male", + "gender_label": "Male", + "lastname": "Corker", + "link": "https://www.govtrack.us/congress/members/bob_corker/412248", + "middlename": "", + "name": "Sen. Bob Corker [R-TN]", + "namemod": "", + "nickname": "", + "osid": "N00027441", + "pvsid": "65905", + "sortname": "Corker, Bob (Sen.) [R-TN]", + "twitterid": "SenBobCorker", + "youtubeid": "senatorcorker" + }, + "phone": "202-224-3344", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "TN", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.corker.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for Wyoming", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "307 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "https://www.barrasso.senate.gov/public/index.cfm/contact-form", + "fax": "202-224-1724", + "office": "307 Dirksen Senate Office Building", + "rss_url": "http://www.barrasso.senate.gov/public/index.cfm?FuseAction=Rss.Feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "B001261", + "birthday": "1952-07-21", + "cspanid": 1024777, + "firstname": "John", + "gender": "male", + "gender_label": "Male", + "lastname": "Barrasso", + "link": "https://www.govtrack.us/congress/members/john_barrasso/412251", + "middlename": "A.", + "name": "Sen. John Barrasso [R-WY]", + "namemod": "", + "nickname": "", + "osid": "N00006236", + "pvsid": "52662", + "sortname": "Barrasso, John (Sen.) [R-WY]", + "twitterid": "SenJohnBarrasso", + "youtubeid": "barrassowyo" + }, + "phone": "202-224-6441", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "WY", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.barrasso.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for New Mexico", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "303 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.heinrich.senate.gov/contact", + "fax": "202-225-4975", + "office": "303 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "H001046", + "birthday": "1971-10-17", + "cspanid": 1030686, + "firstname": "Martin", + "gender": "male", + "gender_label": "Male", + "lastname": "Heinrich", + "link": "https://www.govtrack.us/congress/members/martin_heinrich/412281", + "middlename": "", + "name": "Sen. Martin Heinrich [D-NM]", + "namemod": "", + "nickname": "", + "osid": "N00029835", + "pvsid": "74517", + "sortname": "Heinrich, Martin (Sen.) [D-NM]", + "twitterid": "MartinHeinrich", + "youtubeid": "SenMartinHeinrich" + }, + "phone": "202-224-5521", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "NM", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.heinrich.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for West Virginia", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "306 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.manchin.senate.gov/public/index.cfm/contact-form", + "fax": "202-228-0002", + "office": "306 Hart Senate Office Building", + "rss_url": "http://www.manchin.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "M001183", + "birthday": "1947-08-24", + "cspanid": 62864, + "firstname": "Joe", + "gender": "male", + "gender_label": "Male", + "lastname": "Manchin", + "link": "https://www.govtrack.us/congress/members/joe_manchin/412391", + "middlename": "", + "name": "Sen. Joe Manchin [D-WV]", + "namemod": "III", + "nickname": "", + "osid": "N00032838", + "pvsid": "7547", + "sortname": "Manchin, Joe (Sen.) [D-WV]", + "twitterid": "Sen_JoeManchin", + "youtubeid": "SenatorJoeManchin" + }, + "phone": "202-224-3954", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "WV", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.manchin.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Massachusetts", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "317 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.warren.senate.gov/?p=email_senator", + "fax": "202-228-2072", + "office": "317 Hart Senate Office Building", + "rss_url": "http://www.warren.senate.gov/rss/?p=hot_topic" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "W000817", + "birthday": "1949-06-22", + "cspanid": 1023023, + "firstname": "Elizabeth", + "gender": "female", + "gender_label": "Female", + "lastname": "Warren", + "link": "https://www.govtrack.us/congress/members/elizabeth_warren/412542", + "middlename": "", + "name": "Sen. Elizabeth Warren [D-MA]", + "namemod": "", + "nickname": "", + "osid": "N00033492", + "pvsid": "141272", + "sortname": "Warren, Elizabeth (Sen.) [D-MA]", + "twitterid": "SenWarren", + "youtubeid": "senelizabethwarren" + }, + "phone": "202-224-4543", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "MA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.warren.senate.gov" + }, + { + "caucus": "Democrat", + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for Maine", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "133 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.king.senate.gov/contact", + "fax": "202-224-1946", + "office": "133 Hart Senate Office Building", + "rss_url": "http://www.king.senate.gov/rss/feeds/?type=all" + }, + "leadership_title": null, + "party": "Independent", + "person": { + "bioguideid": "K000383", + "birthday": "1944-03-31", + "cspanid": 37413, + "firstname": "Angus", + "gender": "male", + "gender_label": "Male", + "lastname": "King", + "link": "https://www.govtrack.us/congress/members/angus_king/412545", + "middlename": "", + "name": "Sen. Angus King [I-ME]", + "namemod": "", + "nickname": "", + "osid": "N00034580", + "pvsid": "22381", + "sortname": "King, Angus (Sen.) [I-ME]", + "twitterid": "SenAngusKing", + "youtubeid": "SenatorAngusKing" + }, + "phone": "202-224-5344", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "ME", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.king.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for North Dakota", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "516 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.heitkamp.senate.gov/public/index.cfm/contact", + "fax": "202-224-7776", + "office": "516 Hart Senate Office Building", + "rss_url": "http://www.heitkamp.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "H001069", + "birthday": "1955-10-30", + "cspanid": 95414, + "firstname": "Heidi", + "gender": "female", + "gender_label": "Female", + "lastname": "Heitkamp", + "link": "https://www.govtrack.us/congress/members/heidi_heitkamp/412554", + "middlename": "", + "name": "Sen. Heidi Heitkamp [D-ND]", + "namemod": "", + "nickname": "", + "osid": "N00033782", + "pvsid": "41716", + "sortname": "Heitkamp, Heidi (Sen.) [D-ND]", + "twitterid": "SenatorHeitkamp", + "youtubeid": "senatorheidiheitkamp" + }, + "phone": "202-224-2043", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "ND", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.heitkamp.senate.gov/public" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Nebraska", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "454 Russell Senate Office Building Washington DC 20510", + "contact_form": "http://www.fischer.senate.gov/public/index.cfm/contact", + "fax": "202-228-1325", + "office": "454 Russell Senate Office Building", + "rss_url": "http://www.fischer.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "F000463", + "birthday": "1951-03-01", + "cspanid": 1034067, + "firstname": "Deb", + "gender": "female", + "gender_label": "Female", + "lastname": "Fischer", + "link": "https://www.govtrack.us/congress/members/deb_fischer/412556", + "middlename": "", + "name": "Sen. Deb Fischer [R-NE]", + "namemod": "", + "nickname": "", + "osid": "N00033443", + "pvsid": "41963", + "sortname": "Fischer, Deb (Sen.) [R-NE]", + "twitterid": "SenatorFischer", + "youtubeid": "senatordebfischer" + }, + "phone": "202-224-6551", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "NE", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.fischer.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for Texas", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "404 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.cruz.senate.gov/?p=form&id=16", + "fax": "202-228-3398", + "office": "404 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "C001098", + "birthday": "1970-12-22", + "cspanid": 1019953, + "firstname": "Ted", + "gender": "male", + "gender_label": "Male", + "lastname": "Cruz", + "link": "https://www.govtrack.us/congress/members/ted_cruz/412573", + "middlename": "", + "name": "Sen. Ted Cruz [R-TX]", + "namemod": "", + "nickname": "", + "osid": "N00033085", + "pvsid": "135705", + "sortname": "Cruz, Ted (Sen.) [R-TX]", + "twitterid": "SenTedCruz", + "youtubeid": "sentedcruz" + }, + "phone": "202-224-5922", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "TX", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.cruz.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for Virginia", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "231 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.kaine.senate.gov/contact", + "fax": "202-228-6363", + "office": "231 Russell Senate Office Building", + "rss_url": "http://www.kaine.senate.gov/rss/feeds/?type=all" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "K000384", + "birthday": "1958-02-26", + "cspanid": 49219, + "firstname": "Timothy", + "gender": "male", + "gender_label": "Male", + "lastname": "Kaine", + "link": "https://www.govtrack.us/congress/members/timothy_kaine/412582", + "middlename": "", + "name": "Sen. Timothy Kaine [D-VA]", + "namemod": "", + "nickname": "", + "osid": "N00033177", + "pvsid": "50772", + "sortname": "Kaine, Timothy (Sen.) [D-VA]", + "twitterid": null, + "youtubeid": "SenatorTimKaine" + }, + "phone": "202-224-4024", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "VA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.kaine.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Mississippi", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "555 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "https://www.wicker.senate.gov/public/index.cfm/contact", + "fax": "202-228-0378", + "office": "555 Dirksen Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "W000437", + "birthday": "1951-07-05", + "cspanid": 18203, + "firstname": "Roger", + "gender": "male", + "gender_label": "Male", + "lastname": "Wicker", + "link": "https://www.govtrack.us/congress/members/roger_wicker/400432", + "middlename": "F.", + "name": "Sen. Roger Wicker [R-MS]", + "namemod": "", + "nickname": "", + "osid": "N00003280", + "pvsid": "21926", + "sortname": "Wicker, Roger (Sen.) [R-MS]", + "twitterid": "SenatorWicker", + "youtubeid": "SenatorWicker" + }, + "phone": "202-224-6253", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "MS", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.wicker.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for Tennessee", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "455 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "http://www.alexander.senate.gov/public/index.cfm?p=Email", + "fax": "202-228-3398", + "office": "455 Dirksen Senate Office Building", + "rss_url": "http://www.alexander.senate.gov/public/?a=rss.feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "A000360", + "birthday": "1940-07-03", + "cspanid": 5, + "firstname": "Lamar", + "gender": "male", + "gender_label": "Male", + "lastname": "Alexander", + "link": "https://www.govtrack.us/congress/members/lamar_alexander/300002", + "middlename": "", + "name": "Sen. Lamar Alexander [R-TN]", + "namemod": "", + "nickname": "", + "osid": "N00009888", + "pvsid": "15691", + "sortname": "Alexander, Lamar (Sen.) [R-TN]", + "twitterid": "SenAlexander", + "youtubeid": "lamaralexander" + }, + "phone": "202-224-4944", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "TN", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.alexander.senate.gov/public" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for Maine", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "413 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "http://www.collins.senate.gov/contact", + "fax": "202-224-2693", + "office": "413 Dirksen Senate Office Building", + "rss_url": "http://www.collins.senate.gov/public/?a=rss.feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "C001035", + "birthday": "1952-12-07", + "cspanid": 45738, + "firstname": "Susan", + "gender": "female", + "gender_label": "Female", + "lastname": "Collins", + "link": "https://www.govtrack.us/congress/members/susan_collins/300025", + "middlename": "M.", + "name": "Sen. Susan Collins [R-ME]", + "namemod": "", + "nickname": "", + "osid": "N00000491", + "pvsid": "379", + "sortname": "Collins, Susan (Sen.) [R-ME]", + "twitterid": "SenatorCollins", + "youtubeid": "SenatorSusanCollins" + }, + "phone": "202-224-2523", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "ME", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.collins.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for Texas", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "517 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.cornyn.senate.gov/contact", + "fax": "202-228-2856", + "office": "517 Hart Senate Office Building", + "rss_url": "http://www.cornyn.senate.gov/public/?a=rss.feed" + }, + "leadership_title": "Majority Whip", + "party": "Republican", + "person": { + "bioguideid": "C001056", + "birthday": "1952-02-02", + "cspanid": 93131, + "firstname": "John", + "gender": "male", + "gender_label": "Male", + "lastname": "Cornyn", + "link": "https://www.govtrack.us/congress/members/john_cornyn/300027", + "middlename": "", + "name": "Sen. John Cornyn [R-TX]", + "namemod": "", + "nickname": "", + "osid": "N00024852", + "pvsid": "15375", + "sortname": "Cornyn, John (Sen.) [R-TX]", + "twitterid": "JohnCornyn", + "youtubeid": "senjohncornyn" + }, + "phone": "202-224-2934", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "TX", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.cornyn.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for Illinois", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "711 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.durbin.senate.gov/contact/", + "fax": "202-228-0400", + "office": "711 Hart Senate Office Building", + "rss_url": "http://durbin.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": "Minority Whip", + "party": "Democrat", + "person": { + "bioguideid": "D000563", + "birthday": "1944-11-21", + "cspanid": 6741, + "firstname": "Richard", + "gender": "male", + "gender_label": "Male", + "lastname": "Durbin", + "link": "https://www.govtrack.us/congress/members/richard_durbin/300038", + "middlename": "J.", + "name": "Sen. Richard Durbin [D-IL]", + "namemod": "", + "nickname": "", + "osid": "N00004981", + "pvsid": "26847", + "sortname": "Durbin, Richard (Sen.) [D-IL]", + "twitterid": "SenatorDurbin", + "youtubeid": "SenatorDurbin" + }, + "phone": "202-224-2152", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "IL", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.durbin.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for Wyoming", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "379A Russell Senate Office Building Washington DC 20510", + "contact_form": "http://www.enzi.senate.gov/public/index.cfm/contact?p=e-mail-senator-enzi", + "fax": "202-228-0359", + "office": "379a Russell Senate Office Building", + "rss_url": "http://www.enzi.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "E000285", + "birthday": "1944-02-01", + "cspanid": 45824, + "firstname": "Michael", + "gender": "male", + "gender_label": "Male", + "lastname": "Enzi", + "link": "https://www.govtrack.us/congress/members/michael_enzi/300041", + "middlename": "B.", + "name": "Sen. Michael Enzi [R-WY]", + "namemod": "", + "nickname": "", + "osid": "N00006249", + "pvsid": "558", + "sortname": "Enzi, Michael (Sen.) [R-WY]", + "twitterid": "SenatorEnzi", + "youtubeid": "senatorenzi" + }, + "phone": "202-224-3424", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "WY", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.enzi.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for South Carolina", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "290 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.lgraham.senate.gov/public/index.cfm/e-mail-senator-graham", + "fax": "202-224-3808", + "office": "290 Russell Senate Office Building", + "rss_url": "http://www.lgraham.senate.gov/public/index.cfm?FuseAction=Rss.Feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "G000359", + "birthday": "1955-07-09", + "cspanid": 36782, + "firstname": "Lindsey", + "gender": "male", + "gender_label": "Male", + "lastname": "Graham", + "link": "https://www.govtrack.us/congress/members/lindsey_graham/300047", + "middlename": "O.", + "name": "Sen. Lindsey Graham [R-SC]", + "namemod": "", + "nickname": "", + "osid": "N00009975", + "pvsid": "21992", + "sortname": "Graham, Lindsey (Sen.) [R-SC]", + "twitterid": "GrahamBlog", + "youtubeid": "USSenLindseyGraham" + }, + "phone": "202-224-5972", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "SC", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.lgraham.senate.gov/public" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for Oklahoma", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "205 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.inhofe.senate.gov/contact", + "fax": "202-228-0380", + "office": "205 Russell Senate Office Building", + "rss_url": "http://www.inhofe.senate.gov/rss/feeds/?type=all&cachebuster=eea6c4d7%2d939c%2d5c1e%2db6c7aa3b8b291208" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "I000024", + "birthday": "1934-11-17", + "cspanid": 5619, + "firstname": "James", + "gender": "male", + "gender_label": "Male", + "lastname": "Inhofe", + "link": "https://www.govtrack.us/congress/members/james_inhofe/300055", + "middlename": "M.", + "name": "Sen. James “Jim” Inhofe [R-OK]", + "namemod": "", + "nickname": "Jim", + "osid": "N00005582", + "pvsid": "27027", + "sortname": "Inhofe, James “Jim” (Sen.) [R-OK]", + "twitterid": "InhofePress", + "youtubeid": "jiminhofepressoffice" + }, + "phone": "202-224-4721", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "OK", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.inhofe.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for Kentucky", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "317 Russell Senate Office Building Washington DC 20510", + "contact_form": "http://www.mcconnell.senate.gov/public/index.cfm?p=contact", + "fax": "202-224-2499", + "office": "317 Russell Senate Office Building", + "rss_url": "http://www.mcconnell.senate.gov/public/?a=rss.feed" + }, + "leadership_title": "Majority Leader", + "party": "Republican", + "person": { + "bioguideid": "M000355", + "birthday": "1942-02-20", + "cspanid": 2351, + "firstname": "Mitch", + "gender": "male", + "gender_label": "Male", + "lastname": "McConnell", + "link": "https://www.govtrack.us/congress/members/mitch_mcconnell/300072", + "middlename": "", + "name": "Sen. Mitch McConnell [R-KY]", + "namemod": "", + "nickname": "", + "osid": "N00003389", + "pvsid": "53298", + "sortname": "McConnell, Mitch (Sen.) [R-KY]", + "twitterid": "McConnellPress", + "youtubeid": null + }, + "phone": "202-224-2541", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "KY", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.mcconnell.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for Rhode Island", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "728 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.reed.senate.gov/contact/", + "fax": "202-224-4680", + "office": "728 Hart Senate Office Building", + "rss_url": "https://www.reed.senate.gov//rss/feeds/?type=all" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "R000122", + "birthday": "1949-11-12", + "cspanid": 24239, + "firstname": "John", + "gender": "male", + "gender_label": "Male", + "lastname": "Reed", + "link": "https://www.govtrack.us/congress/members/john_reed/300081", + "middlename": "F.", + "name": "Sen. John “Jack” Reed [D-RI]", + "namemod": "", + "nickname": "Jack", + "osid": "N00000362", + "pvsid": "27060", + "sortname": "Reed, John “Jack” (Sen.) [D-RI]", + "twitterid": "SenJackReed", + "youtubeid": "SenatorReed" + }, + "phone": "202-224-4642", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "RI", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.reed.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for Kansas", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "109 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.roberts.senate.gov/public/?p=EmailPat", + "fax": "202-224-3514", + "office": "109 Hart Senate Office Building", + "rss_url": "http://www.roberts.senate.gov/public/?a=rss.feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "R000307", + "birthday": "1936-04-20", + "cspanid": 16354, + "firstname": "Pat", + "gender": "male", + "gender_label": "Male", + "lastname": "Roberts", + "link": "https://www.govtrack.us/congress/members/pat_roberts/300083", + "middlename": "", + "name": "Sen. Pat Roberts [R-KS]", + "namemod": "", + "nickname": "", + "osid": "N00005285", + "pvsid": "26866", + "sortname": "Roberts, Pat (Sen.) [R-KS]", + "twitterid": "SenPatRoberts", + "youtubeid": "SenPatRoberts" + }, + "phone": "202-224-4774", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "KS", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.roberts.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for West Virginia", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "172 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.capito.senate.gov/contact/contact-shelley", + "fax": "202-225-7856", + "office": "172 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "C001047", + "birthday": "1953-11-26", + "cspanid": 83737, + "firstname": "Shelley", + "gender": "female", + "gender_label": "Female", + "lastname": "Capito", + "link": "https://www.govtrack.us/congress/members/shelley_capito/400061", + "middlename": "Moore", + "name": "Sen. Shelley Capito [R-WV]", + "namemod": "", + "nickname": "", + "osid": "N00009771", + "pvsid": "11701", + "sortname": "Capito, Shelley (Sen.) [R-WV]", + "twitterid": "SenCapito", + "youtubeid": null + }, + "phone": "202-224-6472", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "WV", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.capito.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Massachusetts", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "255 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "https://www.markey.senate.gov/contact", + "office": "255 Dirksen Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "M000133", + "birthday": "1946-07-11", + "cspanid": 260, + "firstname": "Edward", + "gender": "male", + "gender_label": "Male", + "lastname": "Markey", + "link": "https://www.govtrack.us/congress/members/edward_markey/400253", + "middlename": "J.", + "name": "Sen. Edward “Ed” Markey [D-MA]", + "namemod": "", + "nickname": "Ed", + "osid": "N00000270", + "pvsid": "26900", + "sortname": "Markey, Edward “Ed” (Sen.) [D-MA]", + "twitterid": "SenMarkey", + "youtubeid": "RepMarkey" + }, + "phone": "202-224-2742", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "MA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.markey.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for New Mexico", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "531 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.tomudall.senate.gov/?p=contact", + "fax": "202-228-3261", + "office": "531 Hart Senate Office Building", + "rss_url": "http://tomudall.senate.gov/rss/?p=blog" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "U000039", + "birthday": "1948-05-18", + "cspanid": 10075, + "firstname": "Tom", + "gender": "male", + "gender_label": "Male", + "lastname": "Udall", + "link": "https://www.govtrack.us/congress/members/tom_udall/400413", + "middlename": "S.", + "name": "Sen. Tom Udall [D-NM]", + "namemod": "", + "nickname": "", + "osid": "N00006561", + "pvsid": "22658", + "sortname": "Udall, Tom (Sen.) [D-NM]", + "twitterid": "SenatorTomUdall", + "youtubeid": "senatortomudall" + }, + "phone": "202-224-6621", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "NM", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.tomudall.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for Louisiana", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "520 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.cassidy.senate.gov/contact", + "fax": "202-225-7313", + "office": "520 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "C001075", + "birthday": "1957-09-28", + "cspanid": 1030546, + "firstname": "Bill", + "gender": "male", + "gender_label": "Male", + "lastname": "Cassidy", + "link": "https://www.govtrack.us/congress/members/bill_cassidy/412269", + "middlename": "", + "name": "Sen. Bill Cassidy [R-LA]", + "namemod": "", + "nickname": "", + "osid": "N00030245", + "pvsid": "69494", + "sortname": "Cassidy, Bill (Sen.) [R-LA]", + "twitterid": null, + "youtubeid": "SenatorBillCassidy" + }, + "phone": "202-224-5824", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "LA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.cassidy.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Michigan", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "724 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.peters.senate.gov/contact/email-gary", + "fax": "202-226-2356", + "office": "724 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "P000595", + "birthday": "1958-12-01", + "cspanid": 50199, + "firstname": "Gary", + "gender": "male", + "gender_label": "Male", + "lastname": "Peters", + "link": "https://www.govtrack.us/congress/members/gary_peters/412305", + "middlename": "C.", + "name": "Sen. Gary Peters [D-MI]", + "namemod": "", + "nickname": "", + "osid": "N00029277", + "pvsid": "8749", + "sortname": "Peters, Gary (Sen.) [D-MI]", + "twitterid": "SenGaryPeters", + "youtubeid": "RepGaryPeters" + }, + "phone": "202-224-6221", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "MI", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.peters.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for Virginia", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "703 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.warner.senate.gov/public/index.cfm?p=Contact", + "fax": "202-224-6295", + "office": "703 Hart Senate Office Building", + "rss_url": "http://www.warner.senate.gov/public/?a=rss.feed" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "W000805", + "birthday": "1954-12-15", + "cspanid": 7630, + "firstname": "Mark", + "gender": "male", + "gender_label": "Male", + "lastname": "Warner", + "link": "https://www.govtrack.us/congress/members/mark_warner/412321", + "middlename": "", + "name": "Sen. Mark Warner [D-VA]", + "namemod": "", + "nickname": "", + "osid": "N00002097", + "pvsid": "535", + "sortname": "Warner, Mark (Sen.) [D-VA]", + "twitterid": "MarkWarner", + "youtubeid": "SenatorMarkWarner" + }, + "phone": "202-224-2023", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "VA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.warner.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Idaho", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "483 Russell Senate Office Building Washington DC 20510", + "contact_form": "http://www.risch.senate.gov/public/index.cfm?p=Email", + "fax": "202-224-2573", + "office": "483 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "R000584", + "birthday": "1943-05-03", + "cspanid": 1020034, + "firstname": "James", + "gender": "male", + "gender_label": "Male", + "lastname": "Risch", + "link": "https://www.govtrack.us/congress/members/james_risch/412322", + "middlename": "", + "name": "Sen. James Risch [R-ID]", + "namemod": "", + "nickname": "", + "osid": "N00029441", + "pvsid": "2919", + "sortname": "Risch, James (Sen.) [R-ID]", + "twitterid": "SenatorRisch", + "youtubeid": "SenatorJamesRisch" + }, + "phone": "202-224-2752", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "ID", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.risch.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for New Hampshire", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "506 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.shaheen.senate.gov/contact/contact-jeanne", + "fax": "202-228-3194", + "office": "506 Hart Senate Office Building", + "rss_url": "http://www.shaheen.senate.gov/rss/" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "S001181", + "birthday": "1947-01-28", + "cspanid": 22850, + "firstname": "Jeanne", + "gender": "female", + "gender_label": "Female", + "lastname": "Shaheen", + "link": "https://www.govtrack.us/congress/members/jeanne_shaheen/412323", + "middlename": "", + "name": "Sen. Jeanne Shaheen [D-NH]", + "namemod": "", + "nickname": "", + "osid": "N00024790", + "pvsid": "1663", + "sortname": "Shaheen, Jeanne (Sen.) [D-NH]", + "twitterid": "SenatorShaheen", + "youtubeid": "senatorshaheen" + }, + "phone": "202-224-2841", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "NH", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.shaheen.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Oregon", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "313 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.merkley.senate.gov/contact/", + "fax": "202-228-3997", + "office": "313 Hart Senate Office Building", + "rss_url": "http://www.merkley.senate.gov/rss/" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "M001176", + "birthday": "1956-10-24", + "cspanid": 1029842, + "firstname": "Jeff", + "gender": "male", + "gender_label": "Male", + "lastname": "Merkley", + "link": "https://www.govtrack.us/congress/members/jeff_merkley/412325", + "middlename": "", + "name": "Sen. Jeff Merkley [D-OR]", + "namemod": "", + "nickname": "", + "osid": "N00029303", + "pvsid": "23644", + "sortname": "Merkley, Jeff (Sen.) [D-OR]", + "twitterid": "SenJeffMerkley", + "youtubeid": "SenatorJeffMerkley" + }, + "phone": "202-224-3753", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "OR", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.merkley.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Delaware", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "127A Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.coons.senate.gov/contact", + "fax": "202-228-3075", + "office": "127a Russell Senate Office Building", + "rss_url": "http://www.coons.senate.gov/rss/feeds/?type=all" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "C001088", + "birthday": "1963-09-09", + "cspanid": 9269028, + "firstname": "Chris", + "gender": "male", + "gender_label": "Male", + "lastname": "Coons", + "link": "https://www.govtrack.us/congress/members/chris_coons/412390", + "middlename": "Andrew", + "name": "Sen. Chris Coons [D-DE]", + "namemod": "", + "nickname": "", + "osid": "N00031820", + "pvsid": "122834", + "sortname": "Coons, Chris (Sen.) [D-DE]", + "twitterid": "ChrisCoons", + "youtubeid": "senatorchriscoons" + }, + "phone": "202-224-5042", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "DE", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.coons.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Colorado", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "354 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.gardner.senate.gov/contact-cory/email-cory", + "fax": "202-225-5870", + "office": "354 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "G000562", + "birthday": "1974-08-22", + "cspanid": 623308, + "firstname": "Cory", + "gender": "male", + "gender_label": "Male", + "lastname": "Gardner", + "link": "https://www.govtrack.us/congress/members/cory_gardner/412406", + "middlename": "", + "name": "Sen. Cory Gardner [R-CO]", + "namemod": "", + "nickname": "", + "osid": "N00030780", + "pvsid": "30004", + "sortname": "Gardner, Cory (Sen.) [R-CO]", + "twitterid": "SenCoryGardner", + "youtubeid": null + }, + "phone": "202-224-5941", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "CO", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.gardner.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Arkansas", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "124 Russell Senate Office Building Washington DC 20510", + "contact_form": "http://www.cotton.senate.gov/?p=contact", + "office": "124 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "C001095", + "birthday": "1977-05-13", + "cspanid": 63928, + "firstname": "Tom", + "gender": "male", + "gender_label": "Male", + "lastname": "Cotton", + "link": "https://www.govtrack.us/congress/members/tom_cotton/412508", + "middlename": "", + "name": "Sen. Tom Cotton [R-AR]", + "namemod": "", + "nickname": "", + "osid": "N00033363", + "pvsid": "135651", + "sortname": "Cotton, Tom (Sen.) [R-AR]", + "twitterid": "SenTomCotton", + "youtubeid": "RepTomCotton" + }, + "phone": "202-224-2353", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "AR", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.cotton.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Montana", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "320 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.daines.senate.gov/connect/email-steve", + "fax": "202-228-1236", + "office": "320 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "D000618", + "birthday": "1962-08-20", + "cspanid": 1034037, + "firstname": "Steve", + "gender": "male", + "gender_label": "Male", + "lastname": "Daines", + "link": "https://www.govtrack.us/congress/members/steve_daines/412549", + "middlename": "", + "name": "Sen. Steve Daines [R-MT]", + "namemod": "", + "nickname": "", + "osid": "N00033054", + "pvsid": "135720", + "sortname": "Daines, Steve (Sen.) [R-MT]", + "twitterid": "SteveDaines", + "youtubeid": "SteveDainesMT" + }, + "phone": "202-224-2651", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "MT", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.daines.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for New Jersey", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "359 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "https://www.booker.senate.gov/?p=contact", + "fax": "202-224-8378", + "office": "359 Dirksen Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "B001288", + "birthday": "1969-04-27", + "cspanid": 84679, + "firstname": "Cory", + "gender": "male", + "gender_label": "Male", + "lastname": "Booker", + "link": "https://www.govtrack.us/congress/members/cory_booker/412598", + "middlename": "Anthony", + "name": "Sen. Cory Booker [D-NJ]", + "namemod": "", + "nickname": "", + "osid": "N00035267", + "pvsid": "76151", + "sortname": "Booker, Cory (Sen.) [D-NJ]", + "twitterid": "SenBooker", + "youtubeid": "SenCoryBooker" + }, + "phone": "202-224-3224", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "NJ", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.booker.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Alaska", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "702 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.sullivan.senate.gov/contact/email", + "office": "702 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "S001198", + "birthday": "1964-11-13", + "cspanid": 1023262, + "firstname": "Dan", + "gender": "male", + "gender_label": "Male", + "lastname": "Sullivan", + "link": "https://www.govtrack.us/congress/members/dan_sullivan/412665", + "middlename": "", + "name": "Sen. Dan Sullivan [R-AK]", + "namemod": "", + "nickname": "", + "osid": "N00035774", + "pvsid": "114964", + "sortname": "Sullivan, Dan (Sen.) [R-AK]", + "twitterid": "SenDanSullivan", + "youtubeid": null + }, + "phone": "202-224-3004", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "AK", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.sullivan.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Georgia", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "455 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.perdue.senate.gov/connect/email", + "office": "455 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "P000612", + "birthday": "1949-12-10", + "cspanid": 75920, + "firstname": "David", + "gender": "male", + "gender_label": "Male", + "lastname": "Perdue", + "link": "https://www.govtrack.us/congress/members/david_perdue/412666", + "middlename": "", + "name": "Sen. David Perdue [R-GA]", + "namemod": "", + "nickname": "", + "osid": "N00035516", + "pvsid": "151330", + "sortname": "Perdue, David (Sen.) [R-GA]", + "twitterid": "sendavidperdue", + "youtubeid": null + }, + "phone": "202-224-3521", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "GA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.perdue.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Iowa", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "111 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.ernst.senate.gov/public/index.cfm/contact", + "office": "111 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "E000295", + "birthday": "1970-07-01", + "cspanid": 75342, + "firstname": "Joni", + "gender": "female", + "gender_label": "Female", + "lastname": "Ernst", + "link": "https://www.govtrack.us/congress/members/joni_ernst/412667", + "middlename": "", + "name": "Sen. Joni Ernst [R-IA]", + "namemod": "", + "nickname": "", + "osid": "N00035483", + "pvsid": "128583", + "sortname": "Ernst, Joni (Sen.) [R-IA]", + "twitterid": "SenJoniErnst", + "youtubeid": null + }, + "phone": "202-224-3254", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "IA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.ernst.senate.gov/public" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for North Carolina", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "185 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "https://www.tillis.senate.gov/public/index.cfm/email-me", + "office": "185 Dirksen Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "T000476", + "birthday": "1960-08-30", + "cspanid": 77055, + "firstname": "Thom", + "gender": "male", + "gender_label": "Male", + "lastname": "Tillis", + "link": "https://www.govtrack.us/congress/members/thom_tillis/412668", + "middlename": "", + "name": "Sen. Thom Tillis [R-NC]", + "namemod": "", + "nickname": "", + "osid": "N00035492", + "pvsid": "57717", + "sortname": "Tillis, Thom (Sen.) [R-NC]", + "twitterid": "senthomtillis", + "youtubeid": null + }, + "phone": "202-224-6342", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "NC", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.tillis.senate.gov/public" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for South Dakota", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "502 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.rounds.senate.gov/contact/email-mike", + "office": "502 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "R000605", + "birthday": "1954-10-24", + "cspanid": 78317, + "firstname": "Mike", + "gender": "male", + "gender_label": "Male", + "lastname": "Rounds", + "link": "https://www.govtrack.us/congress/members/mike_rounds/412669", + "middlename": "", + "name": "Sen. Mike Rounds [R-SD]", + "namemod": "", + "nickname": "", + "osid": "N00035187", + "pvsid": "7455", + "sortname": "Rounds, Mike (Sen.) [R-SD]", + "twitterid": "SenatorRounds", + "youtubeid": null + }, + "phone": "202-224-5842", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "SD", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.rounds.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Nebraska", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "136 Russell Senate Office Building Washington DC 20510", + "contact_form": "http://www.sasse.senate.gov/public/index.cfm/email-ben", + "office": "136 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "S001197", + "birthday": "1972-02-22", + "cspanid": 77429, + "firstname": "Benjamin", + "gender": "male", + "gender_label": "Male", + "lastname": "Sasse", + "link": "https://www.govtrack.us/congress/members/benjamin_sasse/412671", + "middlename": "Eric", + "name": "Sen. Benjamin Sasse [R-NE]", + "namemod": "", + "nickname": "", + "osid": "N00035544", + "pvsid": "150182", + "sortname": "Sasse, Benjamin (Sen.) [R-NE]", + "twitterid": "SenSasse", + "youtubeid": null + }, + "phone": "202-224-4224", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "NE", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.sasse.senate.gov/public" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Idaho", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "239 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "https://www.crapo.senate.gov/contact", + "fax": "202-228-1375", + "office": "239 Dirksen Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "C000880", + "birthday": "1951-05-20", + "cspanid": 26440, + "firstname": "Michael", + "gender": "male", + "gender_label": "Male", + "lastname": "Crapo", + "link": "https://www.govtrack.us/congress/members/michael_crapo/300030", + "middlename": "D.", + "name": "Sen. Michael Crapo [R-ID]", + "namemod": "", + "nickname": "", + "osid": "N00006267", + "pvsid": "26830", + "sortname": "Crapo, Michael (Sen.) [R-ID]", + "twitterid": "MikeCrapo", + "youtubeid": "senatorcrapo" + }, + "phone": "202-224-6142", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "ID", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.crapo.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Iowa", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "135 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.grassley.senate.gov/contact", + "fax": "202-224-6020", + "office": "135 Hart Senate Office Building", + "rss_url": "http://grassley.senate.gov/customcf/rss_feed.cfm" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "G000386", + "birthday": "1933-09-17", + "cspanid": 1167, + "firstname": "Charles", + "gender": "male", + "gender_label": "Male", + "lastname": "Grassley", + "link": "https://www.govtrack.us/congress/members/charles_grassley/300048", + "middlename": "E.", + "name": "Sen. Charles “Chuck” Grassley [R-IA]", + "namemod": "", + "nickname": "Chuck", + "osid": "N00001758", + "pvsid": "53293", + "sortname": "Grassley, Charles “Chuck” (Sen.) [R-IA]", + "twitterid": "ChuckGrassley", + "youtubeid": "senchuckgrassley" + }, + "phone": "202-224-3744", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "IA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.grassley.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Vermont", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "437 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.leahy.senate.gov/contact/", + "fax": "202-224-3479", + "office": "437 Russell Senate Office Building", + "rss_url": "http://www.leahy.senate.gov/rss/feeds/press/" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "L000174", + "birthday": "1940-03-31", + "cspanid": 1552, + "firstname": "Patrick", + "gender": "male", + "gender_label": "Male", + "lastname": "Leahy", + "link": "https://www.govtrack.us/congress/members/patrick_leahy/300065", + "middlename": "J.", + "name": "Sen. Patrick Leahy [D-VT]", + "namemod": "", + "nickname": "", + "osid": "N00009918", + "pvsid": "53353", + "sortname": "Leahy, Patrick (Sen.) [D-VT]", + "twitterid": "SenatorLeahy", + "youtubeid": "SenatorPatrickLeahy" + }, + "phone": "202-224-4242", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "VT", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.leahy.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Alaska", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "522 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.murkowski.senate.gov/public/index.cfm/contact", + "fax": "202-224-5301", + "office": "522 Hart Senate Office Building", + "rss_url": "http://www.murkowski.senate.gov/public/?a=rss.feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "M001153", + "birthday": "1957-05-22", + "cspanid": 1004138, + "firstname": "Lisa", + "gender": "female", + "gender_label": "Female", + "lastname": "Murkowski", + "link": "https://www.govtrack.us/congress/members/lisa_murkowski/300075", + "middlename": "A.", + "name": "Sen. Lisa Murkowski [R-AK]", + "namemod": "", + "nickname": "", + "osid": "N00026050", + "pvsid": "15841", + "sortname": "Murkowski, Lisa (Sen.) [R-AK]", + "twitterid": "LisaMurkowski", + "youtubeid": "senatormurkowski" + }, + "phone": "202-224-6665", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "AK", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.murkowski.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Washington", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "154 Russell Senate Office Building Washington DC 20510", + "contact_form": "http://www.murray.senate.gov/public/index.cfm/contactme", + "fax": "202-224-0238", + "office": "154 Russell Senate Office Building", + "rss_url": "http://www.murray.senate.gov/public/?a=rss.feed" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "M001111", + "birthday": "1950-10-11", + "cspanid": 25277, + "firstname": "Patty", + "gender": "female", + "gender_label": "Female", + "lastname": "Murray", + "link": "https://www.govtrack.us/congress/members/patty_murray/300076", + "middlename": "", + "name": "Sen. Patty Murray [D-WA]", + "namemod": "", + "nickname": "", + "osid": "N00007876", + "pvsid": "53358", + "sortname": "Murray, Patty (Sen.) [D-WA]", + "twitterid": "PattyMurray", + "youtubeid": "SenatorPattyMurray" + }, + "phone": "202-224-2621", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "WA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.murray.senate.gov/public" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for New York", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "322 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.schumer.senate.gov/contact/email-chuck", + "fax": "202-228-3027", + "office": "322 Hart Senate Office Building" + }, + "leadership_title": "Minority Leader", + "party": "Democrat", + "person": { + "bioguideid": "S000148", + "birthday": "1950-11-23", + "cspanid": 5929, + "firstname": "Charles", + "gender": "male", + "gender_label": "Male", + "lastname": "Schumer", + "link": "https://www.govtrack.us/congress/members/charles_schumer/300087", + "middlename": "E.", + "name": "Sen. Charles “Chuck” Schumer [D-NY]", + "namemod": "", + "nickname": "Chuck", + "osid": "N00001093", + "pvsid": "26976", + "sortname": "Schumer, Charles “Chuck” (Sen.) [D-NY]", + "twitterid": "SenSchumer", + "youtubeid": "SenatorSchumer" + }, + "phone": "202-224-6542", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "NY", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.schumer.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Alabama", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "304 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.shelby.senate.gov/public/index.cfm/emailsenatorshelby", + "fax": "202-224-3416", + "office": "304 Russell Senate Office Building", + "rss_url": "http://www.shelby.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "S000320", + "birthday": "1934-05-06", + "cspanid": 1859, + "firstname": "Richard", + "gender": "male", + "gender_label": "Male", + "lastname": "Shelby", + "link": "https://www.govtrack.us/congress/members/richard_shelby/300089", + "middlename": "C.", + "name": "Sen. Richard Shelby [R-AL]", + "namemod": "", + "nickname": "", + "osid": "N00009920", + "pvsid": "53266", + "sortname": "Shelby, Richard (Sen.) [R-AL]", + "twitterid": "SenShelby", + "youtubeid": "SenatorRichardShelby" + }, + "phone": "202-224-5744", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "AL", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.shelby.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Oregon", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "221 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "https://www.wyden.senate.gov/contact/", + "fax": "202-228-2717", + "office": "221 Dirksen Senate Office Building", + "rss_url": "http://www.wyden.senate.gov/rss/feeds/?type=all&" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "W000779", + "birthday": "1949-05-03", + "cspanid": 1962, + "firstname": "Ron", + "gender": "male", + "gender_label": "Male", + "lastname": "Wyden", + "link": "https://www.govtrack.us/congress/members/ron_wyden/300100", + "middlename": "", + "name": "Sen. Ron Wyden [D-OR]", + "namemod": "", + "nickname": "", + "osid": "N00007724", + "pvsid": "27036", + "sortname": "Wyden, Ron (Sen.) [D-OR]", + "twitterid": "RonWyden", + "youtubeid": "senronwyden" + }, + "phone": "202-224-5244", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "OR", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.wyden.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Missouri", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "260 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.blunt.senate.gov/public/index.cfm/contact-roy", + "fax": "202-224-8149", + "office": "260 Russell Senate Office Building", + "rss_url": "http://www.blunt.senate.gov/public/?a=rss.feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "B000575", + "birthday": "1950-01-10", + "cspanid": 45465, + "firstname": "Roy", + "gender": "male", + "gender_label": "Male", + "lastname": "Blunt", + "link": "https://www.govtrack.us/congress/members/roy_blunt/400034", + "middlename": "", + "name": "Sen. Roy Blunt [R-MO]", + "namemod": "", + "nickname": "", + "osid": "N00005195", + "pvsid": "418", + "sortname": "Blunt, Roy (Sen.) [R-MO]", + "twitterid": "RoyBlunt", + "youtubeid": "SenatorBlunt" + }, + "phone": "202-224-5721", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "MO", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.blunt.senate.gov/public" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Arkansas", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "141 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.boozman.senate.gov/public/index.cfm/contact", + "fax": "202-228-1371", + "office": "141 Hart Senate Office Building", + "rss_url": "http://www.boozman.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "B001236", + "birthday": "1950-12-10", + "cspanid": 92069, + "firstname": "John", + "gender": "male", + "gender_label": "Male", + "lastname": "Boozman", + "link": "https://www.govtrack.us/congress/members/john_boozman/400040", + "middlename": "", + "name": "Sen. John Boozman [R-AR]", + "namemod": "", + "nickname": "", + "osid": "N00013873", + "pvsid": "27958", + "sortname": "Boozman, John (Sen.) [R-AR]", + "twitterid": "JohnBoozman", + "youtubeid": "BoozmanPressOffice" + }, + "phone": "202-224-4843", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "AR", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.boozman.senate.gov/public" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for North Carolina", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "217 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.burr.senate.gov/contact/email", + "fax": "202-228-2981", + "office": "217 Russell Senate Office Building", + "rss_url": "http://www.burr.senate.gov/public/index.cfm?fuseaction=rss.feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "B001135", + "birthday": "1955-11-30", + "cspanid": 31054, + "firstname": "Richard", + "gender": "male", + "gender_label": "Male", + "lastname": "Burr", + "link": "https://www.govtrack.us/congress/members/richard_burr/400054", + "middlename": "M.", + "name": "Sen. Richard Burr [R-NC]", + "namemod": "", + "nickname": "", + "osid": "N00002221", + "pvsid": "21787", + "sortname": "Burr, Richard (Sen.) [R-NC]", + "twitterid": "SenatorBurr", + "youtubeid": "SenatorRichardBurr" + }, + "phone": "202-224-3154", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "NC", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.burr.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Georgia", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "131 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.isakson.senate.gov/public/index.cfm/email-me", + "fax": "202-228-0724", + "office": "131 Russell Senate Office Building", + "rss_url": "http://www.isakson.senate.gov/public/?a=RSS.Feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "I000055", + "birthday": "1944-12-28", + "cspanid": 59135, + "firstname": "John", + "gender": "male", + "gender_label": "Male", + "lastname": "Isakson", + "link": "https://www.govtrack.us/congress/members/john_isakson/400194", + "middlename": "H.", + "name": "Sen. John “Johnny” Isakson [R-GA]", + "namemod": "", + "nickname": "Johnny", + "osid": "N00002593", + "pvsid": "1721", + "sortname": "Isakson, John “Johnny” (Sen.) [R-GA]", + "twitterid": "SenatorIsakson", + "youtubeid": "SenatorIsakson" + }, + "phone": "202-224-3643", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "GA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.isakson.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Kansas", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "521 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "https://www.moran.senate.gov/public/index.cfm/e-mail-jerry", + "fax": "202-228-6966", + "office": "521 Dirksen Senate Office Building", + "rss_url": "http://www.moran.senate.gov/public/index.cfm/rss/feed/" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "M000934", + "birthday": "1954-05-29", + "cspanid": 45469, + "firstname": "Jerry", + "gender": "male", + "gender_label": "Male", + "lastname": "Moran", + "link": "https://www.govtrack.us/congress/members/jerry_moran/400284", + "middlename": "", + "name": "Sen. Jerry Moran [R-KS]", + "namemod": "", + "nickname": "", + "osid": "N00005282", + "pvsid": "542", + "sortname": "Moran, Jerry (Sen.) [R-KS]", + "twitterid": "JerryMoran", + "youtubeid": "senatorjerrymoran" + }, + "phone": "202-224-6521", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "KS", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.moran.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Ohio", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "448 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.portman.senate.gov/public/index.cfm/contact?p=contact-form", + "office": "448 Russell Senate Office Building", + "rss_url": "http://www.portman.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "P000449", + "birthday": "1955-12-19", + "cspanid": 31819, + "firstname": "Robert", + "gender": "male", + "gender_label": "Male", + "lastname": "Portman", + "link": "https://www.govtrack.us/congress/members/robert_portman/400325", + "middlename": "J.", + "name": "Sen. Robert “Rob” Portman [R-OH]", + "namemod": "", + "nickname": "Rob", + "osid": "N00003682", + "pvsid": "27008", + "sortname": "Portman, Robert “Rob” (Sen.) [R-OH]", + "twitterid": "SenRobPortman", + "youtubeid": "SenRobPortman" + }, + "phone": "202-224-3353", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "OH", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.portman.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Pennsylvania", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "248 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.toomey.senate.gov/?p=contact", + "fax": "202-228-0284", + "office": "248 Russell Senate Office Building", + "rss_url": "http://toomey.senate.gov/rss/?p=hot_topic" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "T000461", + "birthday": "1961-11-17", + "cspanid": 7958, + "firstname": "Patrick", + "gender": "male", + "gender_label": "Male", + "lastname": "Toomey", + "link": "https://www.govtrack.us/congress/members/patrick_toomey/400408", + "middlename": "J.", + "name": "Sen. Patrick “Pat” Toomey [R-PA]", + "namemod": "", + "nickname": "Pat", + "osid": "N00001489", + "pvsid": "24096", + "sortname": "Toomey, Patrick “Pat” (Sen.) [R-PA]", + "twitterid": "SenToomey", + "youtubeid": "sentoomey" + }, + "phone": "202-224-4254", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "PA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.toomey.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Maryland", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "110 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.vanhollen.senate.gov/contact/email", + "fax": "202-225-0375", + "office": "110 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "V000128", + "birthday": "1959-01-10", + "cspanid": 20756, + "firstname": "Chris", + "gender": "male", + "gender_label": "Male", + "lastname": "Van Hollen", + "link": "https://www.govtrack.us/congress/members/chris_van_hollen/400415", + "middlename": "", + "name": "Sen. Chris Van Hollen [D-MD]", + "namemod": "Jr.", + "nickname": "", + "osid": "N00013820", + "pvsid": "6098", + "sortname": "Van Hollen, Chris (Sen.) [D-MD]", + "twitterid": "ChrisVanHollen", + "youtubeid": "RepChrisVanHollen" + }, + "phone": "202-224-4654", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "MD", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.vanhollen.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for South Dakota", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "511 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "http://www.thune.senate.gov/public/index.cfm/contact", + "fax": "202-228-5429", + "office": "511 Dirksen Senate Office Building", + "rss_url": "http://www.thune.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "T000250", + "birthday": "1961-01-07", + "cspanid": 45552, + "firstname": "John", + "gender": "male", + "gender_label": "Male", + "lastname": "Thune", + "link": "https://www.govtrack.us/congress/members/john_thune/400546", + "middlename": "", + "name": "Sen. John Thune [R-SD]", + "namemod": "", + "nickname": "", + "osid": "N00004572", + "pvsid": "398", + "sortname": "Thune, John (Sen.) [R-SD]", + "twitterid": "SenJohnThune", + "youtubeid": "johnthune" + }, + "phone": "202-224-2321", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "SD", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.thune.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Colorado", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "261 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.bennet.senate.gov/public/index.cfm/contact", + "fax": "202-228-5097", + "office": "261 Russell Senate Office Building", + "rss_url": "http://www.bennet.senate.gov/rss/feeds/?type=news" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "B001267", + "birthday": "1964-11-28", + "cspanid": 1031622, + "firstname": "Michael", + "gender": "male", + "gender_label": "Male", + "lastname": "Bennet", + "link": "https://www.govtrack.us/congress/members/michael_bennet/412330", + "middlename": "F.", + "name": "Sen. Michael Bennet [D-CO]", + "namemod": "", + "nickname": "", + "osid": "N00030608", + "pvsid": "110942", + "sortname": "Bennet, Michael (Sen.) [D-CO]", + "twitterid": "SenBennetCo", + "youtubeid": "SenatorBennet" + }, + "phone": "202-224-5852", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "CO", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.bennet.senate.gov/public" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Indiana", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "400 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.young.senate.gov/contact", + "fax": "202-226-6866", + "office": "400 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "Y000064", + "birthday": "1972-08-24", + "cspanid": 1033743, + "firstname": "Todd", + "gender": "male", + "gender_label": "Male", + "lastname": "Young", + "link": "https://www.govtrack.us/congress/members/todd_young/412428", + "middlename": "C.", + "name": "Sen. Todd Young [R-IN]", + "namemod": "", + "nickname": "", + "osid": "N00030670", + "pvsid": "120345", + "sortname": "Young, Todd (Sen.) [R-IN]", + "twitterid": "SenToddYoung", + "youtubeid": "RepToddYoung" + }, + "phone": "202-224-5623", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "IN", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.young.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Oklahoma", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "316 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.lankford.senate.gov/contact/email", + "office": "316 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "L000575", + "birthday": "1968-03-04", + "cspanid": 1033847, + "firstname": "James", + "gender": "male", + "gender_label": "Male", + "lastname": "Lankford", + "link": "https://www.govtrack.us/congress/members/james_lankford/412464", + "middlename": "", + "name": "Sen. James Lankford [R-OK]", + "namemod": "", + "nickname": "", + "osid": "N00031129", + "pvsid": "124938", + "sortname": "Lankford, James (Sen.) [R-OK]", + "twitterid": "SenatorLankford", + "youtubeid": "SenatorLankford" + }, + "phone": "202-224-5754", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "OK", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.lankford.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for South Carolina", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "717 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.scott.senate.gov/contact/email-me", + "fax": "202-225-3407", + "office": "717 Hart Senate Office Building", + "rss_url": "http://www.scott.senate.gov/rss.xml" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "S001184", + "birthday": "1965-09-19", + "cspanid": 623506, + "firstname": "Tim", + "gender": "male", + "gender_label": "Male", + "lastname": "Scott", + "link": "https://www.govtrack.us/congress/members/tim_scott/412471", + "middlename": "", + "name": "Sen. Tim Scott [R-SC]", + "namemod": "", + "nickname": "", + "osid": "N00031782", + "pvsid": "11940", + "sortname": "Scott, Tim (Sen.) [R-SC]", + "twitterid": "SenatorTimScott", + "youtubeid": "SenatorTimScott" + }, + "phone": "202-224-6121", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "SC", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.scott.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Connecticut", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "706 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.blumenthal.senate.gov/contact/", + "fax": "202-224-9673", + "office": "706 Hart Senate Office Building", + "rss_url": "http://www.blumenthal.senate.gov/rss/feeds/?type=all" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "B001277", + "birthday": "1946-02-13", + "cspanid": 21799, + "firstname": "Richard", + "gender": "male", + "gender_label": "Male", + "lastname": "Blumenthal", + "link": "https://www.govtrack.us/congress/members/richard_blumenthal/412490", + "middlename": "", + "name": "Sen. Richard Blumenthal [D-CT]", + "namemod": "", + "nickname": "", + "osid": "N00031685", + "pvsid": "1568", + "sortname": "Blumenthal, Richard (Sen.) [D-CT]", + "twitterid": "SenBlumenthal", + "youtubeid": "SenatorBlumenthal" + }, + "phone": "202-224-2823", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "CT", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.blumenthal.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Florida", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "284 Russell Senate Office Building Washington DC 20510", + "contact_form": "http://www.rubio.senate.gov/public/index.cfm/contact", + "fax": "202-228-0285", + "office": "284 Russell Senate Office Building", + "rss_url": "http://www.rubio.senate.gov/public/?a=rss.feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "R000595", + "birthday": "1971-05-28", + "cspanid": 87599, + "firstname": "Marco", + "gender": "male", + "gender_label": "Male", + "lastname": "Rubio", + "link": "https://www.govtrack.us/congress/members/marco_rubio/412491", + "middlename": "", + "name": "Sen. Marco Rubio [R-FL]", + "namemod": "", + "nickname": "", + "osid": "N00030612", + "pvsid": "1601", + "sortname": "Rubio, Marco (Sen.) [R-FL]", + "twitterid": "SenRubioPress", + "youtubeid": "SenatorMarcoRubio" + }, + "phone": "202-224-3041", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "FL", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.rubio.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Kentucky", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "167 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.paul.senate.gov/connect/email-rand", + "fax": "202-228-1373", + "office": "167 Russell Senate Office Building", + "rss_url": "http://paul.senate.gov/rss/?p=news" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "P000603", + "birthday": "1963-01-07", + "cspanid": 9265241, + "firstname": "Rand", + "gender": "male", + "gender_label": "Male", + "lastname": "Paul", + "link": "https://www.govtrack.us/congress/members/rand_paul/412492", + "middlename": "", + "name": "Sen. Rand Paul [R-KY]", + "namemod": "", + "nickname": "", + "osid": "N00030836", + "pvsid": "117285", + "sortname": "Paul, Rand (Sen.) [R-KY]", + "twitterid": "RandPaul", + "youtubeid": "SenatorRandPaul" + }, + "phone": "202-224-4343", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "KY", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.paul.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for North Dakota", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "338 Russell Senate Office Building Washington DC 20510", + "contact_form": "http://www.hoeven.senate.gov/public/index.cfm/email-the-senator", + "fax": "202-224-7999", + "office": "338 Russell Senate Office Building", + "rss_url": "http://www.hoeven.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "H001061", + "birthday": "1957-03-13", + "cspanid": 85233, + "firstname": "John", + "gender": "male", + "gender_label": "Male", + "lastname": "Hoeven", + "link": "https://www.govtrack.us/congress/members/john_hoeven/412494", + "middlename": "", + "name": "Sen. John Hoeven [R-ND]", + "namemod": "", + "nickname": "", + "osid": "N00031688", + "pvsid": "41788", + "sortname": "Hoeven, John (Sen.) [R-ND]", + "twitterid": "SenJohnHoeven", + "youtubeid": "senatorjohnhoevennd" + }, + "phone": "202-224-2551", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "ND", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.hoeven.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Utah", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "361A Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.lee.senate.gov/public/index.cfm/contact", + "office": "361a Russell Senate Office Building", + "rss_url": "http://www.lee.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "L000577", + "birthday": "1971-06-04", + "cspanid": 9267977, + "firstname": "Mike", + "gender": "male", + "gender_label": "Male", + "lastname": "Lee", + "link": "https://www.govtrack.us/congress/members/mike_lee/412495", + "middlename": "", + "name": "Sen. Mike Lee [R-UT]", + "namemod": "", + "nickname": "", + "osid": "N00031696", + "pvsid": "66395", + "sortname": "Lee, Mike (Sen.) [R-UT]", + "twitterid": "SenMikeLee", + "youtubeid": "senatormikelee" + }, + "phone": "202-224-5444", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "UT", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.lee.senate.gov/public" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Wisconsin", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "328 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.ronjohnson.senate.gov/public/index.cfm/email-the-senator", + "fax": "920-230-7262", + "office": "328 Hart Senate Office Building", + "rss_url": "http://www.ronjohnson.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "J000293", + "birthday": "1955-04-08", + "cspanid": 62835, + "firstname": "Ron", + "gender": "male", + "gender_label": "Male", + "lastname": "Johnson", + "link": "https://www.govtrack.us/congress/members/ron_johnson/412496", + "middlename": "", + "name": "Sen. Ron Johnson [R-WI]", + "namemod": "", + "nickname": "", + "osid": "N00032546", + "pvsid": "126217", + "sortname": "Johnson, Ron (Sen.) [R-WI]", + "twitterid": "SenRonJohnson", + "youtubeid": "SenatorRonJohnson" + }, + "phone": "202-224-5323", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "WI", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.ronjohnson.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Hawaii", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "722 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.schatz.senate.gov/contact", + "fax": "202-228-1153", + "office": "722 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "S001194", + "birthday": "1972-10-20", + "cspanid": 87784, + "firstname": "Brian", + "gender": "male", + "gender_label": "Male", + "lastname": "Schatz", + "link": "https://www.govtrack.us/congress/members/brian_schatz/412507", + "middlename": "Emanuel", + "name": "Sen. Brian Schatz [D-HI]", + "namemod": "", + "nickname": "", + "osid": "N00028138", + "pvsid": "17852", + "sortname": "Schatz, Brian (Sen.) [D-HI]", + "twitterid": "SenBrianSchatz", + "youtubeid": "senbrianschatz" + }, + "phone": "202-224-3934", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "HI", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.schatz.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Illinois", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "524 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.duckworth.senate.gov/content/contact-senator", + "office": "524 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "D000622", + "birthday": "1968-03-12", + "cspanid": 94484, + "firstname": "Tammy", + "gender": "female", + "gender_label": "Female", + "lastname": "Duckworth", + "link": "https://www.govtrack.us/congress/members/tammy_duckworth/412533", + "middlename": "", + "name": "Sen. Tammy Duckworth [D-IL]", + "namemod": "", + "nickname": "", + "osid": "N00027860", + "pvsid": "57442", + "sortname": "Duckworth, Tammy (Sen.) [D-IL]", + "twitterid": "SenDuckworth", + "youtubeid": "repduckworth" + }, + "phone": "202-224-2854", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "IL", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.duckworth.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for California", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "112 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.harris.senate.gov/contact", + "office": "112 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "H001075", + "birthday": "1964-10-20", + "cspanid": 1018696, + "firstname": "Kamala", + "gender": "female", + "gender_label": "Female", + "lastname": "Harris", + "link": "https://www.govtrack.us/congress/members/kamala_harris/412678", + "middlename": "", + "name": "Sen. Kamala Harris [D-CA]", + "namemod": "", + "nickname": "", + "osid": "N00036915", + "pvsid": "120012", + "sortname": "Harris, Kamala (Sen.) [D-CA]", + "twitterid": "SenKamalaHarris", + "youtubeid": null + }, + "phone": "202-224-3553", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "CA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.harris.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Louisiana", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "383 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.kennedy.senate.gov/public/email-me", + "office": "383 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "K000393", + "birthday": "1951-11-21", + "cspanid": 1011723, + "firstname": "John", + "gender": "male", + "gender_label": "Male", + "lastname": "Kennedy", + "link": "https://www.govtrack.us/congress/members/john_kennedy/412679", + "middlename": "Neely", + "name": "Sen. John Kennedy [R-LA]", + "namemod": "", + "nickname": "", + "osid": "N00026823", + "pvsid": "35496", + "sortname": "Kennedy, John (Sen.) [R-LA]", + "twitterid": "SenJohnKennedy", + "youtubeid": null + }, + "phone": "202-224-4623", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "LA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.kennedy.senate.gov/public" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for New Hampshire", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "330 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.hassan.senate.gov/content/contact-senator", + "office": "330 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "H001076", + "birthday": "1958-02-27", + "cspanid": 67481, + "firstname": "Margaret", + "gender": "female", + "gender_label": "Female", + "lastname": "Hassan", + "link": "https://www.govtrack.us/congress/members/margaret_hassan/412680", + "middlename": "Wood", + "name": "Sen. Margaret “Maggie” Hassan [D-NH]", + "namemod": "", + "nickname": "Maggie", + "osid": "N00038397", + "pvsid": "42552", + "sortname": "Hassan, Margaret “Maggie” (Sen.) [D-NH]", + "twitterid": "Senatorhassan", + "youtubeid": null + }, + "phone": "202-224-3324", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "NH", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.hassan.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Nevada", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "204 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.cortezmasto.senate.gov/contact", + "office": "204 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "C001113", + "birthday": "1964-03-29", + "cspanid": 105698, + "firstname": "Catherine", + "gender": "female", + "gender_label": "Female", + "lastname": "Cortez Masto", + "link": "https://www.govtrack.us/congress/members/catherine_cortez_masto/412681", + "middlename": "", + "name": "Sen. Catherine Cortez Masto [D-NV]", + "namemod": "", + "nickname": "", + "osid": "N00037161", + "pvsid": "69579", + "sortname": "Cortez Masto, Catherine (Sen.) [D-NV]", + "twitterid": "sencortezmasto", + "youtubeid": null + }, + "phone": "202-224-3542", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "NV", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.cortezmasto.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Alabama", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "326 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.jones.senate.gov/content/contact-senator", + "office": "326 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "J000300", + "birthday": "1954-05-04", + "cspanid": 1024592, + "firstname": "Doug", + "gender": "male", + "gender_label": "Male", + "lastname": "Jones", + "link": "https://www.govtrack.us/congress/members/doug_jones/412741", + "middlename": "", + "name": "Sen. Doug Jones [D-AL]", + "namemod": "", + "nickname": "", + "osid": "N00024817", + "pvsid": "176464", + "sortname": "Jones, Doug (Sen.) [D-AL]", + "twitterid": "sendougjones", + "youtubeid": null + }, + "phone": "202-224-4124", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2018-01-03", + "state": "AL", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.jones.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115 + ], + "current": true, + "description": "Junior Senator for Mississippi", + "district": null, + "enddate": "2018-11-27", + "extra": { + "address": "G12 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "https://www.hydesmith.senate.gov/content/contact-senator", + "end-type": "special-election", + "how": "appointment", + "office": "G12 Dirksen Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "H001079", + "birthday": "1959-05-10", + "cspanid": null, + "firstname": "Cindy", + "gender": "female", + "gender_label": "Female", + "lastname": "Hyde-Smith", + "link": "https://www.govtrack.us/congress/members/cindy_hyde_smith/412743", + "middlename": "", + "name": "Sen. Cindy Hyde-Smith [R-MS]", + "namemod": "", + "nickname": "", + "osid": "N00043298", + "pvsid": null, + "sortname": "Hyde-Smith, Cindy (Sen.) [R-MS]", + "twitterid": "SenHydeSmith", + "youtubeid": null + }, + "phone": "202-224-5054", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2018-04-09", + "state": "MS", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.hydesmith.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Arizona", + "district": null, + "enddate": "2020-11-03", + "extra": { + "address": "G12 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "https://www.kyl.senate.gov/content/contact-senator-kyl", + "end-type": "special-election", + "fax": "202-228-2862", + "how": "appointment", + "office": "G12 Dirksen Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "K000352", + "birthday": "1942-04-25", + "cspanid": null, + "firstname": "Jon", + "gender": "male", + "gender_label": "Male", + "lastname": "Kyl", + "link": "https://www.govtrack.us/congress/members/jon_kyl/300062", + "middlename": "", + "name": "Sen. Jon Kyl [R-AZ]", + "namemod": "", + "nickname": "", + "osid": "N00006406", + "pvsid": "26721", + "sortname": "Kyl, Jon (Sen.) [R-AZ]", + "twitterid": null, + "youtubeid": null + }, + "phone": "202-224-2235", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2018-09-05", + "state": "AZ", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.kyl.senate.gov" + }, + { + "caucus": null, + "congress_numbers": [ + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Minnesota", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "309 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.smith.senate.gov/content/contact-senator", + "office": "309 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "S001203", + "birthday": "1958-03-04", + "cspanid": 111313, + "firstname": "Tina", + "gender": "female", + "gender_label": "Female", + "lastname": "Smith", + "link": "https://www.govtrack.us/congress/members/tina_smith/412742", + "middlename": "Flint", + "name": "Sen. Tina Smith [D-MN]", + "namemod": "", + "nickname": "", + "osid": "N00042353", + "pvsid": "152968", + "sortname": "Smith, Tina (Sen.) [D-MN]", + "twitterid": "SenTinaSmith", + "youtubeid": null + }, + "phone": "202-224-5641", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2018-11-07", + "state": "MN", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.smith.senate.gov" + } + ] +} \ No newline at end of file diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/sample_json_lines.json b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/sample_json_lines.json new file mode 100644 index 00000000..1cecaee0 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/sample_json_lines.json @@ -0,0 +1,5145 @@ +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for Wisconsin", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "709 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.baldwin.senate.gov/feedback", + "fax": "202-225-6942", + "office": "709 Hart Senate Office Building", + "rss_url": "http://www.baldwin.senate.gov/rss/feeds/?type=all" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "B001230", + "birthday": "1962-02-11", + "cspanid": 57884, + "firstname": "Tammy", + "gender": "female", + "gender_label": "Female", + "lastname": "Baldwin", + "link": "https://www.govtrack.us/congress/members/tammy_baldwin/400013", + "middlename": "", + "name": "Sen. Tammy Baldwin [D-WI]", + "namemod": "", + "nickname": "", + "osid": "N00004367", + "pvsid": "3470", + "sortname": "Baldwin, Tammy (Sen.) [D-WI]", + "twitterid": "SenatorBaldwin", + "youtubeid": "witammybaldwin" + }, + "phone": "202-224-5653", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "WI", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.baldwin.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Ohio", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "713 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.brown.senate.gov/contact/", + "fax": "202-228-6321", + "office": "713 Hart Senate Office Building", + "rss_url": "http://www.brown.senate.gov/rss/feeds/?type=all&" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "B000944", + "birthday": "1952-11-09", + "cspanid": 5051, + "firstname": "Sherrod", + "gender": "male", + "gender_label": "Male", + "lastname": "Brown", + "link": "https://www.govtrack.us/congress/members/sherrod_brown/400050", + "middlename": "", + "name": "Sen. Sherrod Brown [D-OH]", + "namemod": "", + "nickname": "", + "osid": "N00003535", + "pvsid": "27018", + "sortname": "Brown, Sherrod (Sen.) [D-OH]", + "twitterid": "SenSherrodBrown", + "youtubeid": "SherrodBrownOhio" + }, + "phone": "202-224-2315", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "OH", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.brown.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Maryland", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "509 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.cardin.senate.gov/contact/", + "fax": "202-224-1651", + "office": "509 Hart Senate Office Building", + "rss_url": "http://www.cardin.senate.gov/rss/feeds/?type=all" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "C000141", + "birthday": "1943-10-05", + "cspanid": 4004, + "firstname": "Benjamin", + "gender": "male", + "gender_label": "Male", + "lastname": "Cardin", + "link": "https://www.govtrack.us/congress/members/benjamin_cardin/400064", + "middlename": "L.", + "name": "Sen. Benjamin Cardin [D-MD]", + "namemod": "", + "nickname": "", + "osid": "N00001955", + "pvsid": "26888", + "sortname": "Cardin, Benjamin (Sen.) [D-MD]", + "twitterid": "SenatorCardin", + "youtubeid": "senatorcardin" + }, + "phone": "202-224-4524", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "MD", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.cardin.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Arizona", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "413 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.flake.senate.gov/public/index.cfm/contact-jeff", + "fax": "202-226-4386", + "office": "413 Russell Senate Office Building", + "rss_url": "http://www.flake.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "F000444", + "birthday": "1962-12-31", + "cspanid": 87582, + "firstname": "Jeff", + "gender": "male", + "gender_label": "Male", + "lastname": "Flake", + "link": "https://www.govtrack.us/congress/members/jeff_flake/400134", + "middlename": "", + "name": "Sen. Jeff Flake [R-AZ]", + "namemod": "", + "nickname": "", + "osid": "N00009573", + "pvsid": "28128", + "sortname": "Flake, Jeff (Sen.) [R-AZ]", + "twitterid": "JeffFlake", + "youtubeid": "flakeoffice" + }, + "phone": "202-224-4521", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "AZ", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.flake.senate.gov/public" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for New Jersey", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "528 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.menendez.senate.gov/contact", + "fax": "202-228-2197", + "office": "528 Hart Senate Office Building", + "rss_url": "http://www.menendez.senate.gov/rss/feeds/index.cfm?type=news" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "M000639", + "birthday": "1954-01-01", + "cspanid": 29608, + "firstname": "Robert", + "gender": "male", + "gender_label": "Male", + "lastname": "Menéndez", + "link": "https://www.govtrack.us/congress/members/robert_menendez/400272", + "middlename": "", + "name": "Sen. Robert “Bob” Menéndez [D-NJ]", + "namemod": "", + "nickname": "Bob", + "osid": "N00000699", + "pvsid": "26961", + "sortname": "Menéndez, Robert “Bob” (Sen.) [D-NJ]", + "twitterid": "SenatorMenendez", + "youtubeid": "SenatorMenendezNJ" + }, + "phone": "202-224-4744", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "NJ", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.menendez.senate.gov" +}, +{ + "caucus": "Democrat", + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for Vermont", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "332 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "http://www.sanders.senate.gov/contact/", + "fax": "202-228-0776", + "office": "332 Dirksen Senate Office Building", + "rss_url": "http://www.sanders.senate.gov/rss/" + }, + "leadership_title": null, + "party": "Independent", + "person": { + "bioguideid": "S000033", + "birthday": "1941-09-08", + "cspanid": 994, + "firstname": "Bernard", + "gender": "male", + "gender_label": "Male", + "lastname": "Sanders", + "link": "https://www.govtrack.us/congress/members/bernard_sanders/400357", + "middlename": "", + "name": "Sen. Bernard “Bernie” Sanders [I-VT]", + "namemod": "", + "nickname": "Bernie", + "osid": "N00000528", + "pvsid": "27110", + "sortname": "Sanders, Bernard “Bernie” (Sen.) [I-VT]", + "twitterid": "SenSanders", + "youtubeid": "senatorsanders" + }, + "phone": "202-224-5141", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "VT", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.sanders.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for Washington", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "511 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.cantwell.senate.gov/public/index.cfm/email-maria", + "fax": "202-228-0514", + "office": "511 Hart Senate Office Building", + "rss_url": "http://www.cantwell.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "C000127", + "birthday": "1958-10-13", + "cspanid": 26137, + "firstname": "Maria", + "gender": "female", + "gender_label": "Female", + "lastname": "Cantwell", + "link": "https://www.govtrack.us/congress/members/maria_cantwell/300018", + "middlename": "", + "name": "Sen. Maria Cantwell [D-WA]", + "namemod": "", + "nickname": "", + "osid": "N00007836", + "pvsid": "27122", + "sortname": "Cantwell, Maria (Sen.) [D-WA]", + "twitterid": "SenatorCantwell", + "youtubeid": "SenatorCantwell" + }, + "phone": "202-224-3441", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "WA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.cantwell.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Delaware", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "513 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.carper.senate.gov/public/index.cfm/email-senator-carper", + "fax": "202-228-2190", + "office": "513 Hart Senate Office Building", + "rss_url": "http://www.carper.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "C000174", + "birthday": "1947-01-23", + "cspanid": 663, + "firstname": "Thomas", + "gender": "male", + "gender_label": "Male", + "lastname": "Carper", + "link": "https://www.govtrack.us/congress/members/thomas_carper/300019", + "middlename": "Richard", + "name": "Sen. Thomas Carper [D-DE]", + "namemod": "", + "nickname": "", + "osid": "N00012508", + "pvsid": "22421", + "sortname": "Carper, Thomas (Sen.) [D-DE]", + "twitterid": "SenatorCarper", + "youtubeid": "senatorcarper" + }, + "phone": "202-224-2441", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "DE", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.carper.senate.gov/public" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for California", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "331 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.feinstein.senate.gov/public/index.cfm/e-mail-me", + "fax": "202-228-3954", + "office": "331 Hart Senate Office Building", + "rss_url": "http://www.feinstein.senate.gov/public/?a=rss.feed" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "F000062", + "birthday": "1933-06-22", + "cspanid": 13061, + "firstname": "Dianne", + "gender": "female", + "gender_label": "Female", + "lastname": "Feinstein", + "link": "https://www.govtrack.us/congress/members/dianne_feinstein/300043", + "middlename": "", + "name": "Sen. Dianne Feinstein [D-CA]", + "namemod": "", + "nickname": "", + "osid": "N00007364", + "pvsid": "53273", + "sortname": "Feinstein, Dianne (Sen.) [D-CA]", + "twitterid": "SenFeinstein", + "youtubeid": "SenatorFeinstein" + }, + "phone": "202-224-3841", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "CA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.feinstein.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Utah", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "104 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.hatch.senate.gov/public/index.cfm/contact?p=Email-Orrin", + "fax": "202-224-6331", + "office": "104 Hart Senate Office Building", + "rss_url": "http://www.hatch.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "H000338", + "birthday": "1934-03-22", + "cspanid": 189, + "firstname": "Orrin", + "gender": "male", + "gender_label": "Male", + "lastname": "Hatch", + "link": "https://www.govtrack.us/congress/members/orrin_hatch/300052", + "middlename": "G.", + "name": "Sen. Orrin Hatch [R-UT]", + "namemod": "", + "nickname": "", + "osid": "N00009869", + "pvsid": "53352", + "sortname": "Hatch, Orrin (Sen.) [R-UT]", + "twitterid": "SenOrrinHatch", + "youtubeid": "SenatorOrrinHatch" + }, + "phone": "202-224-5251", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "UT", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.hatch.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Florida", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "716 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.billnelson.senate.gov/contact-bill", + "fax": "202-228-2183", + "office": "716 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "N000032", + "birthday": "1942-09-29", + "cspanid": 1931, + "firstname": "Bill", + "gender": "male", + "gender_label": "Male", + "lastname": "Nelson", + "link": "https://www.govtrack.us/congress/members/bill_nelson/300078", + "middlename": "", + "name": "Sen. Bill Nelson [D-FL]", + "namemod": "", + "nickname": "", + "osid": "N00009926", + "pvsid": "1606", + "sortname": "Nelson, Bill (Sen.) [D-FL]", + "twitterid": "SenBillNelson", + "youtubeid": "senbillnelson" + }, + "phone": "202-224-5274", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "FL", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.billnelson.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Michigan", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "731 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.stabenow.senate.gov/contact", + "fax": "202-228-0325", + "office": "731 Hart Senate Office Building", + "rss_url": "http://stabenow.senate.gov/rss/?p=news" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "S000770", + "birthday": "1950-04-29", + "cspanid": 45451, + "firstname": "Debbie", + "gender": "female", + "gender_label": "Female", + "lastname": "Stabenow", + "link": "https://www.govtrack.us/congress/members/debbie_stabenow/300093", + "middlename": "Ann", + "name": "Sen. Debbie Stabenow [D-MI]", + "namemod": "", + "nickname": "", + "osid": "N00004118", + "pvsid": "515", + "sortname": "Stabenow, Debbie (Sen.) [D-MI]", + "twitterid": "SenStabenow", + "youtubeid": "senatorstabenow" + }, + "phone": "202-224-4822", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "MI", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.stabenow.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for Connecticut", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "136 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.murphy.senate.gov/contact", + "fax": "202-225-5933", + "office": "136 Hart Senate Office Building", + "rss_url": "http://www.theday.com/article/20121216/nws12/312169935/1069/rss" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "M001169", + "birthday": "1973-08-03", + "cspanid": 1021270, + "firstname": "Christopher", + "gender": "male", + "gender_label": "Male", + "lastname": "Murphy", + "link": "https://www.govtrack.us/congress/members/christopher_murphy/412194", + "middlename": "S.", + "name": "Sen. Christopher Murphy [D-CT]", + "namemod": "", + "nickname": "", + "osid": "N00027566", + "pvsid": "17189", + "sortname": "Murphy, Christopher (Sen.) [D-CT]", + "twitterid": "senmurphyoffice", + "youtubeid": "senchrismurphy" + }, + "phone": "202-224-4041", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "CT", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.murphy.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for Hawaii", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "730 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.hirono.senate.gov/contact", + "fax": "202-225-4987", + "office": "730 Hart Senate Office Building", + "rss_url": "http://www.hirono.senate.gov/rss/feeds/?type=all" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "H001042", + "birthday": "1947-11-03", + "cspanid": 91216, + "firstname": "Mazie", + "gender": "female", + "gender_label": "Female", + "lastname": "Hirono", + "link": "https://www.govtrack.us/congress/members/mazie_hirono/412200", + "middlename": "K.", + "name": "Sen. Mazie Hirono [D-HI]", + "namemod": "", + "nickname": "", + "osid": "N00028139", + "pvsid": "1677", + "sortname": "Hirono, Mazie (Sen.) [D-HI]", + "twitterid": "MazieHirono", + "youtubeid": "CongresswomanHirono" + }, + "phone": "202-224-6361", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "HI", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.hirono.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Indiana", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "720 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.donnelly.senate.gov/contact/email-joe", + "fax": "202-225-6798", + "office": "720 Hart Senate Office Building", + "rss_url": "http://www.donnelly.senate.gov/rss/feeds/?type=all" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "D000607", + "birthday": "1955-09-28", + "cspanid": 1012000, + "firstname": "Joe", + "gender": "male", + "gender_label": "Male", + "lastname": "Donnelly", + "link": "https://www.govtrack.us/congress/members/joe_donnelly/412205", + "middlename": "", + "name": "Sen. Joe Donnelly [D-IN]", + "namemod": "", + "nickname": "", + "osid": "N00026586", + "pvsid": "34212", + "sortname": "Donnelly, Joe (Sen.) [D-IN]", + "twitterid": "SenDonnelly", + "youtubeid": "sendonnelly" + }, + "phone": "202-224-4814", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "IN", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.donnelly.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Nevada", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "324 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.heller.senate.gov/public/index.cfm/contact-form", + "fax": "202-228-6753", + "office": "324 Hart Senate Office Building", + "rss_url": "http://www.heller.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "H001041", + "birthday": "1960-05-10", + "cspanid": 1012368, + "firstname": "Dean", + "gender": "male", + "gender_label": "Male", + "lastname": "Heller", + "link": "https://www.govtrack.us/congress/members/dean_heller/412218", + "middlename": "", + "name": "Sen. Dean Heller [R-NV]", + "namemod": "", + "nickname": "", + "osid": "N00027522", + "pvsid": "2291", + "sortname": "Heller, Dean (Sen.) [R-NV]", + "twitterid": "SenDeanHeller", + "youtubeid": "SenDeanHeller" + }, + "phone": "202-224-6244", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "NV", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.heller.senate.gov/public" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for New York", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "478 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.gillibrand.senate.gov/contact/email-me", + "fax": "202-228-0282", + "office": "478 Russell Senate Office Building", + "rss_url": "http://www.gillibrand.senate.gov/rss/" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "G000555", + "birthday": "1966-12-09", + "cspanid": 1022862, + "firstname": "Kirsten", + "gender": "female", + "gender_label": "Female", + "lastname": "Gillibrand", + "link": "https://www.govtrack.us/congress/members/kirsten_gillibrand/412223", + "middlename": "E.", + "name": "Sen. Kirsten Gillibrand [D-NY]", + "namemod": "", + "nickname": "", + "osid": "N00027658", + "pvsid": "65147", + "sortname": "Gillibrand, Kirsten (Sen.) [D-NY]", + "twitterid": "SenGillibrand", + "youtubeid": "KirstenEGillibrand" + }, + "phone": "202-224-4451", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "NY", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.gillibrand.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Minnesota", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "302 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.klobuchar.senate.gov/public/index.cfm/contact", + "fax": "202-228-2186", + "office": "302 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "K000367", + "birthday": "1960-05-25", + "cspanid": 83701, + "firstname": "Amy", + "gender": "female", + "gender_label": "Female", + "lastname": "Klobuchar", + "link": "https://www.govtrack.us/congress/members/amy_klobuchar/412242", + "middlename": "Jean", + "name": "Sen. Amy Klobuchar [D-MN]", + "namemod": "", + "nickname": "", + "osid": "N00027500", + "pvsid": "65092", + "sortname": "Klobuchar, Amy (Sen.) [D-MN]", + "twitterid": "SenAmyKlobuchar", + "youtubeid": "senatorklobuchar" + }, + "phone": "202-224-3244", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "MN", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.klobuchar.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Missouri", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "503 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.mccaskill.senate.gov/contact", + "fax": "202-228-6326", + "office": "503 Hart Senate Office Building", + "rss_url": "http://mccaskill.senate.gov/rss/?p=news" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "M001170", + "birthday": "1953-07-24", + "cspanid": 44501, + "firstname": "Claire", + "gender": "female", + "gender_label": "Female", + "lastname": "McCaskill", + "link": "https://www.govtrack.us/congress/members/claire_mccaskill/412243", + "middlename": "", + "name": "Sen. Claire McCaskill [D-MO]", + "namemod": "", + "nickname": "", + "osid": "N00027694", + "pvsid": "2109", + "sortname": "McCaskill, Claire (Sen.) [D-MO]", + "twitterid": "McCaskillOffice", + "youtubeid": "SenatorMcCaskill" + }, + "phone": "202-224-6154", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "MO", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.mccaskill.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Montana", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "311 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.tester.senate.gov/?p=email_senator", + "fax": "202-224-8594", + "office": "311 Hart Senate Office Building", + "rss_url": "http://www.tester.senate.gov/rss/?p=hot_topic" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "T000464", + "birthday": "1956-08-21", + "cspanid": 1020176, + "firstname": "Jon", + "gender": "male", + "gender_label": "Male", + "lastname": "Tester", + "link": "https://www.govtrack.us/congress/members/jon_tester/412244", + "middlename": "", + "name": "Sen. Jon Tester [D-MT]", + "namemod": "", + "nickname": "", + "osid": "N00027605", + "pvsid": "20928", + "sortname": "Tester, Jon (Sen.) [D-MT]", + "twitterid": "SenatorTester", + "youtubeid": "senatorjontester" + }, + "phone": "202-224-2644", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "MT", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.tester.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Pennsylvania", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "393 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.casey.senate.gov/contact/", + "fax": "202-228-0604", + "office": "393 Russell Senate Office Building", + "rss_url": "http://www.casey.senate.gov/rss/feeds/?all" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "C001070", + "birthday": "1960-04-13", + "cspanid": 47036, + "firstname": "Robert", + "gender": "male", + "gender_label": "Male", + "lastname": "Casey", + "link": "https://www.govtrack.us/congress/members/robert_casey/412246", + "middlename": "P.", + "name": "Sen. Robert “Bob” Casey [D-PA]", + "namemod": "Jr.", + "nickname": "Bob", + "osid": "N00027503", + "pvsid": "2541", + "sortname": "Casey, Robert “Bob” (Sen.) [D-PA]", + "twitterid": "SenBobCasey", + "youtubeid": "SenatorBobCasey" + }, + "phone": "202-224-6324", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "PA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.casey.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for Rhode Island", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "530 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.whitehouse.senate.gov/contact/email-sheldon", + "fax": "202-228-6362", + "office": "530 Hart Senate Office Building", + "rss_url": "http://www.whitehouse.senate.gov/rss/feeds/?type=all&cachebuster=1" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "W000802", + "birthday": "1955-10-20", + "cspanid": 92235, + "firstname": "Sheldon", + "gender": "male", + "gender_label": "Male", + "lastname": "Whitehouse", + "link": "https://www.govtrack.us/congress/members/sheldon_whitehouse/412247", + "middlename": "", + "name": "Sen. Sheldon Whitehouse [D-RI]", + "namemod": "", + "nickname": "", + "osid": "N00027533", + "pvsid": "2572", + "sortname": "Whitehouse, Sheldon (Sen.) [D-RI]", + "twitterid": "SenWhitehouse", + "youtubeid": "SenatorWhitehouse" + }, + "phone": "202-224-2921", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "RI", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.whitehouse.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for Tennessee", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "425 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "https://www.corker.senate.gov/public/index.cfm/emailme", + "fax": "202-228-0566", + "office": "425 Dirksen Senate Office Building", + "rss_url": "http://www.corker.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "C001071", + "birthday": "1952-08-24", + "cspanid": 1021114, + "firstname": "Bob", + "gender": "male", + "gender_label": "Male", + "lastname": "Corker", + "link": "https://www.govtrack.us/congress/members/bob_corker/412248", + "middlename": "", + "name": "Sen. Bob Corker [R-TN]", + "namemod": "", + "nickname": "", + "osid": "N00027441", + "pvsid": "65905", + "sortname": "Corker, Bob (Sen.) [R-TN]", + "twitterid": "SenBobCorker", + "youtubeid": "senatorcorker" + }, + "phone": "202-224-3344", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "TN", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.corker.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for Wyoming", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "307 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "https://www.barrasso.senate.gov/public/index.cfm/contact-form", + "fax": "202-224-1724", + "office": "307 Dirksen Senate Office Building", + "rss_url": "http://www.barrasso.senate.gov/public/index.cfm?FuseAction=Rss.Feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "B001261", + "birthday": "1952-07-21", + "cspanid": 1024777, + "firstname": "John", + "gender": "male", + "gender_label": "Male", + "lastname": "Barrasso", + "link": "https://www.govtrack.us/congress/members/john_barrasso/412251", + "middlename": "A.", + "name": "Sen. John Barrasso [R-WY]", + "namemod": "", + "nickname": "", + "osid": "N00006236", + "pvsid": "52662", + "sortname": "Barrasso, John (Sen.) [R-WY]", + "twitterid": "SenJohnBarrasso", + "youtubeid": "barrassowyo" + }, + "phone": "202-224-6441", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "WY", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.barrasso.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for New Mexico", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "303 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.heinrich.senate.gov/contact", + "fax": "202-225-4975", + "office": "303 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "H001046", + "birthday": "1971-10-17", + "cspanid": 1030686, + "firstname": "Martin", + "gender": "male", + "gender_label": "Male", + "lastname": "Heinrich", + "link": "https://www.govtrack.us/congress/members/martin_heinrich/412281", + "middlename": "", + "name": "Sen. Martin Heinrich [D-NM]", + "namemod": "", + "nickname": "", + "osid": "N00029835", + "pvsid": "74517", + "sortname": "Heinrich, Martin (Sen.) [D-NM]", + "twitterid": "MartinHeinrich", + "youtubeid": "SenMartinHeinrich" + }, + "phone": "202-224-5521", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "NM", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.heinrich.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for West Virginia", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "306 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.manchin.senate.gov/public/index.cfm/contact-form", + "fax": "202-228-0002", + "office": "306 Hart Senate Office Building", + "rss_url": "http://www.manchin.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "M001183", + "birthday": "1947-08-24", + "cspanid": 62864, + "firstname": "Joe", + "gender": "male", + "gender_label": "Male", + "lastname": "Manchin", + "link": "https://www.govtrack.us/congress/members/joe_manchin/412391", + "middlename": "", + "name": "Sen. Joe Manchin [D-WV]", + "namemod": "III", + "nickname": "", + "osid": "N00032838", + "pvsid": "7547", + "sortname": "Manchin, Joe (Sen.) [D-WV]", + "twitterid": "Sen_JoeManchin", + "youtubeid": "SenatorJoeManchin" + }, + "phone": "202-224-3954", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "WV", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.manchin.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Massachusetts", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "317 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.warren.senate.gov/?p=email_senator", + "fax": "202-228-2072", + "office": "317 Hart Senate Office Building", + "rss_url": "http://www.warren.senate.gov/rss/?p=hot_topic" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "W000817", + "birthday": "1949-06-22", + "cspanid": 1023023, + "firstname": "Elizabeth", + "gender": "female", + "gender_label": "Female", + "lastname": "Warren", + "link": "https://www.govtrack.us/congress/members/elizabeth_warren/412542", + "middlename": "", + "name": "Sen. Elizabeth Warren [D-MA]", + "namemod": "", + "nickname": "", + "osid": "N00033492", + "pvsid": "141272", + "sortname": "Warren, Elizabeth (Sen.) [D-MA]", + "twitterid": "SenWarren", + "youtubeid": "senelizabethwarren" + }, + "phone": "202-224-4543", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "MA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.warren.senate.gov" +}, +{ + "caucus": "Democrat", + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for Maine", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "133 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.king.senate.gov/contact", + "fax": "202-224-1946", + "office": "133 Hart Senate Office Building", + "rss_url": "http://www.king.senate.gov/rss/feeds/?type=all" + }, + "leadership_title": null, + "party": "Independent", + "person": { + "bioguideid": "K000383", + "birthday": "1944-03-31", + "cspanid": 37413, + "firstname": "Angus", + "gender": "male", + "gender_label": "Male", + "lastname": "King", + "link": "https://www.govtrack.us/congress/members/angus_king/412545", + "middlename": "", + "name": "Sen. Angus King [I-ME]", + "namemod": "", + "nickname": "", + "osid": "N00034580", + "pvsid": "22381", + "sortname": "King, Angus (Sen.) [I-ME]", + "twitterid": "SenAngusKing", + "youtubeid": "SenatorAngusKing" + }, + "phone": "202-224-5344", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "ME", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.king.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for North Dakota", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "516 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.heitkamp.senate.gov/public/index.cfm/contact", + "fax": "202-224-7776", + "office": "516 Hart Senate Office Building", + "rss_url": "http://www.heitkamp.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "H001069", + "birthday": "1955-10-30", + "cspanid": 95414, + "firstname": "Heidi", + "gender": "female", + "gender_label": "Female", + "lastname": "Heitkamp", + "link": "https://www.govtrack.us/congress/members/heidi_heitkamp/412554", + "middlename": "", + "name": "Sen. Heidi Heitkamp [D-ND]", + "namemod": "", + "nickname": "", + "osid": "N00033782", + "pvsid": "41716", + "sortname": "Heitkamp, Heidi (Sen.) [D-ND]", + "twitterid": "SenatorHeitkamp", + "youtubeid": "senatorheidiheitkamp" + }, + "phone": "202-224-2043", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "ND", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.heitkamp.senate.gov/public" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Nebraska", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "454 Russell Senate Office Building Washington DC 20510", + "contact_form": "http://www.fischer.senate.gov/public/index.cfm/contact", + "fax": "202-228-1325", + "office": "454 Russell Senate Office Building", + "rss_url": "http://www.fischer.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "F000463", + "birthday": "1951-03-01", + "cspanid": 1034067, + "firstname": "Deb", + "gender": "female", + "gender_label": "Female", + "lastname": "Fischer", + "link": "https://www.govtrack.us/congress/members/deb_fischer/412556", + "middlename": "", + "name": "Sen. Deb Fischer [R-NE]", + "namemod": "", + "nickname": "", + "osid": "N00033443", + "pvsid": "41963", + "sortname": "Fischer, Deb (Sen.) [R-NE]", + "twitterid": "SenatorFischer", + "youtubeid": "senatordebfischer" + }, + "phone": "202-224-6551", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "NE", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.fischer.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for Texas", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "404 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.cruz.senate.gov/?p=form&id=16", + "fax": "202-228-3398", + "office": "404 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "C001098", + "birthday": "1970-12-22", + "cspanid": 1019953, + "firstname": "Ted", + "gender": "male", + "gender_label": "Male", + "lastname": "Cruz", + "link": "https://www.govtrack.us/congress/members/ted_cruz/412573", + "middlename": "", + "name": "Sen. Ted Cruz [R-TX]", + "namemod": "", + "nickname": "", + "osid": "N00033085", + "pvsid": "135705", + "sortname": "Cruz, Ted (Sen.) [R-TX]", + "twitterid": "SenTedCruz", + "youtubeid": "sentedcruz" + }, + "phone": "202-224-5922", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "TX", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.cruz.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Junior Senator for Virginia", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "231 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.kaine.senate.gov/contact", + "fax": "202-228-6363", + "office": "231 Russell Senate Office Building", + "rss_url": "http://www.kaine.senate.gov/rss/feeds/?type=all" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "K000384", + "birthday": "1958-02-26", + "cspanid": 49219, + "firstname": "Timothy", + "gender": "male", + "gender_label": "Male", + "lastname": "Kaine", + "link": "https://www.govtrack.us/congress/members/timothy_kaine/412582", + "middlename": "", + "name": "Sen. Timothy Kaine [D-VA]", + "namemod": "", + "nickname": "", + "osid": "N00033177", + "pvsid": "50772", + "sortname": "Kaine, Timothy (Sen.) [D-VA]", + "twitterid": null, + "youtubeid": "SenatorTimKaine" + }, + "phone": "202-224-4024", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2013-01-03", + "state": "VA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.kaine.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 113, + 114, + 115 + ], + "current": true, + "description": "Senior Senator for Mississippi", + "district": null, + "enddate": "2019-01-03", + "extra": { + "address": "555 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "https://www.wicker.senate.gov/public/index.cfm/contact", + "fax": "202-228-0378", + "office": "555 Dirksen Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "W000437", + "birthday": "1951-07-05", + "cspanid": 18203, + "firstname": "Roger", + "gender": "male", + "gender_label": "Male", + "lastname": "Wicker", + "link": "https://www.govtrack.us/congress/members/roger_wicker/400432", + "middlename": "F.", + "name": "Sen. Roger Wicker [R-MS]", + "namemod": "", + "nickname": "", + "osid": "N00003280", + "pvsid": "21926", + "sortname": "Wicker, Roger (Sen.) [R-MS]", + "twitterid": "SenatorWicker", + "youtubeid": "SenatorWicker" + }, + "phone": "202-224-6253", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class1", + "senator_class_label": "Class 1", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2013-01-03", + "state": "MS", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.wicker.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for Tennessee", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "455 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "http://www.alexander.senate.gov/public/index.cfm?p=Email", + "fax": "202-228-3398", + "office": "455 Dirksen Senate Office Building", + "rss_url": "http://www.alexander.senate.gov/public/?a=rss.feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "A000360", + "birthday": "1940-07-03", + "cspanid": 5, + "firstname": "Lamar", + "gender": "male", + "gender_label": "Male", + "lastname": "Alexander", + "link": "https://www.govtrack.us/congress/members/lamar_alexander/300002", + "middlename": "", + "name": "Sen. Lamar Alexander [R-TN]", + "namemod": "", + "nickname": "", + "osid": "N00009888", + "pvsid": "15691", + "sortname": "Alexander, Lamar (Sen.) [R-TN]", + "twitterid": "SenAlexander", + "youtubeid": "lamaralexander" + }, + "phone": "202-224-4944", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "TN", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.alexander.senate.gov/public" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for Maine", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "413 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "http://www.collins.senate.gov/contact", + "fax": "202-224-2693", + "office": "413 Dirksen Senate Office Building", + "rss_url": "http://www.collins.senate.gov/public/?a=rss.feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "C001035", + "birthday": "1952-12-07", + "cspanid": 45738, + "firstname": "Susan", + "gender": "female", + "gender_label": "Female", + "lastname": "Collins", + "link": "https://www.govtrack.us/congress/members/susan_collins/300025", + "middlename": "M.", + "name": "Sen. Susan Collins [R-ME]", + "namemod": "", + "nickname": "", + "osid": "N00000491", + "pvsid": "379", + "sortname": "Collins, Susan (Sen.) [R-ME]", + "twitterid": "SenatorCollins", + "youtubeid": "SenatorSusanCollins" + }, + "phone": "202-224-2523", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "ME", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.collins.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for Texas", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "517 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.cornyn.senate.gov/contact", + "fax": "202-228-2856", + "office": "517 Hart Senate Office Building", + "rss_url": "http://www.cornyn.senate.gov/public/?a=rss.feed" + }, + "leadership_title": "Majority Whip", + "party": "Republican", + "person": { + "bioguideid": "C001056", + "birthday": "1952-02-02", + "cspanid": 93131, + "firstname": "John", + "gender": "male", + "gender_label": "Male", + "lastname": "Cornyn", + "link": "https://www.govtrack.us/congress/members/john_cornyn/300027", + "middlename": "", + "name": "Sen. John Cornyn [R-TX]", + "namemod": "", + "nickname": "", + "osid": "N00024852", + "pvsid": "15375", + "sortname": "Cornyn, John (Sen.) [R-TX]", + "twitterid": "JohnCornyn", + "youtubeid": "senjohncornyn" + }, + "phone": "202-224-2934", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "TX", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.cornyn.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for Illinois", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "711 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.durbin.senate.gov/contact/", + "fax": "202-228-0400", + "office": "711 Hart Senate Office Building", + "rss_url": "http://durbin.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": "Minority Whip", + "party": "Democrat", + "person": { + "bioguideid": "D000563", + "birthday": "1944-11-21", + "cspanid": 6741, + "firstname": "Richard", + "gender": "male", + "gender_label": "Male", + "lastname": "Durbin", + "link": "https://www.govtrack.us/congress/members/richard_durbin/300038", + "middlename": "J.", + "name": "Sen. Richard Durbin [D-IL]", + "namemod": "", + "nickname": "", + "osid": "N00004981", + "pvsid": "26847", + "sortname": "Durbin, Richard (Sen.) [D-IL]", + "twitterid": "SenatorDurbin", + "youtubeid": "SenatorDurbin" + }, + "phone": "202-224-2152", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "IL", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.durbin.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for Wyoming", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "379A Russell Senate Office Building Washington DC 20510", + "contact_form": "http://www.enzi.senate.gov/public/index.cfm/contact?p=e-mail-senator-enzi", + "fax": "202-228-0359", + "office": "379a Russell Senate Office Building", + "rss_url": "http://www.enzi.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "E000285", + "birthday": "1944-02-01", + "cspanid": 45824, + "firstname": "Michael", + "gender": "male", + "gender_label": "Male", + "lastname": "Enzi", + "link": "https://www.govtrack.us/congress/members/michael_enzi/300041", + "middlename": "B.", + "name": "Sen. Michael Enzi [R-WY]", + "namemod": "", + "nickname": "", + "osid": "N00006249", + "pvsid": "558", + "sortname": "Enzi, Michael (Sen.) [R-WY]", + "twitterid": "SenatorEnzi", + "youtubeid": "senatorenzi" + }, + "phone": "202-224-3424", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "WY", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.enzi.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for South Carolina", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "290 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.lgraham.senate.gov/public/index.cfm/e-mail-senator-graham", + "fax": "202-224-3808", + "office": "290 Russell Senate Office Building", + "rss_url": "http://www.lgraham.senate.gov/public/index.cfm?FuseAction=Rss.Feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "G000359", + "birthday": "1955-07-09", + "cspanid": 36782, + "firstname": "Lindsey", + "gender": "male", + "gender_label": "Male", + "lastname": "Graham", + "link": "https://www.govtrack.us/congress/members/lindsey_graham/300047", + "middlename": "O.", + "name": "Sen. Lindsey Graham [R-SC]", + "namemod": "", + "nickname": "", + "osid": "N00009975", + "pvsid": "21992", + "sortname": "Graham, Lindsey (Sen.) [R-SC]", + "twitterid": "GrahamBlog", + "youtubeid": "USSenLindseyGraham" + }, + "phone": "202-224-5972", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "SC", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.lgraham.senate.gov/public" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for Oklahoma", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "205 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.inhofe.senate.gov/contact", + "fax": "202-228-0380", + "office": "205 Russell Senate Office Building", + "rss_url": "http://www.inhofe.senate.gov/rss/feeds/?type=all&cachebuster=eea6c4d7%2d939c%2d5c1e%2db6c7aa3b8b291208" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "I000024", + "birthday": "1934-11-17", + "cspanid": 5619, + "firstname": "James", + "gender": "male", + "gender_label": "Male", + "lastname": "Inhofe", + "link": "https://www.govtrack.us/congress/members/james_inhofe/300055", + "middlename": "M.", + "name": "Sen. James “Jim” Inhofe [R-OK]", + "namemod": "", + "nickname": "Jim", + "osid": "N00005582", + "pvsid": "27027", + "sortname": "Inhofe, James “Jim” (Sen.) [R-OK]", + "twitterid": "InhofePress", + "youtubeid": "jiminhofepressoffice" + }, + "phone": "202-224-4721", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "OK", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.inhofe.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for Kentucky", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "317 Russell Senate Office Building Washington DC 20510", + "contact_form": "http://www.mcconnell.senate.gov/public/index.cfm?p=contact", + "fax": "202-224-2499", + "office": "317 Russell Senate Office Building", + "rss_url": "http://www.mcconnell.senate.gov/public/?a=rss.feed" + }, + "leadership_title": "Majority Leader", + "party": "Republican", + "person": { + "bioguideid": "M000355", + "birthday": "1942-02-20", + "cspanid": 2351, + "firstname": "Mitch", + "gender": "male", + "gender_label": "Male", + "lastname": "McConnell", + "link": "https://www.govtrack.us/congress/members/mitch_mcconnell/300072", + "middlename": "", + "name": "Sen. Mitch McConnell [R-KY]", + "namemod": "", + "nickname": "", + "osid": "N00003389", + "pvsid": "53298", + "sortname": "McConnell, Mitch (Sen.) [R-KY]", + "twitterid": "McConnellPress", + "youtubeid": null + }, + "phone": "202-224-2541", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "KY", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.mcconnell.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for Rhode Island", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "728 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.reed.senate.gov/contact/", + "fax": "202-224-4680", + "office": "728 Hart Senate Office Building", + "rss_url": "https://www.reed.senate.gov//rss/feeds/?type=all" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "R000122", + "birthday": "1949-11-12", + "cspanid": 24239, + "firstname": "John", + "gender": "male", + "gender_label": "Male", + "lastname": "Reed", + "link": "https://www.govtrack.us/congress/members/john_reed/300081", + "middlename": "F.", + "name": "Sen. John “Jack” Reed [D-RI]", + "namemod": "", + "nickname": "Jack", + "osid": "N00000362", + "pvsid": "27060", + "sortname": "Reed, John “Jack” (Sen.) [D-RI]", + "twitterid": "SenJackReed", + "youtubeid": "SenatorReed" + }, + "phone": "202-224-4642", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "RI", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.reed.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for Kansas", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "109 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.roberts.senate.gov/public/?p=EmailPat", + "fax": "202-224-3514", + "office": "109 Hart Senate Office Building", + "rss_url": "http://www.roberts.senate.gov/public/?a=rss.feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "R000307", + "birthday": "1936-04-20", + "cspanid": 16354, + "firstname": "Pat", + "gender": "male", + "gender_label": "Male", + "lastname": "Roberts", + "link": "https://www.govtrack.us/congress/members/pat_roberts/300083", + "middlename": "", + "name": "Sen. Pat Roberts [R-KS]", + "namemod": "", + "nickname": "", + "osid": "N00005285", + "pvsid": "26866", + "sortname": "Roberts, Pat (Sen.) [R-KS]", + "twitterid": "SenPatRoberts", + "youtubeid": "SenPatRoberts" + }, + "phone": "202-224-4774", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "KS", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.roberts.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for West Virginia", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "172 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.capito.senate.gov/contact/contact-shelley", + "fax": "202-225-7856", + "office": "172 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "C001047", + "birthday": "1953-11-26", + "cspanid": 83737, + "firstname": "Shelley", + "gender": "female", + "gender_label": "Female", + "lastname": "Capito", + "link": "https://www.govtrack.us/congress/members/shelley_capito/400061", + "middlename": "Moore", + "name": "Sen. Shelley Capito [R-WV]", + "namemod": "", + "nickname": "", + "osid": "N00009771", + "pvsid": "11701", + "sortname": "Capito, Shelley (Sen.) [R-WV]", + "twitterid": "SenCapito", + "youtubeid": null + }, + "phone": "202-224-6472", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "WV", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.capito.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Massachusetts", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "255 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "https://www.markey.senate.gov/contact", + "office": "255 Dirksen Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "M000133", + "birthday": "1946-07-11", + "cspanid": 260, + "firstname": "Edward", + "gender": "male", + "gender_label": "Male", + "lastname": "Markey", + "link": "https://www.govtrack.us/congress/members/edward_markey/400253", + "middlename": "J.", + "name": "Sen. Edward “Ed” Markey [D-MA]", + "namemod": "", + "nickname": "Ed", + "osid": "N00000270", + "pvsid": "26900", + "sortname": "Markey, Edward “Ed” (Sen.) [D-MA]", + "twitterid": "SenMarkey", + "youtubeid": "RepMarkey" + }, + "phone": "202-224-2742", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "MA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.markey.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for New Mexico", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "531 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.tomudall.senate.gov/?p=contact", + "fax": "202-228-3261", + "office": "531 Hart Senate Office Building", + "rss_url": "http://tomudall.senate.gov/rss/?p=blog" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "U000039", + "birthday": "1948-05-18", + "cspanid": 10075, + "firstname": "Tom", + "gender": "male", + "gender_label": "Male", + "lastname": "Udall", + "link": "https://www.govtrack.us/congress/members/tom_udall/400413", + "middlename": "S.", + "name": "Sen. Tom Udall [D-NM]", + "namemod": "", + "nickname": "", + "osid": "N00006561", + "pvsid": "22658", + "sortname": "Udall, Tom (Sen.) [D-NM]", + "twitterid": "SenatorTomUdall", + "youtubeid": "senatortomudall" + }, + "phone": "202-224-6621", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "NM", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.tomudall.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for Louisiana", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "520 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.cassidy.senate.gov/contact", + "fax": "202-225-7313", + "office": "520 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "C001075", + "birthday": "1957-09-28", + "cspanid": 1030546, + "firstname": "Bill", + "gender": "male", + "gender_label": "Male", + "lastname": "Cassidy", + "link": "https://www.govtrack.us/congress/members/bill_cassidy/412269", + "middlename": "", + "name": "Sen. Bill Cassidy [R-LA]", + "namemod": "", + "nickname": "", + "osid": "N00030245", + "pvsid": "69494", + "sortname": "Cassidy, Bill (Sen.) [R-LA]", + "twitterid": null, + "youtubeid": "SenatorBillCassidy" + }, + "phone": "202-224-5824", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "LA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.cassidy.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Michigan", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "724 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.peters.senate.gov/contact/email-gary", + "fax": "202-226-2356", + "office": "724 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "P000595", + "birthday": "1958-12-01", + "cspanid": 50199, + "firstname": "Gary", + "gender": "male", + "gender_label": "Male", + "lastname": "Peters", + "link": "https://www.govtrack.us/congress/members/gary_peters/412305", + "middlename": "C.", + "name": "Sen. Gary Peters [D-MI]", + "namemod": "", + "nickname": "", + "osid": "N00029277", + "pvsid": "8749", + "sortname": "Peters, Gary (Sen.) [D-MI]", + "twitterid": "SenGaryPeters", + "youtubeid": "RepGaryPeters" + }, + "phone": "202-224-6221", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "MI", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.peters.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for Virginia", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "703 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.warner.senate.gov/public/index.cfm?p=Contact", + "fax": "202-224-6295", + "office": "703 Hart Senate Office Building", + "rss_url": "http://www.warner.senate.gov/public/?a=rss.feed" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "W000805", + "birthday": "1954-12-15", + "cspanid": 7630, + "firstname": "Mark", + "gender": "male", + "gender_label": "Male", + "lastname": "Warner", + "link": "https://www.govtrack.us/congress/members/mark_warner/412321", + "middlename": "", + "name": "Sen. Mark Warner [D-VA]", + "namemod": "", + "nickname": "", + "osid": "N00002097", + "pvsid": "535", + "sortname": "Warner, Mark (Sen.) [D-VA]", + "twitterid": "MarkWarner", + "youtubeid": "SenatorMarkWarner" + }, + "phone": "202-224-2023", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "VA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.warner.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Idaho", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "483 Russell Senate Office Building Washington DC 20510", + "contact_form": "http://www.risch.senate.gov/public/index.cfm?p=Email", + "fax": "202-224-2573", + "office": "483 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "R000584", + "birthday": "1943-05-03", + "cspanid": 1020034, + "firstname": "James", + "gender": "male", + "gender_label": "Male", + "lastname": "Risch", + "link": "https://www.govtrack.us/congress/members/james_risch/412322", + "middlename": "", + "name": "Sen. James Risch [R-ID]", + "namemod": "", + "nickname": "", + "osid": "N00029441", + "pvsid": "2919", + "sortname": "Risch, James (Sen.) [R-ID]", + "twitterid": "SenatorRisch", + "youtubeid": "SenatorJamesRisch" + }, + "phone": "202-224-2752", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "ID", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.risch.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Senior Senator for New Hampshire", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "506 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.shaheen.senate.gov/contact/contact-jeanne", + "fax": "202-228-3194", + "office": "506 Hart Senate Office Building", + "rss_url": "http://www.shaheen.senate.gov/rss/" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "S001181", + "birthday": "1947-01-28", + "cspanid": 22850, + "firstname": "Jeanne", + "gender": "female", + "gender_label": "Female", + "lastname": "Shaheen", + "link": "https://www.govtrack.us/congress/members/jeanne_shaheen/412323", + "middlename": "", + "name": "Sen. Jeanne Shaheen [D-NH]", + "namemod": "", + "nickname": "", + "osid": "N00024790", + "pvsid": "1663", + "sortname": "Shaheen, Jeanne (Sen.) [D-NH]", + "twitterid": "SenatorShaheen", + "youtubeid": "senatorshaheen" + }, + "phone": "202-224-2841", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2015-01-06", + "state": "NH", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.shaheen.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Oregon", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "313 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.merkley.senate.gov/contact/", + "fax": "202-228-3997", + "office": "313 Hart Senate Office Building", + "rss_url": "http://www.merkley.senate.gov/rss/" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "M001176", + "birthday": "1956-10-24", + "cspanid": 1029842, + "firstname": "Jeff", + "gender": "male", + "gender_label": "Male", + "lastname": "Merkley", + "link": "https://www.govtrack.us/congress/members/jeff_merkley/412325", + "middlename": "", + "name": "Sen. Jeff Merkley [D-OR]", + "namemod": "", + "nickname": "", + "osid": "N00029303", + "pvsid": "23644", + "sortname": "Merkley, Jeff (Sen.) [D-OR]", + "twitterid": "SenJeffMerkley", + "youtubeid": "SenatorJeffMerkley" + }, + "phone": "202-224-3753", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "OR", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.merkley.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Delaware", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "127A Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.coons.senate.gov/contact", + "fax": "202-228-3075", + "office": "127a Russell Senate Office Building", + "rss_url": "http://www.coons.senate.gov/rss/feeds/?type=all" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "C001088", + "birthday": "1963-09-09", + "cspanid": 9269028, + "firstname": "Chris", + "gender": "male", + "gender_label": "Male", + "lastname": "Coons", + "link": "https://www.govtrack.us/congress/members/chris_coons/412390", + "middlename": "Andrew", + "name": "Sen. Chris Coons [D-DE]", + "namemod": "", + "nickname": "", + "osid": "N00031820", + "pvsid": "122834", + "sortname": "Coons, Chris (Sen.) [D-DE]", + "twitterid": "ChrisCoons", + "youtubeid": "senatorchriscoons" + }, + "phone": "202-224-5042", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "DE", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.coons.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Colorado", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "354 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.gardner.senate.gov/contact-cory/email-cory", + "fax": "202-225-5870", + "office": "354 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "G000562", + "birthday": "1974-08-22", + "cspanid": 623308, + "firstname": "Cory", + "gender": "male", + "gender_label": "Male", + "lastname": "Gardner", + "link": "https://www.govtrack.us/congress/members/cory_gardner/412406", + "middlename": "", + "name": "Sen. Cory Gardner [R-CO]", + "namemod": "", + "nickname": "", + "osid": "N00030780", + "pvsid": "30004", + "sortname": "Gardner, Cory (Sen.) [R-CO]", + "twitterid": "SenCoryGardner", + "youtubeid": null + }, + "phone": "202-224-5941", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "CO", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.gardner.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Arkansas", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "124 Russell Senate Office Building Washington DC 20510", + "contact_form": "http://www.cotton.senate.gov/?p=contact", + "office": "124 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "C001095", + "birthday": "1977-05-13", + "cspanid": 63928, + "firstname": "Tom", + "gender": "male", + "gender_label": "Male", + "lastname": "Cotton", + "link": "https://www.govtrack.us/congress/members/tom_cotton/412508", + "middlename": "", + "name": "Sen. Tom Cotton [R-AR]", + "namemod": "", + "nickname": "", + "osid": "N00033363", + "pvsid": "135651", + "sortname": "Cotton, Tom (Sen.) [R-AR]", + "twitterid": "SenTomCotton", + "youtubeid": "RepTomCotton" + }, + "phone": "202-224-2353", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "AR", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.cotton.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Montana", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "320 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.daines.senate.gov/connect/email-steve", + "fax": "202-228-1236", + "office": "320 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "D000618", + "birthday": "1962-08-20", + "cspanid": 1034037, + "firstname": "Steve", + "gender": "male", + "gender_label": "Male", + "lastname": "Daines", + "link": "https://www.govtrack.us/congress/members/steve_daines/412549", + "middlename": "", + "name": "Sen. Steve Daines [R-MT]", + "namemod": "", + "nickname": "", + "osid": "N00033054", + "pvsid": "135720", + "sortname": "Daines, Steve (Sen.) [R-MT]", + "twitterid": "SteveDaines", + "youtubeid": "SteveDainesMT" + }, + "phone": "202-224-2651", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "MT", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.daines.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for New Jersey", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "359 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "https://www.booker.senate.gov/?p=contact", + "fax": "202-224-8378", + "office": "359 Dirksen Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "B001288", + "birthday": "1969-04-27", + "cspanid": 84679, + "firstname": "Cory", + "gender": "male", + "gender_label": "Male", + "lastname": "Booker", + "link": "https://www.govtrack.us/congress/members/cory_booker/412598", + "middlename": "Anthony", + "name": "Sen. Cory Booker [D-NJ]", + "namemod": "", + "nickname": "", + "osid": "N00035267", + "pvsid": "76151", + "sortname": "Booker, Cory (Sen.) [D-NJ]", + "twitterid": "SenBooker", + "youtubeid": "SenCoryBooker" + }, + "phone": "202-224-3224", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "NJ", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.booker.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Alaska", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "702 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.sullivan.senate.gov/contact/email", + "office": "702 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "S001198", + "birthday": "1964-11-13", + "cspanid": 1023262, + "firstname": "Dan", + "gender": "male", + "gender_label": "Male", + "lastname": "Sullivan", + "link": "https://www.govtrack.us/congress/members/dan_sullivan/412665", + "middlename": "", + "name": "Sen. Dan Sullivan [R-AK]", + "namemod": "", + "nickname": "", + "osid": "N00035774", + "pvsid": "114964", + "sortname": "Sullivan, Dan (Sen.) [R-AK]", + "twitterid": "SenDanSullivan", + "youtubeid": null + }, + "phone": "202-224-3004", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "AK", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.sullivan.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Georgia", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "455 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.perdue.senate.gov/connect/email", + "office": "455 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "P000612", + "birthday": "1949-12-10", + "cspanid": 75920, + "firstname": "David", + "gender": "male", + "gender_label": "Male", + "lastname": "Perdue", + "link": "https://www.govtrack.us/congress/members/david_perdue/412666", + "middlename": "", + "name": "Sen. David Perdue [R-GA]", + "namemod": "", + "nickname": "", + "osid": "N00035516", + "pvsid": "151330", + "sortname": "Perdue, David (Sen.) [R-GA]", + "twitterid": "sendavidperdue", + "youtubeid": null + }, + "phone": "202-224-3521", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "GA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.perdue.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Iowa", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "111 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.ernst.senate.gov/public/index.cfm/contact", + "office": "111 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "E000295", + "birthday": "1970-07-01", + "cspanid": 75342, + "firstname": "Joni", + "gender": "female", + "gender_label": "Female", + "lastname": "Ernst", + "link": "https://www.govtrack.us/congress/members/joni_ernst/412667", + "middlename": "", + "name": "Sen. Joni Ernst [R-IA]", + "namemod": "", + "nickname": "", + "osid": "N00035483", + "pvsid": "128583", + "sortname": "Ernst, Joni (Sen.) [R-IA]", + "twitterid": "SenJoniErnst", + "youtubeid": null + }, + "phone": "202-224-3254", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "IA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.ernst.senate.gov/public" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for North Carolina", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "185 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "https://www.tillis.senate.gov/public/index.cfm/email-me", + "office": "185 Dirksen Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "T000476", + "birthday": "1960-08-30", + "cspanid": 77055, + "firstname": "Thom", + "gender": "male", + "gender_label": "Male", + "lastname": "Tillis", + "link": "https://www.govtrack.us/congress/members/thom_tillis/412668", + "middlename": "", + "name": "Sen. Thom Tillis [R-NC]", + "namemod": "", + "nickname": "", + "osid": "N00035492", + "pvsid": "57717", + "sortname": "Tillis, Thom (Sen.) [R-NC]", + "twitterid": "senthomtillis", + "youtubeid": null + }, + "phone": "202-224-6342", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "NC", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.tillis.senate.gov/public" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for South Dakota", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "502 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.rounds.senate.gov/contact/email-mike", + "office": "502 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "R000605", + "birthday": "1954-10-24", + "cspanid": 78317, + "firstname": "Mike", + "gender": "male", + "gender_label": "Male", + "lastname": "Rounds", + "link": "https://www.govtrack.us/congress/members/mike_rounds/412669", + "middlename": "", + "name": "Sen. Mike Rounds [R-SD]", + "namemod": "", + "nickname": "", + "osid": "N00035187", + "pvsid": "7455", + "sortname": "Rounds, Mike (Sen.) [R-SD]", + "twitterid": "SenatorRounds", + "youtubeid": null + }, + "phone": "202-224-5842", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "SD", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.rounds.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 114, + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Nebraska", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "136 Russell Senate Office Building Washington DC 20510", + "contact_form": "http://www.sasse.senate.gov/public/index.cfm/email-ben", + "office": "136 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "S001197", + "birthday": "1972-02-22", + "cspanid": 77429, + "firstname": "Benjamin", + "gender": "male", + "gender_label": "Male", + "lastname": "Sasse", + "link": "https://www.govtrack.us/congress/members/benjamin_sasse/412671", + "middlename": "Eric", + "name": "Sen. Benjamin Sasse [R-NE]", + "namemod": "", + "nickname": "", + "osid": "N00035544", + "pvsid": "150182", + "sortname": "Sasse, Benjamin (Sen.) [R-NE]", + "twitterid": "SenSasse", + "youtubeid": null + }, + "phone": "202-224-4224", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2015-01-06", + "state": "NE", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.sasse.senate.gov/public" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Idaho", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "239 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "https://www.crapo.senate.gov/contact", + "fax": "202-228-1375", + "office": "239 Dirksen Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "C000880", + "birthday": "1951-05-20", + "cspanid": 26440, + "firstname": "Michael", + "gender": "male", + "gender_label": "Male", + "lastname": "Crapo", + "link": "https://www.govtrack.us/congress/members/michael_crapo/300030", + "middlename": "D.", + "name": "Sen. Michael Crapo [R-ID]", + "namemod": "", + "nickname": "", + "osid": "N00006267", + "pvsid": "26830", + "sortname": "Crapo, Michael (Sen.) [R-ID]", + "twitterid": "MikeCrapo", + "youtubeid": "senatorcrapo" + }, + "phone": "202-224-6142", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "ID", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.crapo.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Iowa", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "135 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.grassley.senate.gov/contact", + "fax": "202-224-6020", + "office": "135 Hart Senate Office Building", + "rss_url": "http://grassley.senate.gov/customcf/rss_feed.cfm" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "G000386", + "birthday": "1933-09-17", + "cspanid": 1167, + "firstname": "Charles", + "gender": "male", + "gender_label": "Male", + "lastname": "Grassley", + "link": "https://www.govtrack.us/congress/members/charles_grassley/300048", + "middlename": "E.", + "name": "Sen. Charles “Chuck” Grassley [R-IA]", + "namemod": "", + "nickname": "Chuck", + "osid": "N00001758", + "pvsid": "53293", + "sortname": "Grassley, Charles “Chuck” (Sen.) [R-IA]", + "twitterid": "ChuckGrassley", + "youtubeid": "senchuckgrassley" + }, + "phone": "202-224-3744", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "IA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.grassley.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Vermont", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "437 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.leahy.senate.gov/contact/", + "fax": "202-224-3479", + "office": "437 Russell Senate Office Building", + "rss_url": "http://www.leahy.senate.gov/rss/feeds/press/" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "L000174", + "birthday": "1940-03-31", + "cspanid": 1552, + "firstname": "Patrick", + "gender": "male", + "gender_label": "Male", + "lastname": "Leahy", + "link": "https://www.govtrack.us/congress/members/patrick_leahy/300065", + "middlename": "J.", + "name": "Sen. Patrick Leahy [D-VT]", + "namemod": "", + "nickname": "", + "osid": "N00009918", + "pvsid": "53353", + "sortname": "Leahy, Patrick (Sen.) [D-VT]", + "twitterid": "SenatorLeahy", + "youtubeid": "SenatorPatrickLeahy" + }, + "phone": "202-224-4242", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "VT", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.leahy.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Alaska", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "522 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.murkowski.senate.gov/public/index.cfm/contact", + "fax": "202-224-5301", + "office": "522 Hart Senate Office Building", + "rss_url": "http://www.murkowski.senate.gov/public/?a=rss.feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "M001153", + "birthday": "1957-05-22", + "cspanid": 1004138, + "firstname": "Lisa", + "gender": "female", + "gender_label": "Female", + "lastname": "Murkowski", + "link": "https://www.govtrack.us/congress/members/lisa_murkowski/300075", + "middlename": "A.", + "name": "Sen. Lisa Murkowski [R-AK]", + "namemod": "", + "nickname": "", + "osid": "N00026050", + "pvsid": "15841", + "sortname": "Murkowski, Lisa (Sen.) [R-AK]", + "twitterid": "LisaMurkowski", + "youtubeid": "senatormurkowski" + }, + "phone": "202-224-6665", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "AK", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.murkowski.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Washington", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "154 Russell Senate Office Building Washington DC 20510", + "contact_form": "http://www.murray.senate.gov/public/index.cfm/contactme", + "fax": "202-224-0238", + "office": "154 Russell Senate Office Building", + "rss_url": "http://www.murray.senate.gov/public/?a=rss.feed" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "M001111", + "birthday": "1950-10-11", + "cspanid": 25277, + "firstname": "Patty", + "gender": "female", + "gender_label": "Female", + "lastname": "Murray", + "link": "https://www.govtrack.us/congress/members/patty_murray/300076", + "middlename": "", + "name": "Sen. Patty Murray [D-WA]", + "namemod": "", + "nickname": "", + "osid": "N00007876", + "pvsid": "53358", + "sortname": "Murray, Patty (Sen.) [D-WA]", + "twitterid": "PattyMurray", + "youtubeid": "SenatorPattyMurray" + }, + "phone": "202-224-2621", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "WA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.murray.senate.gov/public" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for New York", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "322 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.schumer.senate.gov/contact/email-chuck", + "fax": "202-228-3027", + "office": "322 Hart Senate Office Building" + }, + "leadership_title": "Minority Leader", + "party": "Democrat", + "person": { + "bioguideid": "S000148", + "birthday": "1950-11-23", + "cspanid": 5929, + "firstname": "Charles", + "gender": "male", + "gender_label": "Male", + "lastname": "Schumer", + "link": "https://www.govtrack.us/congress/members/charles_schumer/300087", + "middlename": "E.", + "name": "Sen. Charles “Chuck” Schumer [D-NY]", + "namemod": "", + "nickname": "Chuck", + "osid": "N00001093", + "pvsid": "26976", + "sortname": "Schumer, Charles “Chuck” (Sen.) [D-NY]", + "twitterid": "SenSchumer", + "youtubeid": "SenatorSchumer" + }, + "phone": "202-224-6542", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "NY", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.schumer.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Alabama", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "304 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.shelby.senate.gov/public/index.cfm/emailsenatorshelby", + "fax": "202-224-3416", + "office": "304 Russell Senate Office Building", + "rss_url": "http://www.shelby.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "S000320", + "birthday": "1934-05-06", + "cspanid": 1859, + "firstname": "Richard", + "gender": "male", + "gender_label": "Male", + "lastname": "Shelby", + "link": "https://www.govtrack.us/congress/members/richard_shelby/300089", + "middlename": "C.", + "name": "Sen. Richard Shelby [R-AL]", + "namemod": "", + "nickname": "", + "osid": "N00009920", + "pvsid": "53266", + "sortname": "Shelby, Richard (Sen.) [R-AL]", + "twitterid": "SenShelby", + "youtubeid": "SenatorRichardShelby" + }, + "phone": "202-224-5744", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "AL", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.shelby.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Oregon", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "221 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "https://www.wyden.senate.gov/contact/", + "fax": "202-228-2717", + "office": "221 Dirksen Senate Office Building", + "rss_url": "http://www.wyden.senate.gov/rss/feeds/?type=all&" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "W000779", + "birthday": "1949-05-03", + "cspanid": 1962, + "firstname": "Ron", + "gender": "male", + "gender_label": "Male", + "lastname": "Wyden", + "link": "https://www.govtrack.us/congress/members/ron_wyden/300100", + "middlename": "", + "name": "Sen. Ron Wyden [D-OR]", + "namemod": "", + "nickname": "", + "osid": "N00007724", + "pvsid": "27036", + "sortname": "Wyden, Ron (Sen.) [D-OR]", + "twitterid": "RonWyden", + "youtubeid": "senronwyden" + }, + "phone": "202-224-5244", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "OR", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.wyden.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Missouri", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "260 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.blunt.senate.gov/public/index.cfm/contact-roy", + "fax": "202-224-8149", + "office": "260 Russell Senate Office Building", + "rss_url": "http://www.blunt.senate.gov/public/?a=rss.feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "B000575", + "birthday": "1950-01-10", + "cspanid": 45465, + "firstname": "Roy", + "gender": "male", + "gender_label": "Male", + "lastname": "Blunt", + "link": "https://www.govtrack.us/congress/members/roy_blunt/400034", + "middlename": "", + "name": "Sen. Roy Blunt [R-MO]", + "namemod": "", + "nickname": "", + "osid": "N00005195", + "pvsid": "418", + "sortname": "Blunt, Roy (Sen.) [R-MO]", + "twitterid": "RoyBlunt", + "youtubeid": "SenatorBlunt" + }, + "phone": "202-224-5721", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "MO", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.blunt.senate.gov/public" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Arkansas", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "141 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.boozman.senate.gov/public/index.cfm/contact", + "fax": "202-228-1371", + "office": "141 Hart Senate Office Building", + "rss_url": "http://www.boozman.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "B001236", + "birthday": "1950-12-10", + "cspanid": 92069, + "firstname": "John", + "gender": "male", + "gender_label": "Male", + "lastname": "Boozman", + "link": "https://www.govtrack.us/congress/members/john_boozman/400040", + "middlename": "", + "name": "Sen. John Boozman [R-AR]", + "namemod": "", + "nickname": "", + "osid": "N00013873", + "pvsid": "27958", + "sortname": "Boozman, John (Sen.) [R-AR]", + "twitterid": "JohnBoozman", + "youtubeid": "BoozmanPressOffice" + }, + "phone": "202-224-4843", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "AR", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.boozman.senate.gov/public" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for North Carolina", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "217 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.burr.senate.gov/contact/email", + "fax": "202-228-2981", + "office": "217 Russell Senate Office Building", + "rss_url": "http://www.burr.senate.gov/public/index.cfm?fuseaction=rss.feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "B001135", + "birthday": "1955-11-30", + "cspanid": 31054, + "firstname": "Richard", + "gender": "male", + "gender_label": "Male", + "lastname": "Burr", + "link": "https://www.govtrack.us/congress/members/richard_burr/400054", + "middlename": "M.", + "name": "Sen. Richard Burr [R-NC]", + "namemod": "", + "nickname": "", + "osid": "N00002221", + "pvsid": "21787", + "sortname": "Burr, Richard (Sen.) [R-NC]", + "twitterid": "SenatorBurr", + "youtubeid": "SenatorRichardBurr" + }, + "phone": "202-224-3154", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "NC", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.burr.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Georgia", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "131 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.isakson.senate.gov/public/index.cfm/email-me", + "fax": "202-228-0724", + "office": "131 Russell Senate Office Building", + "rss_url": "http://www.isakson.senate.gov/public/?a=RSS.Feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "I000055", + "birthday": "1944-12-28", + "cspanid": 59135, + "firstname": "John", + "gender": "male", + "gender_label": "Male", + "lastname": "Isakson", + "link": "https://www.govtrack.us/congress/members/john_isakson/400194", + "middlename": "H.", + "name": "Sen. John “Johnny” Isakson [R-GA]", + "namemod": "", + "nickname": "Johnny", + "osid": "N00002593", + "pvsid": "1721", + "sortname": "Isakson, John “Johnny” (Sen.) [R-GA]", + "twitterid": "SenatorIsakson", + "youtubeid": "SenatorIsakson" + }, + "phone": "202-224-3643", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "GA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.isakson.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Kansas", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "521 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "https://www.moran.senate.gov/public/index.cfm/e-mail-jerry", + "fax": "202-228-6966", + "office": "521 Dirksen Senate Office Building", + "rss_url": "http://www.moran.senate.gov/public/index.cfm/rss/feed/" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "M000934", + "birthday": "1954-05-29", + "cspanid": 45469, + "firstname": "Jerry", + "gender": "male", + "gender_label": "Male", + "lastname": "Moran", + "link": "https://www.govtrack.us/congress/members/jerry_moran/400284", + "middlename": "", + "name": "Sen. Jerry Moran [R-KS]", + "namemod": "", + "nickname": "", + "osid": "N00005282", + "pvsid": "542", + "sortname": "Moran, Jerry (Sen.) [R-KS]", + "twitterid": "JerryMoran", + "youtubeid": "senatorjerrymoran" + }, + "phone": "202-224-6521", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "KS", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.moran.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Ohio", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "448 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.portman.senate.gov/public/index.cfm/contact?p=contact-form", + "office": "448 Russell Senate Office Building", + "rss_url": "http://www.portman.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "P000449", + "birthday": "1955-12-19", + "cspanid": 31819, + "firstname": "Robert", + "gender": "male", + "gender_label": "Male", + "lastname": "Portman", + "link": "https://www.govtrack.us/congress/members/robert_portman/400325", + "middlename": "J.", + "name": "Sen. Robert “Rob” Portman [R-OH]", + "namemod": "", + "nickname": "Rob", + "osid": "N00003682", + "pvsid": "27008", + "sortname": "Portman, Robert “Rob” (Sen.) [R-OH]", + "twitterid": "SenRobPortman", + "youtubeid": "SenRobPortman" + }, + "phone": "202-224-3353", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "OH", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.portman.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Pennsylvania", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "248 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.toomey.senate.gov/?p=contact", + "fax": "202-228-0284", + "office": "248 Russell Senate Office Building", + "rss_url": "http://toomey.senate.gov/rss/?p=hot_topic" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "T000461", + "birthday": "1961-11-17", + "cspanid": 7958, + "firstname": "Patrick", + "gender": "male", + "gender_label": "Male", + "lastname": "Toomey", + "link": "https://www.govtrack.us/congress/members/patrick_toomey/400408", + "middlename": "J.", + "name": "Sen. Patrick “Pat” Toomey [R-PA]", + "namemod": "", + "nickname": "Pat", + "osid": "N00001489", + "pvsid": "24096", + "sortname": "Toomey, Patrick “Pat” (Sen.) [R-PA]", + "twitterid": "SenToomey", + "youtubeid": "sentoomey" + }, + "phone": "202-224-4254", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "PA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.toomey.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Maryland", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "110 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.vanhollen.senate.gov/contact/email", + "fax": "202-225-0375", + "office": "110 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "V000128", + "birthday": "1959-01-10", + "cspanid": 20756, + "firstname": "Chris", + "gender": "male", + "gender_label": "Male", + "lastname": "Van Hollen", + "link": "https://www.govtrack.us/congress/members/chris_van_hollen/400415", + "middlename": "", + "name": "Sen. Chris Van Hollen [D-MD]", + "namemod": "Jr.", + "nickname": "", + "osid": "N00013820", + "pvsid": "6098", + "sortname": "Van Hollen, Chris (Sen.) [D-MD]", + "twitterid": "ChrisVanHollen", + "youtubeid": "RepChrisVanHollen" + }, + "phone": "202-224-4654", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "MD", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.vanhollen.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for South Dakota", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "511 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "http://www.thune.senate.gov/public/index.cfm/contact", + "fax": "202-228-5429", + "office": "511 Dirksen Senate Office Building", + "rss_url": "http://www.thune.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "T000250", + "birthday": "1961-01-07", + "cspanid": 45552, + "firstname": "John", + "gender": "male", + "gender_label": "Male", + "lastname": "Thune", + "link": "https://www.govtrack.us/congress/members/john_thune/400546", + "middlename": "", + "name": "Sen. John Thune [R-SD]", + "namemod": "", + "nickname": "", + "osid": "N00004572", + "pvsid": "398", + "sortname": "Thune, John (Sen.) [R-SD]", + "twitterid": "SenJohnThune", + "youtubeid": "johnthune" + }, + "phone": "202-224-2321", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "SD", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.thune.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Colorado", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "261 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.bennet.senate.gov/public/index.cfm/contact", + "fax": "202-228-5097", + "office": "261 Russell Senate Office Building", + "rss_url": "http://www.bennet.senate.gov/rss/feeds/?type=news" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "B001267", + "birthday": "1964-11-28", + "cspanid": 1031622, + "firstname": "Michael", + "gender": "male", + "gender_label": "Male", + "lastname": "Bennet", + "link": "https://www.govtrack.us/congress/members/michael_bennet/412330", + "middlename": "F.", + "name": "Sen. Michael Bennet [D-CO]", + "namemod": "", + "nickname": "", + "osid": "N00030608", + "pvsid": "110942", + "sortname": "Bennet, Michael (Sen.) [D-CO]", + "twitterid": "SenBennetCo", + "youtubeid": "SenatorBennet" + }, + "phone": "202-224-5852", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "CO", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.bennet.senate.gov/public" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Indiana", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "400 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.young.senate.gov/contact", + "fax": "202-226-6866", + "office": "400 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "Y000064", + "birthday": "1972-08-24", + "cspanid": 1033743, + "firstname": "Todd", + "gender": "male", + "gender_label": "Male", + "lastname": "Young", + "link": "https://www.govtrack.us/congress/members/todd_young/412428", + "middlename": "C.", + "name": "Sen. Todd Young [R-IN]", + "namemod": "", + "nickname": "", + "osid": "N00030670", + "pvsid": "120345", + "sortname": "Young, Todd (Sen.) [R-IN]", + "twitterid": "SenToddYoung", + "youtubeid": "RepToddYoung" + }, + "phone": "202-224-5623", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "IN", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.young.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Oklahoma", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "316 Hart Senate Office Building Washington DC 20510", + "contact_form": "http://www.lankford.senate.gov/contact/email", + "office": "316 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "L000575", + "birthday": "1968-03-04", + "cspanid": 1033847, + "firstname": "James", + "gender": "male", + "gender_label": "Male", + "lastname": "Lankford", + "link": "https://www.govtrack.us/congress/members/james_lankford/412464", + "middlename": "", + "name": "Sen. James Lankford [R-OK]", + "namemod": "", + "nickname": "", + "osid": "N00031129", + "pvsid": "124938", + "sortname": "Lankford, James (Sen.) [R-OK]", + "twitterid": "SenatorLankford", + "youtubeid": "SenatorLankford" + }, + "phone": "202-224-5754", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "OK", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.lankford.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for South Carolina", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "717 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.scott.senate.gov/contact/email-me", + "fax": "202-225-3407", + "office": "717 Hart Senate Office Building", + "rss_url": "http://www.scott.senate.gov/rss.xml" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "S001184", + "birthday": "1965-09-19", + "cspanid": 623506, + "firstname": "Tim", + "gender": "male", + "gender_label": "Male", + "lastname": "Scott", + "link": "https://www.govtrack.us/congress/members/tim_scott/412471", + "middlename": "", + "name": "Sen. Tim Scott [R-SC]", + "namemod": "", + "nickname": "", + "osid": "N00031782", + "pvsid": "11940", + "sortname": "Scott, Tim (Sen.) [R-SC]", + "twitterid": "SenatorTimScott", + "youtubeid": "SenatorTimScott" + }, + "phone": "202-224-6121", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "SC", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.scott.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Connecticut", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "706 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.blumenthal.senate.gov/contact/", + "fax": "202-224-9673", + "office": "706 Hart Senate Office Building", + "rss_url": "http://www.blumenthal.senate.gov/rss/feeds/?type=all" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "B001277", + "birthday": "1946-02-13", + "cspanid": 21799, + "firstname": "Richard", + "gender": "male", + "gender_label": "Male", + "lastname": "Blumenthal", + "link": "https://www.govtrack.us/congress/members/richard_blumenthal/412490", + "middlename": "", + "name": "Sen. Richard Blumenthal [D-CT]", + "namemod": "", + "nickname": "", + "osid": "N00031685", + "pvsid": "1568", + "sortname": "Blumenthal, Richard (Sen.) [D-CT]", + "twitterid": "SenBlumenthal", + "youtubeid": "SenatorBlumenthal" + }, + "phone": "202-224-2823", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "CT", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.blumenthal.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Florida", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "284 Russell Senate Office Building Washington DC 20510", + "contact_form": "http://www.rubio.senate.gov/public/index.cfm/contact", + "fax": "202-228-0285", + "office": "284 Russell Senate Office Building", + "rss_url": "http://www.rubio.senate.gov/public/?a=rss.feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "R000595", + "birthday": "1971-05-28", + "cspanid": 87599, + "firstname": "Marco", + "gender": "male", + "gender_label": "Male", + "lastname": "Rubio", + "link": "https://www.govtrack.us/congress/members/marco_rubio/412491", + "middlename": "", + "name": "Sen. Marco Rubio [R-FL]", + "namemod": "", + "nickname": "", + "osid": "N00030612", + "pvsid": "1601", + "sortname": "Rubio, Marco (Sen.) [R-FL]", + "twitterid": "SenRubioPress", + "youtubeid": "SenatorMarcoRubio" + }, + "phone": "202-224-3041", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "FL", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.rubio.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Kentucky", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "167 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.paul.senate.gov/connect/email-rand", + "fax": "202-228-1373", + "office": "167 Russell Senate Office Building", + "rss_url": "http://paul.senate.gov/rss/?p=news" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "P000603", + "birthday": "1963-01-07", + "cspanid": 9265241, + "firstname": "Rand", + "gender": "male", + "gender_label": "Male", + "lastname": "Paul", + "link": "https://www.govtrack.us/congress/members/rand_paul/412492", + "middlename": "", + "name": "Sen. Rand Paul [R-KY]", + "namemod": "", + "nickname": "", + "osid": "N00030836", + "pvsid": "117285", + "sortname": "Paul, Rand (Sen.) [R-KY]", + "twitterid": "RandPaul", + "youtubeid": "SenatorRandPaul" + }, + "phone": "202-224-4343", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "KY", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.paul.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for North Dakota", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "338 Russell Senate Office Building Washington DC 20510", + "contact_form": "http://www.hoeven.senate.gov/public/index.cfm/email-the-senator", + "fax": "202-224-7999", + "office": "338 Russell Senate Office Building", + "rss_url": "http://www.hoeven.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "H001061", + "birthday": "1957-03-13", + "cspanid": 85233, + "firstname": "John", + "gender": "male", + "gender_label": "Male", + "lastname": "Hoeven", + "link": "https://www.govtrack.us/congress/members/john_hoeven/412494", + "middlename": "", + "name": "Sen. John Hoeven [R-ND]", + "namemod": "", + "nickname": "", + "osid": "N00031688", + "pvsid": "41788", + "sortname": "Hoeven, John (Sen.) [R-ND]", + "twitterid": "SenJohnHoeven", + "youtubeid": "senatorjohnhoevennd" + }, + "phone": "202-224-2551", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "ND", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.hoeven.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Utah", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "361A Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.lee.senate.gov/public/index.cfm/contact", + "office": "361a Russell Senate Office Building", + "rss_url": "http://www.lee.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "L000577", + "birthday": "1971-06-04", + "cspanid": 9267977, + "firstname": "Mike", + "gender": "male", + "gender_label": "Male", + "lastname": "Lee", + "link": "https://www.govtrack.us/congress/members/mike_lee/412495", + "middlename": "", + "name": "Sen. Mike Lee [R-UT]", + "namemod": "", + "nickname": "", + "osid": "N00031696", + "pvsid": "66395", + "sortname": "Lee, Mike (Sen.) [R-UT]", + "twitterid": "SenMikeLee", + "youtubeid": "senatormikelee" + }, + "phone": "202-224-5444", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "UT", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.lee.senate.gov/public" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Wisconsin", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "328 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.ronjohnson.senate.gov/public/index.cfm/email-the-senator", + "fax": "920-230-7262", + "office": "328 Hart Senate Office Building", + "rss_url": "http://www.ronjohnson.senate.gov/public/index.cfm/rss/feed" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "J000293", + "birthday": "1955-04-08", + "cspanid": 62835, + "firstname": "Ron", + "gender": "male", + "gender_label": "Male", + "lastname": "Johnson", + "link": "https://www.govtrack.us/congress/members/ron_johnson/412496", + "middlename": "", + "name": "Sen. Ron Johnson [R-WI]", + "namemod": "", + "nickname": "", + "osid": "N00032546", + "pvsid": "126217", + "sortname": "Johnson, Ron (Sen.) [R-WI]", + "twitterid": "SenRonJohnson", + "youtubeid": "SenatorRonJohnson" + }, + "phone": "202-224-5323", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "WI", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.ronjohnson.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Senior Senator for Hawaii", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "722 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.schatz.senate.gov/contact", + "fax": "202-228-1153", + "office": "722 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "S001194", + "birthday": "1972-10-20", + "cspanid": 87784, + "firstname": "Brian", + "gender": "male", + "gender_label": "Male", + "lastname": "Schatz", + "link": "https://www.govtrack.us/congress/members/brian_schatz/412507", + "middlename": "Emanuel", + "name": "Sen. Brian Schatz [D-HI]", + "namemod": "", + "nickname": "", + "osid": "N00028138", + "pvsid": "17852", + "sortname": "Schatz, Brian (Sen.) [D-HI]", + "twitterid": "SenBrianSchatz", + "youtubeid": "senbrianschatz" + }, + "phone": "202-224-3934", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "senior", + "senator_rank_label": "Senior", + "startdate": "2017-01-03", + "state": "HI", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.schatz.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Illinois", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "524 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.duckworth.senate.gov/content/contact-senator", + "office": "524 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "D000622", + "birthday": "1968-03-12", + "cspanid": 94484, + "firstname": "Tammy", + "gender": "female", + "gender_label": "Female", + "lastname": "Duckworth", + "link": "https://www.govtrack.us/congress/members/tammy_duckworth/412533", + "middlename": "", + "name": "Sen. Tammy Duckworth [D-IL]", + "namemod": "", + "nickname": "", + "osid": "N00027860", + "pvsid": "57442", + "sortname": "Duckworth, Tammy (Sen.) [D-IL]", + "twitterid": "SenDuckworth", + "youtubeid": "repduckworth" + }, + "phone": "202-224-2854", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "IL", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.duckworth.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for California", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "112 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.harris.senate.gov/contact", + "office": "112 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "H001075", + "birthday": "1964-10-20", + "cspanid": 1018696, + "firstname": "Kamala", + "gender": "female", + "gender_label": "Female", + "lastname": "Harris", + "link": "https://www.govtrack.us/congress/members/kamala_harris/412678", + "middlename": "", + "name": "Sen. Kamala Harris [D-CA]", + "namemod": "", + "nickname": "", + "osid": "N00036915", + "pvsid": "120012", + "sortname": "Harris, Kamala (Sen.) [D-CA]", + "twitterid": "SenKamalaHarris", + "youtubeid": null + }, + "phone": "202-224-3553", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "CA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.harris.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Louisiana", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "383 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.kennedy.senate.gov/public/email-me", + "office": "383 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "K000393", + "birthday": "1951-11-21", + "cspanid": 1011723, + "firstname": "John", + "gender": "male", + "gender_label": "Male", + "lastname": "Kennedy", + "link": "https://www.govtrack.us/congress/members/john_kennedy/412679", + "middlename": "Neely", + "name": "Sen. John Kennedy [R-LA]", + "namemod": "", + "nickname": "", + "osid": "N00026823", + "pvsid": "35496", + "sortname": "Kennedy, John (Sen.) [R-LA]", + "twitterid": "SenJohnKennedy", + "youtubeid": null + }, + "phone": "202-224-4623", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "LA", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.kennedy.senate.gov/public" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for New Hampshire", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "330 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.hassan.senate.gov/content/contact-senator", + "office": "330 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "H001076", + "birthday": "1958-02-27", + "cspanid": 67481, + "firstname": "Margaret", + "gender": "female", + "gender_label": "Female", + "lastname": "Hassan", + "link": "https://www.govtrack.us/congress/members/margaret_hassan/412680", + "middlename": "Wood", + "name": "Sen. Margaret “Maggie” Hassan [D-NH]", + "namemod": "", + "nickname": "Maggie", + "osid": "N00038397", + "pvsid": "42552", + "sortname": "Hassan, Margaret “Maggie” (Sen.) [D-NH]", + "twitterid": "Senatorhassan", + "youtubeid": null + }, + "phone": "202-224-3324", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "NH", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.hassan.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116, + 117 + ], + "current": true, + "description": "Junior Senator for Nevada", + "district": null, + "enddate": "2023-01-03", + "extra": { + "address": "204 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.cortezmasto.senate.gov/contact", + "office": "204 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "C001113", + "birthday": "1964-03-29", + "cspanid": 105698, + "firstname": "Catherine", + "gender": "female", + "gender_label": "Female", + "lastname": "Cortez Masto", + "link": "https://www.govtrack.us/congress/members/catherine_cortez_masto/412681", + "middlename": "", + "name": "Sen. Catherine Cortez Masto [D-NV]", + "namemod": "", + "nickname": "", + "osid": "N00037161", + "pvsid": "69579", + "sortname": "Cortez Masto, Catherine (Sen.) [D-NV]", + "twitterid": "sencortezmasto", + "youtubeid": null + }, + "phone": "202-224-3542", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2017-01-03", + "state": "NV", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.cortezmasto.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Alabama", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "326 Russell Senate Office Building Washington DC 20510", + "contact_form": "https://www.jones.senate.gov/content/contact-senator", + "office": "326 Russell Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "J000300", + "birthday": "1954-05-04", + "cspanid": 1024592, + "firstname": "Doug", + "gender": "male", + "gender_label": "Male", + "lastname": "Jones", + "link": "https://www.govtrack.us/congress/members/doug_jones/412741", + "middlename": "", + "name": "Sen. Doug Jones [D-AL]", + "namemod": "", + "nickname": "", + "osid": "N00024817", + "pvsid": "176464", + "sortname": "Jones, Doug (Sen.) [D-AL]", + "twitterid": "sendougjones", + "youtubeid": null + }, + "phone": "202-224-4124", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2018-01-03", + "state": "AL", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.jones.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115 + ], + "current": true, + "description": "Junior Senator for Mississippi", + "district": null, + "enddate": "2018-11-27", + "extra": { + "address": "G12 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "https://www.hydesmith.senate.gov/content/contact-senator", + "end-type": "special-election", + "how": "appointment", + "office": "G12 Dirksen Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "H001079", + "birthday": "1959-05-10", + "cspanid": null, + "firstname": "Cindy", + "gender": "female", + "gender_label": "Female", + "lastname": "Hyde-Smith", + "link": "https://www.govtrack.us/congress/members/cindy_hyde_smith/412743", + "middlename": "", + "name": "Sen. Cindy Hyde-Smith [R-MS]", + "namemod": "", + "nickname": "", + "osid": "N00043298", + "pvsid": null, + "sortname": "Hyde-Smith, Cindy (Sen.) [R-MS]", + "twitterid": "SenHydeSmith", + "youtubeid": null + }, + "phone": "202-224-5054", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2018-04-09", + "state": "MS", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.hydesmith.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Arizona", + "district": null, + "enddate": "2020-11-03", + "extra": { + "address": "G12 Dirksen Senate Office Building Washington DC 20510", + "contact_form": "https://www.kyl.senate.gov/content/contact-senator-kyl", + "end-type": "special-election", + "fax": "202-228-2862", + "how": "appointment", + "office": "G12 Dirksen Senate Office Building" + }, + "leadership_title": null, + "party": "Republican", + "person": { + "bioguideid": "K000352", + "birthday": "1942-04-25", + "cspanid": null, + "firstname": "Jon", + "gender": "male", + "gender_label": "Male", + "lastname": "Kyl", + "link": "https://www.govtrack.us/congress/members/jon_kyl/300062", + "middlename": "", + "name": "Sen. Jon Kyl [R-AZ]", + "namemod": "", + "nickname": "", + "osid": "N00006406", + "pvsid": "26721", + "sortname": "Kyl, Jon (Sen.) [R-AZ]", + "twitterid": null, + "youtubeid": null + }, + "phone": "202-224-2235", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class3", + "senator_class_label": "Class 3", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2018-09-05", + "state": "AZ", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.kyl.senate.gov" +}, +{ + "caucus": null, + "congress_numbers": [ + 115, + 116 + ], + "current": true, + "description": "Junior Senator for Minnesota", + "district": null, + "enddate": "2021-01-03", + "extra": { + "address": "309 Hart Senate Office Building Washington DC 20510", + "contact_form": "https://www.smith.senate.gov/content/contact-senator", + "office": "309 Hart Senate Office Building" + }, + "leadership_title": null, + "party": "Democrat", + "person": { + "bioguideid": "S001203", + "birthday": "1958-03-04", + "cspanid": 111313, + "firstname": "Tina", + "gender": "female", + "gender_label": "Female", + "lastname": "Smith", + "link": "https://www.govtrack.us/congress/members/tina_smith/412742", + "middlename": "Flint", + "name": "Sen. Tina Smith [D-MN]", + "namemod": "", + "nickname": "", + "osid": "N00042353", + "pvsid": "152968", + "sortname": "Smith, Tina (Sen.) [D-MN]", + "twitterid": "SenTinaSmith", + "youtubeid": null + }, + "phone": "202-224-5641", + "role_type": "senator", + "role_type_label": "Senator", + "senator_class": "class2", + "senator_class_label": "Class 2", + "senator_rank": "junior", + "senator_rank_label": "Junior", + "startdate": "2018-11-07", + "state": "MN", + "title": "Sen.", + "title_long": "Senator", + "website": "https://www.smith.senate.gov" +} \ No newline at end of file diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/select_object.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/select_object.go new file mode 100644 index 00000000..7b1c1261 --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/select_object.go @@ -0,0 +1,203 @@ +package sample + +import ( + "fmt" + "io/ioutil" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// SelectObjectSample shows how to get data from csv/json object by sql +func SelectObjectSample() { + // Create a bucket + bucket, err := GetTestBucket(bucketName) + if err != nil { + HandleError(err) + } + + // + // Create a Csv object + // + err = bucket.PutObjectFromFile(objectKey, localCsvFile) + if err != nil { + HandleError(err) + } + + // Create Csv Meta + csvMeta := oss.CsvMetaRequest{} + ret, err := bucket.CreateSelectCsvObjectMeta(objectKey, csvMeta) + if err != nil { + HandleError(err) + } + fmt.Println("csv file meta:", ret) + + // case 1: Isn't NULL + selReq := oss.SelectRequest{} + selReq.Expression = "select Year, StateAbbr, CityName, PopulationCount from ossobject where CityName != ''" + selReq.InputSerializationSelect.CsvBodyInput.FileHeaderInfo = "Use" + + body, err := bucket.SelectObject(objectKey, selReq) + if err != nil { + HandleError(err) + } + defer body.Close() + + databyte, err := ioutil.ReadAll(body) + if err != nil { + HandleError(err) + } + fmt.Println("some data in SelectCSVObject result:", string(databyte[:9])) + + // case 2: Like + selReq = oss.SelectRequest{} + selReq.Expression = "select Year, StateAbbr, CityName, Short_Question_Text from ossobject where Measure like '%blood pressure%Years'" + selReq.InputSerializationSelect.CsvBodyInput.FileHeaderInfo = "Use" + body, err = bucket.SelectObject(objectKey, selReq) + if err != nil { + HandleError(err) + } + defer body.Close() + + databyte, err = ioutil.ReadAll(body) + if err != nil { + HandleError(err) + } + fmt.Println("some data in SelectCSVObject result:", string(databyte[:9])) + + // delete object + err = bucket.DeleteObject(objectKey) + if err != nil { + HandleError(err) + } + + // + // Create a LINES json object + // + err = bucket.PutObjectFromFile(objectKey, localJSONLinesFile) + if err != nil { + HandleError(err) + } + + // Create LINES JSON Meta + jsonMeta := oss.JsonMetaRequest{ + InputSerialization: oss.InputSerialization { + JSON: oss.JSON { + JSONType:"LINES", + }, + }, + } + restSt, err := bucket.CreateSelectJsonObjectMeta(objectKey, jsonMeta) + if err != nil { + HandleError(err) + } + fmt.Println("csv json meta:", restSt) + + // case 1: sql where A=B + selReq = oss.SelectRequest{} + selReq.Expression = "select * from ossobject where party = 'Democrat'" + selReq.OutputSerializationSelect.JsonBodyOutput.RecordDelimiter = "," + selReq.InputSerializationSelect.JsonBodyInput.JSONType = "LINES" + + body, err = bucket.SelectObject(objectKey, selReq) + if err != nil { + HandleError(err) + } + defer body.Close() + + databyte, err = ioutil.ReadAll(body) + if err != nil { + HandleError(err) + } + fmt.Println("some data in SelectJsonObject result:", string(databyte[:9])) + + // case 2: LIKE + selReq = oss.SelectRequest{} + selReq.Expression = "select person.firstname, person.lastname from ossobject where person.birthday like '1959%'" + selReq.OutputSerializationSelect.JsonBodyOutput.RecordDelimiter = "," + selReq.InputSerializationSelect.JsonBodyInput.JSONType = "LINES" + + body, err = bucket.SelectObject(objectKey, selReq) + if err != nil { + HandleError(err) + } + defer body.Close() + + databyte, err = ioutil.ReadAll(body) + if err != nil { + HandleError(err) + } + fmt.Println("some data in SelectJsonObject result:", string(databyte[:9])) + + // delete object + err = bucket.DeleteObject(objectKey) + if err != nil { + HandleError(err) + } + + // + // Create a Document json object + // + err = bucket.PutObjectFromFile(objectKey, localJSONFile) + if err != nil { + HandleError(err) + } + + // case 1: int avg, max, min + selReq = oss.SelectRequest{} + selReq.Expression = ` + select + avg(cast(person.cspanid as int)), max(cast(person.cspanid as int)), + min(cast(person.cspanid as int)) + from + ossobject.objects[*] + where + person.cspanid = 1011723 + ` + selReq.OutputSerializationSelect.JsonBodyOutput.RecordDelimiter = "," + selReq.InputSerializationSelect.JsonBodyInput.JSONType = "Document" + + body, err = bucket.SelectObject(objectKey, selReq) + if err != nil { + HandleError(err) + } + defer body.Close() + + databyte, err = ioutil.ReadAll(body) + if err != nil { + HandleError(err) + } + fmt.Println("data:", string(databyte)) + + // case 2: Concat + selReq = oss.SelectRequest{} + selReq.Expression = ` + select + person + from + ossobject.objects[*] + where + (person.firstname || person.lastname) = 'JohnKennedy' + ` + selReq.OutputSerializationSelect.JsonBodyOutput.RecordDelimiter = "," + selReq.InputSerializationSelect.JsonBodyInput.JSONType = "Document" + + body, err = bucket.SelectObject(objectKey, selReq) + if err != nil { + HandleError(err) + } + defer body.Close() + + databyte, err = ioutil.ReadAll(body) + if err != nil { + HandleError(err) + } + fmt.Println("some data in SelectJsonObject result:", string(databyte[:9])) + + // Delete the object and bucket + err = DeleteTestBucketAndObject(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("SelectObjectSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/sign_url.go b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/sign_url.go new file mode 100755 index 00000000..8a73f4fa --- /dev/null +++ b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/sign_url.go @@ -0,0 +1,80 @@ +package sample + +import ( + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/aliyun/aliyun-oss-go-sdk/oss" +) + +// SignURLSample signs URL sample +func SignURLSample() { + // Create bucket + bucket, err := GetTestBucket(bucketName) + if err != nil { + HandleError(err) + } + + // Put object + signedURL, err := bucket.SignURL(objectKey, oss.HTTPPut, 60) + if err != nil { + HandleError(err) + } + + var val = "花间一壶酒,独酌无相亲。 举杯邀明月,对影成三人。" + err = bucket.PutObjectWithURL(signedURL, strings.NewReader(val)) + if err != nil { + HandleError(err) + } + + // Put object with option + options := []oss.Option{ + oss.Meta("myprop", "mypropval"), + oss.ContentType("image/tiff"), + } + + signedURL, err = bucket.SignURL(objectKey, oss.HTTPPut, 60, options...) + if err != nil { + HandleError(err) + } + + err = bucket.PutObjectFromFileWithURL(signedURL, localFile, options...) + if err != nil { + HandleError(err) + } + + // Get object + signedURL, err = bucket.SignURL(objectKey, oss.HTTPGet, 60) + if err != nil { + HandleError(err) + } + + body, err := bucket.GetObjectWithURL(signedURL) + if err != nil { + HandleError(err) + } + defer body.Close() + + // Read content + data, err := ioutil.ReadAll(body) + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + fmt.Println("data:", string(data)) + + err = bucket.GetObjectToFileWithURL(signedURL, "mynewfile-1.jpg") + if err != nil { + HandleError(err) + } + + // Delete the object and bucket + err = DeleteTestBucketAndObject(bucketName) + if err != nil { + HandleError(err) + } + + fmt.Println("SignURLSample completed") +} diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/test-client-encryption-crypto-cpp-rsa.jpg b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/test-client-encryption-crypto-cpp-rsa.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e498969bc41f1f5b8c942d83a3336abcf92c9560 GIT binary patch literal 21839 zcmV(nK=QvNRg_Hk4x+SM2m7F|ke+1Mn>sO@qbs06!kR?lxDnz$rH(!ebD_;iszZ?( z9X28*R&M_hj);i=S8A1|J0k%OiCzd&%&h%MWX3ez=UNss>0^*dHCtQ`o2FAxqM@(% z$g%$vhuUs$VW2I(XGcY$j@8}WC)ol9pA@Qr1rutx@oXd&V4D z(kX-HzgrsFVWuYPH8Mh)?yjr>3<>NCU^r)*P%yc?R?mMnjx>B;d$G3gR(E3`q?u6B zV1O39p9f7_%O*!2e3d+nZZeTP9U06;#wgj~IFvW^H=n&GaJI_52-YVjdy1YVutMLb zIAtS-xyD1;%0>+qAV+4$V}>L*KF_G7*V)cw{!GjgvxLRgRW7Zqxy|8tx1Jhgp7C6J8KCx+(|qpBcjfG!0N_GL1v@H0 z=k$tr_ta$YFUnR9$Y$VC>w^R=W^8rL+WTiGkb-D&Swo#~2;^awGI5?F*dP1?DC!Au zvm3iEJIE74%HIpw`p5@~6*@@OJ)lA3p32#P%Aw4BJMb~`T+6TJLb-r1)2ZCN3<*_0 zeU}5b5s#i$(&>WBA)rks-j>dMp2mq??z1kkph#O955?JbL`#G^E+bwTaC5MMI^UwI zK`m8RT<#L}ZVXMq9b^{YV@%f!t{C>C9>ZQ@pmbqO0JQO*SHTbuJ zYOA){<7bVY?)_|Qb_QV?cPH8B9lrrII(ILT=EDK-Pz+~Vw%aBx!KxlJ;Yj1vHb^F= zD}r)A^ISCA0aV>RVa6v6-^P}XFNs0Ke;$W-2y_BV#Vn9%StZUOL-{oIXwb1IXqdh?*TV zZZqNPwG-s-B$fY3m;(Y}bhICms8tu0QU{3-swOh-sMID$-LGgZ8t%aD-Bnz#6^or& zCC5(wI}g3UA=m>j^ab6o(7_ExW)3kK@HYvrXmA>#5ZV%H276U0p;@NkU!$s2UQT1; zsURwenpEc3q!U$V?Ql#2*@%o72QOe9nUD}}A1xc*^#=s`7Lrx4)Mq!5Lll+~Lu8^v zw7#YfnhTz;NE2wPGTL4)NCl8RL46&mLvohw*7_B-tfnrQj{K$5xOUxG1mNpVroa#vehZd3*9zU>8h;!ktN8~EtAwhBiK&k-p#2fvPwk%LLa+47AbbF^Ct>DSKsllAjvI|pvG%4U9 z=e=C_e`m!GvinS;(lVo1*CGl(A7rf`cw|ejUgrL&46+Yd+HcagO=HNTo`;Zot|%)=pL;&s+z!FCESmzoP9f46-jsnf9K2Dv6q)T+i$))!b%u&XEfG zml-Up?4BosT=t9W(@dm@2XFN>QfMV7{Kf(X9z2>!Zh4eYn%DoSR7W4s-H-e*rrPKr z1+wO;3Y>)nM;MR5Wr1wMkUh`9*u$tz9XefQ!F zNqauxW&el?9;)_X$JNr&mU~+O`IC+_K!cb08(yCa>P)6iX8Hr0PwimQ9YHW?k@a^3 zbNi?pn4A9M*b-;(LegFkq|Z-QOJ=Y@I5#K*NDQ(d={F>X12Z=ahS?bvG8=?5Q}J+1 zy<;55euT@WaLkSJ=vEMDseq(DV#q0k7Tk~C(oFRz#OKv70+By-K$8W=oFEG9zYk)) zo#xSJnTuu(r%Tltgy;&F*vnkR5Q(2Sa1wXQcXVOtr6?*y{ljTTKY%0FQA-Daq|WPV znqmFFl{retR)PFm)p0iqK?GhPrF~S&{*PO%W!PMDu$6sCl0LLj^u-6XORa3DjGU4p z|Hf%v)*D01n*9NK92aZ|Tt_+UKs7fAcdG8an5~W~Q%e~zF^40c4Y1zd!Le+dz>h;49aSsfj+c% z#rajMRy$SU$rRp|-BTX(Y6{Z!24dz7c>LGy>lH4djxo6{gq+w_=V16L5;XpwIe@bh^Omw22WU?8v7{lhbTN7Xy}MLj z4w4VCaowBF0A%9!L~8+OH!>$2bNrD=ui^E85+6@X;zu+)Su8L!8I_8(8QJm8>`M!rondo9kU@&i?r^K3Mig z_Mz)r6`bc*`DiQDyZ-bouprf)`*rGYuoojA+={tA`iOyttciW@JY?b6Zu8=I!2$>= zHy-kXPw)Opk~C{bTCLYQb%mp(&QBULPlb}Y_p)e;;}g?BTD1Sx+WKS4CDs`KdTuzm#UJ=Tx}^iv?LR(fmG;)>u7_`5Bp%s(aV zj)2b8;$GiGbyPo1sUc(yrLmd6S?Z3bZfgu0D&v4FmkOvO_YVRztDmiR9>!A?FMpZq z@0tVi9C3JUwTuX6K#M0!tfy{WCm~Wlq)OfN%YeYUdSY1|D=3HkFo_I5_QD6-Vh3pO zTs)UgPEHd2W^ba|RQ&=P{&vC|B*TI3Q(iQi)Np&LAmsyI9H_9t9Cc;P-98m$Z-9Y` zv+L<|4I)-wNZ~B85YP#|JK&Jd{_uE>*&{;IM3=#i?M16^fPL!-tJ@O>c`Kb-aW^jbd;6A$y!w#~v@li?pC#Lg4B2Y{L5kpj~U(#Hme76h?} z^(H&3t$ZQxdh#dI&<)vFGh{UzQb$(X9SS`j3f`JE9$MKc3U(U&fZl>PcW=pxNZDnq z%=VD=5|`ujZPmLGw)|ZZ@Cozk@1(H)yD;aR?%?ZH#O(mbzod)7d`_V3mAu4$OLb4A zi&A?etkz*w2{-`X>hv^LH75=qWC^PbGT-kztET;aIcBL;aOiBUSr69`s%&mSCRQu^ zGk8Q||GUG@Qc)cf4WjGCJQvc9gqpzqu+$xu@ZTyc^#GAZu3dK&%B*?noVJsStjn;@ zF1XeTm%$%U&=96{Y)zV;YCr|BfeCjo-4n?!t({(z5S=?7>Q6h;;s<&{>jhPCzZGRR z`%n4PIt-o)oiPA8F$>t&gRMK^AT@224VZMw5wf4(2{Ac_E}WR!?Vo@YKCqul$HUZg z!ReQrEc~m_b&y!HbH1`5Ecj0k7azQ+rGV3i_m+f9eD`rsCpam9TrO)lXudG7159~~ zucWYf50Ky8N%-bl90b>&9M0QOz0z=z4i|t7D82%2Q`J^X#^7~6T$+cB_#j>! z1gecy-U)L)U2UNun&-nDPmR*0a5kdl+$$8>!z>xalaC>O*jZOtC4rHf63{8NLKzI& z#_``^dm{ZG317!jyY9$M&~IdGirs5u zzIzpwwFNWy_5@hs%PWZkWP+KM&7-gXOg4%mzXW(bPP7pt5vyRkVLEd4s?)yb{{vaL zWieTk__7jr2jP!)#vU&KXJsn?OvNtSHL$@#Va2p{)Yu#u%g2A}woUy(mG%WyAJe9J z`|FOnL_%e5ugU>x-S^6gjDj)=1g<QbQS*V zpJuMb0bA4>=c$;QcNw%21>DTt(eqY&0Kn*W&VH;_cGRBWiS^?PWYmmo&I?k!+1qR? z08K)k>GK)wfSt;><`nt$YlN&@ZJvHBh!B^!y6^r^e8$W_s4)Q2p!!dO8V8heC!+tc z+riR}z4tUR+wn>8srdc0juGi%JC#A>@CTfQOjnEjS?m@ofV9zv2c!un3pP~hZEO(& z!5a*gzd8Df5E!$BVy9W03NZ|>xtZsyB-|BEZQ9|Am*vD0v?agBKFs8qm6eE|t?$~h zZ3&%l^YT{;kCZr9Wu?5NuL@*M(LCh6VwdI$c;%iSjBTSmM-An|ntb?M&uUesS0|HZ z1*983&w5|@y&k0#>Gd~q4%ZpWlMIvjQ~>TCMX(>xK_d*5;tk~1a1wYxaF@@nMq8{^ zK`XEB91pyRq12vBwvI^9l*QFlVJ-;5zJz?&ncO3zO>{Gr88#^U=6?LS&J@DC zDn1q3JBRTW24EXr$3_S6BcHUCe~FNsiHD8tYH=hpj-=fsDbQr7T`N0YT@8V?sgsT3E_|^B(vqx~0l5y41(5Mrw9vC<{aUTCMN;td?E!%2psCHq?4T8}B_m%i$t^-k20 zH%L|+oM%2?5_!K>f$%G9XN(OBBWkt0APuR;+G=KUecr#KBy_eK5z`L|LJCK-oS=_>CGV3AE@v;`%E$(y4>2#XA$#6>}@eU27fVJQ1 z4WOxw*qszKgBgYt)UUk1T{0SB?BKkol`3!KxJx|$8UV5N|cs&D9Tyw znVCN;|FCy;P#Sf0z8p)YIHnjZkw8f!fhN*HPVl4th8(zCxxL%I z99NC^#yMJmN}peQ9JMr?eKb(nD`fe$0wivVD_?qW|K*)qo1)~Z&Q5e2liK=#7Y0Y* zmA{i>!P8pI{vQd#s`=b|<~nAxE!kdx>N>$6uqKN^2wm-o)h*xPGX0I;f5t_7^OGDY znJ;&V|NL4+*G~1L3I$!nrY}3cQ4&6b3WTaA{z?@&vD=We>?w#mm8M1Q#|bZ}b?3@O zGS(PG4biM7am&PF47G%?L9Wl28mQ_;ji&{OO_^tkSz!~LsOOL;;=*N}0Ui>m ziMa;LcY29FTunW*xY=91=V_;=OxPoq#Ld5o=2q}kg)h89*4qp~oMFz`nWPwe85u0- z8zlH>V0$6yC_dApy2L>B;hH&^;_tyB@cNZa9ub5}NwN8bBl?ay43HSN}2GgQ&D!6oFHFP{eA-@T&M@I+a0l`Mj-M_iV&^0rD zYBce03@6t0J=<)NU5BeC(VWj$%+mksvRF1caCcSA2^g8=oG(^4f66ZnkGHY^E2Cs7 zX^uNlGI$}N8NXU(bD3kIp893LL8wN`+n0pKUffx+G_gG+y0J7nF}zpq-%iPezkv~c z4iuN&BwMS}#Sw_5g9`lfwuV{LRoAe@n=6)mIFC(^KNr1zY+fMKw7gLvl$~!X1V9_q zUpHBOyR+r7jbJHebuOaDBov6mE?<*vK(@ItClCy#iw*8rGUI~B>3_lhW!>{d5q4}S zNk<*`Gx$%oHfGflqeDZ1LQ7ok(`S5ouomVH^Od&)g^<0Y;rdpGIr69y)JmF+ljs5K z%w5sQ^&P|!sgH-_xqAt`WIEW#>j%S!!A=l*?Vc`)LeckGdRt_u?u+kI~!&*e0yq4+ko?v>|j&9}oJ7dt>C zK5R||dQb*>8VHv3+}9B0z$tV%GYO^cKGYt1z}zH|KgMHzng_eMMmbEZ#{zu%Y%@)% z0-^E>|2SEDPtGJNCodHVshV+Rah5J=YxI_7?8mHKL+CvB%{=rqJ0O2N*4L)1CL`}c zd%D)fBdlIB`a732g~DI%rQIZ6rnD}rg~wdOcP6_^y;zO!I(4-^x{sXL{gyQn0Zvdf z5>t3f`v~59d7X_W?&aW&(Sf`Bvu~e5poTE?e(E{BiW*{zc+wGrJUb9Lw8`=MT~j4T zMLS`SlTNe=31{^1>IPclaXe^2QIfM+E`SZs7PWCzMI>EDWa*F%<&}!3{Vg5P7hmJX zVKh~(BSpP47te^n78nMafsl~wQJiq-wKFkD1&Kz<#W*_0P_;*9KW-k^eTH=gKbC_KeLoTQc5JIV`;{jr0C<$&e zv2?=;CV8&lzx?={QI%w8{EPKLKYfgn@6i@5{rKqdQ(O|?7g_M^bj3p3HprZ%GHHrKYuqVMNg zB;Sw%T}-3+E?3l*94DKa?c%k5{jQ)}PxZA(a@a&0*!Xe5^~w~a`Uu$`2V~w ztbxg^tNhsd<<`vNVgb^(BH2VX#lTn6?8n%*)11tBYa)FmIoH-aXqKSi9&7N$tj91J z=B{kOa$O3JkMoJNSfi-o!r@0mga^t4m z<2_Y{(X}I?^6AIRz9bMCeW)${=NSN}9zHa=;_IrEUE5WNgf3iPuL} zMtZc+13`T0yjst;VW*%E36zj&jCV+jO05Bpps#wrRbGU2q*x6vO_zMc+STS;)uxC` zM+f1>4cLy4&)CbOA{X%klaKnH3`8|ngqZleq>@3a{GNj(H{zVMrX$#m3d~LR9$u{T z6lGC)Im#X<`Z?#HAxluAc14KZK4&zVHFul5Yy&9K=3Q#4!X2DTsX7$^bEI*qw5F!c z%H-e@_~Z|}LP?Q(!}k1hkx91Vl{vaeHks!LMw=Z4Dy8}xSbzs%HXDNTi7tP;@bi?f z%}9AhYpKwZ3Qx+dOTPyFen)!+FZ2g`JjvIc;yj}ByYi;fQbwM<*e#dB#U5b_o1r@) z?ZKsK;7#+b-9#C3i@=I+hGo})6BJIpyY1^m`}n`va8`m*vGe&OVQ}n&v|Z6Ga`b>< zvi>|?SC(6Qv^_KIynE5cfMi|dvw#@rKz`uSMoT{42(h+H<`c6On4m}QCjgzEo6`c| zGLtCX9fVFDINF7Kgp;@t+H!7s=BB~xUs#M-!oK@deEBpJ?L(@Dz;wkr?WE3n6)Fr9&V3yE8;`Sb0mS5B339RT8Ci8_`~7_{3cf z%W$0}hGTD!KY5=5Q8^u=mi5>HC#OITwChKwUWHNHugjj1*Abr_^*bMQJG|I_e3Dij zgDGe_*=0-2H+sX>KWqe`}n*S;-?cmg-yr9u)1atuj5S8Ax5BzXor z@>?K9iyO7=osrn$uFeFSG(OgY>bMmFI8IRJBrGW$d>bGlla&nF*?)pH&hIr8#q!gM zNNiyd!?I*MHy$-%ow}Qy@2Gr6P>Vq(+&u1`+d)2p)NG9QNW8EYFjZFw=}P|vJ}mT^ zW8?T`C|#MsCrxD>j3D2nrxDnxwz~^A(y45_TD7i@a^{A63Rp z!Y&NmgP&M!#X|BUdIwc$YG$`1KdGEP+zz^)iY8`XvV)J4KFo=!32h-5O^-U(=twrzF?$Lm zt@zCMTzJ*B&=%$4C3y0=KK_&gV>4Vb`GM`viMDp8C86~s;rKQ(!Y&;s)Qqx}r$7Pb&15x2;97o@D;)A#*a&RsMH4k$LWbzh?X64z2RO*ZTOlY~v+ux| z8m4(@A3=IgR;m0B1IHMW67=spmWz-a@{T^@sEvH4jG3;+8E~2f=jeQp{?AQMPx?*i zIqDuvRVJ%)#}lg`RI+;OTL!H8F`Ti!-vgx0NMs=X*+iO$tVDuW9|h_>zC} z5m7^)`!KLKakG=2!<9HI$G4S1^z;#65%iRpDFX%w@ICaizGT0bE$cz(rw?(4p<<+V zjSIO;wxDMgo%Ka)_oo7tvFiM!Y_Xs~RKf{P4j9A;Za`N-NTJu(!TiYcG2Qo{wWx&9 zOy|;!>Dq@{c`Z30iHq}gynytS9P+N>x823v>TJb+SV&z%H+c2=2yQu4nYjI~-}mfP zTa4&q2QOcDV<(k;MIv?2kz)@Xz&l)b4<6y{bX(y*VVEVkBMN5oz=^)g zbQ@>kHb+S*_=I6|Gi(7UqP3ET~7C*l63x?mwk|XfRUiEzNrtpuDacO`)G8&>9L8Z})`jABL zZCbFX?7Uq6=;lR_z+Na`YB(KGA)Rc8j><64TmE$e#iu?4(u*j4Q(%R=YdKKA@m@oY zQxqcL;(W$tj5=Idi_2oJ&1F`DDt<+1JCB{&Khi@_|8Npl9whrvNYw2TT^!vea-DO44`nYV1{FE3%tzY)0 z*oGu;V_aQzcGuu`z&TP!QTecsWcdH7;_RYSl*p(w)Zc1o!IOHfJDe_uR;Xj z%eJC{pyQ)jm;k^WCEBmdsP%e$3jMGvYb6p-#O6!bjbMS%mw`8lMOzLK13=8VG;*IB z;fklYB&=1&a3yyi7qys*iNDU{Lv{j6o{q&nL{SHF4(jH2q-IU@v`8~6)FVKX+tUZ+ z?{d>B{eW9N<3Ha*wSa&<2%PhEsXU*&0W;67KDBeVgAn#P8{cKoEH+CLH6wc$@H8A@ zF*k+fgRH)Wf5O0VGS_!je+caG<-nb7x;Gv1Q1oB15IONX>l~vC`y6tjYq>+4OmI1C z%CEj!(C?rZ`$SF*Pk4n<`Hg-V(L;+{WDH>>#1=~Fn`joi*2z|7*i;Nas6J9aQ>?=x zVC9C?2D*m4a%c(m1hJ?!i(?KmUtuEgO136Gz?EU9;RPsIlwW^*!yZ0&uUlI&{Dz!C zRc}zUO~_v-Pg@_Bd7oI-pb0Wwf37e$LPi~@rKIJmei`58PW5)&g7V!ZjuU!GCF3<) zgLD3%mzwRd(}ORWsBMcLm(LkNz#iqPFmef_K|*{v<9!p9{d*`d8FL0c#EL-G<%w$; zZ}AyC*#57y>UMI^1lrSM4Q#6;vvueC&O-+{3W)r5==(mrm?WJ&1Dvar@%MLgX!ubg zyHo46GL&nOer<{C1H!89Y3x6YDFN8KT+Yw_zCtG z!t=U{^e0B~*(L#oCR8k<47>9(k2p5sSoY&dp*7$VL0YFMXt6RmM~L+bHBhfJz$g_O zffH+kq`cSITi*;@w8s~1^nt&MLw)%t_$?CGo;o}8nO`RC@F>RTg$7o>M-ycK4b&-u z9+&h;%!59mA3cWGm%<*Dkx(@@Do>1m?(^e#WAr=f;&LEmA9&k5V5)6%G#(#&Fv7Rp zluejPk|yEo*)@1pPX@hknRQ`@0CV)W%a;8tn&N(^?H+=u3)8C1iF0+tVLHR=Aa|)0 z9zjX)xOPQ@ZI*^KLuEdVTof9MQs8}1+DzfVd+ZJ~vT2A3G|ZkUxf(m7fOG3C$)|~o zzX;nO4^jh)S%QIaC4c@7)841O`v;>XH-~2Mq?9Qi`L6rJh{lL#gfz=ZkieYS7LRrA z>d`9x9J?(|bE2!=d(PG}>r>FROidBdVOlQKpE~$+pWKU4z@^2Agd9>8Rm_VMi-YIL z5^+lKCB|UO(%6N-dUiu&lnE3AZ!&3M!~;`=^(HxBnZ_cLC;e{Fuu-05N9LEtN}Js# zWM45v~@-$B#-$8BYNE@y;mB!^J?a|L5S_oph zub0!@C0vV7YuK{^&ArojlqgnVX_x>kxo<=*HtVSI9NFuYKAdm?A1=yOpd&0(_Q6FD z=G^5nTLD)KUR!)bEpQV{M3l@@#{2JhS6ln_h*cmT$wXxHq5E$qX;;2}mQ*uSU#Q}F zOST}fW-}&88hu^FatAPHdk17xF^`)h_$wEOPeJ=IDvsNE0X-o?b-nOsz7?)vZZ6|7 zXUcDP+`{dnrvh5@6TTfbz#x#7|N?cAV~K9VG7cDPiww z1r*wa(@qdYy6nE^VDOId@qm*U7a6>n;x6-l?IB)p)9QPaw}L{WXms)S&@ruqM`!DX z5A3#zGD5V4sR=&kexyB z9RvdhcgUHEK!-GcIH#{)gJ9#ZpUCFe!Rv3tsK>ee0>`8iHfNgQQQm>$;6790OnDf* z9=bHQI|vn*pF2+bpq|o04$4)mx$S&>8ns}01_d{4{%V`4TF3m=jnGqL!q#BS)g)!C z6WBzb5X4l;ELfc9?>9HyG^Iz@(6R3hSMMhMkFr7;wsuWHm~5~`{FKE*o@=d#jq>%4 z(@BM-S>HLX*aDx_OrGfbfs^XppW1}B!0{&hjJp|)<{}`Mo6YY6^F!dheVd@t>kGXB z^|3DzpL@6CB9WFeD6o)d;e}`sdZ(nh-D<0t7P8jlXOr2TO1s1Tym=Vtl`=zVoMXnn zHSz{##2C0<^HKp#k0qCWpZdTVrWIP}2j1$mKu*%iF&EzG-9V)Que}^hvgQ#woTmh^ zBbmTh-pul(j$iWUqsAD}x?=)q=bD3r)EJRzj+}&ra8_DArVVF5ZS(n3Qw6nNzWuf8 zM?RVOL^;S7n+zb@A+aSL8RUlbgrJc;JocF1>`J_5Bl#tTsT}^WRHw*Sp;Kk?N+&6n z)X>O<$U1CED>n1tJ-l0ghR+ zap$4gk`5qFjd{Z-TUq0mcG7)q>~To|UC-ovd#;!M;&5!31%;JEroqAwArrkYxW_A* z$qP=_CKC@Wm77)jk5qVO*6de;E$Q4tE6EuMem+eXYH?{T9^Hj8!-02`$))PKCT3rPag{G|rk1jD=R#rBrwr&H~qwS{FDjgt2j zT*!X|xQrP|DrZaghV}4GXcXK@;S6t?0@t|fpP;_H7pY(`{V{0jTekdBXM~7D`0dYk z%T}_%=SdBM-eNCrn4n{z5q^p;3Y_c9_ER54CRr+>WK+!!y!Ux@ZsORjoYyzI;FWu^ zhxc@eW1tJ$ZJ>M;Zmrb_SNhdoAq=wYM#mSPUJZ}h9dm43&6`skQ`M@9#gE4%V&zrn z5|PNnM4WOt%ywsf)QvJ)P+SCl$7d5lxG$bv*mOnCIoSD~|5C>m3JuE$h3TP_hiOF; zyXp0KubX0};WUF7Vl6)uZ#M_T8X_mjR~$s42@of?jGne0g%$N|FU~dY`Tfw$2b^F4 zx%;EN%(Wlu?mz6OHMt*#pt;yxn;~5zqpg3%l9o=cO<Eo!l4*10R##bIIayD`*Qpcwy!NU>ImKG&(1N0t0nXZ8{GhzkbOd(=ZmSX7Z z)PBOJ$5601--;DdF8xEC+H9e3j3@)C8g-m3B{eg_Fb?asXnU(WaH(9|EpuF1Ml4PG zxfI*2$A<5(CwdQ_|5^UH6KjWPwHJOpZjSR<_M#p%6cLiXd63+HV-GNRK9$7rh z>`TwhW_t!MUo8V@@rJFwGPJCnyEg^@ui&RZJ>@mV*pe!A6-e}3>}L@>v{9UTgeYVi zCMT_0%i!_1t}0TZD2F3{mM!V%+E9Pgw+V!stI4m;j&RoNC7y+XP|tUG>v9!ribKTB zfDY_qjaTnsE}bxZcJ{zokB84n%g{{P<_k4# zDG4&8*I?*xlrKjZ)tC~w>G15#BAN%&Ma2oq5p`Knr36Q(tek4)#7_@t=s*_q4s%2E zf@TEDJtFAHiZ8VJM47rbb7>?P2$R0q|w6L4RT@8J?0jOWm?yO&3^5F1w0g`w0=+|IZF`S z3ay+>o|6dW;6f)sx{N^sDAtDZBhWo+m^cmxyqvY-I?|E$P`I@J>)+P+PC6heXFUrC zYQ(FP;q+HkHKmCjy=2w7bYd-w?OWu@{YuafJ@@=9g9SSL-a^kIROLsQI=F3Kx!asL z7>`Hbw#b$`pZD$jp`Z9Tig|9cNz`Q$owAer&X1ZQI(3c^( z5tI|UJdb0~@?6}!-2OXG(Ci#q+$XVGN5h z^G%OL4H)2@&+X#)@(%pZcw)560$4|Or$Fl|7sfw(kh+i&gUPu>)ZTnoAN$u>5J~tK zLZuHGgr2>M=}QV+m|@kOrTCDOkOdhP%UV5u(;30WH~PZGfvFK?(EqfxcKan@?MIuX?uz^tI6IJ6*bUF#q%>b>hj6SQF!XHEPU z1~T*wpmpls_%5W|+edT!6Ba%t~v?YrAz)oy#mJ1*jbGZO13dLn2@~Qx^*8a1bESB5r=e$Ly zL6>;w9-9_Y+mQ)JC+rYr=(6^+0YQ$9BhYdh|W1&ua zrPSI1soXpp#$Oz^PUiQIjW&h#>d4`*xQI&GpcI#L%e*DzJA4BFe(gJC(>nHO$nTB*gpy9@~Y+{;@fL5 zd7-`t;07OAnl(_}8m51`{yku7_E+&Kz^=HX2Dz9j1jmj@XDE&c21V@Ck1M;N_=*`* zFa`OF(S=w}P*rTnn~;)zsI^*Z!6K?#*NdTB!R=zwbek8RTxRL^(ZIsba%buRn+5x$ zf!@<_x(M9ByHGD@z%||K@S|i}5kfSsG+*z7XJY)vmI5_&Bq&lDR8QMqf_7+~s43sp zu5M${i%_}CcHO0ROwM2DGQJRusv67vzU>5}>)v8B_A((KeP?uqyZZSf{+{LZ83%?{ zrlI>`6IwDLb$$r#x#fK41X2BEAX0tfOQ4p0hFuf7VSc`ul~c^PA&^FZGMSKguRiqA z5V0B9MBKD-@;ovorRQ{Z!f~#D#2*E{Q9RgSs@N3`O4Om7c~QUEcv)IErI2V<$4X^mQj-sjxCN)5x2(eU)fsjDRbyJEA!#zo45O2+p7}) z465OzsCuzfCTc)^l!a{d0u0RQKTxKnsG;tn{G@NzG|PS*?NK6W(VzucCphZd?Tthj z%L0YGB5k#;%9O40 z`{8^r@ooAn86S!N1Zj=8J|S?jh04bIBN>FmWjMj zq`&2P#Oxze6mr`5$P(`h8Tm1H_D0d?51dRnx|cWCMLU^#Yp?pyN0)g(5Gp+4XYxeB z#iv!3+oleau8gh|;a|nj7WQiR!-tbrl)hKmh@hRr^P`TZ@Y!-$oe1-+3NM*Y3Rena z6I%Q)z@~ZHHDq2Wjf&$o#=&#ez~Itow8bY%V(WjduH$uH-1v4@!R9t4i-(;a0G>Ty z?r=jjzBfrqKkq$GUw5nWi6}yXcgcBp-bmfSzq60Ay1po55#|H9^5B+^HN?spQ#W zZmQG|T;j zvdL&PU42-v5#-y}=jy`b&{Q3MI*b^p=O`6Oq$PR_zker0*5V~GYIgZV?~RXkDcQi* z(jMm8=+tUTkrv~$<-F?DpED2nIbgo~P)qdF@~1YndX6t8NjWv3H54hptF-n&>B@xV z^3-Tz9!7@c%fTS(46KHZ{s%`74X$mqf@fpr;mVUG(5W2~p;?}29Er7}G&+d-tvVQq zn)gHRo78re%XY4yQEQO)=XFT?`sFaO5w+GAJk|!Nt1eBxI6fhehbgMXaP9{!u30FY zlkFC`Ttu1H76OdkKY~E}B-6^&`=s&if0}2tV6*|IgJvAXVju^=qO)MYax_s*3P|Q(1;+0h&5~watB|dWgOl z@_Z~QEEp7*l3heMm4MF0@mSY?2ft7iTG!2?*)t6{OQN&O5tF~<&edltUM^!7S-~yt z5?Reh6FyE?fh$dIwEhH*@tjS>2X%09#`S0n!u|~jNGAb{FWi!fiOH@tL4!T8p99I1 z2Ub-n=tRbdxqt)Qv4d_0a&w?P{Xd{)Im7TBoxY8pFw8oe;N*_yaXKtv#8KD=ft3Mk z@)an-AnYg>B{Y_w<$CvJ0=z58`xv9?Oc8DvW=Qz-sctge>wx_fNj!IAk}le6I<01# zJ3|2LIOTwXjbGFf-1zsJ%b?70D=U5=q6mBm2$jjIrEgM#ah-LeI>ULe*q^2oyKM)D zO#As0a~1+!0ByY{6<{zq5zcabEKkRxp=FGwMQM{KDXr0lP7bv)&?CpH;BpO;61U+e zstQ%7Rx=3?KF=hHB`Oq#4g}7l6yl)zo%iqL$u+UsU2~%~$v9u!2cXw;}z*VxPB*DCuQGNi@}8?Zehb|Nd{WMkEONU57GekqVuZ zTyxl^D<^D%8QVVtHsY3VVK=uVlAKVelO^~zF0`x#B6x!G|+tO1>kJ7OsQfZBEf!aup!Ou9qtFg8}=gN8v}z&_oJTcQnflyqtl> zDV6Ac3Af@;c^nrwCn41$e%XXqNJ!-$Zmv8fD5B?N@_C7feaLDxie$(+xpdAh=VeFAfiWu`cjh%jIuI!dykaCGH1DCXXNV-jr!qMH<3 z?>_2m6&9NC@izuk^!F>^!U%o^3n80qaPlaMd=3avDYLIB(H)nDLShJZLV5zTPB?bZ z=PR(dE8M!UR!U$H2)@UoVaCY&0_gF#<+eXz6zHtZBtrP*+EPY;AQ|qFw9>_^Ut?-U zs7mu`-6O*EPsFHfoj@ykMRhr}wMtH#W4}klv3K-mc+!n{Lon~)0F>hRDh#S{_;}Q# z^?R7Z+|PXHK|Z-V45;{bHeroqIt*G(&Q+uGxOY-rw9F{Ta6Lxx&u6yVcpq$xRw`#s zfiygkoG!@Z2sZ=cHni##TQgT|hCX7xUsR9RMxX0SBXzVT9^@_yTR5{Ke<75}{xdW# zY?61XF+uxDo2hCdhL8oJsd08mxq$Qcsjdt1}Xk+7% z11ddl?f&B-X#H(&P^NGBut}E8i<1t8xxI#w;WjlYVglx$69U1n_=i@Sa4JTocnML z_ndeqETOjJQs#IZ1Cdl$)bEY*-gLq;rp9XL&8|MB+=H!@o_a?#<`@g5F27JkKj#BZ zjIY}T6Rgc{?rkx(=&j1SG<>uz7W(-M z5tWKK2nWdK68nI#C<7#4)6rj*cPJa|V);PNVLnLSuF5M29`(pXoSQH|O2#zgC>_Gs zCPwcWTr7XQ(rq#ueIxF0kxtE%>HZC>`THmnvam>$4D1=1U@sw|LlI4R2^%;(fiFr^ zx|>;XE}86WO_hB%GJ!;}Y<#Q=q?%GtjhILu_>@5zj{OOBO>>It?iOKIX%L#EVJ&ux z0G@{>pdf5glDVxT2>=Rh6LD$D2jK&=ei%5qb%};VG45@gtwz<-JsfC~8zDk2mYesd zgaa>R)k(eF9CFinQ(t{K>bg$DE=UB(n0zTSLdvHjctvGU!<@#+qZgnea3Fa=5uoIz z_J(m^nS3Nt#T^iAyRBET!h3d;MJe^G;i1y+1%7t>IaK$JzC>Ys6%7R6g~V*3A$AERL!FoUvY~MIj%MdoUYlAsd{fQp`zz zbKiu^8_;x=+cDrf3x}aWr+MFABJb;;)X@-9e|mmQ2M9=ldgvaf%2(;6?v%5f&-Wb% z^*52ip$W05t>)x9J8fV)M3y10q{_z=0YVkM>Z5hQM-8y6V~=Mv}U+Y(j*>c-?o`{de-)!gSV;eecXdyUZ4$q2@Mu^!{*CV@|OcL_Y{YW zHy2(&eVi3y)t6v-nu;u$#RRG`B7nI9zaIe=XGwbhcVm>0H~z0HA(h<&vx)*IO8U zw~)OGI(Fn?bW52e{-ovFk2XL?=AU&!O1BObWW_-kH~{kFC2aKk%}uX?6+yxi=Gkv8 zE^DIGtVFq4I#mmo(s?u~HYC~Plu6l7K2?Uu)kk#1`63+tEuS?Y`X}KK5G;CrRTez$ zi&CIw!hjuV1HY>wx)XSR?iAo{Nt1=PQ-z&PMykONS|xKbsplezBpUcAeq@9$YsO>O zrqPpLa|S&|{{`3=@#E?tflH9qs-bbRpUKu^hI3ii5bHZ}Yb%PORVt(QiC!-q;}ko7 zh~|D0n+pCglci=IjW@-dhO#7V-A%wAhcE{@tj}v|@_s>~1qP*0MW~*+Q<}G2J2yww zAc}+U=AuA>Vi?IqrUoVSLhI8+*0^`@z4SYWSu(PzVF9~ z#KrHtt>ifCij-0IUghfanrTv~K1K3>MZ|LJRyvMoWgjp|h< zJby@t6lTqzi=%dRG{NkSI50$WQDue*1xY_MMyN*Q`Q-(U8n!PyFKBi1GHM2KZ3d=s zG5RYl&qPK5&|UHmyroC`l_aP8X;9Jh>@c|MocR(AAvJ%1Jsi!+=`uBdMNNGQ^341I zP29jz5S!PPs1_TR6DO0n_qM9PtR4IA*=o93{%n&9gvfJ;C4GFpO8u9*5iXt|I{KK- zgsvFYAs)3rI^n;B2}U331@Zh<$&V;bI#S=!qt$Y$rtA6}h>vz_*^vQXv9eExLt}tW z4_pj(6UYOwalr4+Wox%pfQ-(b*0Y9XJJwTtJY|^#&vCtz%Y)>paZpxOAUNO(?51|H z=)Rk+-xZd6N~b!gx8BN2297yX)x>aB3h# zbNt^_?BF9_JUR_XkqG7gSCm{+25J zhKl=;)WX?mxJ@=JjyOJM99BF*d?18Dh`L(tEPR5uJ~dlAVW;4{Drt~KrH$<@jV#o3 zK)Y|pO{c6GJp_c#N5ns%iibP%j+m3bRqDR|&By24yv|mx5l%$YD(U6|SKjwQh-Ddzl z*ZlLm+Gw}nq)0GmipX}lSzqV+vX7tPf_sbvd@_Qa!iOieYa`260^w(YsC+)+KJ~?j zFAJ)3yMkA9Xy?QTU_vw_IMsQIv%oL~J!JL{n=&sZ{pC%7ou_0%3h5z7P$_?RhcQ%X zaL2GJcZ2?8U>K7B1WrA)_8`1AI*qbU=Fv#bP}wGEdL2G21I+z!xYqpP`z}J zn!kpElo5mV6uFlBBc`(HlYw~8g2Q-zeRaI$(qSD|L#z}e{w)!nb*!VAB1^%V&F=`ALIgDxGJ$m#0N55t}qpH$DpCrMK(@p@fTlUf2``!O9xCAC&T#}(OyQTEEgUv8RA8SAO*qpU~hj?hjhuw*wxV%OM(Xuekd)nT>0(R zd)pw5twgo8yB^)bm@EZB)RW8-5MW_Ttv4pD*y+^UP*Vqx0@AoKqy_dVD#|eRG3s_@ z4j#ji0r3jJxT#ZNViB{nuF&-@<9cpl!5(Ci+jCw!H@MgOf1MlRtjT+9h+j*KoYOOwr2&KdKDSiTmb2rRDXe~#if^M4YiR^tT{gew z8HT$K7RXDr3$^4)p?Vhb?Tjrrq=vB2rp;gZW65!$YD)3@ixJV{UG5j<%BkCjS3+BMkKntMj0b5^-pQ2%`$ zel1pcXF$Rar{A!wf0oMHfCa@U?zk!|M;;;GORTX z;^c}_@e4s*1aw{flyuv6+efzeqM0~)k(tat-d`rK0&Rxx5JHPrM5F5a2dUHPN)^~ zc5Qu|ZgXkWEK8x9oWNOJejNaMDl(p(Y9MuQbkPs9nD^B=^P;cY+jxpfmgnvlOf9Rt z_Lb35w&g0#>IU_q+R;tmBs1Wxi84qsQO<@zt?fB|$(oK#RdH2T-L1c{*&0@jzAhQ7 zqm1;JEFmRHOye4>eS%o(3`R*oh(~!${a%49DWfXN8x0ONgf{(*O1>^SG=Xflp^~Be z_0uZha%YOrNqqNnLHbRXH%06NL()kaL!qF0Cj8p=WW0bK2M&5hz%3TK(4^$35t~%P z2R^qkr>qM`+D`875w18BF+AHoB>Z!R+uv=}H?zVw^U8yB!=$4a8+op$AZoIaL9XZB zgg%zlxwNfKEfLOCA(x*tf`(*d)2v60k_ztw(;Sl2TWI7*&$!xBQSM27ltFAV%e?QC(BZU9%)c~F*G&OnA|s+67}?%{Zw)Gn2U9i zLYCekb2=`%*hK`C3{WlD#d7ghvywW>b2%qxZUiA3R#v?c3%?#KUP>%$grHk=9c|~= z;`U_T3+eM*qHg0vf!Tn#CF3?Fh&e-*!RG44!FzcQNEYY?(!0YV5ZCoo>UaimZmGyM zT^AK*F7y?VP~_uY{xz*iqkP`rw}Pu@tXB~A21&8w~S*$ zJ_~fjI59i3(H|aUBTm+pD92;{khB8kgwEO1LyxLF>zy0Nu?uaNGOF!d36SS*fkSbY}=20=q;h8a&u zISquHi1AqPt-2@H3w=U{3!cbFRQ<4}9{TDyo&ZX9cX=}S|B@sIQ@4m>Zu`ouU%8gk zJZ-Bg?pa|8@v30bf6?$`_92Sgnf`ik%kwABm6S_ICM8)3n58%RU2a7saJ1rKeqVBP zN^%;mY1y2i=AYk=$pQJN#0uCcMeqD2K)rE424&00h_+PazAo3* zL<2oB1!aRhY|YgB+c@A_ID+WbQgN2G6$`#Bl`e#>*_mRo-;a_8<5c+Evw7tU4j$A| z5CHY4Ob?{lox(7q65xV6$MwxC$Xn%*Z}+2S7UPfGG9bl@|FSI2g=E$AFS(I8$z~wN zZ71(9)QmzLJ=7Qf-d=J&SN}})LYdxrjr$R2>`pQsM|}RqeDbnJQh&r+vYe;&G?;)A z8*Wz|FFIqQCHwJOnWo38rl7gj;&HL9VB*a2`U2m-_v~9TLE zY^0u&4k74v)!j!r>LCO{bq;zJdksd*PN|pCjHA!glMm>baXO4y_m+kR65_=j2a>{v zs&?pC#$xPELz|D|`J&<{_(JX2pluKN25=JHPY;mlMlb;%#{dCwBBy zdETGfx$`?V^TRx&Xwn<_LMYuJHzr063i0x`W4h>P*pX!e3nlno#4+(+l$QQZZvrZY zcyI|(P;YJT5ls~+3j>075(H0C^2o*ZO_vR*WZ{$5eBZ>Yev{B?(4r! zaFo4_;=vMF`84W(ai>NRmd#)PFL^=URT{}x6!Gta){IH^O<>uJU?-~lyc|d^#*+P; zOY?a6YY{TYI%^&cGWij!A%3V3gH_gqt=d6@<&|@6YcZ$A6viGr6uZE1%gqMUSo#p1 zBNrx+*K5C)G6fLOgQu{y5zqrUVASfST+}@I723i^RsLkMNlpsiG$%!2McQ=tzG<)q z#pd^2`yOtW>3oYHCApcq@UFYnVpQJNh1L(WTKg`ZeA@VMcR71@9Ha&U56Mp#BI9$n zlZV^4m#S!)MnQWoM-t;5nfgt7&%)*l4}Z7bPD(rJ-7kG2yXEVCGJPAt2Ym%V={L6? z1hV5fouY?qY3!{VAJShFBkdpPGv9R@Z`L=I(E@@@2Um?=U;9yQJ^$z&47L^2>u>>P z>Z^{A-Aj`PpdwFrGkqq!jgytqt2f>pCv#-Z+el`@R zwXP!~(It1xDih|deDLI|@ZskIwT6}V#t_6CovxcH$zQ_K2J5~O(ya_X8ja?`3tlsK zoE`At+|7-@Y3ZAB55pywA<$gfBdkY@b+Hi8G`AAy*twe_gZAReq-%6iJ>+YB1C{$1 zJm1Kih8tv*42_{}D8Xq*Oi`4*#3Z^!R2LPMnH*L_UauM^Bu7+p(P;g&r&ItN8g6Ch z=q##otxWj};iM?Pky?!YCZu49v_ZX+0mCt}^Ta0J%`zV1TlA4Sn3Jj%KC2cETOMB# ze)sn#w5~cyGm*QOHDnowIgjFB%5`DVl}Mxm&KnL4v-D-b(LojUp}eG9H2Dgh+`Sej z=ato3r1aM%gYMh!FLcItL3!ByK_q#>65{_Mjpydpd=Ol`a?s5H{Aw$yj!?<16ZtO{ zy9z&B=`F4DYSx+ZUCDgo7Eppb^@9Hg_o^cWUzcqiIunR3s_QrOpD*9OXxQZ%)m><@ zXp<)OHElwXDa#@XYff`#twp}c#H8NkOMZ))8FAKxHLZDkrUbuqNEv(cR*Ak+ioJ!L zlN25S8!$ztiZi@N_+fs~ePT_aPq45Ll~eEYUYq-PAN}xlH-NUmk1Ns>z0h+o@rYlJ z56`n&fI&W~JD`mGXBfj8-gxpOz3cOb>%~JQ|7lSI3wAiCxr8UsOEkqR6)%TCut-#Y znd~eLuzf7MU4)E*MNBSMxjNVlc*PM-PxdnS=|NMASKCqK<2djDgVA!sARLz~(tmt! z|Mx3gVZeaUs6-m=os_3out#EvgdgWT__(T2RT}rl1jWG#nhLr@CR$WRpxBIoQzzsj zWoPv)KMl2<@*9`;m4$G0FYg|r@slff7>jNDTrG-xFcED@WkpNxueoHKUMtDT+6p^Y z^o`ugA37}P{8%G$7dSn@-`cytbvr_AdrN8IJof~lWL-nyXuP%^GnTX1T?O$Q56Hf3 zu-YB7q(TKIVkcgX3f|`KpvfQHv^jXgH6M;#@vf-%$hdmPl=j35$Gv%HK|NpM_U8rgV; zIpl{D@J~kk_=y^K+hJfPl7f}Br literal 0 HcmV?d00001 diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/test-client-encryption-crypto-python-rsa.jpg b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/test-client-encryption-crypto-python-rsa.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cfe64644cd919622ed53e43ee574872a09bc98a3 GIT binary patch literal 21839 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 OMDY8{$!G-MoX(kD>46;p literal 0 HcmV?d00001 diff --git a/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/test-client-encryption-src.jpg b/vendor/github.com/aliyun/aliyun-oss-go-sdk/sample/test-client-encryption-src.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ffd46a2f81b1fe020ed0673f27711124110eed3f GIT binary patch literal 21839 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", "", "") + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + + // Create a description of the master key. Once created, it cannot be modified. The master key description and the master key are one-to-one correspondence. + // If all objects use the same master key, the master key description can also be empty, but subsequent replacement of the master key is not supported. + // Because if the description is empty, it is impossible to determine which master key is used when decrypting object. + // It is strongly recommended that: configure the master key description(json string) for each master key, and the client should save the correspondence between them. + // The server does not save their correspondence + + // Map converted by the master key description information (json string) + materialDesc := make(map[string]string) + materialDesc["desc"] = "" + + // Create a master key object based on the master key description + masterRsaCipher, err := osscrypto.CreateMasterRsa(materialDesc, "", "") + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + + // Create an interface for encryption based on the master key object, encrypt using aec ctr mode + contentProvider := osscrypto.CreateAesCtrCipher(masterRsaCipher) + + // Get a storage space for client encryption, the bucket has to be created + // Client-side encrypted buckets have similar usages to ordinary buckets. + cryptoBucket, err := osscrypto.GetCryptoBucket(client, "", contentProvider) + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + + // put object ,will be automatically encrypted + err = cryptoBucket.PutObject("", bytes.NewReader([]byte("yourObjectValueByteArrary"))) + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + + // get object ,will be automatically decrypted + body, err := cryptoBucket.GetObject("") + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + defer body.Close() + + data, err := ioutil.ReadAll(body) + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + fmt.Println("data:", string(data)) +} + +func SampleRsaMultiPartObject() { + // create oss client + client, err := oss.New("", "", "") + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + + // Create a description of the master key. Once created, it cannot be modified. The master key description and the master key are one-to-one correspondence. + // If all objects use the same master key, the master key description can also be empty, but subsequent replacement of the master key is not supported. + // Because if the description is empty, it is impossible to determine which master key is used when decrypting object. + // It is strongly recommended that: configure the master key description(json string) for each master key, and the client should save the correspondence between them. + // The server does not save their correspondence + + // Map converted by the master key description information (json string) + materialDesc := make(map[string]string) + materialDesc["desc"] = "" + + // Create a master key object based on the master key description + masterRsaCipher, err := osscrypto.CreateMasterRsa(materialDesc, "", "") + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + + // Create an interface for encryption based on the master key object, encrypt using aec ctr mode + contentProvider := osscrypto.CreateAesCtrCipher(masterRsaCipher) + + // Get a storage space for client encryption, the bucket has to be created + // Client-side encrypted buckets have similar usages to ordinary buckets. + cryptoBucket, err := osscrypto.GetCryptoBucket(client, "", contentProvider) + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + + fileName := "" + fileInfo, err := os.Stat(fileName) + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + fileSize := fileInfo.Size() + + // Encryption context information + var cryptoContext osscrypto.PartCryptoContext + cryptoContext.DataSize = fileSize + + // The expected number of parts, the actual number of parts is subject to subsequent calculations. + expectPartCount := int64(10) + + //Currently aes ctr encryption block size requires 16 byte alignment + cryptoContext.PartSize = (fileSize / expectPartCount / 16) * 16 + + imur, err := cryptoBucket.InitiateMultipartUpload("", &cryptoContext) + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + + chunks, err := oss.SplitFileByPartSize(fileName, cryptoContext.PartSize) + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + + var partsUpload []oss.UploadPart + for _, chunk := range chunks { + part, err := cryptoBucket.UploadPartFromFile(imur, fileName, chunk.Offset, chunk.Size, (int)(chunk.Number), cryptoContext) + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + partsUpload = append(partsUpload, part) + } + + // Complete + _, err = cryptoBucket.CompleteMultipartUpload(imur, partsUpload) + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } +} + +// Query the master key according to the master key description information. +// If you need to decrypt different master key encryption objects, you need to provide this interface. +type MockRsaManager struct { +} + +func (mg *MockRsaManager) GetMasterKey(matDesc map[string]string) ([]string, error) { + // to do + keyList := []string{"", ""} + return keyList, nil +} + +// Decrypt the object encrypted by different master keys +func SampleMultipleMasterRsa() { + // create oss client + client, err := oss.New("", "", "") + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + + // Create a description of the master key. Once created, it cannot be modified. The master key description and the master key are one-to-one correspondence. + // If all objects use the same master key, the master key description can also be empty, but subsequent replacement of the master key is not supported. + // Because if the description is empty, it is impossible to determine which master key is used when decrypting object. + // It is strongly recommended that: configure the master key description(json string) for each master key, and the client should save the correspondence between them. + // The server does not save their correspondence + + // Map converted by the master key description information (json string) + materialDesc := make(map[string]string) + materialDesc["desc"] = "" + + // Create a master key object based on the master key description + masterRsaCipher, err := osscrypto.CreateMasterRsa(materialDesc, "", "") + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + + // Create an interface for encryption based on the master key object, encrypt using aec ctr mode + contentProvider := osscrypto.CreateAesCtrCipher(masterRsaCipher) + + // If you need to decrypt objects encrypted by different ma keys, you need to provide this interface. + var mockRsaManager MockRsaManager + var options []osscrypto.CryptoBucketOption + options = append(options, osscrypto.SetMasterCipherManager(&mockRsaManager)) + + // Get a storage space for client encryption, the bucket has to be created + // Client-side encrypted buckets have similar usages to ordinary buckets. + cryptoBucket, err := osscrypto.GetCryptoBucket(client, "", contentProvider, options...) + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + + // put object ,will be automatically encrypted + err = cryptoBucket.PutObject("", bytes.NewReader([]byte("yourObjectValueByteArrary"))) + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + + // get object ,will be automatically decrypted + body, err := cryptoBucket.GetObject("") + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + defer body.Close() + + data, err := ioutil.ReadAll(body) + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + fmt.Println("data:", string(data)) +} + +func SampleKmsNormalObject() { + // create oss client + client, err := oss.New("", "", "") + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + + // create kms client + kmsClient, err := kms.NewClientWithAccessKey("", "", "") + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + + // Create a description of the master key. Once created, it cannot be modified. The master key description and the master key are one-to-one correspondence. + // If all objects use the same master key, the master key description can also be empty, but subsequent replacement of the master key is not supported. + // Because if the description is empty, it is impossible to determine which master key is used when decrypting object. + // It is strongly recommended that: configure the master key description(json string) for each master key, and the client should save the correspondence between them. + // The server does not save their correspondence + + // Map converted by the master key description information (json string) + materialDesc := make(map[string]string) + materialDesc["desc"] = "" + + // Create a master key object based on the master key description + masterkmsCipher, err := osscrypto.CreateMasterAliKms(materialDesc, "", kmsClient) + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + + // Create an interface for encryption based on the master key object, encrypt using aec ctr mode + contentProvider := osscrypto.CreateAesCtrCipher(masterkmsCipher) + + // Get a storage space for client encryption, the bucket has to be created + // Client-side encrypted buckets have similar usages to ordinary buckets. + cryptoBucket, err := osscrypto.GetCryptoBucket(client, "", contentProvider) + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + + // put object ,will be automatically encrypted + err = cryptoBucket.PutObject("", bytes.NewReader([]byte("yourObjectValueByteArrary"))) + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + + // get object ,will be automatically decrypted + body, err := cryptoBucket.GetObject("") + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + defer body.Close() + + data, err := ioutil.ReadAll(body) + if err != nil { + fmt.Println("Error:", err) + os.Exit(-1) + } + fmt.Println("data:", string(data)) +} + +func main() { + SampleRsaNormalObject() + SampleRsaMultiPartObject() + SampleMultipleMasterRsa() + SampleKmsNormalObject() +} diff --git a/vendor/golang.org/x/time/AUTHORS b/vendor/golang.org/x/time/AUTHORS new file mode 100644 index 00000000..15167cd7 --- /dev/null +++ b/vendor/golang.org/x/time/AUTHORS @@ -0,0 +1,3 @@ +# This source code refers to The Go Authors for copyright purposes. +# The master list of authors is in the main Go distribution, +# visible at http://tip.golang.org/AUTHORS. diff --git a/vendor/golang.org/x/time/CONTRIBUTING.md b/vendor/golang.org/x/time/CONTRIBUTING.md new file mode 100644 index 00000000..d0485e88 --- /dev/null +++ b/vendor/golang.org/x/time/CONTRIBUTING.md @@ -0,0 +1,26 @@ +# Contributing to Go + +Go is an open source project. + +It is the work of hundreds of contributors. We appreciate your help! + +## Filing issues + +When [filing an issue](https://golang.org/issue/new), make sure to answer these five questions: + +1. What version of Go are you using (`go version`)? +2. What operating system and processor architecture are you using? +3. What did you do? +4. What did you expect to see? +5. What did you see instead? + +General questions should go to the [golang-nuts mailing list](https://groups.google.com/group/golang-nuts) instead of the issue tracker. +The gophers there will answer or ask you to file an issue if you've tripped over a bug. + +## Contributing code + +Please read the [Contribution Guidelines](https://golang.org/doc/contribute.html) +before sending patches. + +Unless otherwise noted, the Go source files are distributed under +the BSD-style license found in the LICENSE file. diff --git a/vendor/golang.org/x/time/CONTRIBUTORS b/vendor/golang.org/x/time/CONTRIBUTORS new file mode 100644 index 00000000..1c4577e9 --- /dev/null +++ b/vendor/golang.org/x/time/CONTRIBUTORS @@ -0,0 +1,3 @@ +# This source code was written by the Go contributors. +# The master list of contributors is in the main Go distribution, +# visible at http://tip.golang.org/CONTRIBUTORS. diff --git a/vendor/golang.org/x/time/LICENSE b/vendor/golang.org/x/time/LICENSE new file mode 100644 index 00000000..6a66aea5 --- /dev/null +++ b/vendor/golang.org/x/time/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/golang.org/x/time/PATENTS b/vendor/golang.org/x/time/PATENTS new file mode 100644 index 00000000..73309904 --- /dev/null +++ b/vendor/golang.org/x/time/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the Go project. + +Google 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, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/vendor/golang.org/x/time/README.md b/vendor/golang.org/x/time/README.md new file mode 100644 index 00000000..705a4979 --- /dev/null +++ b/vendor/golang.org/x/time/README.md @@ -0,0 +1,19 @@ +# Go Time + +[![Go Reference](https://pkg.go.dev/badge/golang.org/x/time.svg)](https://pkg.go.dev/golang.org/x/time) + +This repository provides supplementary Go time packages. + +## Download/Install + +The easiest way to install is to run `go get -u golang.org/x/time`. You can +also manually git clone the repository to `$GOPATH/src/golang.org/x/time`. + +## Report Issues / Send Patches + +This repository uses Gerrit for code changes. To learn how to submit changes to +this repository, see https://golang.org/doc/contribute.html. + +The main issue tracker for the time repository is located at +https://github.com/golang/go/issues. Prefix your issue with "x/time:" in the +subject line, so it is easy to find. diff --git a/vendor/golang.org/x/time/go.mod b/vendor/golang.org/x/time/go.mod new file mode 100644 index 00000000..46ac9176 --- /dev/null +++ b/vendor/golang.org/x/time/go.mod @@ -0,0 +1 @@ +module golang.org/x/time diff --git a/vendor/golang.org/x/time/rate/rate.go b/vendor/golang.org/x/time/rate/rate.go new file mode 100644 index 00000000..a98fe778 --- /dev/null +++ b/vendor/golang.org/x/time/rate/rate.go @@ -0,0 +1,402 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package rate provides a rate limiter. +package rate + +import ( + "context" + "fmt" + "math" + "sync" + "time" +) + +// Limit defines the maximum frequency of some events. +// Limit is represented as number of events per second. +// A zero Limit allows no events. +type Limit float64 + +// Inf is the infinite rate limit; it allows all events (even if burst is zero). +const Inf = Limit(math.MaxFloat64) + +// Every converts a minimum time interval between events to a Limit. +func Every(interval time.Duration) Limit { + if interval <= 0 { + return Inf + } + return 1 / Limit(interval.Seconds()) +} + +// A Limiter controls how frequently events are allowed to happen. +// It implements a "token bucket" of size b, initially full and refilled +// at rate r tokens per second. +// Informally, in any large enough time interval, the Limiter limits the +// rate to r tokens per second, with a maximum burst size of b events. +// As a special case, if r == Inf (the infinite rate), b is ignored. +// See https://en.wikipedia.org/wiki/Token_bucket for more about token buckets. +// +// The zero value is a valid Limiter, but it will reject all events. +// Use NewLimiter to create non-zero Limiters. +// +// Limiter has three main methods, Allow, Reserve, and Wait. +// Most callers should use Wait. +// +// Each of the three methods consumes a single token. +// They differ in their behavior when no token is available. +// If no token is available, Allow returns false. +// If no token is available, Reserve returns a reservation for a future token +// and the amount of time the caller must wait before using it. +// If no token is available, Wait blocks until one can be obtained +// or its associated context.Context is canceled. +// +// The methods AllowN, ReserveN, and WaitN consume n tokens. +type Limiter struct { + mu sync.Mutex + limit Limit + burst int + tokens float64 + // last is the last time the limiter's tokens field was updated + last time.Time + // lastEvent is the latest time of a rate-limited event (past or future) + lastEvent time.Time +} + +// Limit returns the maximum overall event rate. +func (lim *Limiter) Limit() Limit { + lim.mu.Lock() + defer lim.mu.Unlock() + return lim.limit +} + +// Burst returns the maximum burst size. Burst is the maximum number of tokens +// that can be consumed in a single call to Allow, Reserve, or Wait, so higher +// Burst values allow more events to happen at once. +// A zero Burst allows no events, unless limit == Inf. +func (lim *Limiter) Burst() int { + lim.mu.Lock() + defer lim.mu.Unlock() + return lim.burst +} + +// NewLimiter returns a new Limiter that allows events up to rate r and permits +// bursts of at most b tokens. +func NewLimiter(r Limit, b int) *Limiter { + return &Limiter{ + limit: r, + burst: b, + } +} + +// Allow is shorthand for AllowN(time.Now(), 1). +func (lim *Limiter) Allow() bool { + return lim.AllowN(time.Now(), 1) +} + +// AllowN reports whether n events may happen at time now. +// Use this method if you intend to drop / skip events that exceed the rate limit. +// Otherwise use Reserve or Wait. +func (lim *Limiter) AllowN(now time.Time, n int) bool { + return lim.reserveN(now, n, 0).ok +} + +// A Reservation holds information about events that are permitted by a Limiter to happen after a delay. +// A Reservation may be canceled, which may enable the Limiter to permit additional events. +type Reservation struct { + ok bool + lim *Limiter + tokens int + timeToAct time.Time + // This is the Limit at reservation time, it can change later. + limit Limit +} + +// OK returns whether the limiter can provide the requested number of tokens +// within the maximum wait time. If OK is false, Delay returns InfDuration, and +// Cancel does nothing. +func (r *Reservation) OK() bool { + return r.ok +} + +// Delay is shorthand for DelayFrom(time.Now()). +func (r *Reservation) Delay() time.Duration { + return r.DelayFrom(time.Now()) +} + +// InfDuration is the duration returned by Delay when a Reservation is not OK. +const InfDuration = time.Duration(1<<63 - 1) + +// DelayFrom returns the duration for which the reservation holder must wait +// before taking the reserved action. Zero duration means act immediately. +// InfDuration means the limiter cannot grant the tokens requested in this +// Reservation within the maximum wait time. +func (r *Reservation) DelayFrom(now time.Time) time.Duration { + if !r.ok { + return InfDuration + } + delay := r.timeToAct.Sub(now) + if delay < 0 { + return 0 + } + return delay +} + +// Cancel is shorthand for CancelAt(time.Now()). +func (r *Reservation) Cancel() { + r.CancelAt(time.Now()) + return +} + +// CancelAt indicates that the reservation holder will not perform the reserved action +// and reverses the effects of this Reservation on the rate limit as much as possible, +// considering that other reservations may have already been made. +func (r *Reservation) CancelAt(now time.Time) { + if !r.ok { + return + } + + r.lim.mu.Lock() + defer r.lim.mu.Unlock() + + if r.lim.limit == Inf || r.tokens == 0 || r.timeToAct.Before(now) { + return + } + + // calculate tokens to restore + // The duration between lim.lastEvent and r.timeToAct tells us how many tokens were reserved + // after r was obtained. These tokens should not be restored. + restoreTokens := float64(r.tokens) - r.limit.tokensFromDuration(r.lim.lastEvent.Sub(r.timeToAct)) + if restoreTokens <= 0 { + return + } + // advance time to now + now, _, tokens := r.lim.advance(now) + // calculate new number of tokens + tokens += restoreTokens + if burst := float64(r.lim.burst); tokens > burst { + tokens = burst + } + // update state + r.lim.last = now + r.lim.tokens = tokens + if r.timeToAct == r.lim.lastEvent { + prevEvent := r.timeToAct.Add(r.limit.durationFromTokens(float64(-r.tokens))) + if !prevEvent.Before(now) { + r.lim.lastEvent = prevEvent + } + } + + return +} + +// Reserve is shorthand for ReserveN(time.Now(), 1). +func (lim *Limiter) Reserve() *Reservation { + return lim.ReserveN(time.Now(), 1) +} + +// ReserveN returns a Reservation that indicates how long the caller must wait before n events happen. +// The Limiter takes this Reservation into account when allowing future events. +// The returned Reservation’s OK() method returns false if n exceeds the Limiter's burst size. +// Usage example: +// r := lim.ReserveN(time.Now(), 1) +// if !r.OK() { +// // Not allowed to act! Did you remember to set lim.burst to be > 0 ? +// return +// } +// time.Sleep(r.Delay()) +// Act() +// Use this method if you wish to wait and slow down in accordance with the rate limit without dropping events. +// If you need to respect a deadline or cancel the delay, use Wait instead. +// To drop or skip events exceeding rate limit, use Allow instead. +func (lim *Limiter) ReserveN(now time.Time, n int) *Reservation { + r := lim.reserveN(now, n, InfDuration) + return &r +} + +// Wait is shorthand for WaitN(ctx, 1). +func (lim *Limiter) Wait(ctx context.Context) (err error) { + return lim.WaitN(ctx, 1) +} + +// WaitN blocks until lim permits n events to happen. +// It returns an error if n exceeds the Limiter's burst size, the Context is +// canceled, or the expected wait time exceeds the Context's Deadline. +// The burst limit is ignored if the rate limit is Inf. +func (lim *Limiter) WaitN(ctx context.Context, n int) (err error) { + lim.mu.Lock() + burst := lim.burst + limit := lim.limit + lim.mu.Unlock() + + if n > burst && limit != Inf { + return fmt.Errorf("rate: Wait(n=%d) exceeds limiter's burst %d", n, burst) + } + // Check if ctx is already cancelled + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + // Determine wait limit + now := time.Now() + waitLimit := InfDuration + if deadline, ok := ctx.Deadline(); ok { + waitLimit = deadline.Sub(now) + } + // Reserve + r := lim.reserveN(now, n, waitLimit) + if !r.ok { + return fmt.Errorf("rate: Wait(n=%d) would exceed context deadline", n) + } + // Wait if necessary + delay := r.DelayFrom(now) + if delay == 0 { + return nil + } + t := time.NewTimer(delay) + defer t.Stop() + select { + case <-t.C: + // We can proceed. + return nil + case <-ctx.Done(): + // Context was canceled before we could proceed. Cancel the + // reservation, which may permit other events to proceed sooner. + r.Cancel() + return ctx.Err() + } +} + +// SetLimit is shorthand for SetLimitAt(time.Now(), newLimit). +func (lim *Limiter) SetLimit(newLimit Limit) { + lim.SetLimitAt(time.Now(), newLimit) +} + +// SetLimitAt sets a new Limit for the limiter. The new Limit, and Burst, may be violated +// or underutilized by those which reserved (using Reserve or Wait) but did not yet act +// before SetLimitAt was called. +func (lim *Limiter) SetLimitAt(now time.Time, newLimit Limit) { + lim.mu.Lock() + defer lim.mu.Unlock() + + now, _, tokens := lim.advance(now) + + lim.last = now + lim.tokens = tokens + lim.limit = newLimit +} + +// SetBurst is shorthand for SetBurstAt(time.Now(), newBurst). +func (lim *Limiter) SetBurst(newBurst int) { + lim.SetBurstAt(time.Now(), newBurst) +} + +// SetBurstAt sets a new burst size for the limiter. +func (lim *Limiter) SetBurstAt(now time.Time, newBurst int) { + lim.mu.Lock() + defer lim.mu.Unlock() + + now, _, tokens := lim.advance(now) + + lim.last = now + lim.tokens = tokens + lim.burst = newBurst +} + +// reserveN is a helper method for AllowN, ReserveN, and WaitN. +// maxFutureReserve specifies the maximum reservation wait duration allowed. +// reserveN returns Reservation, not *Reservation, to avoid allocation in AllowN and WaitN. +func (lim *Limiter) reserveN(now time.Time, n int, maxFutureReserve time.Duration) Reservation { + lim.mu.Lock() + + if lim.limit == Inf { + lim.mu.Unlock() + return Reservation{ + ok: true, + lim: lim, + tokens: n, + timeToAct: now, + } + } + + now, last, tokens := lim.advance(now) + + // Calculate the remaining number of tokens resulting from the request. + tokens -= float64(n) + + // Calculate the wait duration + var waitDuration time.Duration + if tokens < 0 { + waitDuration = lim.limit.durationFromTokens(-tokens) + } + + // Decide result + ok := n <= lim.burst && waitDuration <= maxFutureReserve + + // Prepare reservation + r := Reservation{ + ok: ok, + lim: lim, + limit: lim.limit, + } + if ok { + r.tokens = n + r.timeToAct = now.Add(waitDuration) + } + + // Update state + if ok { + lim.last = now + lim.tokens = tokens + lim.lastEvent = r.timeToAct + } else { + lim.last = last + } + + lim.mu.Unlock() + return r +} + +// advance calculates and returns an updated state for lim resulting from the passage of time. +// lim is not changed. +// advance requires that lim.mu is held. +func (lim *Limiter) advance(now time.Time) (newNow time.Time, newLast time.Time, newTokens float64) { + last := lim.last + if now.Before(last) { + last = now + } + + // Avoid making delta overflow below when last is very old. + maxElapsed := lim.limit.durationFromTokens(float64(lim.burst) - lim.tokens) + elapsed := now.Sub(last) + if elapsed > maxElapsed { + elapsed = maxElapsed + } + + // Calculate the new number of tokens, due to time that passed. + delta := lim.limit.tokensFromDuration(elapsed) + tokens := lim.tokens + delta + if burst := float64(lim.burst); tokens > burst { + tokens = burst + } + + return now, last, tokens +} + +// durationFromTokens is a unit conversion function from the number of tokens to the duration +// of time it takes to accumulate them at a rate of limit tokens per second. +func (limit Limit) durationFromTokens(tokens float64) time.Duration { + seconds := tokens / float64(limit) + return time.Nanosecond * time.Duration(1e9*seconds) +} + +// tokensFromDuration is a unit conversion function from a time duration to the number of tokens +// which could be accumulated during that duration at a rate of limit tokens per second. +func (limit Limit) tokensFromDuration(d time.Duration) float64 { + // Split the integer and fractional parts ourself to minimize rounding errors. + // See golang.org/issues/34861. + sec := float64(d/time.Second) * float64(limit) + nsec := float64(d%time.Second) * float64(limit) + return sec + nsec/1e9 +} diff --git a/vendor/golang.org/x/time/rate/rate_test.go b/vendor/golang.org/x/time/rate/rate_test.go new file mode 100644 index 00000000..b0ed34df --- /dev/null +++ b/vendor/golang.org/x/time/rate/rate_test.go @@ -0,0 +1,477 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build go1.7 + +package rate + +import ( + "context" + "math" + "runtime" + "sync" + "sync/atomic" + "testing" + "time" +) + +func TestLimit(t *testing.T) { + if Limit(10) == Inf { + t.Errorf("Limit(10) == Inf should be false") + } +} + +func closeEnough(a, b Limit) bool { + return (math.Abs(float64(a)/float64(b)) - 1.0) < 1e-9 +} + +func TestEvery(t *testing.T) { + cases := []struct { + interval time.Duration + lim Limit + }{ + {0, Inf}, + {-1, Inf}, + {1 * time.Nanosecond, Limit(1e9)}, + {1 * time.Microsecond, Limit(1e6)}, + {1 * time.Millisecond, Limit(1e3)}, + {10 * time.Millisecond, Limit(100)}, + {100 * time.Millisecond, Limit(10)}, + {1 * time.Second, Limit(1)}, + {2 * time.Second, Limit(0.5)}, + {time.Duration(2.5 * float64(time.Second)), Limit(0.4)}, + {4 * time.Second, Limit(0.25)}, + {10 * time.Second, Limit(0.1)}, + {time.Duration(math.MaxInt64), Limit(1e9 / float64(math.MaxInt64))}, + } + for _, tc := range cases { + lim := Every(tc.interval) + if !closeEnough(lim, tc.lim) { + t.Errorf("Every(%v) = %v want %v", tc.interval, lim, tc.lim) + } + } +} + +const ( + d = 100 * time.Millisecond +) + +var ( + t0 = time.Now() + t1 = t0.Add(time.Duration(1) * d) + t2 = t0.Add(time.Duration(2) * d) + t3 = t0.Add(time.Duration(3) * d) + t4 = t0.Add(time.Duration(4) * d) + t5 = t0.Add(time.Duration(5) * d) + t9 = t0.Add(time.Duration(9) * d) +) + +type allow struct { + t time.Time + n int + ok bool +} + +func run(t *testing.T, lim *Limiter, allows []allow) { + for i, allow := range allows { + ok := lim.AllowN(allow.t, allow.n) + if ok != allow.ok { + t.Errorf("step %d: lim.AllowN(%v, %v) = %v want %v", + i, allow.t, allow.n, ok, allow.ok) + } + } +} + +func TestLimiterBurst1(t *testing.T) { + run(t, NewLimiter(10, 1), []allow{ + {t0, 1, true}, + {t0, 1, false}, + {t0, 1, false}, + {t1, 1, true}, + {t1, 1, false}, + {t1, 1, false}, + {t2, 2, false}, // burst size is 1, so n=2 always fails + {t2, 1, true}, + {t2, 1, false}, + }) +} + +func TestLimiterBurst3(t *testing.T) { + run(t, NewLimiter(10, 3), []allow{ + {t0, 2, true}, + {t0, 2, false}, + {t0, 1, true}, + {t0, 1, false}, + {t1, 4, false}, + {t2, 1, true}, + {t3, 1, true}, + {t4, 1, true}, + {t4, 1, true}, + {t4, 1, false}, + {t4, 1, false}, + {t9, 3, true}, + {t9, 0, true}, + }) +} + +func TestLimiterJumpBackwards(t *testing.T) { + run(t, NewLimiter(10, 3), []allow{ + {t1, 1, true}, // start at t1 + {t0, 1, true}, // jump back to t0, two tokens remain + {t0, 1, true}, + {t0, 1, false}, + {t0, 1, false}, + {t1, 1, true}, // got a token + {t1, 1, false}, + {t1, 1, false}, + {t2, 1, true}, // got another token + {t2, 1, false}, + {t2, 1, false}, + }) +} + +// Ensure that tokensFromDuration doesn't produce +// rounding errors by truncating nanoseconds. +// See golang.org/issues/34861. +func TestLimiter_noTruncationErrors(t *testing.T) { + if !NewLimiter(0.7692307692307693, 1).Allow() { + t.Fatal("expected true") + } +} + +func TestSimultaneousRequests(t *testing.T) { + const ( + limit = 1 + burst = 5 + numRequests = 15 + ) + var ( + wg sync.WaitGroup + numOK = uint32(0) + ) + + // Very slow replenishing bucket. + lim := NewLimiter(limit, burst) + + // Tries to take a token, atomically updates the counter and decreases the wait + // group counter. + f := func() { + defer wg.Done() + if ok := lim.Allow(); ok { + atomic.AddUint32(&numOK, 1) + } + } + + wg.Add(numRequests) + for i := 0; i < numRequests; i++ { + go f() + } + wg.Wait() + if numOK != burst { + t.Errorf("numOK = %d, want %d", numOK, burst) + } +} + +func TestLongRunningQPS(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode") + } + if runtime.GOOS == "openbsd" { + t.Skip("low resolution time.Sleep invalidates test (golang.org/issue/14183)") + return + } + + // The test runs for a few seconds executing many requests and then checks + // that overall number of requests is reasonable. + const ( + limit = 100 + burst = 100 + ) + var numOK = int32(0) + + lim := NewLimiter(limit, burst) + + var wg sync.WaitGroup + f := func() { + if ok := lim.Allow(); ok { + atomic.AddInt32(&numOK, 1) + } + wg.Done() + } + + start := time.Now() + end := start.Add(5 * time.Second) + for time.Now().Before(end) { + wg.Add(1) + go f() + + // This will still offer ~500 requests per second, but won't consume + // outrageous amount of CPU. + time.Sleep(2 * time.Millisecond) + } + wg.Wait() + elapsed := time.Since(start) + ideal := burst + (limit * float64(elapsed) / float64(time.Second)) + + // We should never get more requests than allowed. + if want := int32(ideal + 1); numOK > want { + t.Errorf("numOK = %d, want %d (ideal %f)", numOK, want, ideal) + } + // We should get very close to the number of requests allowed. + if want := int32(0.999 * ideal); numOK < want { + t.Errorf("numOK = %d, want %d (ideal %f)", numOK, want, ideal) + } +} + +type request struct { + t time.Time + n int + act time.Time + ok bool +} + +// dFromDuration converts a duration to a multiple of the global constant d +func dFromDuration(dur time.Duration) int { + // Adding a millisecond to be swallowed by the integer division + // because we don't care about small inaccuracies + return int((dur + time.Millisecond) / d) +} + +// dSince returns multiples of d since t0 +func dSince(t time.Time) int { + return dFromDuration(t.Sub(t0)) +} + +func runReserve(t *testing.T, lim *Limiter, req request) *Reservation { + return runReserveMax(t, lim, req, InfDuration) +} + +func runReserveMax(t *testing.T, lim *Limiter, req request, maxReserve time.Duration) *Reservation { + r := lim.reserveN(req.t, req.n, maxReserve) + if r.ok && (dSince(r.timeToAct) != dSince(req.act)) || r.ok != req.ok { + t.Errorf("lim.reserveN(t%d, %v, %v) = (t%d, %v) want (t%d, %v)", + dSince(req.t), req.n, maxReserve, dSince(r.timeToAct), r.ok, dSince(req.act), req.ok) + } + return &r +} + +func TestSimpleReserve(t *testing.T) { + lim := NewLimiter(10, 2) + + runReserve(t, lim, request{t0, 2, t0, true}) + runReserve(t, lim, request{t0, 2, t2, true}) + runReserve(t, lim, request{t3, 2, t4, true}) +} + +func TestMix(t *testing.T) { + lim := NewLimiter(10, 2) + + runReserve(t, lim, request{t0, 3, t1, false}) // should return false because n > Burst + runReserve(t, lim, request{t0, 2, t0, true}) + run(t, lim, []allow{{t1, 2, false}}) // not enought tokens - don't allow + runReserve(t, lim, request{t1, 2, t2, true}) + run(t, lim, []allow{{t1, 1, false}}) // negative tokens - don't allow + run(t, lim, []allow{{t3, 1, true}}) +} + +func TestCancelInvalid(t *testing.T) { + lim := NewLimiter(10, 2) + + runReserve(t, lim, request{t0, 2, t0, true}) + r := runReserve(t, lim, request{t0, 3, t3, false}) + r.CancelAt(t0) // should have no effect + runReserve(t, lim, request{t0, 2, t2, true}) // did not get extra tokens +} + +func TestCancelLast(t *testing.T) { + lim := NewLimiter(10, 2) + + runReserve(t, lim, request{t0, 2, t0, true}) + r := runReserve(t, lim, request{t0, 2, t2, true}) + r.CancelAt(t1) // got 2 tokens back + runReserve(t, lim, request{t1, 2, t2, true}) +} + +func TestCancelTooLate(t *testing.T) { + lim := NewLimiter(10, 2) + + runReserve(t, lim, request{t0, 2, t0, true}) + r := runReserve(t, lim, request{t0, 2, t2, true}) + r.CancelAt(t3) // too late to cancel - should have no effect + runReserve(t, lim, request{t3, 2, t4, true}) +} + +func TestCancel0Tokens(t *testing.T) { + lim := NewLimiter(10, 2) + + runReserve(t, lim, request{t0, 2, t0, true}) + r := runReserve(t, lim, request{t0, 1, t1, true}) + runReserve(t, lim, request{t0, 1, t2, true}) + r.CancelAt(t0) // got 0 tokens back + runReserve(t, lim, request{t0, 1, t3, true}) +} + +func TestCancel1Token(t *testing.T) { + lim := NewLimiter(10, 2) + + runReserve(t, lim, request{t0, 2, t0, true}) + r := runReserve(t, lim, request{t0, 2, t2, true}) + runReserve(t, lim, request{t0, 1, t3, true}) + r.CancelAt(t2) // got 1 token back + runReserve(t, lim, request{t2, 2, t4, true}) +} + +func TestCancelMulti(t *testing.T) { + lim := NewLimiter(10, 4) + + runReserve(t, lim, request{t0, 4, t0, true}) + rA := runReserve(t, lim, request{t0, 3, t3, true}) + runReserve(t, lim, request{t0, 1, t4, true}) + rC := runReserve(t, lim, request{t0, 1, t5, true}) + rC.CancelAt(t1) // get 1 token back + rA.CancelAt(t1) // get 2 tokens back, as if C was never reserved + runReserve(t, lim, request{t1, 3, t5, true}) +} + +func TestReserveJumpBack(t *testing.T) { + lim := NewLimiter(10, 2) + + runReserve(t, lim, request{t1, 2, t1, true}) // start at t1 + runReserve(t, lim, request{t0, 1, t1, true}) // should violate Limit,Burst + runReserve(t, lim, request{t2, 2, t3, true}) +} + +func TestReserveJumpBackCancel(t *testing.T) { + lim := NewLimiter(10, 2) + + runReserve(t, lim, request{t1, 2, t1, true}) // start at t1 + r := runReserve(t, lim, request{t1, 2, t3, true}) + runReserve(t, lim, request{t1, 1, t4, true}) + r.CancelAt(t0) // cancel at t0, get 1 token back + runReserve(t, lim, request{t1, 2, t4, true}) // should violate Limit,Burst +} + +func TestReserveSetLimit(t *testing.T) { + lim := NewLimiter(5, 2) + + runReserve(t, lim, request{t0, 2, t0, true}) + runReserve(t, lim, request{t0, 2, t4, true}) + lim.SetLimitAt(t2, 10) + runReserve(t, lim, request{t2, 1, t4, true}) // violates Limit and Burst +} + +func TestReserveSetBurst(t *testing.T) { + lim := NewLimiter(5, 2) + + runReserve(t, lim, request{t0, 2, t0, true}) + runReserve(t, lim, request{t0, 2, t4, true}) + lim.SetBurstAt(t3, 4) + runReserve(t, lim, request{t0, 4, t9, true}) // violates Limit and Burst +} + +func TestReserveSetLimitCancel(t *testing.T) { + lim := NewLimiter(5, 2) + + runReserve(t, lim, request{t0, 2, t0, true}) + r := runReserve(t, lim, request{t0, 2, t4, true}) + lim.SetLimitAt(t2, 10) + r.CancelAt(t2) // 2 tokens back + runReserve(t, lim, request{t2, 2, t3, true}) +} + +func TestReserveMax(t *testing.T) { + lim := NewLimiter(10, 2) + maxT := d + + runReserveMax(t, lim, request{t0, 2, t0, true}, maxT) + runReserveMax(t, lim, request{t0, 1, t1, true}, maxT) // reserve for close future + runReserveMax(t, lim, request{t0, 1, t2, false}, maxT) // time to act too far in the future +} + +type wait struct { + name string + ctx context.Context + n int + delay int // in multiples of d + nilErr bool +} + +func runWait(t *testing.T, lim *Limiter, w wait) { + start := time.Now() + err := lim.WaitN(w.ctx, w.n) + delay := time.Now().Sub(start) + if (w.nilErr && err != nil) || (!w.nilErr && err == nil) || w.delay != dFromDuration(delay) { + errString := "" + if !w.nilErr { + errString = "" + } + t.Errorf("lim.WaitN(%v, lim, %v) = %v with delay %v ; want %v with delay %v", + w.name, w.n, err, delay, errString, d*time.Duration(w.delay)) + } +} + +func TestWaitSimple(t *testing.T) { + lim := NewLimiter(10, 3) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + runWait(t, lim, wait{"already-cancelled", ctx, 1, 0, false}) + + runWait(t, lim, wait{"exceed-burst-error", context.Background(), 4, 0, false}) + + runWait(t, lim, wait{"act-now", context.Background(), 2, 0, true}) + runWait(t, lim, wait{"act-later", context.Background(), 3, 2, true}) +} + +func TestWaitCancel(t *testing.T) { + lim := NewLimiter(10, 3) + + ctx, cancel := context.WithCancel(context.Background()) + runWait(t, lim, wait{"act-now", ctx, 2, 0, true}) // after this lim.tokens = 1 + go func() { + time.Sleep(d) + cancel() + }() + runWait(t, lim, wait{"will-cancel", ctx, 3, 1, false}) + // should get 3 tokens back, and have lim.tokens = 2 + t.Logf("tokens:%v last:%v lastEvent:%v", lim.tokens, lim.last, lim.lastEvent) + runWait(t, lim, wait{"act-now-after-cancel", context.Background(), 2, 0, true}) +} + +func TestWaitTimeout(t *testing.T) { + lim := NewLimiter(10, 3) + + ctx, cancel := context.WithTimeout(context.Background(), d) + defer cancel() + runWait(t, lim, wait{"act-now", ctx, 2, 0, true}) + runWait(t, lim, wait{"w-timeout-err", ctx, 3, 0, false}) +} + +func TestWaitInf(t *testing.T) { + lim := NewLimiter(Inf, 0) + + runWait(t, lim, wait{"exceed-burst-no-error", context.Background(), 3, 0, true}) +} + +func BenchmarkAllowN(b *testing.B) { + lim := NewLimiter(Every(1*time.Second), 1) + now := time.Now() + b.ReportAllocs() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + lim.AllowN(now, 1) + } + }) +} + +func BenchmarkWaitNNoDelay(b *testing.B) { + lim := NewLimiter(Limit(b.N), b.N) + ctx := context.Background() + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + lim.WaitN(ctx, 1) + } +} -- 2.11.0