├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ └── test.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── TEST.md ├── api ├── admin │ ├── acceptance_test.go │ ├── analysis.go │ ├── analysis_acceptance_test.go │ ├── api.go │ ├── api_acceptance_test.go │ ├── api_test.go │ ├── asset.go │ ├── asset_acceptance_test.go │ ├── asset_test.go │ ├── assets.go │ ├── assets_acceptance_test.go │ ├── assets_test.go │ ├── folders.go │ ├── folders_test.go │ ├── metadata │ │ └── metadata.go │ ├── metadata_fields.go │ ├── metadata_fields_test.go │ ├── misc.go │ ├── misc_test.go │ ├── search.go │ ├── search │ │ └── search.go │ ├── search_folders.go │ ├── search_folders_test.go │ ├── search_test.go │ ├── streaming_profiles.go │ ├── streaming_profiles_test.go │ ├── transformations.go │ ├── transformations_test.go │ ├── upload_mappings.go │ ├── upload_mappings_test.go │ ├── upload_presets.go │ └── upload_presets_test.go ├── api.go └── uploader │ ├── acceptance_test.go │ ├── archive.go │ ├── archive_test.go │ ├── context.go │ ├── context_test.go │ ├── creative.go │ ├── creative_test.go │ ├── edit_asset.go │ ├── edit_asset_test.go │ ├── tag.go │ ├── tag_test.go │ ├── upload.go │ ├── upload_acceptance_test.go │ ├── upload_asset.go │ └── upload_asset_test.go ├── asset ├── analytics.go ├── analytics_test.go ├── asset.go ├── asset_test.go ├── auth_token.go ├── auth_token_test.go ├── distribution_test.go ├── image_test.go ├── search_url_asset.go └── search_url_asset_test.go ├── cloudinary.go ├── cloudinary_test.go ├── config ├── api.go ├── auth_token.go ├── cloud.go ├── configuration.go ├── configuration_test.go └── url.go ├── example ├── .env.example ├── .gitignore ├── example.go ├── go.mod └── go.sum ├── gen └── generate_setters │ ├── README.md │ ├── ast.go │ ├── ast_test.go │ ├── main.go │ ├── misc.go │ ├── misc_test.go │ ├── template.go │ ├── template_test.go │ └── testdata │ ├── anotherpackage │ └── struct3.go │ ├── cases.go │ ├── struct1.go │ └── struct2.go ├── go.mod ├── go.sum ├── internal ├── cldtest │ ├── cldtest.go │ └── testdata │ │ ├── cloudinary_logo.png │ │ └── movie.mp4 └── signature │ └── signature.go ├── logger ├── README.md ├── logger.go └── logger_test.go ├── scripts ├── allocate_test_cloud.go ├── get_test_cloud.sh └── update_version.sh └── transformation └── transformation.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Bug report for Cloudinary Go SDK 11 | Before proceeding, please update to the latest version and test if the issue persists 12 | 13 | ## Describe the bug in a sentence or two. 14 | … 15 | 16 | ## Issue Type (Can be multiple) 17 | - [ ] Build - Cannot install or import the SDK 18 | - [ ] Performance - Performance issues 19 | - [ ] Behaviour - Functions are not working as expected (such as generate URL) 20 | - [ ] Documentation - Inconsistency between the docs and behaviour 21 | - [ ] Other (Specify) 22 | 23 | ## Steps to reproduce 24 | 25 | … if applicable 26 | 27 | ## Error screenshots 28 | 29 | … if applicable 30 | 31 | ## OS 32 | 33 | - [ ] Linux 34 | - [ ] Windows 35 | - [ ] macOS 36 | - [ ] Other (Specify) 37 | - [ ] All 38 | 39 | ## Versions and Libraries (fill in the version numbers) 40 | 41 | - Cloudinary Go SDK Version - 0.0.0 42 | - GoLang Version - 0.0.0 43 | 44 | ## Repository 45 | 46 | If possible, please provide a link to a reproducible repository that showcases the problem 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Feature request for Cloudinary Go SDK 11 | …(If your feature is for other SDKs, please request them in the relevant repo) 12 | 13 | ## Explain your use case 14 | … (A high-level explanation of why you need this feature) 15 | 16 | ## Describe the problem you’re trying to solve 17 | … (A more technical view of what you’d like to accomplish, and how this feature will help you achieve it) 18 | 19 | ## Do you have a proposed solution? 20 | … (yes, no? Please elaborate if needed) 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Brief Summary of Changes 2 | 3 | 4 | 5 | #### What does this PR address? 6 | 7 | - [ ] GitHub issue (Add reference - #XX) 8 | - [ ] Refactoring 9 | - [ ] New feature 10 | - [ ] Bug fix 11 | - [ ] Adds more tests 12 | 13 | #### Are tests included? 14 | 15 | - [ ] Yes 16 | - [ ] No 17 | 18 | #### Reviewer, please note: 19 | 20 | 27 | 28 | #### Checklist: 29 | 30 | 31 | 32 | 33 | - [ ] My code follows the code style of this project. 34 | - [ ] My change requires a change to the documentation. 35 | - [ ] I ran the full test suite before pushing the changes and all the tests pass. 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Go Test 🚀 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: 🐹 Test with Go ${{ matrix.go-version }} 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | go-version: 13 | - 1.20.x 14 | - 1.21.x 15 | - 1.22.x 16 | - 1.23.x 17 | 18 | steps: 19 | - name: 🔄 Checkout Repository 20 | uses: actions/checkout@v4 21 | 22 | - name: 🛠️ Setup Go ${{ matrix.go-version }} 23 | uses: actions/setup-go@v4 24 | with: 25 | go-version: ${{ matrix.go-version }} 26 | 27 | - name: 📦 Install Dependencies 28 | run: go mod download 29 | 30 | - name: 🌐 Set CLOUDINARY_URL 31 | run: | 32 | export CLOUDINARY_URL=$(bash scripts/get_test_cloud.sh) 33 | echo "CLOUDINARY_URL=$CLOUDINARY_URL" >> $GITHUB_ENV 34 | echo "cloud_name: $(echo $CLOUDINARY_URL | cut -d'@' -f2)" 35 | 36 | - name: 🧰 Install gotestsum 37 | run: go install gotest.tools/gotestsum@latest 38 | 39 | - name: 🧪 Run Tests 40 | run: | 41 | gotestsum --junitfile unit-tests.xml --format pkgname ./... 42 | 43 | - name: 📊 Test Summary 44 | uses: test-summary/action@v2 45 | with: 46 | paths: "unit-tests.xml" 47 | if: always() 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Cloudinary Go library 2 | 3 | Contributions are welcome and greatly appreciated! 4 | 5 | ## Reporting a bug 6 | 7 | - Make sure that the bug was not already reported by searching in GitHub under [Issues](https://github.com/cloudinary/cloudinary-go) and the Cloudinary [Support forms](https://support.cloudinary.com). 8 | - If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/cloudinary/cloudinary-go/issues/new/choose). 9 | Be sure to include a **title and clear description**, as relevant information as possible, and a **code sample**, or an **executable test case** demonstrating the expected behavior that is not occurring. 10 | - If you require assistance in the implementation of cloudinary-go please [submit a request](https://support.cloudinary.com/hc/en-us/requests/new) on Cloudinary's site. 11 | 12 | ## Requesting a feature 13 | 14 | We would love to receive your requests! 15 | Please be aware that the library is used in a wide variety of environments and that some features may not be applicable to all users. 16 | 17 | - Open a GitHub [issue](https://github.com/cloudinary/cloudinary-go) describing the benefits (and possible drawbacks) of the requested feature 18 | 19 | ## Fixing a bug / Implementing a new feature 20 | 21 | - Follow the instructions detailed in [Code contribution](#code-contribution) 22 | - Open a new GitHub pull request 23 | - Ensure the PR description clearly describes the bug / feature. Include relevant issue number if applicable. 24 | - Provide test code that covers the new code. See [TEST.md](TEST.md) for additional information about tests writing. 25 | 26 | ## Code contribution 27 | 28 | When contributing code, either to fix a bug or to implement a new feature, please follow these guidelines: 29 | 30 | #### Fork the Project 31 | 32 | Fork [project on Github](https://github.com/cloudinary/cloudinary-go) and check your copy. 33 | 34 | ``` 35 | git clone https://github.com/contributor/cloudinary-go.git 36 | cd cloudinary-go 37 | git remote add upstream https://github.com/cloudinary/cloudinary-go.git 38 | ``` 39 | 40 | #### Create a Topic Branch 41 | 42 | Make sure your fork is up-to-date and create a topic branch for your feature or bug fix. 43 | 44 | ``` 45 | git checkout main 46 | git pull upstream main 47 | git checkout -b feature/my-feature-branch 48 | ``` 49 | #### Rebase 50 | 51 | If you've been working on a change for a while, rebase with upstream/main. 52 | 53 | ``` 54 | git fetch upstream 55 | git rebase upstream/main 56 | git push origin feature/my-feature-branch -f 57 | ``` 58 | 59 | 60 | #### Write Tests 61 | 62 | Try to write a test that reproduces the problem you're trying to fix or describes a feature you would like to build. 63 | 64 | We definitely appreciate pull requests that highlight or reproduce a problem, even without a fix. 65 | 66 | See [TEST.md](TEST.md) for additional information about tests writing. 67 | 68 | #### Write Code 69 | 70 | Implement your feature or bug fix. 71 | Follow the following Go coding standards, described in [Effective Go](https://golang.org/doc/effective_go) documentation. 72 | 73 | Make sure that `go test ./...` completes without errors. 74 | 75 | #### Write Documentation 76 | 77 | Document any external behavior in the [README](README.md). 78 | 79 | #### Commit Changes 80 | 81 | Make sure git knows your name and email address: 82 | 83 | ``` 84 | git config --global user.name "Your Name" 85 | git config --global user.email "contributor@example.com" 86 | ``` 87 | 88 | Writing good commit logs is important. A commit log should describe what changed and why. 89 | 90 | ``` 91 | git add ... 92 | git commit 93 | ``` 94 | 95 | 96 | > Please squash your commits into a single commit when appropriate. This simplifies future cherry-picks and keeps the git log clean. 97 | 98 | #### Push 99 | 100 | ``` 101 | git push origin feature/my-feature-branch 102 | ``` 103 | 104 | #### Make a Pull Request 105 | 106 | Go to https://github.com/cloudinary/cloudinary-go/pulls. Click the 'New pull Request' button and fill out the form. Pull requests are normally reviewed within a few days. 107 | Ensure the PR description clearly describes the problem and solution. Include relevant issue number if applicable. 108 | 109 | #### Rebase 110 | 111 | If you've been working on a change for a while, rebase with upstream/main. 112 | 113 | ``` 114 | git fetch upstream 115 | git rebase upstream/main 116 | git push origin feature/my-feature-branch -f 117 | ``` 118 | 119 | #### Check on Your Pull Request 120 | 121 | Go back to your pull request after a few minutes and see whether it passed muster with Travis-CI. Everything should look green, otherwise - fix issues and amend your commit as described above. 122 | 123 | #### Be Patient 124 | 125 | It's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang on there! 126 | 127 | #### Thank You 128 | 129 | Please do know that we really appreciate and value your time and work. We love you, really. 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Cloudinary 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/bash -O extglob -c 2 | generate: 3 | go run gen/generate_setters/!(*_test).go -e gen/generate_setters/test_data,gen/generate_setters/test_data/another_package 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Tests](https://github.com/cloudinary/cloudinary-go/actions/workflows/test.yaml/badge.svg)](https://github.com/cloudinary/cloudinary-go/actions) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/cloudinary/cloudinary-go/v2)](https://goreportcard.com/report/github.com/cloudinary/cloudinary-go/v2) 3 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/cloudinary/cloudinary-go/v2)](https://pkg.go.dev/github.com/cloudinary/cloudinary-go/v2) 4 | 5 | Cloudinary Go SDK 6 | ================== 7 | 8 | ## About 9 | 10 | The Cloudinary Go SDK allows you to quickly and easily integrate your application with Cloudinary. 11 | Effortlessly optimize, transform, upload and manage your cloud's assets. 12 | 13 | #### Note 14 | 15 | This Readme provides basic installation and usage information. 16 | For the complete documentation, see the [Go SDK Guide](https://cloudinary.com/documentation/go_integration). 17 | 18 | ## Table of Contents 19 | 20 | - [Key Features](#key-features) 21 | - [Version Support](#Version-Support) 22 | - [Installation](#installation) 23 | - [Usage](#usage) 24 | - [Setup](#Setup) 25 | - [Transform and Optimize Assets](#Transform-and-Optimize-Assets) 26 | 27 | ## Key Features 28 | 29 | - [Transform](https://cloudinary.com/documentation/go_media_transformations) assets. 30 | - [Asset Management](https://cloudinary.com/documentation/go_asset_administration). 31 | - [Secure URLs](https://cloudinary.com/documentation/video_manipulation_and_delivery#generating_secure_https_urls_using_sdks). 32 | 33 | ## Version Support 34 | 35 | | **SDK Version** | **Go 1.13 - 1.19** | **Go 1.20** | **Go 1.21** | **Go 1.22** | **Go 1.23** | 36 | |-----------------|--------------------|-------------|-------------|-------------|-------------| 37 | | **2.8 & Up** | ❌ | ✔️ | ✔️ | ✔️ | ✔️ | 38 | | **2.7** | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | 39 | | **1.x** | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | 40 | 41 | ## Installation 42 | 43 | ```bash 44 | go get github.com/cloudinary/cloudinary-go/v2 45 | ``` 46 | 47 | # Usage 48 | 49 | ### Setup 50 | 51 | ```go 52 | import ( 53 | "github.com/cloudinary/cloudinary-go/v2" 54 | ) 55 | 56 | cld, _ := cloudinary.New() 57 | ``` 58 | 59 | - [See full documentation](https://cloudinary.com/documentation/go_integration#configuration). 60 | 61 | ### Transform and Optimize Assets 62 | 63 | - [See full documentation](https://cloudinary.com/documentation/go_media_transformations). 64 | 65 | ```go 66 | image, err := cld.Image("sample.jpg") 67 | if err != nil {...} 68 | 69 | image.Transformation = "c_fill,h_150,w_100" 70 | 71 | imageURL, err := image.String() 72 | ``` 73 | 74 | ### Upload 75 | 76 | - [See full documentation](https://cloudinary.com/documentation/go_image_and_video_upload). 77 | - [Learn more about configuring your uploads with upload presets](https://cloudinary.com/documentation/upload_presets). 78 | 79 | ```go 80 | resp, err := cld.Upload.Upload(ctx, "my_picture.jpg", uploader.UploadParams{}) 81 | ``` 82 | 83 | ### Security options 84 | 85 | - [See full documentation](https://cloudinary.com/documentation/solution_overview#security). 86 | 87 | ### Logging 88 | 89 | Cloudinary SDK logs errors using standard `go log` functions. 90 | 91 | For details on redefining the logger or adjusting the logging level, see [Logging](logger/README.md). 92 | 93 | ### Complete SDK Example 94 | 95 | See [Complete SDK Example](example/example.go). 96 | 97 | ## Contributions 98 | 99 | - Ensure tests run locally 100 | - Open a PR and ensure Travis tests pass 101 | - For more information on how to contribute, take a look at the [contributing](CONTRIBUTING.md) page. 102 | 103 | ## Get Help 104 | 105 | If you run into an issue or have a question, you can either: 106 | 107 | - Issues related to the SDK: [Open a GitHub issue](https://github.com/cloudinary/cloudinary-go/issues). 108 | - Issues related to your account: [Open a support ticket](https://cloudinary.com/contact) 109 | 110 | ## About Cloudinary 111 | 112 | Cloudinary is a powerful media API for websites and mobile apps alike, Cloudinary enables developers to efficiently 113 | manage, transform, optimize, and deliver images and videos through multiple CDNs. Ultimately, viewers enjoy responsive 114 | and personalized visual-media experiences—irrespective of the viewing device. 115 | 116 | ## Additional Resources 117 | 118 | - [Cloudinary Transformation and REST API References](https://cloudinary.com/documentation/cloudinary_references): 119 | Comprehensive references, including syntax and examples for all SDKs. 120 | - [MediaJams.dev](https://mediajams.dev/): Bite-size use-case tutorials written by and for Cloudinary Developers 121 | - [DevJams](https://www.youtube.com/playlist?list=PL8dVGjLA2oMr09amgERARsZyrOz_sPvqw): Cloudinary developer podcasts on 122 | YouTube. 123 | - [Cloudinary Academy](https://training.cloudinary.com/): Free self-paced courses, instructor-led virtual courses, and 124 | on-site courses. 125 | - [Code Explorers and Feature Demos](https://cloudinary.com/documentation/code_explorers_demos_index): A one-stop shop 126 | for all code explorers, Postman collections, and feature demos found in the docs. 127 | - [Cloudinary Roadmap](https://cloudinary.com/roadmap): Your chance to follow, vote, or suggest what Cloudinary should 128 | develop next. 129 | - [Cloudinary Facebook Community](https://www.facebook.com/groups/CloudinaryCommunity): Learn from and offer help to 130 | other Cloudinary developers. 131 | - [Cloudinary Account Registration](https://cloudinary.com/users/register/free): Free Cloudinary account registration. 132 | - [Cloudinary Website](https://cloudinary.com): Learn about Cloudinary's products, partners, customers, pricing, and 133 | more. 134 | 135 | ## Licence 136 | 137 | Released under the MIT license. 138 | -------------------------------------------------------------------------------- /TEST.md: -------------------------------------------------------------------------------- 1 | # Writing tests for Cloudinary SDK 2 | 3 | When you are creating a new feature it is highly recommended adding some tests for it. 4 | There are two types of tests in Cloudinary SDK: E2E and acceptance. 5 | 6 | ### End-to-End tests (E2E) 7 | You can find E2E tests in `*_test.go` files. 8 | The main purpose of these tests is to check the main scenarios in the SDK integration with Cloudinary server. 9 | These tests performing some HTTP queries to the Cloudinary server and requires `CLOUDINARY_URL` to be given to run. 10 | 11 | #### Writing an E2E test 12 | Check [api/admin/api_test.go](api/admin/api_test.go) for an example of E2E test. 13 | These tests are using a basic Go test approach [https://golang.org/pkg/testing/](https://golang.org/pkg/testing/). 14 | 15 | *Example of E2E test:* 16 | ```go 17 | 18 | func TestAPI_Timeout(t *testing.T) { 19 | var originalTimeout = adminAPI.Config.API.Timeout 20 | 21 | adminAPI.Config.API.Timeout = 0 // should timeout immediately 22 | 23 | _, err := adminAPI.Ping(ctx) 24 | 25 | if err == nil || !strings.HasSuffix(err.Error(), "context deadline exceeded") { 26 | t.Error("Expected context timeout did not happen") 27 | } 28 | 29 | adminAPI.Config.API.Timeout = originalTimeout 30 | } 31 | 32 | ``` 33 | 34 | 35 | ### Acceptance tests 36 | These tests are placed in `*_acceptance_test.go` files. 37 | The main purpose of these tests is to check whether SDK generates proper HTTP requests or not with mocked HTTP-server. 38 | 39 | #### Writing an acceptance test 40 | Check [api/admin/asset_acceptance_test.go](api/admin/assets_acceptance_test.go) for an example of acceptance test. 41 | These tests are using a basic Go test approach with [httptest](https://golang.org/pkg/net/http/httptest/) as a mocking HTTP server. 42 | 43 | 44 | A basic function to run in test has this reference: 45 | ```go 46 | func testApiByTestCases(cases []ApiAcceptanceTestCase, t *testing.T) 47 | ``` 48 | 49 | ApiAcceptanceTestCase is a structure that defines a test case for the test: 50 | 51 | ```go 52 | // Acceptance test case definition. See `TEST.md` for an additional information. 53 | type ApiAcceptanceTestCase struct { 54 | Name string // Name of the test case 55 | RequestTest ApiRequestTest // Function which will be called as a API request. Put SDK calls here. 56 | ResponseTest ApiResponseTest // Function which will be called to test an API response. 57 | ExpectedRequest expectedRequestParams // Expected HTTP request to be sent to the server 58 | JsonResponse string // Mock of the JSON response from server. This is used to check JSON parsing. 59 | ExpectedStatus string // Expected HTTP status of the request. This status will be returned from the HTTP mock. 60 | ExpectedCallCount int // Expected call count to the server. 61 | } 62 | ``` 63 | 64 | *Example of test case:* 65 | 66 | ```go 67 | { 68 | Name: "Ping", 69 | RequestTest: func(api *admin.API, ctx context.Context) (interface{}, error) { 70 | return api.Ping(ctx) 71 | }, 72 | ResponseTest: func(response interface{}, t *testing.T) { 73 | _, ok := response.(*admin.PingResult) 74 | if !ok { 75 | t.Errorf("Response should be type of PingResult, %s given", reflect.TypeOf(response)) 76 | } 77 | }, 78 | ExpectedRequest: expectedRequestParams{Method: "GET", Uri: "/ping"}, 79 | JsonResponse: "{\"status\": \"OK\"}", 80 | ExpectedCallCount: 1, 81 | }, 82 | ``` 83 | 84 | You can find more examples in [api/admin/asset_acceptance_test.go](api/admin/assets_acceptance_test.go). -------------------------------------------------------------------------------- /api/admin/acceptance_test.go: -------------------------------------------------------------------------------- 1 | package admin_test 2 | 3 | import ( 4 | "context" 5 | "github.com/cloudinary/cloudinary-go/v2/api/admin" 6 | "github.com/cloudinary/cloudinary-go/v2/config" 7 | "github.com/cloudinary/cloudinary-go/v2/internal/cldtest" 8 | "testing" 9 | ) 10 | 11 | // Function that will be executed during the test 12 | type AdminAPIRequestTest func(api *admin.API, ctx context.Context) (interface{}, error) 13 | 14 | // Acceptance test case definition. See `TEST.md` for additional information. 15 | type AdminAPIAcceptanceTestCase struct { 16 | Name string // Name of the test case 17 | RequestTest AdminAPIRequestTest // Function which will be called as an API request. Put SDK calls here. 18 | ResponseTest cldtest.APIResponseTest // Function which will be called to test an API response. 19 | ExpectedRequest cldtest.ExpectedRequestParams // Expected HTTP request to be sent to the server 20 | JsonResponse string // Mock of the JSON response from server. This is used to check JSON parsing. 21 | ExpectedStatus string // Expected HTTP status of the request. This status will be returned from the HTTP mock. 22 | ExpectedCallCount int // Expected call count to the server. 23 | Config *config.Configuration // Configuration 24 | } 25 | 26 | // Run acceptance tests by given test cases. See `TEST.md` for additional information. 27 | func testAdminAPIByTestCases(cases []AdminAPIAcceptanceTestCase, t *testing.T) { 28 | for num, test := range cases { 29 | if test.Name == "" { 30 | t.Skipf("Test name should be set for test #%d. Skipping it.", num) 31 | } 32 | 33 | t.Run(test.Name, func(t *testing.T) { 34 | callCounter := 0 35 | srv := cldtest.GetServerMock(cldtest.GetTestHandler(test.JsonResponse, t, &callCounter, test.ExpectedRequest)) 36 | 37 | res, _ := test.RequestTest(getTestableAdminAPI(srv.URL, test.Config, t), ctx) 38 | test.ResponseTest(res, t) 39 | 40 | if callCounter != test.ExpectedCallCount { 41 | t.Errorf("Expected %d call, %d given", test.ExpectedCallCount, callCounter) 42 | } 43 | 44 | srv.Close() 45 | }) 46 | } 47 | } 48 | 49 | // Get configured API for test 50 | func getTestableAdminAPI(mockServerUrl string, c *config.Configuration, t *testing.T) *admin.API { 51 | if c == nil { 52 | var err error 53 | c, err = config.NewFromParams(cldtest.CloudName, cldtest.APIKey, cldtest.APISecret) 54 | if err != nil { 55 | t.Error(err) 56 | } 57 | } 58 | 59 | c.API.UploadPrefix = mockServerUrl 60 | 61 | api, err := admin.NewWithConfiguration(c) 62 | if err != nil { 63 | t.Error(err) 64 | } 65 | 66 | return api 67 | } 68 | -------------------------------------------------------------------------------- /api/admin/analysis.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "context" 5 | "github.com/cloudinary/cloudinary-go/v2/api" 6 | ) 7 | 8 | const ( 9 | analysis api.EndPoint = "analysis" 10 | analyze api.EndPoint = "analyze" 11 | uri api.EndPoint = "uri" 12 | ) 13 | 14 | // AnalyzeParams are the parameters for Analyze. 15 | type AnalyzeParams struct { 16 | // The URI of the asset to analyze 17 | Uri string `json:"uri,omitempty"` 18 | AnalysisType string `json:"analysis_type,omitempty"` 19 | Parameters *AnalyzeUriRequestParameters `json:"parameters,omitempty"` 20 | } 21 | 22 | // AnalyzeUriRequestParameters struct for AnalyzeUriRequestParameters 23 | type AnalyzeUriRequestParameters struct { 24 | Custom *CustomParameters `json:"custom,omitempty"` 25 | } 26 | 27 | // CustomParameters struct for CustomParameters 28 | type CustomParameters struct { 29 | ModelName string `json:"model_name,omitempty"` 30 | ModelVersion int `json:"model_version,omitempty"` 31 | } 32 | 33 | /* 34 | Analyze Analyzes an asset with the requested analysis type. 35 | 36 | Currently supports the following analysis options: 37 | * [Google tagging](https://cloudinary.com/documentation/google_auto_tagging_addon) 38 | * [Captioning](https://cloudinary.com/documentation/cloudinary_ai_content_analysis_addon#ai_based_image_captioning) 39 | * [Cld Fashion](https://cloudinary.com/documentation/cloudinary_ai_content_analysis_addon#supported_content_aware_detection_models) 40 | * [Coco](https://cloudinary.com/documentation/cloudinary_ai_content_analysis_addon#supported_content_aware_detection_models) 41 | * [Lvis](https://cloudinary.com/documentation/cloudinary_ai_content_analysis_addon#supported_content_aware_detection_models) 42 | * [Unidet](https://cloudinary.com/documentation/cloudinary_ai_content_analysis_addon#supported_content_aware_detection_models) 43 | * [Human Anatomy](https://cloudinary.com/documentation/cloudinary_ai_content_analysis_addon#supported_content_aware_detection_models) 44 | * [Cld Text](https://cloudinary.com/documentation/cloudinary_ai_content_analysis_addon#supported_content_aware_detection_models) 45 | * [Shop Classifier](https://cloudinary.com/documentation/cloudinary_ai_content_analysis_addon#supported_content_aware_detection_models) 46 | * Custom 47 | */ 48 | func (a *API) Analyze(ctx context.Context, params AnalyzeParams) (*AnalyzeResult, error) { 49 | v2APICtx := context.WithValue(ctx, "api_version", "2") 50 | res := &AnalyzeResult{} 51 | _, err := a.post(v2APICtx, api.BuildPath(analysis, analyze, uri), params, res) 52 | 53 | return res, err 54 | } 55 | 56 | // AnalyzeResult is the result of Analyze. 57 | type AnalyzeResult struct { 58 | Data AnalysisPayload `json:"data"` 59 | RequestId string `json:"request_id"` 60 | Error api.ErrorResp `json:"error,omitempty"` 61 | Response interface{} 62 | } 63 | 64 | // AnalysisPayload struct for AnalysisPayload 65 | type AnalysisPayload struct { 66 | Entity string `json:"entity"` 67 | Analysis map[string]interface{} `json:"analysis"` 68 | } 69 | -------------------------------------------------------------------------------- /api/admin/analysis_acceptance_test.go: -------------------------------------------------------------------------------- 1 | package admin_test 2 | 3 | // Acceptance tests for API. See `TEST.md` for additional information. 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | "fmt" 9 | "github.com/cloudinary/cloudinary-go/v2/api/admin" 10 | "github.com/cloudinary/cloudinary-go/v2/internal/cldtest" 11 | "reflect" 12 | "testing" 13 | ) 14 | 15 | // Acceptance test cases for `analyze` method 16 | func getAnalysisTestCases() []AdminAPIAcceptanceTestCase { 17 | type analyzeTestCase struct { 18 | requestParams admin.AnalyzeParams 19 | uri string 20 | expectedBody string 21 | } 22 | 23 | getTestCase := func(num int, t analyzeTestCase) AdminAPIAcceptanceTestCase { 24 | return AdminAPIAcceptanceTestCase{ 25 | Name: fmt.Sprintf("Analyze #%d", num), 26 | RequestTest: func(api *admin.API, ctx context.Context) (interface{}, error) { 27 | return api.Analyze(ctx, t.requestParams) 28 | }, 29 | ResponseTest: func(response interface{}, t *testing.T) { 30 | _, ok := response.(*admin.AnalyzeResult) 31 | if !ok { 32 | t.Errorf("Response should be type of AnalyzeResult, %s given", reflect.TypeOf(response)) 33 | } 34 | }, 35 | ExpectedRequest: cldtest.ExpectedRequestParams{ 36 | Method: "POST", 37 | URI: t.uri, 38 | APIVersion: "v2", 39 | Body: &t.expectedBody, 40 | }, 41 | JsonResponse: "{}", 42 | ExpectedCallCount: 1, 43 | } 44 | } 45 | 46 | var testCases []AdminAPIAcceptanceTestCase 47 | 48 | analysisTestCases := []analyzeTestCase{ 49 | { 50 | requestParams: admin.AnalyzeParams{ 51 | Uri: cldtest.LogoURL, 52 | AnalysisType: "captioning", 53 | }, 54 | uri: "/analysis/analyze/uri", 55 | expectedBody: `{"uri":"` + cldtest.LogoURL + `","analysis_type":"captioning"}`, 56 | }, 57 | } 58 | 59 | for num, testCase := range analysisTestCases { 60 | testCases = append(testCases, getTestCase(num, testCase)) 61 | } 62 | 63 | analyzeRes := admin.AnalyzeResult{ 64 | Data: admin.AnalysisPayload{ 65 | Entity: cldtest.LogoURL, 66 | Analysis: map[string]interface{}{"data": "data"}, 67 | }, 68 | RequestId: cldtest.AssetID, 69 | } 70 | responseJson, _ := json.Marshal(analyzeRes) 71 | 72 | testCases = append(testCases, AdminAPIAcceptanceTestCase{ 73 | Name: "Analyze response parsing case", 74 | RequestTest: func(api *admin.API, ctx context.Context) (interface{}, error) { 75 | return api.Analyze(ctx, admin.AnalyzeParams{ 76 | Uri: cldtest.LogoURL, 77 | AnalysisType: "captioning", 78 | }) 79 | }, 80 | ResponseTest: func(response interface{}, t *testing.T) { 81 | v, ok := response.(*admin.AnalyzeResult) 82 | if !ok { 83 | t.Errorf("Response should be type of AnalyzeResult, %s given", reflect.TypeOf(response)) 84 | } 85 | v.Response = nil // omit raw response comparison 86 | if !reflect.DeepEqual(*v, analyzeRes) { 87 | t.Errorf("Response analyzeRes should be %v, %v given", analyzeRes, *v) 88 | } 89 | }, 90 | 91 | ExpectedRequest: cldtest.ExpectedRequestParams{ 92 | Method: "POST", 93 | APIVersion: "v2", 94 | URI: "/analysis/analyze/uri", 95 | }, 96 | JsonResponse: string(responseJson), 97 | ExpectedCallCount: 1, 98 | }, 99 | ) 100 | 101 | return testCases 102 | } 103 | 104 | // Run tests 105 | func TestAnalysis_Acceptance(t *testing.T) { 106 | t.Parallel() 107 | testAdminAPIByTestCases(getAnalysisTestCases(), t) 108 | } 109 | -------------------------------------------------------------------------------- /api/admin/api_acceptance_test.go: -------------------------------------------------------------------------------- 1 | package admin_test 2 | 3 | // Acceptance tests for API. See `TEST.md` for additional information. 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "github.com/cloudinary/cloudinary-go/v2/api" 9 | "github.com/cloudinary/cloudinary-go/v2/api/admin" 10 | "github.com/cloudinary/cloudinary-go/v2/config" 11 | "github.com/cloudinary/cloudinary-go/v2/internal/cldtest" 12 | "reflect" 13 | "testing" 14 | ) 15 | 16 | var oAuthTokenConfig, _ = config.NewFromOAuthToken(cldtest.CloudName, "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4") 17 | 18 | // Acceptance test cases for `ping` method 19 | func getPingTestCases() []AdminAPIAcceptanceTestCase { 20 | return []AdminAPIAcceptanceTestCase{ 21 | { 22 | Name: "Ping", 23 | RequestTest: func(api *admin.API, ctx context.Context) (interface{}, error) { 24 | return api.Ping(ctx) 25 | }, 26 | ResponseTest: func(response interface{}, t *testing.T) { 27 | _, ok := response.(*admin.PingResult) 28 | if !ok { 29 | t.Errorf("Response should be type of PingResult, %s given", reflect.TypeOf(response)) 30 | } 31 | }, 32 | ExpectedRequest: cldtest.ExpectedRequestParams{Method: "GET", URI: "/ping"}, 33 | JsonResponse: "{\"status\": \"OK\"}", 34 | ExpectedCallCount: 1, 35 | }, 36 | { 37 | Name: "Ping error check", 38 | RequestTest: func(api *admin.API, ctx context.Context) (interface{}, error) { 39 | return api.Ping(ctx) 40 | }, 41 | ResponseTest: func(response interface{}, t *testing.T) { 42 | v, ok := response.(*admin.PingResult) 43 | if !ok { 44 | t.Errorf("Response should be type %s, %s given", reflect.TypeOf(admin.PingResult{}), reflect.TypeOf(response)) 45 | } else { 46 | if v.Status == "OK" { 47 | t.Error("Response status should not be OK") 48 | } 49 | 50 | if v.Error.Message != "ERROR MESSAGE" { 51 | t.Errorf("Error message should be %s, %s given", "ERROR MESSAGE", v.Error.Message) 52 | } 53 | } 54 | }, 55 | ExpectedRequest: cldtest.ExpectedRequestParams{Method: "GET", URI: "/ping"}, 56 | JsonResponse: "{\"error\":{\"message\": \"ERROR MESSAGE\"}}", 57 | ExpectedCallCount: 1, 58 | }, 59 | { 60 | Name: "Ping result struct check", 61 | RequestTest: func(api *admin.API, ctx context.Context) (interface{}, error) { 62 | return api.Ping(ctx) 63 | }, 64 | ResponseTest: func(response interface{}, t *testing.T) { 65 | v, ok := response.(*admin.PingResult) 66 | if ok { 67 | if v.Status != "OK" { 68 | t.Errorf("Status should be %s, %s given\n", "OK", v.Status) 69 | } 70 | } else { 71 | t.Errorf("Response should be type %s, %s given", reflect.TypeOf(admin.PingResult{}), reflect.TypeOf(response)) 72 | } 73 | }, 74 | ExpectedRequest: cldtest.ExpectedRequestParams{Method: "GET", URI: "/ping"}, 75 | JsonResponse: "{\"status\":\"OK\"}", 76 | ExpectedCallCount: 1, 77 | }, 78 | } 79 | } 80 | 81 | // Acceptance test cases for user agent and user platform 82 | func getUserAgentTestCases() []AdminAPIAcceptanceTestCase { 83 | return []AdminAPIAcceptanceTestCase{ 84 | { 85 | Name: "Test User Agent", 86 | RequestTest: func(api *admin.API, ctx context.Context) (interface{}, error) { 87 | return api.Ping(ctx) 88 | }, 89 | ResponseTest: func(response interface{}, t *testing.T) {}, 90 | ExpectedRequest: cldtest.ExpectedRequestParams{ 91 | Method: "GET", 92 | URI: "/ping", 93 | Headers: &map[string]string{"User-Agent": api.UserAgent}, 94 | }, 95 | JsonResponse: "{\"status\": \"OK\"}", 96 | ExpectedCallCount: 1, 97 | }, 98 | { 99 | Name: "Test User Agent With User Platform", 100 | RequestTest: func(adminAPI *admin.API, ctx context.Context) (interface{}, error) { 101 | api.UserPlatform = "Test/1.2.3" 102 | return adminAPI.Ping(ctx) 103 | }, 104 | ResponseTest: func(response interface{}, t *testing.T) {}, 105 | ExpectedRequest: cldtest.ExpectedRequestParams{ 106 | Method: "GET", 107 | URI: "/ping", 108 | Headers: &map[string]string{"User-Agent": fmt.Sprintf("Test/1.2.3 %s", api.UserAgent)}, 109 | }, 110 | JsonResponse: "{\"status\": \"OK\"}", 111 | ExpectedCallCount: 1, 112 | }, 113 | } 114 | } 115 | 116 | // Acceptance test cases for Authorization 117 | func getAuthorizationTestCases() []AdminAPIAcceptanceTestCase { 118 | return []AdminAPIAcceptanceTestCase{ 119 | { 120 | Name: "Test Basic Authorization", 121 | RequestTest: func(api *admin.API, ctx context.Context) (interface{}, error) { 122 | return api.Ping(ctx) 123 | }, 124 | ResponseTest: func(response interface{}, t *testing.T) {}, 125 | ExpectedRequest: cldtest.ExpectedRequestParams{ 126 | Method: "GET", 127 | URI: "/ping", 128 | Headers: &map[string]string{"Authorization": "Basic a2V5OnNlY3JldA=="}, 129 | }, 130 | JsonResponse: "{\"status\": \"OK\"}", 131 | ExpectedCallCount: 1, 132 | }, 133 | { 134 | Name: "Test OAuth Authorization", 135 | Config: oAuthTokenConfig, 136 | RequestTest: func(adminAPI *admin.API, ctx context.Context) (interface{}, error) { 137 | return adminAPI.Ping(ctx) 138 | }, 139 | ResponseTest: func(response interface{}, t *testing.T) {}, 140 | ExpectedRequest: cldtest.ExpectedRequestParams{ 141 | Method: "GET", 142 | URI: "/ping", 143 | Headers: &map[string]string{"Authorization": "Bearer MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4"}, 144 | }, 145 | JsonResponse: "{\"status\": \"OK\"}", 146 | ExpectedCallCount: 1, 147 | }, 148 | } 149 | } 150 | 151 | // Run tests 152 | func TestAdminAPI_Acceptance(t *testing.T) { 153 | t.Parallel() 154 | testAdminAPIByTestCases(getPingTestCases(), t) 155 | testAdminAPIByTestCases(getUserAgentTestCases(), t) 156 | testAdminAPIByTestCases(getAuthorizationTestCases(), t) 157 | } 158 | -------------------------------------------------------------------------------- /api/admin/api_test.go: -------------------------------------------------------------------------------- 1 | package admin_test 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/cloudinary/cloudinary-go/v2/api/admin" 9 | ) 10 | 11 | var ctx = context.Background() 12 | var adminAPI, _ = admin.New() 13 | 14 | func TestAPI_Timeout(t *testing.T) { 15 | var originalTimeout = adminAPI.Config.API.Timeout 16 | 17 | adminAPI.Config.API.Timeout = 0 // should time out immediately 18 | 19 | _, err := adminAPI.Ping(ctx) 20 | 21 | if err == nil || !strings.HasSuffix(err.Error(), "context deadline exceeded") { 22 | t.Error("Expected context timeout did not happen") 23 | } 24 | 25 | adminAPI.Config.API.Timeout = originalTimeout 26 | } 27 | -------------------------------------------------------------------------------- /api/admin/asset_acceptance_test.go: -------------------------------------------------------------------------------- 1 | package admin_test 2 | 3 | // Acceptance tests for API. See `TEST.md` for additional information. 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | "fmt" 9 | "github.com/cloudinary/cloudinary-go/v2/api" 10 | "github.com/cloudinary/cloudinary-go/v2/api/admin" 11 | "github.com/cloudinary/cloudinary-go/v2/internal/cldtest" 12 | "net/url" 13 | "reflect" 14 | "testing" 15 | ) 16 | 17 | // Acceptance test cases for `asset` method 18 | func getAssetTestCases() []AdminAPIAcceptanceTestCase { 19 | type assetTestCase struct { 20 | requestParams admin.AssetParams 21 | uri string 22 | expectedParams *url.Values 23 | } 24 | 25 | getTestCase := func(num int, t assetTestCase) AdminAPIAcceptanceTestCase { 26 | return AdminAPIAcceptanceTestCase{ 27 | Name: fmt.Sprintf("Asset #%d", num), 28 | RequestTest: func(api *admin.API, ctx context.Context) (interface{}, error) { 29 | return api.Asset(ctx, t.requestParams) 30 | }, 31 | ResponseTest: func(response interface{}, t *testing.T) { 32 | _, ok := response.(*admin.AssetResult) 33 | if !ok { 34 | t.Errorf("Response should be type of AssetResult, %s given", reflect.TypeOf(response)) 35 | } 36 | }, 37 | ExpectedRequest: cldtest.ExpectedRequestParams{ 38 | Method: "GET", 39 | URI: t.uri, 40 | Params: t.expectedParams, 41 | }, 42 | JsonResponse: "{}", 43 | ExpectedCallCount: 1, 44 | } 45 | } 46 | 47 | var testCases []AdminAPIAcceptanceTestCase 48 | 49 | assetTestCases := []assetTestCase{ 50 | { 51 | requestParams: admin.AssetParams{PublicID: cldtest.PublicID}, 52 | uri: "/resources/image/upload/" + cldtest.PublicID, 53 | expectedParams: &url.Values{}, 54 | }, 55 | { 56 | requestParams: admin.AssetParams{PublicID: cldtest.PublicID, 57 | Related: api.Bool(true), RelatedNextCursor: "NEXT_CURSOR"}, 58 | uri: "/resources/image/upload/" + cldtest.PublicID, 59 | expectedParams: &url.Values{ 60 | "related": []string{"true"}, 61 | "related_next_cursor": []string{"NEXT_CURSOR"}, 62 | }, 63 | }, 64 | } 65 | 66 | for num, testCase := range assetTestCases { 67 | testCases = append(testCases, getTestCase(num, testCase)) 68 | } 69 | 70 | asset := admin.AssetResult{AssetID: "1", PublicID: cldtest.PublicID} 71 | responseJson, _ := json.Marshal(asset) 72 | 73 | testCases = append(testCases, AdminAPIAcceptanceTestCase{ 74 | Name: "Asset response parsing case", 75 | RequestTest: func(api *admin.API, ctx context.Context) (interface{}, error) { 76 | return api.Asset(ctx, admin.AssetParams{PublicID: cldtest.PublicID}) 77 | }, 78 | ResponseTest: func(response interface{}, t *testing.T) { 79 | v, ok := response.(*admin.AssetResult) 80 | if !ok { 81 | t.Errorf("Response should be type of AssetResult, %s given", reflect.TypeOf(response)) 82 | } 83 | v.Response = nil // omit raw response comparison 84 | if !reflect.DeepEqual(*v, asset) { 85 | t.Errorf("Response asset should be %v, %v given", asset, *v) 86 | } 87 | }, 88 | 89 | ExpectedRequest: cldtest.ExpectedRequestParams{ 90 | Method: "GET", 91 | URI: "/resources/image/upload/" + cldtest.PublicID, 92 | }, 93 | JsonResponse: string(responseJson), 94 | ExpectedCallCount: 1, 95 | }, 96 | ) 97 | 98 | return testCases 99 | } 100 | 101 | // Run tests 102 | func TestAsset_Acceptance(t *testing.T) { 103 | t.Parallel() 104 | testAdminAPIByTestCases(getAssetTestCases(), t) 105 | } 106 | -------------------------------------------------------------------------------- /api/admin/asset_test.go: -------------------------------------------------------------------------------- 1 | package admin_test 2 | 3 | import ( 4 | "github.com/cloudinary/cloudinary-go/v2/api" 5 | "testing" 6 | 7 | "github.com/cloudinary/cloudinary-go/v2/api/admin" 8 | "github.com/cloudinary/cloudinary-go/v2/internal/cldtest" 9 | ) 10 | 11 | func TestAsset_Asset(t *testing.T) { 12 | cldtest.UploadTestAsset(t, cldtest.PublicID) 13 | 14 | resp, err := adminAPI.Asset(ctx, admin.AssetParams{ 15 | PublicID: cldtest.PublicID, 16 | Exif: api.Bool(true), 17 | Colors: api.Bool(true), 18 | Faces: api.Bool(true), 19 | QualityAnalysis: api.Bool(true), 20 | MediaMetadata: api.Bool(true), 21 | Phash: api.Bool(true), 22 | Pages: api.Bool(true), 23 | AccessibilityAnalysis: api.Bool(true), 24 | CinemagraphAnalysis: api.Bool(true), 25 | Coordinates: api.Bool(true), 26 | }) 27 | 28 | if err != nil || resp.PublicID == "" { 29 | t.Error(resp) 30 | } 31 | } 32 | 33 | func TestAsset_UpdateAsset(t *testing.T) { 34 | resp, err := adminAPI.UpdateAsset(ctx, admin.UpdateAssetParams{ 35 | PublicID: cldtest.PublicID, 36 | Tags: []string{"tagA", "tagB", "TagC"}, 37 | }) 38 | 39 | if err != nil || len(resp.Tags) != 3 { 40 | t.Error(resp, err) 41 | } 42 | } 43 | 44 | func TestAsset_AssetByAssetID(t *testing.T) { 45 | asset := cldtest.UploadTestAsset(t, cldtest.PublicID) 46 | 47 | t.Run("", func(t *testing.T) { 48 | resp, err := adminAPI.AssetByAssetID(ctx, admin.AssetByAssetIDParams{AssetID: asset.AssetID}) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | if resp.Colors != nil { 54 | t.Error() 55 | } 56 | 57 | if resp.Exif != nil { 58 | t.Error() 59 | } 60 | 61 | if resp.Faces != nil { 62 | t.Error() 63 | } 64 | }) 65 | 66 | t.Run("With Extra Info", func(t *testing.T) { 67 | resp, err := adminAPI.AssetByAssetID(ctx, admin.AssetByAssetIDParams{ 68 | AssetID: asset.AssetID, 69 | Colors: api.Bool(true), 70 | Exif: api.Bool(true), 71 | Faces: api.Bool(true), 72 | }) 73 | 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | 78 | if resp.Colors == nil { 79 | t.Error() 80 | } 81 | 82 | if resp.Exif == nil { 83 | t.Error() 84 | } 85 | 86 | if resp.Faces == nil { 87 | t.Error() 88 | } 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /api/admin/assets_test.go: -------------------------------------------------------------------------------- 1 | package admin_test 2 | 3 | import ( 4 | "github.com/cloudinary/cloudinary-go/v2/api/uploader" 5 | "github.com/stretchr/testify/assert" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/cloudinary/cloudinary-go/v2/api" 10 | "github.com/cloudinary/cloudinary-go/v2/api/admin" 11 | "github.com/cloudinary/cloudinary-go/v2/internal/cldtest" 12 | ) 13 | 14 | func TestAssets_AssetTypes(t *testing.T) { 15 | resp, err := adminAPI.AssetTypes(ctx) 16 | 17 | if err != nil || len(resp.AssetTypes) < 1 { 18 | t.Error(err, resp) 19 | } 20 | } 21 | 22 | func TestAssets_Assets(t *testing.T) { 23 | cldtest.UploadTestAsset(t, cldtest.PublicID) 24 | resp, err := adminAPI.Assets(ctx, admin.AssetsParams{ 25 | Tags: api.Bool(true), 26 | Context: api.Bool(true), 27 | Moderations: api.Bool(true), 28 | MaxResults: 1}) 29 | 30 | if err != nil || len(resp.Assets) != 1 { 31 | t.Error(err, resp) 32 | } 33 | } 34 | 35 | func TestAssets_AssetsByIDs(t *testing.T) { 36 | cldtest.UploadTestVideoAsset(t, cldtest.VideoPublicID) 37 | resp, err := adminAPI.AssetsByIDs(ctx, admin.AssetsByIDsParams{ 38 | PublicIDs: []string{cldtest.PublicID}, 39 | Tags: api.Bool(true)}) 40 | 41 | if err != nil || len(resp.Assets) != 1 { 42 | t.Error(err, resp) 43 | } 44 | 45 | resp, err = adminAPI.AssetsByIDs(ctx, admin.AssetsByIDsParams{PublicIDs: []string{cldtest.VideoPublicID}, AssetType: api.Video}) 46 | 47 | if err != nil || len(resp.Assets) != 1 { 48 | t.Error(err, resp) 49 | } 50 | } 51 | 52 | func TestAssets_AssetsByAssetFolder(t *testing.T) { 53 | cldtest.SkipFeature(t, cldtest.SkipDynamicFolders) 54 | 55 | asset1 := cldtest.UploadTestAsset(t, cldtest.PublicID) 56 | asset2 := cldtest.UploadTestAsset(t, cldtest.PublicID2) 57 | 58 | resp, err := adminAPI.AssetsByAssetFolder(ctx, admin.AssetsByAssetFolderParams{ 59 | AssetFolder: cldtest.UniqueFolder, 60 | }) 61 | 62 | if err != nil || len(resp.Assets) != 2 { 63 | t.Error(err, resp) 64 | } 65 | 66 | assert.Equal(t, asset1.AssetFolder, resp.Assets[0].AssetFolder) 67 | assert.Equal(t, asset2.AssetFolder, resp.Assets[1].AssetFolder) 68 | } 69 | 70 | func TestAssets_AssetsFields(t *testing.T) { 71 | cldtest.UploadTestAsset(t, cldtest.PublicID) 72 | resp, err := adminAPI.AssetsByTag(ctx, admin.AssetsByTagParams{ 73 | Tag: cldtest.Tag1, 74 | Fields: []string{"tags", "secure_url"}, 75 | MaxResults: 1}) 76 | 77 | if err != nil || len(resp.Assets) != 1 { 78 | t.Error(err, resp) 79 | } 80 | 81 | assert.NotEmpty(t, resp.Assets[0].SecureURL) 82 | assert.NotEmpty(t, resp.Assets[0].Tags) 83 | assert.Empty(t, resp.Assets[0].URL) 84 | } 85 | 86 | func TestAssets_RestoreAssets(t *testing.T) { 87 | resp, err := adminAPI.RestoreAssets(ctx, admin.RestoreAssetsParams{PublicIDs: []string{"api_test_restore_20891", "api_test_restore_94060"}}) 88 | if err != nil { 89 | t.Error(err, resp) 90 | } 91 | } 92 | 93 | func TestAssets_DeleteAssets(t *testing.T) { 94 | resp, err := 95 | adminAPI.DeleteAssets(ctx, admin.DeleteAssetsParams{PublicIDs: []string{"api_test_restore_20891", "api_test_restore_94060"}}) 96 | if err != nil { 97 | t.Error(err, resp) 98 | } 99 | } 100 | 101 | func TestAssets_VisualSearchImageFile(t *testing.T) { 102 | 103 | cldtest.SkipFeature(t, cldtest.SkipVisualSearch) 104 | 105 | uploadAPI, _ := uploader.New() 106 | 107 | asset1, err := uploadAPI.Upload(ctx, cldtest.LogoURL, uploader.UploadParams{VisualSearch: api.Bool(true)}) 108 | if err != nil { 109 | t.Error(err, asset1) 110 | } 111 | 112 | // Get the data 113 | res, err := http.Get(cldtest.LogoURL) 114 | if err != nil { 115 | t.Error(err, res) 116 | } 117 | 118 | defer api.DeferredClose(res.Body) 119 | 120 | resp, err := adminAPI.VisualSearch(ctx, admin.VisualSearchParams{ImageFile: res.Body}) 121 | if err != nil { 122 | t.Error(err, resp) 123 | } 124 | 125 | assert.GreaterOrEqual(t, len(resp.Assets), 1) 126 | } 127 | -------------------------------------------------------------------------------- /api/admin/folders.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | // Enables you to manage the folders in your account or cloud. 4 | // 5 | // https://cloudinary.com/documentation/admin_api#folders 6 | import ( 7 | "context" 8 | 9 | "github.com/cloudinary/cloudinary-go/v2/api" 10 | ) 11 | 12 | const ( 13 | folders api.EndPoint = "folders" 14 | ) 15 | 16 | // RootFoldersParams are the parameters for RootFolders. 17 | type RootFoldersParams struct { 18 | MaxResults int `json:"max_results,omitempty"` 19 | NextCursor string `json:"next_cursor,omitempty"` 20 | } 21 | 22 | // RootFolders lists all root folders. 23 | // 24 | // https://cloudinary.com/documentation/admin_api#get_root_folders 25 | func (a *API) RootFolders(ctx context.Context, params RootFoldersParams) (*FoldersResult, error) { 26 | res := &FoldersResult{} 27 | _, err := a.get(ctx, folders, params, res) 28 | 29 | return res, err 30 | } 31 | 32 | // FoldersResult is the result of RootFolders, SubFolders. 33 | type FoldersResult struct { 34 | Folders []FolderResult `json:"folders"` 35 | TotalCount int `json:"total_count"` 36 | NextCursor string `json:"next_cursor"` 37 | Error api.ErrorResp `json:"error,omitempty"` 38 | } 39 | 40 | // FolderResult contains details of a single folder. 41 | type FolderResult struct { 42 | Name string `json:"name"` 43 | Path string `json:"path"` 44 | } 45 | 46 | // SubFoldersParams are the parameters for SubFolders. 47 | type SubFoldersParams struct { 48 | Folder string `json:"-"` 49 | MaxResults int `json:"max_results,omitempty"` 50 | NextCursor string `json:"next_cursor,omitempty"` 51 | } 52 | 53 | // SubFolders lists sub-folders. 54 | // 55 | // Returns the name and path of all the sub-folders of a specified parent folder. Limited to 2000 results. 56 | // 57 | // https://cloudinary.com/documentation/admin_api#get_subfolders 58 | func (a *API) SubFolders(ctx context.Context, params SubFoldersParams) (*FoldersResult, error) { 59 | res := &FoldersResult{} 60 | _, err := a.get(ctx, api.BuildPath(folders, params.Folder), params, res) 61 | 62 | return res, err 63 | } 64 | 65 | // CreateFolderParams are the parameters for CreateFolder. 66 | type CreateFolderParams struct { 67 | Folder string `json:"-"` // The full path of the new folder to create. 68 | } 69 | 70 | // CreateFolder creates a new empty folder. 71 | // 72 | // https://cloudinary.com/documentation/admin_api#create_folder 73 | func (a *API) CreateFolder(ctx context.Context, params CreateFolderParams) (*CreateFolderResult, error) { 74 | res := &CreateFolderResult{} 75 | _, err := a.post(ctx, api.BuildPath(folders, params.Folder), params, res) 76 | 77 | return res, err 78 | } 79 | 80 | // CreateFolderResult is the result of CreateFolder. 81 | type CreateFolderResult struct { 82 | Success bool `json:"success"` 83 | Path string `json:"path"` 84 | Name string `json:"name"` 85 | Error api.ErrorResp `json:"error,omitempty"` 86 | } 87 | 88 | // RenameFolderParams are the parameters for RenameFolder. 89 | type RenameFolderParams struct { 90 | FromPath string `json:"-"` // The full path of the existing folder. 91 | ToPath string `json:"to_folder"` // The full path of the new folder. 92 | } 93 | 94 | // RenameFolder renames an existing asset folder. 95 | // 96 | // https://cloudinary.com/documentation/admin_api#update_folder 97 | func (a *API) RenameFolder(ctx context.Context, params RenameFolderParams) (*RenameFolderResult, error) { 98 | res := &RenameFolderResult{} 99 | _, err := a.put(ctx, api.BuildPath(folders, params.FromPath), params, res) 100 | 101 | return res, err 102 | } 103 | 104 | type RenameFolderResult struct { 105 | From FolderResult `json:"from"` 106 | To FolderResult `json:"to"` 107 | Error api.ErrorResp `json:"error,omitempty"` 108 | } 109 | 110 | // DeleteFolderParams are the parameters for DeleteFolder. 111 | type DeleteFolderParams struct { 112 | Folder string `json:"-"` // The full path of the empty folder to delete. 113 | } 114 | 115 | // DeleteFolder deletes an empty folder. 116 | // 117 | // The specified folder cannot contain any assets, but can have empty descendant sub-folders. 118 | // 119 | // https://cloudinary.com/documentation/admin_api#delete_folder 120 | func (a *API) DeleteFolder(ctx context.Context, params DeleteFolderParams) (*DeleteFolderResult, error) { 121 | res := &DeleteFolderResult{} 122 | _, err := a.delete(ctx, api.BuildPath(folders, params.Folder), params, res) 123 | 124 | return res, err 125 | } 126 | 127 | // DeleteFolderResult is the result of DeleteFolder. 128 | type DeleteFolderResult struct { 129 | Deleted []string `json:"deleted"` 130 | Error api.ErrorResp `json:"error,omitempty"` 131 | } 132 | -------------------------------------------------------------------------------- /api/admin/folders_test.go: -------------------------------------------------------------------------------- 1 | package admin_test 2 | 3 | import ( 4 | "github.com/cloudinary/cloudinary-go/v2/internal/cldtest" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | "time" 8 | 9 | "github.com/cloudinary/cloudinary-go/v2/api/admin" 10 | ) 11 | 12 | const testFolder = "000-go-folder" 13 | const testFolderRenamed = testFolder + "-renamed" 14 | 15 | func TestFolders_CreateFolder(t *testing.T) { 16 | resp, err := adminAPI.CreateFolder(ctx, admin.CreateFolderParams{Folder: testFolder}) 17 | 18 | if err != nil || resp.Success != true { 19 | t.Error(resp, err) 20 | } 21 | } 22 | 23 | func TestFolders_RootFolders(t *testing.T) { 24 | resp, err := adminAPI.RootFolders(ctx, admin.RootFoldersParams{MaxResults: 5}) 25 | 26 | if err != nil || resp.TotalCount < 1 { 27 | t.Error(resp, err) 28 | } 29 | } 30 | 31 | func TestFolders_DeleteFolder(t *testing.T) { 32 | resp, err := adminAPI.DeleteFolder(ctx, admin.DeleteFolderParams{Folder: testFolder}) 33 | 34 | if err != nil || len(resp.Deleted) < 1 { 35 | t.Error(resp, err) 36 | } 37 | } 38 | 39 | func TestFolders_RenameFolder(t *testing.T) { 40 | cldtest.SkipFixedFolderMode(t) 41 | 42 | resp, err := adminAPI.CreateFolder(ctx, admin.CreateFolderParams{Folder: testFolder}) 43 | 44 | if err != nil || resp.Success != true { 45 | t.Error(resp, err) 46 | } 47 | 48 | renameResp, err := adminAPI.RenameFolder(ctx, admin.RenameFolderParams{FromPath: testFolder, ToPath: testFolderRenamed}) 49 | 50 | if err != nil || renameResp.Error.Message != "" { 51 | t.Error(renameResp, err) 52 | } 53 | 54 | assert.Equal(t, testFolder, renameResp.From.Path) 55 | assert.Equal(t, testFolder, renameResp.From.Name) 56 | assert.Equal(t, testFolderRenamed, renameResp.To.Path) 57 | assert.Equal(t, testFolderRenamed, renameResp.To.Name) 58 | 59 | delResp, err := adminAPI.DeleteFolder(ctx, admin.DeleteFolderParams{Folder: testFolderRenamed}) 60 | 61 | if err != nil || len(delResp.Deleted) < 1 { 62 | t.Error(delResp, err) 63 | } 64 | } 65 | 66 | func TestFolders_SubFolders(t *testing.T) { 67 | cfResp, err := adminAPI.CreateFolder(ctx, admin.CreateFolderParams{Folder: testFolder}) 68 | if err != nil || cfResp.Success != true { 69 | t.Error(cfResp, err) 70 | } 71 | 72 | cfResp, err = adminAPI.CreateFolder(ctx, admin.CreateFolderParams{Folder: testFolder + "/" + testFolder}) 73 | if err != nil || cfResp.Success != true { 74 | t.Error(cfResp, err) 75 | } 76 | 77 | time.Sleep(1 * time.Second) 78 | 79 | resp, err := adminAPI.RootFolders(ctx, admin.RootFoldersParams{MaxResults: 1}) 80 | if err != nil || resp == nil || resp.TotalCount < 1 { 81 | t.Error(resp, err) 82 | } 83 | 84 | resp, err = adminAPI.SubFolders(ctx, admin.SubFoldersParams{Folder: resp.Folders[0].Path, MaxResults: 2}) 85 | if err != nil || resp.TotalCount < 1 { 86 | t.Error(resp, err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /api/admin/metadata/metadata.go: -------------------------------------------------------------------------------- 1 | // Package metadata defines the structured metadata. 2 | // 3 | // https://cloudinary.com/documentation/metadata_api 4 | package metadata 5 | 6 | // Field is a single metadata field. 7 | type Field struct { 8 | Type FieldType `json:"type"` 9 | ExternalID string `json:"external_id"` 10 | Label string `json:"label"` 11 | Mandatory bool `json:"mandatory"` 12 | DefaultValue interface{} `json:"default_value,omitempty"` 13 | Validation interface{} `json:"validation,omitempty"` 14 | DataSource DataSource `json:"datasource,omitempty"` 15 | } 16 | 17 | // FieldType is the type of the metadata field. 18 | type FieldType string 19 | 20 | const ( 21 | // StringFieldType is a string metadata field type. 22 | StringFieldType FieldType = "string" 23 | // IntegerFieldType is an integer metadata field type. 24 | IntegerFieldType FieldType = "integer" 25 | // DateFieldType is a date metadata field type. 26 | DateFieldType FieldType = "date" 27 | // EnumFieldType is an enum metadata field type. 28 | EnumFieldType FieldType = "enum" 29 | // SetFieldType is a set metadata field type. 30 | SetFieldType FieldType = "set" 31 | ) 32 | 33 | // Validation is the validation of the metadata field. 34 | type Validation interface{} 35 | 36 | // ValidationType is the type of the metadata field validation. 37 | type ValidationType string 38 | 39 | const ( 40 | greaterThanValidationType ValidationType = "greater_than" 41 | lessThanValidationType ValidationType = "less_than" 42 | stringLengthValidationType ValidationType = "strlen" 43 | andValidationType ValidationType = "and" 44 | ) 45 | 46 | type compositeValidation struct { 47 | Type ValidationType `json:"type"` 48 | Rules []interface{} `json:"rules"` 49 | } 50 | 51 | type comparisonValidationRule struct { 52 | Type ValidationType `json:"type"` 53 | Value interface{} `json:"value"` 54 | Equals bool `json:"equals"` 55 | } 56 | 57 | type valueLengthValidationRule struct { 58 | Type ValidationType `json:"type"` 59 | Min int `json:"min,omitempty"` 60 | Max int `json:"max,omitempty"` 61 | } 62 | 63 | // AndValidation is relevant for all field types. Allows to include more than one validation rule to be evaluated. 64 | func AndValidation(rules []interface{}) *compositeValidation { 65 | return &compositeValidation{Type: andValidationType, Rules: rules} 66 | } 67 | 68 | // GreaterThanValidation rule for integers. 69 | func GreaterThanValidation(value interface{}, equals bool) *comparisonValidationRule { 70 | return &comparisonValidationRule{Type: greaterThanValidationType, Value: value, Equals: equals} 71 | } 72 | 73 | // LessThanValidation rule for integers. 74 | func LessThanValidation(value interface{}, equals bool) *comparisonValidationRule { 75 | return &comparisonValidationRule{Type: lessThanValidationType, Value: value, Equals: equals} 76 | } 77 | 78 | // StringLengthValidation rule for strings. 79 | func StringLengthValidation(min, max int) *valueLengthValidationRule { 80 | return &valueLengthValidationRule{Type: stringLengthValidationType, Min: min, Max: max} 81 | } 82 | 83 | // DataSource is the data source definition. 84 | type DataSource struct { 85 | Values []DataSourceValue `json:"values"` 86 | } 87 | 88 | // DataSourceValue is the value of a single data source. 89 | type DataSourceValue struct { 90 | ExternalID string `json:"external_id"` 91 | Value string `json:"value"` 92 | State string `json:"state"` 93 | } 94 | -------------------------------------------------------------------------------- /api/admin/misc.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/cloudinary/cloudinary-go/v2/api" 8 | ) 9 | 10 | const ( 11 | ping api.EndPoint = "ping" 12 | cloudConfig api.EndPoint = "config" 13 | usage api.EndPoint = "usage" 14 | ) 15 | 16 | // Ping tests the reachability of the Cloudinary API. 17 | // 18 | // https://cloudinary.com/documentation/admin_api#ping 19 | func (a *API) Ping(ctx context.Context) (*PingResult, error) { 20 | res := &PingResult{} 21 | _, err := a.get(ctx, ping, nil, res) 22 | 23 | return res, err 24 | } 25 | 26 | // PingResult represents the result of the Ping request. 27 | type PingResult struct { 28 | Status string `json:"status"` 29 | Error api.ErrorResp `json:"error,omitempty"` 30 | Response interface{} 31 | } 32 | 33 | // GetConfigParams are the parameters for GetConfig. 34 | type GetConfigParams struct { 35 | Settings *bool `json:"settings,omitempty"` 36 | } 37 | 38 | // GetConfig gets cloud config details. 39 | // 40 | // https://cloudinary.com/documentation/admin_api#config 41 | func (a *API) GetConfig(ctx context.Context, params GetConfigParams) (*GetConfigResult, error) { 42 | res := &GetConfigResult{} 43 | _, err := a.get(ctx, cloudConfig, params, res) 44 | 45 | return res, err 46 | } 47 | 48 | // GetConfigResult represents the result of the GetConfig request. 49 | type GetConfigResult struct { 50 | CloudName string `json:"cloud_name"` 51 | CreatedAt time.Time `json:"created_at"` 52 | Settings CloudSettings `json:"settings"` 53 | Error api.ErrorResp `json:"error,omitempty"` 54 | Response interface{} 55 | } 56 | 57 | // CloudSettings represents the settings of the cloud. 58 | type CloudSettings struct { 59 | FolderMode string `json:"folder_mode"` 60 | } 61 | 62 | // UsageParams are the parameters for Usage. 63 | type UsageParams struct { 64 | Date time.Time `json:"-"` 65 | } 66 | 67 | // Usage gets account usage details. 68 | // 69 | // Returns a report detailing your current Cloudinary account usage details, including 70 | // storage, bandwidth, requests, number of resources, and add-on usage. 71 | // Note that numbers are updated periodically. 72 | // 73 | // https://cloudinary.com/documentation/admin_api#usage 74 | func (a *API) Usage(ctx context.Context, params UsageParams) (*UsageResult, error) { 75 | date := "" 76 | if !params.Date.IsZero() { 77 | date = params.Date.Format("02-01-2006") 78 | } 79 | res := &UsageResult{} 80 | _, err := a.get(ctx, api.BuildPath(usage, date), params, res) 81 | 82 | return res, err 83 | } 84 | 85 | // UsageResult is the result of Usage. 86 | type UsageResult struct { 87 | Plan string `json:"plan"` 88 | LastUpdated string `json:"last_updated"` 89 | Transformations struct { 90 | Usage int `json:"usage"` 91 | CreditsUsage float64 `json:"credits_usage"` 92 | Limit int `json:"limit"` 93 | UsedPercent float64 `json:"used_percent"` 94 | } `json:"transformations"` 95 | Objects struct { 96 | Usage int `json:"usage"` 97 | Limit int `json:"limit"` 98 | UsedPercent float64 `json:"used_percent"` 99 | } `json:"objects"` 100 | Bandwidth struct { 101 | Usage int64 `json:"usage"` 102 | CreditsUsage float64 `json:"credits_usage"` 103 | Limit int64 `json:"limit"` 104 | UsedPercent float64 `json:"used_percent"` 105 | } `json:"bandwidth"` 106 | Storage struct { 107 | Usage int64 `json:"usage"` 108 | CreditsUsage float64 `json:"credits_usage"` 109 | Limit int64 `json:"limit"` 110 | UsedPercent float64 `json:"used_percent"` 111 | } `json:"storage"` 112 | Credits struct { 113 | Usage float64 `json:"usage"` 114 | } `json:"credits"` 115 | Requests int64 `json:"requests"` 116 | Resources int `json:"resources"` 117 | DerivedResources int `json:"derived_resources"` 118 | MediaLimits struct { 119 | ImageMaxSizeBytes int `json:"image_max_size_bytes"` 120 | VideoMaxSizeBytes int `json:"video_max_size_bytes"` 121 | RawMaxSizeBytes int `json:"raw_max_size_bytes"` 122 | ImageMaxPx int `json:"image_max_px"` 123 | AssetMaxTotalPx int `json:"asset_max_total_px"` 124 | } `json:"media_limits"` 125 | Error api.ErrorResp `json:"error,omitempty"` 126 | Response interface{} 127 | } 128 | 129 | // TagsParams are the parameters for Tags. 130 | type TagsParams struct { 131 | AssetType api.AssetType `json:"-"` // The type of asset. 132 | NextCursor string `json:"next_cursor,omitempty"` // The cursor used for pagination. 133 | MaxResults int `json:"max_results,omitempty"` // Maximum number of tags to return (up to 500). Default: 10. 134 | Prefix string `json:"prefix,omitempty"` // Find all tags that start with the given prefix. 135 | } 136 | 137 | // Tags lists all the tags currently used for a specified asset type. 138 | // 139 | // https://cloudinary.com/documentation/admin_api#get_tags 140 | func (a *API) Tags(ctx context.Context, params TagsParams) (*TagsResult, error) { 141 | res := &TagsResult{} 142 | _, err := a.get(ctx, api.BuildPath(tags, params.AssetType), params, res) 143 | 144 | return res, err 145 | } 146 | 147 | // TagsResult is the result of Tags. 148 | type TagsResult struct { 149 | Tags []string `json:"tags"` 150 | NextCursor string `json:"next_cursor"` 151 | Error api.ErrorResp `json:"error,omitempty"` 152 | Response interface{} 153 | } 154 | -------------------------------------------------------------------------------- /api/admin/misc_test.go: -------------------------------------------------------------------------------- 1 | package admin_test 2 | 3 | import ( 4 | "github.com/cloudinary/cloudinary-go/v2/api" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | "time" 8 | 9 | "github.com/cloudinary/cloudinary-go/v2/api/admin" 10 | ) 11 | 12 | func TestAdmin_Ping(t *testing.T) { 13 | resp, err := adminAPI.Ping(ctx) 14 | 15 | if err != nil || resp.Status != "ok" { 16 | t.Error(resp) 17 | } 18 | 19 | rawResponse := resp.Response.(*map[string]interface{}) 20 | 21 | if (*rawResponse)["status"] != "ok" { 22 | t.Error(resp) 23 | } 24 | } 25 | 26 | func TestAdmin_GetConfig(t *testing.T) { 27 | resp, err := adminAPI.GetConfig(ctx, admin.GetConfigParams{Settings: api.Bool(true)}) 28 | 29 | if err != nil || resp.Error.Message != "" { 30 | t.Error(resp) 31 | } 32 | 33 | assert.Equal(t, adminAPI.Config.Cloud.CloudName, resp.CloudName) 34 | assert.NotEmpty(t, resp.Settings.FolderMode) 35 | } 36 | 37 | func TestAdmin_Usage(t *testing.T) { 38 | resp, err := adminAPI.Usage(ctx, admin.UsageParams{}) 39 | 40 | if err != nil || len(resp.Plan) < 1 { 41 | t.Error(err, resp) 42 | } 43 | } 44 | 45 | func TestAdmin_UsageYesterday(t *testing.T) { 46 | resp, err := adminAPI.Usage(ctx, admin.UsageParams{Date: time.Now().AddDate(0, 0, -2)}) 47 | 48 | if err != nil || len(resp.Plan) < 1 { 49 | t.Error(err, resp) 50 | } 51 | } 52 | 53 | func TestAdmin_Tags(t *testing.T) { 54 | resp, err := adminAPI.Tags(ctx, admin.TagsParams{}) 55 | 56 | if err != nil || len(resp.Tags) < 1 { 57 | t.Error(err, resp) 58 | } 59 | 60 | if resp.NextCursor != "" { 61 | resp2, err := adminAPI.Tags(ctx, admin.TagsParams{NextCursor: resp.NextCursor}) 62 | 63 | if err != nil || len(resp2.Tags) < 1 { 64 | t.Error(err, resp) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /api/admin/search.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | // The Cloudinary API search method allows you fine control on filtering and retrieving information on all the assets 4 | // in your account with the help of query expressions in a Lucene-like query language. A few examples of what you can 5 | // accomplish using the search method include: 6 | // 7 | // * Searching by descriptive attributes such as public ID, filename, folders, tags, context, etc. 8 | // * Searching by file details such as type, format, file size, dimensions, etc. 9 | // * Searching by embedded data such as Exif, XMP, etc. 10 | // * Searching by analyzed data such as the number of faces, predominant colors, auto-tags, etc. 11 | // * Requesting aggregation counts on specified parameters, for example the number of assets found broken down by file 12 | // format. 13 | // 14 | // https://cloudinary.com/documentation/search_api 15 | import ( 16 | "context" 17 | "time" 18 | 19 | "github.com/cloudinary/cloudinary-go/v2/api" 20 | "github.com/cloudinary/cloudinary-go/v2/api/admin/search" 21 | ) 22 | 23 | const searchEndPoint api.EndPoint = "resources/search" 24 | 25 | // Search executes the search API request. 26 | func (a *API) Search(ctx context.Context, searchQuery search.Query) (*SearchResult, error) { 27 | res := &SearchResult{} 28 | _, err := a.post(ctx, api.BuildPath(searchEndPoint), searchQuery, res) 29 | 30 | return res, err 31 | } 32 | 33 | // SearchResult is the result of Search. 34 | type SearchResult struct { 35 | TotalCount int `json:"total_count"` 36 | Time int `json:"time"` 37 | Assets []SearchAsset `json:"resources"` 38 | Error api.ErrorResp `json:"error,omitempty"` 39 | NextCursor string `json:"next_cursor,omitempty"` 40 | Response interface{} 41 | } 42 | 43 | // SearchAsset represents the details of a single asset that was found. 44 | type SearchAsset struct { 45 | PublicID string `json:"public_id"` 46 | AssetID string `json:"asset_id"` 47 | Folder string `json:"folder"` 48 | AssetFolder string `json:"asset_folder"` 49 | Filename string `json:"filename"` 50 | DisplayName string `json:"display_name"` 51 | Format string `json:"format"` 52 | Version int `json:"version"` 53 | ResourceType string `json:"resource_type"` 54 | Type string `json:"type"` 55 | CreatedAt time.Time `json:"created_at"` 56 | UploadedAt time.Time `json:"uploaded_at"` 57 | Bytes int `json:"bytes"` 58 | BackupBytes int `json:"backup_bytes"` 59 | Width int `json:"width"` 60 | Height int `json:"height"` 61 | AspectRatio float64 `json:"aspect_ratio"` 62 | Pixels int `json:"pixels"` 63 | Tags []string `json:"tags"` 64 | Context ImageMetadataResult `json:"context"` 65 | ImageMetadata ImageMetadataResult `json:"image_metadata"` 66 | VideoMetadata MediaMetadataResult `json:"video_metadata"` 67 | ImageAnalysis ImageAnalysis `json:"image_analysis"` 68 | URL string `json:"url"` 69 | SecureURL string `json:"secure_url"` 70 | Status string `json:"status"` 71 | AccessMode string `json:"access_mode"` 72 | AccessControl interface{} `json:"access_control"` 73 | Etag string `json:"etag"` 74 | CreatedBy SearchUser `json:"created_by"` 75 | UploadedBy SearchUser `json:"uploaded_by"` 76 | LastUpdated api.LastUpdated `json:"last_updated"` 77 | } 78 | 79 | // ImageAnalysis contains details about image analysis. 80 | type ImageAnalysis struct { 81 | FaceCount int `json:"face_count"` 82 | Faces [][]int `json:"faces"` 83 | Grayscale bool `json:"grayscale"` 84 | IllustrationScore int `json:"illustration_score"` 85 | Transparent bool `json:"transparent"` 86 | Etag string `json:"etag"` 87 | Colors map[string]float64 `json:"colors"` 88 | } 89 | 90 | // SearchUser contains details about the user. 91 | type SearchUser struct { 92 | AccessKey string `json:"access_key"` 93 | } 94 | -------------------------------------------------------------------------------- /api/admin/search/search.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | // Query struct includes the search query. 4 | type Query struct { 5 | // Expression is the (Lucene-like) string expression specifying the search query. 6 | // If not provided then all resources are listed (up to MaxResults). 7 | Expression string `json:"expression,omitempty"` 8 | // SortBy is the field to sort by. You can specify more than one SortBy parameter; results will be sorted 9 | // according to the order of the fields provided. 10 | SortBy []SortByField `json:"sort_by,omitempty"` 11 | // Aggregate is the name of a field (attribute) for which an aggregation count should be calculated and returned in the response. 12 | // (Tier 2 only) 13 | // You can specify more than one aggregate parameter. 14 | // For aggregation fields without discrete values, the results are divided into categories. 15 | Aggregate []Aggregation `json:"aggregate,omitempty"` 16 | // WithField contains names of additional asset attributes to include for each asset in the response. 17 | WithField []WithField `json:"with_field,omitempty"` 18 | // Fields contains names of the asset attributes to keep for each asset in the response. 19 | Fields []string `json:"fields,omitempty"` 20 | // MaxResults is the maximum number of results to return. Default 50. Maximum 500. 21 | MaxResults int `json:"max_results,omitempty"` 22 | // NextCursor value is returned as part of the response when a search request has more results to return than MaxResults. 23 | // You can then specify this value as the NextCursor parameter of the following request. 24 | NextCursor string `json:"next_cursor,omitempty"` 25 | } 26 | 27 | // Aggregation is the aggregation field. 28 | type Aggregation = string 29 | 30 | const ( 31 | // AssetType aggregation field. 32 | AssetType Aggregation = "resource_type" 33 | // DeliveryType aggregation field. 34 | DeliveryType = "type" 35 | // Pixels aggregation field. Only the image assets in the response are aggregated. 36 | Pixels = "pixels" 37 | // Duration aggregation field. Only the video assets in the response are aggregated. 38 | Duration = "duration" 39 | // Format aggregation field. 40 | Format = "format" 41 | // Bytes aggregation field. 42 | Bytes = "bytes" 43 | ) 44 | 45 | // WithField is the name of the addition filed to include in result. 46 | type WithField = string 47 | 48 | const ( 49 | // ContextField is the context field. 50 | ContextField WithField = "context" 51 | // TagsField is the tags field. 52 | TagsField = "tags" 53 | // ImageMetadataField is the image metadata field. 54 | ImageMetadataField = "image_metadata" 55 | // ImageAnalysisField is the image analysis field. 56 | ImageAnalysisField = "image_analysis" 57 | ) 58 | 59 | // Direction is the sorting direction. 60 | type Direction string 61 | 62 | const ( 63 | // Ascending direction. 64 | Ascending Direction = "asc" 65 | // Descending direction. 66 | Descending = "desc" 67 | ) 68 | 69 | // SortByField is the field to sort by and direction. 70 | type SortByField map[string]Direction 71 | -------------------------------------------------------------------------------- /api/admin/search_folders.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | // The Cloudinary API search folders method allows you fine control on filtering and retrieving information on all the 4 | // folders in your account with the help of query expressions in a Lucene-like query language. 5 | // 6 | // https://cloudinary.com/documentation/search_api 7 | import ( 8 | "context" 9 | "time" 10 | 11 | "github.com/cloudinary/cloudinary-go/v2/api" 12 | "github.com/cloudinary/cloudinary-go/v2/api/admin/search" 13 | ) 14 | 15 | const searchFoldersEndPoint api.EndPoint = "folders/search" 16 | 17 | // SearchFolders executes the search folders API request. 18 | func (a *API) SearchFolders(ctx context.Context, searchQuery search.Query) (*SearchFoldersResult, error) { 19 | res := &SearchFoldersResult{} 20 | _, err := a.post(ctx, api.BuildPath(searchFoldersEndPoint), searchQuery, res) 21 | 22 | return res, err 23 | } 24 | 25 | // SearchFoldersResult is the result of SearchFolders. 26 | type SearchFoldersResult struct { 27 | TotalCount int `json:"total_count"` 28 | Time int `json:"time"` 29 | Folders []SearchFolder `json:"folders"` 30 | Error api.ErrorResp `json:"error,omitempty"` 31 | NextCursor string `json:"next_cursor,omitempty"` 32 | Response interface{} 33 | } 34 | 35 | // SearchFolder represents the details of a single folder that was found. 36 | type SearchFolder struct { 37 | Name string `json:"name"` 38 | Path string `json:"path"` 39 | CreatedAt time.Time `json:"created_at"` 40 | ExternalID string `json:"external_id"` 41 | } 42 | -------------------------------------------------------------------------------- /api/admin/search_folders_test.go: -------------------------------------------------------------------------------- 1 | package admin_test 2 | 3 | import ( 4 | "github.com/cloudinary/cloudinary-go/v2/api/admin" 5 | "testing" 6 | 7 | "github.com/cloudinary/cloudinary-go/v2/api/admin/search" 8 | ) 9 | 10 | func TestSearchFolders_SearchQuery(t *testing.T) { 11 | cfResp, err := adminAPI.CreateFolder(ctx, admin.CreateFolderParams{Folder: testFolder}) 12 | 13 | if err != nil || cfResp.Success != true { 14 | t.Error(cfResp, err) 15 | } 16 | 17 | sq := search.Query{ 18 | Expression: "path=" + testFolder, 19 | MaxResults: 1, 20 | } 21 | 22 | resp, err := adminAPI.SearchFolders(ctx, sq) 23 | 24 | if err != nil || resp.TotalCount < 1 { 25 | t.Error(resp, err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /api/admin/search_test.go: -------------------------------------------------------------------------------- 1 | package admin_test 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | 7 | "github.com/cloudinary/cloudinary-go/v2/api/admin/search" 8 | ) 9 | 10 | func TestSearch_SearchQuery(t *testing.T) { 11 | sq := search.Query{ 12 | Expression: "format:png", 13 | WithField: []string{search.TagsField, search.ContextField, search.ImageMetadataField, search.ImageAnalysisField}, 14 | SortBy: []search.SortByField{{"created_at": search.Descending}}, 15 | Fields: []string{"tags", "secure_url"}, 16 | MaxResults: 2, 17 | } 18 | 19 | resp, err := adminAPI.Search(ctx, sq) 20 | 21 | if err != nil || resp.TotalCount < 1 { 22 | t.Error(resp, err) 23 | } 24 | 25 | assert.NotEmpty(t, resp.Assets[0].AssetID) 26 | assert.NotEmpty(t, resp.Assets[0].SecureURL) 27 | assert.Empty(t, resp.Assets[0].URL) 28 | } 29 | -------------------------------------------------------------------------------- /api/admin/streaming_profiles_test.go: -------------------------------------------------------------------------------- 1 | package admin_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cloudinary/cloudinary-go/v2/api/admin" 7 | ) 8 | 9 | const SPName = "00-go-sp" 10 | 11 | func TestStreamingProfiles_List(t *testing.T) { 12 | resp, err := adminAPI.ListStreamingProfiles(ctx) 13 | 14 | if err != nil || len(resp.Data) < 1 { 15 | t.Error(resp, err) 16 | } 17 | } 18 | 19 | func TestStreamingProfiles_Get(t *testing.T) { 20 | lResp, err := adminAPI.ListStreamingProfiles(ctx) 21 | 22 | if err != nil || lResp.Error.Message != "" { 23 | t.Error(lResp, err) 24 | } 25 | 26 | resp, err := adminAPI.GetStreamingProfile(ctx, admin.GetStreamingProfileParams{Name: lResp.Data[0].Name}) 27 | 28 | if err != nil { 29 | t.Error(resp, err) 30 | } 31 | } 32 | 33 | func TestStreamingProfiles_Create(t *testing.T) { 34 | resp, err := adminAPI.CreateStreamingProfile(ctx, admin.CreateStreamingProfileParams{ 35 | Name: SPName, 36 | DisplayName: "Go SP", 37 | Representations: admin.StreamingProfileRepresentations{{Transformation: "c_fill,w_1000,h_1000"}}, 38 | }) 39 | 40 | if err != nil || resp.Error.Message != "" { 41 | t.Error(resp, err) 42 | } 43 | } 44 | 45 | func TestStreamingProfiles_Update(t *testing.T) { 46 | resp, err := adminAPI.UpdateStreamingProfile(ctx, admin.UpdateStreamingProfileParams{ 47 | Name: SPName, 48 | DisplayName: "Go SP Updated", 49 | Representations: admin.StreamingProfileRepresentations{{"c_fill,w_1001,h_1001"}}, 50 | }) 51 | 52 | if err != nil || resp.Data.DisplayName != "Go SP Updated" { 53 | t.Error(resp, err) 54 | } 55 | } 56 | 57 | func TestStreamingProfiles_Delete(t *testing.T) { 58 | resp, err := adminAPI.DeleteStreamingProfile(ctx, admin.DeleteStreamingProfileParams{Name: SPName}) 59 | 60 | if err != nil || resp.Message != "deleted" { 61 | t.Error(resp, err) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /api/admin/transformations.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | // Enables you to manage stored transformations. 4 | // 5 | // https://cloudinary.com/documentation/admin_api#transformations 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/cloudinary/cloudinary-go/v2/api" 11 | "github.com/cloudinary/cloudinary-go/v2/transformation" 12 | ) 13 | 14 | const ( 15 | transformations api.EndPoint = "transformations" 16 | ) 17 | 18 | // ListTransformationsParams are the parameters for ListTransformations. 19 | type ListTransformationsParams struct { 20 | Named *bool `json:"named,omitempty"` 21 | MaxResults int `json:"max_results,omitempty"` 22 | NextCursor string `json:"next_cursor,omitempty"` 23 | } 24 | 25 | // ListTransformations lists stored transformations. 26 | // 27 | // https://cloudinary.com/documentation/admin_api#get_transformations 28 | func (a *API) ListTransformations(ctx context.Context, params ListTransformationsParams) (*ListTransformationsResult, error) { 29 | res := &ListTransformationsResult{} 30 | _, err := a.get(ctx, transformations, params, res) 31 | 32 | return res, err 33 | } 34 | 35 | // ListTransformationsResult is the result of ListTransformations. 36 | type ListTransformationsResult struct { 37 | Transformations []TransformationListItem `json:"transformations"` 38 | Error api.ErrorResp `json:"error,omitempty"` 39 | } 40 | 41 | // TransformationListItem represents a single transformation. 42 | type TransformationListItem struct { 43 | Name string `json:"name"` 44 | AllowedForStrict bool `json:"allowed_for_strict"` 45 | Used bool `json:"used"` 46 | Named bool `json:"named"` 47 | } 48 | 49 | // GetTransformationParams are the parameters for GetTransformation. 50 | type GetTransformationParams struct { 51 | Transformation transformation.RawTransformation `json:"transformation"` // The transformation string. 52 | MaxResults int `json:"max_results,omitempty"` 53 | NextCursor string `json:"next_cursor,omitempty"` 54 | } 55 | 56 | // GetTransformation returns the details of a single transformation. 57 | // 58 | // https://cloudinary.com/documentation/admin_api#get_transformation_details 59 | func (a *API) GetTransformation(ctx context.Context, params GetTransformationParams) (*GetTransformationResult, error) { 60 | res := &GetTransformationResult{} 61 | _, err := a.get(ctx, api.BuildPath(transformations), params, res) 62 | 63 | return res, err 64 | } 65 | 66 | // GetTransformationResult is the result of GetTransformation. 67 | type GetTransformationResult struct { 68 | Name string `json:"name"` 69 | AllowedForStrict bool `json:"allowed_for_strict"` 70 | Used bool `json:"used"` 71 | Named bool `json:"named"` 72 | Info transformation.Transformation `json:"info"` 73 | Derived []DerivedAsset `json:"derived"` 74 | NextCursor string `json:"next_cursor"` 75 | Error api.ErrorResp `json:"error,omitempty"` 76 | } 77 | 78 | // DerivedAsset represents a single derived asset. 79 | type DerivedAsset struct { 80 | PublicID string `json:"public_id"` 81 | ResourceType string `json:"resource_type"` 82 | Type string `json:"type"` 83 | Format string `json:"format"` 84 | URL string `json:"url"` 85 | SecureURL string `json:"secure_url"` 86 | Bytes int `json:"bytes"` 87 | ID string `json:"id"` 88 | } 89 | 90 | // CreateTransformationParams are the parameters for CreateTransformation. 91 | type CreateTransformationParams struct { 92 | Name string `json:"name"` 93 | Transformation transformation.RawTransformation `json:"transformation"` 94 | } 95 | 96 | // CreateTransformation creates a named transformation. 97 | // 98 | // https://cloudinary.com/documentation/admin_api#create_named_transformation 99 | func (a *API) CreateTransformation(ctx context.Context, params CreateTransformationParams) (*TransformationResult, error) { 100 | res := &TransformationResult{} 101 | _, err := a.post(ctx, api.BuildPath(transformations), params, res) 102 | 103 | return res, err 104 | } 105 | 106 | // TransformationResult is the result of CreateTransformation, UpdateTransformation, DeleteTransformation. 107 | type TransformationResult struct { 108 | Message string `json:"message"` 109 | Error api.ErrorResp `json:"error,omitempty"` 110 | } 111 | 112 | // UpdateTransformationParams are the parameters for UpdateTransformation. 113 | type UpdateTransformationParams struct { 114 | Transformation transformation.RawTransformation `json:"transformation"` 115 | AllowedForStrict *bool `json:"allowed_for_strict,omitempty"` 116 | UnsafeUpdate transformation.RawTransformation `json:"unsafe_update,omitempty"` 117 | } 118 | 119 | // UpdateTransformation updates the specified transformation. 120 | // 121 | // https://cloudinary.com/documentation/admin_api#update_transformation 122 | func (a *API) UpdateTransformation(ctx context.Context, params UpdateTransformationParams) (*TransformationResult, error) { 123 | res := &TransformationResult{} 124 | _, err := a.put(ctx, api.BuildPath(transformations), params, res) 125 | 126 | return res, err 127 | } 128 | 129 | // DeleteTransformationParams are the parameters for DeleteTransformation. 130 | type DeleteTransformationParams struct { 131 | Transformation transformation.RawTransformation `json:"transformation"` 132 | } 133 | 134 | // DeleteTransformation deletes the specified stored transformation. 135 | // 136 | // Deleting a transformation also deletes all the stored derived resources based on this transformation (up to 1000). 137 | // The method returns an error if there are more than 1000 derived resources based on this transformation. 138 | // 139 | // https://cloudinary.com/documentation/admin_api#delete_transformation 140 | func (a *API) DeleteTransformation(ctx context.Context, params DeleteTransformationParams) (*TransformationResult, error) { 141 | res := &TransformationResult{} 142 | _, err := a.delete(ctx, api.BuildPath(transformations), params, res) 143 | 144 | return res, err 145 | } 146 | -------------------------------------------------------------------------------- /api/admin/transformations_test.go: -------------------------------------------------------------------------------- 1 | package admin_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cloudinary/cloudinary-go/v2/api/admin" 7 | ) 8 | 9 | const TName = "go_transformation" 10 | const TTransformation = "c_fill,h_500,w_500" 11 | const TTransformationUpdated = "c_fill,h_501,w_501" 12 | 13 | func TestTransformations_Create(t *testing.T) { 14 | resp, err := adminAPI.CreateTransformation(ctx, admin.CreateTransformationParams{ 15 | Name: TName, 16 | Transformation: TTransformation, 17 | }) 18 | 19 | if err != nil || resp.Error.Message != "" { 20 | t.Error(resp, err) 21 | } 22 | } 23 | 24 | func TestTransformations_List(t *testing.T) { 25 | resp, err := adminAPI.ListTransformations(ctx, admin.ListTransformationsParams{}) 26 | 27 | if err != nil || len(resp.Transformations) < 1 { 28 | t.Error(resp, err) 29 | } 30 | } 31 | 32 | func TestTransformations_Get(t *testing.T) { 33 | lResp, err := adminAPI.ListTransformations(ctx, admin.ListTransformationsParams{}) 34 | 35 | if err != nil || lResp.Error.Message != "" { 36 | t.Error(lResp, err) 37 | } 38 | 39 | resp, err := adminAPI.GetTransformation(ctx, admin.GetTransformationParams{Transformation: lResp.Transformations[0].Name}) 40 | 41 | if err != nil || len(resp.Info) < 1 { 42 | t.Error(resp, err) 43 | } 44 | } 45 | 46 | func TestTransformations_Update(t *testing.T) { 47 | resp, err := adminAPI.UpdateTransformation(ctx, admin.UpdateTransformationParams{ 48 | Transformation: TName, 49 | UnsafeUpdate: TTransformationUpdated, 50 | }) 51 | 52 | if err != nil || resp.Message != "updated" { 53 | t.Error(resp, err) 54 | } 55 | } 56 | 57 | func TestTransformations_Delete(t *testing.T) { 58 | resp, err := adminAPI.DeleteTransformation(ctx, admin.DeleteTransformationParams{Transformation: TName}) 59 | 60 | if err != nil || resp.Message != "deleted" { 61 | t.Error(resp, err) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /api/admin/upload_mappings.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | // Enables you to manage the upload mappings. 4 | // 5 | // https://cloudinary.com/documentation/admin_api#upload_mappings 6 | import ( 7 | "context" 8 | 9 | "github.com/cloudinary/cloudinary-go/v2/api" 10 | ) 11 | 12 | const ( 13 | uploadMappings api.EndPoint = "upload_mappings" 14 | ) 15 | 16 | // ListUploadMappingsParams are the parameters for ListUploadMappings. 17 | type ListUploadMappingsParams struct { 18 | MaxResults int `json:"max_results,omitempty"` 19 | NextCursor string `json:"next_cursor,omitempty"` 20 | } 21 | 22 | // ListUploadMappings lists upload mappings by folder and its mapped template (URL). 23 | // 24 | // https://cloudinary.com/documentation/admin_api#get_upload_mappings 25 | func (a *API) ListUploadMappings(ctx context.Context, params ListUploadMappingsParams) (*ListUploadMappingsResult, error) { 26 | res := &ListUploadMappingsResult{} 27 | _, err := a.get(ctx, uploadMappings, params, res) 28 | 29 | return res, err 30 | } 31 | 32 | // ListUploadMappingsResult is the result of ListUploadMappings. 33 | type ListUploadMappingsResult struct { 34 | Mappings []UploadMapping `json:"mappings"` 35 | Error api.ErrorResp `json:"error,omitempty"` 36 | } 37 | 38 | // UploadMapping represents a single upload mapping. 39 | type UploadMapping struct { 40 | Folder string `json:"folder"` 41 | Template string `json:"template"` 42 | } 43 | 44 | // GetUploadMappingParams are the parameters for GetUploadMapping. 45 | type GetUploadMappingParams struct { 46 | Folder string `json:"folder"` // The name of the upload mapping folder. 47 | } 48 | 49 | // GetUploadMapping returns the details of the specified upload mapping. 50 | // 51 | // Retrieve the mapped template (URL) of a specified upload mapping folder. 52 | // 53 | // https://cloudinary.com/documentation/admin_api#get_the_details_of_a_single_upload_mapping 54 | func (a *API) GetUploadMapping(ctx context.Context, params GetUploadMappingParams) (*GetUploadMappingResult, error) { 55 | res := &GetUploadMappingResult{} 56 | _, err := a.get(ctx, api.BuildPath(uploadMappings), params, res) 57 | 58 | return res, err 59 | } 60 | 61 | // GetUploadMappingResult is the result of GetUploadMapping. 62 | type GetUploadMappingResult struct { 63 | Folder string `json:"folder"` 64 | Template string `json:"template"` 65 | Error api.ErrorResp `json:"error,omitempty"` 66 | } 67 | 68 | // CreateUploadMappingParams are the parameters for CreateUploadMapping. 69 | type CreateUploadMappingParams struct { 70 | Folder string `json:"folder"` // The name of the folder to map. 71 | Template string `json:"template"` // The URL to be mapped to the folder. 72 | } 73 | 74 | // CreateUploadMapping creates a new upload mapping. 75 | // 76 | // https://cloudinary.com/documentation/admin_api#create_an_upload_mapping 77 | func (a *API) CreateUploadMapping(ctx context.Context, params CreateUploadMappingParams) (*CreateUploadMappingResult, error) { 78 | res := &CreateUploadMappingResult{} 79 | _, err := a.post(ctx, api.BuildPath(uploadMappings), params, res) 80 | 81 | return res, err 82 | } 83 | 84 | // CreateUploadMappingResult is the result of CreateUploadMapping. 85 | type CreateUploadMappingResult struct { 86 | Message string `json:"message"` 87 | Folder string `json:"folder"` 88 | Error api.ErrorResp `json:"error,omitempty"` 89 | } 90 | 91 | // UploadMappingResult is the result of UpdateUploadMapping, DeleteUploadMapping. 92 | type UploadMappingResult struct { 93 | Message string `json:"message"` 94 | Error api.ErrorResp `json:"error,omitempty"` 95 | } 96 | 97 | // UpdateUploadMappingParams are the parameters for UpdateUploadMapping. 98 | type UpdateUploadMappingParams struct { 99 | Folder string `json:"folder"` // The name of the upload mapping folder to remap. 100 | Template string `json:"template"` 101 | } 102 | 103 | // UpdateUploadMapping updates an existing upload mapping with a new template (URL). 104 | // 105 | // https://cloudinary.com/documentation/admin_api#update_an_upload_mapping 106 | func (a *API) UpdateUploadMapping(ctx context.Context, params UpdateUploadMappingParams) (*UploadMappingResult, error) { 107 | res := &UploadMappingResult{} 108 | _, err := a.put(ctx, api.BuildPath(uploadMappings), params, res) 109 | 110 | return res, err 111 | } 112 | 113 | // DeleteUploadMappingParams are the parameters for DeleteUploadMapping. 114 | type DeleteUploadMappingParams struct { 115 | Folder string `json:"folder"` // The name of the upload mapping folder to delete. 116 | } 117 | 118 | // DeleteUploadMapping deletes an upload mapping. 119 | // 120 | // https://cloudinary.com/documentation/admin_api#delete_an_upload_mapping 121 | func (a *API) DeleteUploadMapping(ctx context.Context, params DeleteUploadMappingParams) (*UploadMappingResult, error) { 122 | res := &UploadMappingResult{} 123 | _, err := a.delete(ctx, api.BuildPath(uploadMappings), params, res) 124 | 125 | return res, err 126 | } 127 | -------------------------------------------------------------------------------- /api/admin/upload_mappings_test.go: -------------------------------------------------------------------------------- 1 | package admin_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cloudinary/cloudinary-go/v2/api/admin" 7 | ) 8 | 9 | const UMFolder = "wiki" 10 | const UMTemplate = "https://u.wiki.example.com/wiki-images/" 11 | const UMTemplateUpdated = "https://images.example.com/product_assets/images/" 12 | 13 | func TestUploadMappings_Create(t *testing.T) { 14 | resp, err := adminAPI.CreateUploadMapping(ctx, admin.CreateUploadMappingParams{ 15 | Folder: UMFolder, 16 | Template: UMTemplate, 17 | }) 18 | 19 | if err != nil || resp.Message != "created" { 20 | t.Error(resp, err) 21 | } 22 | } 23 | 24 | func TestUploadMappings_List(t *testing.T) { 25 | resp, err := adminAPI.ListUploadMappings(ctx, admin.ListUploadMappingsParams{}) 26 | 27 | if err != nil || len(resp.Mappings) < 1 { 28 | t.Error(resp, err) 29 | } 30 | } 31 | 32 | func TestUploadMappings_Get(t *testing.T) { 33 | lResp, err := adminAPI.ListUploadMappings(ctx, admin.ListUploadMappingsParams{}) 34 | 35 | if err != nil || lResp.Error.Message != "" { 36 | t.Error(lResp, err) 37 | } 38 | 39 | resp, err := adminAPI.GetUploadMapping(ctx, admin.GetUploadMappingParams{Folder: lResp.Mappings[0].Folder}) 40 | 41 | if err != nil { 42 | t.Error(resp, err) 43 | } 44 | } 45 | 46 | func TestUploadMappings_Update(t *testing.T) { 47 | resp, err := adminAPI.UpdateUploadMapping(ctx, admin.UpdateUploadMappingParams{ 48 | Folder: UMFolder, 49 | Template: UMTemplateUpdated, 50 | }) 51 | 52 | if err != nil || resp.Message != "updated" { 53 | t.Error(resp, err) 54 | } 55 | } 56 | 57 | func TestUploadMappings_Delete(t *testing.T) { 58 | resp, err := adminAPI.DeleteUploadMapping(ctx, admin.DeleteUploadMappingParams{Folder: UMFolder}) 59 | 60 | if err != nil || resp.Message != "deleted" { 61 | t.Error(resp, err) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /api/admin/upload_presets.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | // Enables you to manage upload presets. 4 | // 5 | // https://cloudinary.com/documentation/admin_api#upload_presets 6 | import ( 7 | "context" 8 | 9 | "github.com/cloudinary/cloudinary-go/v2/api" 10 | "github.com/cloudinary/cloudinary-go/v2/api/uploader" 11 | ) 12 | 13 | const ( 14 | uploadPresets api.EndPoint = "upload_presets" 15 | ) 16 | 17 | // ListUploadPresetsParams are the parameters for ListUploadPresets. 18 | type ListUploadPresetsParams struct { 19 | MaxResults int `json:"max_results,omitempty"` 20 | NextCursor string `json:"next_cursor,omitempty"` 21 | } 22 | 23 | // ListUploadPresets lists existing upload presets. 24 | // 25 | // https://cloudinary.com/documentation/admin_api#get_upload_presets 26 | func (a *API) ListUploadPresets(ctx context.Context, params ListUploadPresetsParams) (*ListUploadPresetsResult, error) { 27 | res := &ListUploadPresetsResult{} 28 | _, err := a.get(ctx, uploadPresets, params, res) 29 | 30 | return res, err 31 | } 32 | 33 | // ListUploadPresetsResult is the result of ListUploadPresets. 34 | type ListUploadPresetsResult struct { 35 | Presets []UploadPreset `json:"presets"` 36 | Error api.ErrorResp `json:"error,omitempty"` 37 | } 38 | 39 | // UploadPreset represents the details of the upload preset. 40 | type UploadPreset struct { 41 | Name string `json:"name"` 42 | Unsigned bool `json:"unsigned"` 43 | Settings interface{} `json:"settings"` 44 | } 45 | 46 | // GetUploadPresetParams are the parameters for GetUploadPreset. 47 | type GetUploadPresetParams struct { 48 | Name string `json:"-"` 49 | MaxResults int `json:"max_results,omitempty"` 50 | } 51 | 52 | // GetUploadPreset retrieves the details of the specified upload preset. 53 | // 54 | // https://cloudinary.com/documentation/admin_api#get_the_details_of_a_single_upload_preset 55 | func (a *API) GetUploadPreset(ctx context.Context, params GetUploadPresetParams) (*GetUploadPresetResult, error) { 56 | res := &GetUploadPresetResult{} 57 | _, err := a.get(ctx, api.BuildPath(uploadPresets, params.Name), params, res) 58 | 59 | return res, err 60 | } 61 | 62 | // GetUploadPresetResult is the result of GetUploadPreset. 63 | type GetUploadPresetResult struct { 64 | Name string `json:"name"` 65 | Unsigned bool `json:"unsigned"` 66 | Settings interface{} `json:"settings"` 67 | Error api.ErrorResp `json:"error,omitempty"` 68 | } 69 | 70 | // CreateUploadPresetParams are the parameters for CreateUploadPreset. 71 | type CreateUploadPresetParams struct { 72 | Name string `json:"name,omitempty"` 73 | Unsigned *bool `json:"unsigned,omitempty"` 74 | DisallowPublicID *bool `json:"disallow_public_id,omitempty"` 75 | Live *bool `json:"live,omitempty"` 76 | uploader.UploadParams 77 | } 78 | 79 | // CreateUploadPreset creates a new upload preset. 80 | // 81 | // https://cloudinary.com/documentation/admin_api#create_an_upload_preset 82 | func (a *API) CreateUploadPreset(ctx context.Context, params CreateUploadPresetParams) (*CreateUploadPresetResult, error) { 83 | res := &CreateUploadPresetResult{} 84 | _, err := a.post(ctx, api.BuildPath(uploadPresets), params, res) 85 | 86 | return res, err 87 | } 88 | 89 | // CreateUploadPresetResult is the result of CreateUploadPreset. 90 | type CreateUploadPresetResult struct { 91 | Message string `json:"message"` 92 | Name string `json:"name"` 93 | Error api.ErrorResp `json:"error,omitempty"` 94 | } 95 | 96 | // UpdateUploadPresetParams are the parameters for UpdateUploadPreset. 97 | type UpdateUploadPresetParams struct { 98 | Name string `json:"name"` 99 | Unsigned *bool `json:"unsigned,omitempty"` 100 | DisallowPublicID *bool `json:"disallow_public_id,omitempty"` 101 | Live *bool `json:"live,omitempty"` 102 | uploader.UploadParams 103 | } 104 | 105 | // UpdateUploadPreset updates the specified upload preset. 106 | // 107 | // https://cloudinary.com/documentation/admin_api#update_an_upload_preset 108 | func (a *API) UpdateUploadPreset(ctx context.Context, params UpdateUploadPresetParams) (*UploadPresetResult, error) { 109 | res := &UploadPresetResult{} 110 | _, err := a.put(ctx, api.BuildPath(uploadPresets, params.Name), params, res) 111 | 112 | return res, err 113 | } 114 | 115 | // UploadPresetResult is the result of UpdateUploadPreset, DeleteUploadPreset. 116 | type UploadPresetResult struct { 117 | Message string `json:"message"` 118 | Error api.ErrorResp `json:"error,omitempty"` 119 | } 120 | 121 | // DeleteUploadPresetParams are the parameters for DeleteUploadPreset. 122 | type DeleteUploadPresetParams struct { 123 | Name string `json:"-"` 124 | } 125 | 126 | // DeleteUploadPreset deletes the specified upload preset. 127 | // 128 | // https://cloudinary.com/documentation/admin_api#delete_an_upload_preset 129 | func (a *API) DeleteUploadPreset(ctx context.Context, params DeleteUploadPresetParams) (*UploadPresetResult, error) { 130 | res := &UploadPresetResult{} 131 | _, err := a.delete(ctx, api.BuildPath(uploadPresets, params.Name), params, res) 132 | 133 | return res, err 134 | } 135 | -------------------------------------------------------------------------------- /api/admin/upload_presets_test.go: -------------------------------------------------------------------------------- 1 | package admin_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cloudinary/cloudinary-go/v2/api" 7 | "github.com/cloudinary/cloudinary-go/v2/api/admin" 8 | "github.com/cloudinary/cloudinary-go/v2/api/uploader" 9 | ) 10 | 11 | const UPName = "go-upload-preset" 12 | 13 | func TestUploadPresets_Create(t *testing.T) { 14 | 15 | params := admin.CreateUploadPresetParams{ 16 | Name: UPName, 17 | Unsigned: api.Bool(true), 18 | Live: api.Bool(true), 19 | UploadParams: uploader.UploadParams{Tags: api.CldAPIArray{"go-tag1", "go-tag2"}}, 20 | } 21 | 22 | resp, err := adminAPI.CreateUploadPreset(ctx, params) 23 | 24 | if err != nil || resp.Message != "created" { 25 | t.Error(resp, err) 26 | } 27 | } 28 | 29 | func TestUploadPresets_List(t *testing.T) { 30 | resp, err := adminAPI.ListUploadPresets(ctx, admin.ListUploadPresetsParams{}) 31 | 32 | if err != nil || len(resp.Presets) < 1 { 33 | t.Error(resp, err) 34 | } 35 | } 36 | 37 | func TestUploadPresets_Get(t *testing.T) { 38 | resp, err := adminAPI.GetUploadPreset(ctx, admin.GetUploadPresetParams{Name: UPName}) 39 | 40 | if err != nil { 41 | t.Error(resp, err) 42 | } 43 | } 44 | 45 | func TestUploadPresets_Update(t *testing.T) { 46 | updateUPParams := admin.UpdateUploadPresetParams{ 47 | Name: UPName, 48 | Unsigned: api.Bool(false), 49 | Live: api.Bool(false), 50 | UploadParams: uploader.UploadParams{Tags: api.CldAPIArray{"go-tag3", "go-tag4"}}, 51 | } 52 | 53 | resp, err := adminAPI.UpdateUploadPreset(ctx, updateUPParams) 54 | 55 | if err != nil || resp.Message != "updated" { 56 | t.Error(resp, err) 57 | } 58 | 59 | gResp, err := adminAPI.GetUploadPreset(ctx, admin.GetUploadPresetParams{Name: UPName}) 60 | 61 | if err != nil { 62 | t.Error(gResp, err) 63 | } 64 | } 65 | 66 | func TestUploadPresets_Delete(t *testing.T) { 67 | resp, err := adminAPI.DeleteUploadPreset(ctx, admin.DeleteUploadPresetParams{Name: UPName}) 68 | 69 | if err != nil || resp.Message != "deleted" { 70 | t.Error(resp, err) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /api/uploader/acceptance_test.go: -------------------------------------------------------------------------------- 1 | package uploader_test 2 | 3 | import ( 4 | "context" 5 | "github.com/cloudinary/cloudinary-go/v2/api/uploader" 6 | "github.com/cloudinary/cloudinary-go/v2/config" 7 | "github.com/cloudinary/cloudinary-go/v2/internal/cldtest" 8 | "testing" 9 | ) 10 | 11 | // UploadAPIRequestTest is a function that will be executed during the test. 12 | type UploadAPIRequestTest func(api *uploader.API, ctx context.Context) (interface{}, error) 13 | 14 | // UploadAPIAcceptanceTestCase is the acceptance test case definition. See `TEST.md` for additional information. 15 | type UploadAPIAcceptanceTestCase struct { 16 | Name string // Name of the test case 17 | RequestTest UploadAPIRequestTest // Function which will be called as an API request. Put SDK calls here. 18 | ResponseTest cldtest.APIResponseTest // Function which will be called to test an API response. 19 | ExpectedRequest cldtest.ExpectedRequestParams // Expected HTTP request to be sent to the server 20 | JsonResponse string // Mock of the JSON response from server. This is used to check JSON parsing. 21 | ExpectedStatus string // Expected HTTP status of the request. This status will be returned from the HTTP mock. 22 | ExpectedCallCount int // Expected call count to the server. 23 | Config *config.Configuration // Configuration 24 | } 25 | 26 | // testUploadAPIByTestCases run acceptance tests by the given test cases. See `TEST.md` for additional information. 27 | func testUploadAPIByTestCases(cases []UploadAPIAcceptanceTestCase, t *testing.T) { 28 | for num, test := range cases { 29 | if test.Name == "" { 30 | t.Skipf("Test name should be set for test #%d. Skipping it.", num) 31 | } 32 | 33 | t.Run(test.Name, func(t *testing.T) { 34 | callCounter := 0 35 | srv := cldtest.GetServerMock(cldtest.GetTestHandler(test.JsonResponse, t, &callCounter, test.ExpectedRequest)) 36 | 37 | res, _ := test.RequestTest(getTestableUploadAPI(srv.URL, test.Config, t), ctx) 38 | test.ResponseTest(res, t) 39 | 40 | if callCounter != test.ExpectedCallCount { 41 | t.Errorf("Expected %d call, %d given", test.ExpectedCallCount, callCounter) 42 | } 43 | 44 | srv.Close() 45 | }) 46 | } 47 | } 48 | 49 | // Get configured API for test 50 | func getTestableUploadAPI(mockServerUrl string, c *config.Configuration, t *testing.T) *uploader.API { 51 | if c == nil { 52 | var err error 53 | c, err = config.NewFromParams(cldtest.CloudName, cldtest.APIKey, cldtest.APISecret) 54 | if err != nil { 55 | t.Error(err) 56 | } 57 | } 58 | 59 | c.API.UploadPrefix = mockServerUrl 60 | 61 | api, err := uploader.NewWithConfiguration(c) 62 | if err != nil { 63 | t.Error(err) 64 | } 65 | 66 | return api 67 | } 68 | -------------------------------------------------------------------------------- /api/uploader/archive_test.go: -------------------------------------------------------------------------------- 1 | package uploader_test 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/cloudinary/cloudinary-go/v2/api" 9 | "github.com/cloudinary/cloudinary-go/v2/api/uploader" 10 | "github.com/cloudinary/cloudinary-go/v2/internal/cldtest" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | const folder = "go-folder" 15 | 16 | func TestUploader_CreateZip(t *testing.T) { 17 | cldtest.UploadTestAsset(t, cldtest.PublicID) 18 | cldtest.UploadTestAsset(t, cldtest.PublicID2) 19 | 20 | params := uploader.CreateArchiveParams{ 21 | Tags: cldtest.Tags, 22 | } 23 | 24 | resp, err := uploadAPI.CreateZip(ctx, params) 25 | 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | 30 | if resp == nil || resp.AssetID == "" { 31 | t.Error(resp) 32 | } 33 | } 34 | 35 | func TestUploader_DownloadZipURL(t *testing.T) { 36 | cldtest.UploadTestAsset(t, cldtest.PublicID) 37 | cldtest.UploadTestAsset(t, cldtest.PublicID2) 38 | 39 | params := uploader.CreateArchiveParams{ 40 | PublicIDs: []string{cldtest.PublicID, cldtest.PublicID2}, 41 | TargetPublicID: "goArchive", 42 | } 43 | 44 | arURL, err := uploadAPI.DownloadZipURL(params) 45 | 46 | if err != nil { 47 | t.Error(err) 48 | } 49 | 50 | if arURL == "" { 51 | t.Error(arURL) 52 | } 53 | 54 | assert.Contains(t, arURL, url.QueryEscape("public_ids[0]")+"="+cldtest.PublicID) 55 | assert.Contains(t, arURL, url.QueryEscape("public_ids[1]")+"="+cldtest.PublicID2) 56 | } 57 | 58 | func TestUploader_DownloadFolder(t *testing.T) { 59 | 60 | cldtest.UploadTestAsset(t, folder+"/"+cldtest.PublicID) 61 | cldtest.UploadTestAsset(t, folder+"/"+cldtest.PublicID2) 62 | 63 | params := uploader.CreateArchiveParams{ 64 | Tags: cldtest.Tags, 65 | TargetPublicID: "goArchive", 66 | } 67 | 68 | folderURL, _ := uploadAPI.DownloadFolder(folder, params) 69 | assert.Contains(t, folderURL, "generate_archive") 70 | assert.Contains(t, folderURL, folder) 71 | assert.Contains(t, folderURL, params.TargetPublicID) 72 | assert.Contains(t, folderURL, api.All) 73 | assert.Contains(t, folderURL, url.QueryEscape(strings.Join(cldtest.Tags, ","))) 74 | 75 | folderURL, _ = uploadAPI.DownloadFolder(folder, uploader.CreateArchiveParams{ResourceType: api.Image}) 76 | assert.Contains(t, folderURL, api.Image) 77 | } 78 | 79 | func TestUploader_DownloadBackedUpAsset(t *testing.T) { 80 | params := uploader.DownloadBackedUpAssetParams{ 81 | AssetID: "b71b23d9c89a81a254b88a91a9dad8cd", 82 | VersionID: "0e493356d8a40b856c4863c026891a4e", 83 | } 84 | 85 | downloadURL, _ := uploadAPI.DownloadBackedUpAsset(params) 86 | assert.Contains(t, downloadURL, "asset_id") 87 | assert.Contains(t, downloadURL, "version_id") 88 | assert.Contains(t, downloadURL, "download_backup") 89 | } 90 | 91 | func TestUploader_PrivateDownloadURL(t *testing.T) { 92 | params := uploader.PrivateDownloadURLParams{ 93 | PublicID: cldtest.PublicID, 94 | Format: "png", 95 | } 96 | 97 | privateDownloadURL, _ := uploadAPI.PrivateDownloadURL(params) 98 | 99 | assert.Contains(t, privateDownloadURL, "api_key") 100 | assert.Contains(t, privateDownloadURL, "signature") 101 | assert.Contains(t, privateDownloadURL, "timestamp") 102 | } 103 | -------------------------------------------------------------------------------- /api/uploader/context.go: -------------------------------------------------------------------------------- 1 | package uploader 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/cloudinary/cloudinary-go/v2/api" 7 | ) 8 | 9 | const ( 10 | cldContext api.EndPoint = "context" 11 | ) 12 | 13 | type contextCommand string 14 | 15 | const ( 16 | addContext contextCommand = "add" 17 | removeAllContext contextCommand = "remove_all" 18 | ) 19 | 20 | // AddContextParams are the parameters for AddContext. 21 | type AddContextParams struct { 22 | Context api.CldAPIMap `json:"context,omitempty"` 23 | PublicIDs api.CldAPIArray `json:"public_ids"` 24 | Type string `json:"type,omitempty"` 25 | ResourceType string `json:"-"` 26 | } 27 | 28 | // AddContext adds context metadata as key-value pairs to the the specified assets. 29 | // 30 | // https://cloudinary.com/documentation/image_upload_api_reference#context_method 31 | func (u *API) AddContext(ctx context.Context, params AddContextParams) (*AddContextResult, error) { 32 | res := &AddContextResult{} 33 | err := u.callContextAPI(ctx, addContext, params, res) 34 | 35 | return res, err 36 | } 37 | 38 | // AddContextResult is the result of AddContext. 39 | type AddContextResult = ContextResult 40 | 41 | // ContextResult is the result of Context APIs. 42 | type ContextResult struct { 43 | PublicIDs []string `json:"public_ids"` 44 | Error api.ErrorResp `json:"error,omitempty"` 45 | Response interface{} 46 | } 47 | 48 | // RemoveAllContextParams struct 49 | type RemoveAllContextParams struct { 50 | PublicIDs api.CldAPIArray `json:"public_ids"` 51 | Type string `json:"type,omitempty"` 52 | ResourceType string `json:"-"` 53 | } 54 | 55 | // RemoveAllContext removes all context metadata from the specified assets. 56 | // 57 | // https://cloudinary.com/documentation/image_upload_api_reference#context_method 58 | func (u *API) RemoveAllContext(ctx context.Context, params RemoveAllContextParams) (*RemoveAllContextResult, error) { 59 | res := &RemoveAllContextResult{} 60 | err := u.callContextAPI(ctx, removeAllContext, params, res) 61 | 62 | return res, err 63 | } 64 | 65 | // RemoveAllContextResult is the result of RemoveAllContext. 66 | type RemoveAllContextResult = ContextResult 67 | 68 | // callContextAPI is an internal method that is used to call to the context API. 69 | func (u *API) callContextAPI(ctx context.Context, command contextCommand, requestParams interface{}, result interface{}) error { 70 | formParams, err := api.StructToParams(requestParams) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | formParams.Add("command", fmt.Sprintf("%v", command)) 76 | 77 | return u.callUploadAPIWithParams(ctx, api.BuildPath(getAssetType(requestParams), cldContext), formParams, result) 78 | } 79 | -------------------------------------------------------------------------------- /api/uploader/context_test.go: -------------------------------------------------------------------------------- 1 | package uploader_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cloudinary/cloudinary-go/v2/api" 7 | "github.com/cloudinary/cloudinary-go/v2/api/uploader" 8 | "github.com/cloudinary/cloudinary-go/v2/internal/cldtest" 9 | ) 10 | 11 | func TestUploader_Context(t *testing.T) { 12 | cldtest.UploadTestAsset(t, cldtest.PublicID) 13 | 14 | params := uploader.AddContextParams{ 15 | PublicIDs: api.CldAPIArray{cldtest.PublicID}, 16 | Context: cldtest.CldContext, 17 | } 18 | 19 | resp, err := uploadAPI.AddContext(ctx, params) 20 | 21 | if err != nil { 22 | t.Error(err) 23 | } 24 | 25 | if resp == nil || len(resp.PublicIDs) != 1 || resp.PublicIDs[0] != cldtest.PublicID { 26 | t.Error(resp) 27 | } 28 | 29 | raParams := uploader.RemoveAllContextParams{ 30 | PublicIDs: api.CldAPIArray{cldtest.PublicID}, 31 | } 32 | 33 | raResp, err := uploadAPI.RemoveAllContext(ctx, raParams) 34 | 35 | if err != nil { 36 | t.Error(err) 37 | } 38 | 39 | if raResp == nil || len(raResp.PublicIDs) != 1 || raResp.PublicIDs[0] != cldtest.PublicID { 40 | t.Error(resp) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /api/uploader/creative.go: -------------------------------------------------------------------------------- 1 | package uploader 2 | 3 | import ( 4 | "context" 5 | "github.com/cloudinary/cloudinary-go/v2/api" 6 | ) 7 | 8 | const ( 9 | sprite api.EndPoint = "sprite" 10 | multi api.EndPoint = "multi" 11 | explode api.EndPoint = "explode" 12 | text api.EndPoint = "text" 13 | ) 14 | 15 | // GenerateSpriteParams are the parameters for GenerateSprite. 16 | type GenerateSpriteParams struct { 17 | Tag string `json:"tag,omitempty"` 18 | NotificationURL string `json:"notification_url,omitempty"` 19 | Async *bool `json:"async,omitempty"` 20 | Transformation string `json:"transformation,omitempty"` 21 | ResourceType string `json:"-"` 22 | } 23 | 24 | // GenerateSprite creates a sprite from all images that have been assigned a specified tag. 25 | // 26 | // The process produces two files: 27 | // * A single image file containing all the images with the specified tag (PNG by default). 28 | // * A CSS file that includes the style class names and the location of the individual images in the sprite. 29 | // 30 | // https://cloudinary.com/documentation/image_upload_api_reference#sprite_method 31 | func (u *API) GenerateSprite(ctx context.Context, params GenerateSpriteParams) (*GenerateSpriteResult, error) { 32 | res := &GenerateSpriteResult{} 33 | err := u.callUploadAPI(ctx, sprite, params, res) 34 | 35 | return res, err 36 | } 37 | 38 | // GenerateSpriteResult is the result of GenerateSprite. 39 | type GenerateSpriteResult struct { 40 | CSSURL string `json:"css_url"` 41 | ImageURL string `json:"image_url"` 42 | SecureCSSURL string `json:"secure_css_url"` 43 | SecureImageURL string `json:"secure_image_url"` 44 | JSONURL string `json:"json_url"` 45 | SecureJSONURL string `json:"secure_json_url"` 46 | Version int `json:"version"` 47 | PublicID string `json:"public_id"` 48 | ImageInfos map[string]ImageInfo `json:"image_infos"` 49 | Error api.ErrorResp `json:"error,omitempty"` 50 | Response interface{} 51 | } 52 | 53 | // ImageInfo contains information about the image. 54 | type ImageInfo struct { 55 | Width int `json:"width"` 56 | Height int `json:"height"` 57 | X int `json:"x"` 58 | Y int `json:"y"` 59 | } 60 | 61 | // MultiParams are the parameters for Multi. 62 | type MultiParams struct { 63 | Tag string `json:"tag,omitempty"` 64 | Format string `json:"format,omitempty"` 65 | NotificationURL string `json:"notification_url,omitempty"` 66 | Async *bool `json:"async,omitempty"` 67 | Transformation string `json:"transformation,omitempty"` 68 | ResourceType string `json:"-"` 69 | } 70 | 71 | // Multi Creates a single animated image, video or PDF from all image assets that have been assigned a specified tag. 72 | // 73 | // https://cloudinary.com/documentation/image_upload_api_reference#multi_method 74 | func (u *API) Multi(ctx context.Context, params MultiParams) (*MultiResult, error) { 75 | res := &MultiResult{} 76 | err := u.callUploadAPI(ctx, multi, params, res) 77 | 78 | return res, err 79 | } 80 | 81 | // MultiResult is the result of Multi. 82 | type MultiResult struct { 83 | URL string `json:"url"` 84 | SecureURL string `json:"secure_url"` 85 | AssetID string `json:"asset_id"` 86 | PublicID string `json:"public_id"` 87 | Version int `json:"version"` 88 | Error api.ErrorResp `json:"error,omitempty"` 89 | Response interface{} 90 | } 91 | 92 | // ExplodeParams are the parameters for Explode. 93 | type ExplodeParams struct { 94 | PublicID string `json:"public_id"` 95 | Format string `json:"format,omitempty"` 96 | Type string `json:"type,omitempty"` 97 | NotificationURL string `json:"notification_url,omitempty"` 98 | Transformation string `json:"transformation,omitempty"` 99 | ResourceType string `json:"-"` 100 | } 101 | 102 | // Explode creates derived images for all of the individual pages in a multi-page file (PDF or animated GIF). 103 | // 104 | // Each derived image is stored with the same public ID as the original file, and can be accessed using the page 105 | // parameter, in order to deliver a specific image. 106 | // 107 | // https://cloudinary.com/documentation/image_upload_api_reference#explode_method 108 | func (u *API) Explode(ctx context.Context, params ExplodeParams) (*ExplodeResult, error) { 109 | params.Transformation = "pg_all" // Transformation must contain exactly one "pg_all" transformation parameter 110 | res := &ExplodeResult{} 111 | err := u.callUploadAPI(ctx, explode, params, res) 112 | 113 | return res, err 114 | } 115 | 116 | // ExplodeResult is the result of Explode. 117 | type ExplodeResult struct { 118 | Status string `json:"status"` 119 | BatchID string `json:"batch_id"` 120 | Error api.ErrorResp `json:"error,omitempty"` 121 | Response interface{} 122 | } 123 | 124 | // TextParams are the parameters for Text. 125 | type TextParams struct { 126 | Text string `json:"text"` 127 | PublicID string `json:"public_id,omitempty"` 128 | FontFamily string `json:"font_family,omitempty"` 129 | FontSize int `json:"font_size,omitempty"` 130 | FontColor string `json:"font_color,omitempty"` 131 | TextAlign string `json:"text_align,omitempty"` 132 | FontWeight string `json:"font_weight,omitempty"` 133 | FontStyle string `json:"font_style,omitempty"` 134 | Background string `json:"background,omitempty"` 135 | Opacity string `json:"opacity,omitempty"` 136 | TextDecoration string `json:"text_decoration,omitempty"` 137 | ResourceType string `json:"-"` 138 | } 139 | 140 | // Text dynamically generates an image from a given textual string. 141 | // 142 | // https://cloudinary.com/documentation/image_upload_api_reference#text_method 143 | func (u *API) Text(ctx context.Context, params TextParams) (*UploadResult, error) { 144 | res := &UploadResult{} 145 | err := u.callUploadAPI(ctx, text, params, res) 146 | 147 | return res, err 148 | } 149 | -------------------------------------------------------------------------------- /api/uploader/creative_test.go: -------------------------------------------------------------------------------- 1 | package uploader_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cloudinary/cloudinary-go/v2/api/uploader" 7 | "github.com/cloudinary/cloudinary-go/v2/internal/cldtest" 8 | ) 9 | 10 | func TestUploader_GenerateSprite(t *testing.T) { 11 | cldtest.UploadTestAsset(t, cldtest.PublicID) 12 | cldtest.UploadTestAsset(t, cldtest.PublicID2) 13 | 14 | params := uploader.GenerateSpriteParams{ 15 | Tag: cldtest.Tag1, 16 | } 17 | 18 | resp, err := uploadAPI.GenerateSprite(ctx, params) 19 | 20 | if err != nil { 21 | t.Error(err) 22 | } 23 | 24 | if resp == nil || len(resp.ImageInfos) < 2 { 25 | t.Error(resp) 26 | } 27 | } 28 | func TestUploader_Multi(t *testing.T) { 29 | mParams := uploader.MultiParams{ 30 | Tag: cldtest.Tag1, 31 | } 32 | 33 | mResp, err := uploadAPI.Multi(ctx, mParams) 34 | 35 | if err != nil { 36 | t.Error(err) 37 | } 38 | 39 | if mResp == nil || mResp.PublicID != cldtest.Tag1 { 40 | t.Error(mResp) 41 | } 42 | } 43 | func TestUploader_Explode(t *testing.T) { 44 | eParams := uploader.ExplodeParams{ 45 | PublicID: cldtest.Tag1, 46 | Type: "multi", 47 | } 48 | 49 | eResp, err := uploadAPI.Explode(ctx, eParams) 50 | 51 | if err != nil { 52 | t.Error(err) 53 | } 54 | 55 | if eResp == nil || eResp.BatchID == "" { 56 | t.Error(eResp) 57 | } 58 | } 59 | 60 | func TestUploader_Text(t *testing.T) { 61 | tParams := uploader.TextParams{ 62 | Text: "HelloGo", 63 | PublicID: cldtest.PublicID, 64 | FontFamily: "Arial", 65 | FontSize: 20, 66 | } 67 | 68 | tResp, err := uploadAPI.Text(ctx, tParams) 69 | 70 | if err != nil { 71 | t.Error(err) 72 | } 73 | 74 | if tResp == nil || tResp.PublicID == "" { 75 | t.Error(tResp) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /api/uploader/edit_asset.go: -------------------------------------------------------------------------------- 1 | package uploader 2 | 3 | import ( 4 | "context" 5 | "github.com/cloudinary/cloudinary-go/v2/api" 6 | ) 7 | 8 | const ( 9 | upload api.EndPoint = "upload" 10 | destroy api.EndPoint = "destroy" 11 | rename api.EndPoint = "rename" 12 | explicit api.EndPoint = "explicit" 13 | metadata api.EndPoint = "metadata" 14 | ) 15 | 16 | // DestroyParams are the parameters for Destroy. 17 | type DestroyParams struct { 18 | PublicID string `json:"public_id,omitempty"` 19 | Type string `json:"type,omitempty"` 20 | ResourceType string `json:"-"` 21 | Invalidate *bool `json:"invalidate,omitempty"` 22 | } 23 | 24 | // Destroy immediately and permanently deletes a single asset from your Cloudinary account. 25 | // 26 | // Backed up assets are not deleted, and any assets and transformed assets already downloaded by visitors to your 27 | // website might still be accessible through cached copies on the CDN. You can invalidate any cached copies on the 28 | // CDN with the `Invalidate` parameter. 29 | // 30 | // https://cloudinary.com/documentation/image_upload_api_reference#destroy_method 31 | func (u *API) Destroy(ctx context.Context, params DestroyParams) (*DestroyResult, error) { 32 | res := &DestroyResult{} 33 | err := u.callUploadAPI(ctx, destroy, params, res) 34 | 35 | return res, err 36 | } 37 | 38 | // DestroyResult is the result of Destroy. 39 | type DestroyResult struct { 40 | Result string `json:"result"` 41 | Error api.ErrorResp `json:"error,omitempty"` 42 | Response interface{} 43 | } 44 | 45 | // RenameParams are the parameters for Rename. 46 | type RenameParams struct { 47 | FromPublicID string `json:"from_public_id,omitempty"` 48 | ToPublicID string `json:"to_public_id,omitempty"` 49 | Type string `json:"type,omitempty"` 50 | ToType string `json:"to_type,omitempty"` 51 | ResourceType string `json:"-"` 52 | Overwrite *bool `json:"overwrite,omitempty"` 53 | Invalidate *bool `json:"invalidate,omitempty"` 54 | } 55 | 56 | // Rename renames the specified asset in your Cloudinary account. 57 | // 58 | // The existing URLs of renamed assets and their associated derived resources are no longer valid, although any 59 | // assets and transformed assets already downloaded by visitors to your website might still be accessible through 60 | // cached copies on the CDN. You can invalidate any cached copies on the CDN with the `invalidate` parameter. 61 | // 62 | // https://cloudinary.com/documentation/image_upload_api_reference#rename_method 63 | func (u *API) Rename(ctx context.Context, params RenameParams) (*RenameResult, error) { 64 | res := &RenameResult{} 65 | err := u.callUploadAPI(ctx, rename, params, res) 66 | 67 | return res, err 68 | } 69 | 70 | // RenameResult is the result of Rename. 71 | type RenameResult struct { 72 | api.BriefAssetResult 73 | Error interface{} `json:"error,omitempty"` 74 | } 75 | 76 | // ExplicitParams are the parameters for Explicit. 77 | type ExplicitParams = UploadParams 78 | 79 | // Explicit applies actions to already uploaded assets. 80 | // 81 | // https://cloudinary.com/documentation/image_upload_api_reference#explicit_method 82 | func (u *API) Explicit(ctx context.Context, params ExplicitParams) (*ExplicitResult, error) { 83 | res := &ExplicitResult{} 84 | err := u.callUploadAPI(ctx, explicit, params, res) 85 | 86 | return res, err 87 | } 88 | 89 | // ExplicitResult is the result of Explicit. 90 | type ExplicitResult struct { 91 | UploadResult 92 | } 93 | 94 | // UpdateMetadataParams are the parameters for UpdateMetadata. 95 | type UpdateMetadataParams struct { 96 | PublicIDs []string `json:"public_ids"` 97 | Metadata api.CldAPIMap `json:"metadata"` 98 | Type string `json:"type,omitempty"` 99 | ResourceType string `json:"-"` 100 | } 101 | 102 | // UpdateMetadata populates metadata fields with the given values. Existing values will be overwritten. 103 | // 104 | // Any metadata-value pairs given are merged with any existing metadata-value pairs 105 | // (an empty value for an existing metadata field clears the value). 106 | // 107 | // https://cloudinary.com/documentation/image_upload_api_reference#metadata_method 108 | func (u *API) UpdateMetadata(ctx context.Context, params UpdateMetadataParams) (*UpdateMetadataResult, error) { 109 | res := &UpdateMetadataResult{} 110 | err := u.callUploadAPI(ctx, metadata, params, res) 111 | 112 | return res, err 113 | } 114 | 115 | // UpdateMetadataResult is the result of UpdateMetadata. 116 | type UpdateMetadataResult struct { 117 | PublicIDs []string `json:"public_ids"` 118 | Error interface{} `json:"error,omitempty"` 119 | } 120 | -------------------------------------------------------------------------------- /api/uploader/edit_asset_test.go: -------------------------------------------------------------------------------- 1 | package uploader_test 2 | 3 | import ( 4 | "github.com/cloudinary/cloudinary-go/v2/api" 5 | "github.com/cloudinary/cloudinary-go/v2/api/uploader" 6 | "github.com/cloudinary/cloudinary-go/v2/internal/cldtest" 7 | "github.com/stretchr/testify/assert" 8 | "testing" 9 | ) 10 | 11 | func TestUploader_Explicit(t *testing.T) { 12 | cldtest.UploadTestAsset(t, cldtest.PublicID) 13 | 14 | params := uploader.ExplicitParams{ 15 | PublicID: cldtest.PublicID, 16 | Type: api.Upload, 17 | Tags: cldtest.Tags, 18 | Moderation: "manual", 19 | } 20 | 21 | resp, err := uploadAPI.Explicit(ctx, params) 22 | 23 | if err != nil { 24 | t.Error(err) 25 | } 26 | 27 | if resp == nil || len(resp.Tags) != 2 { 28 | t.Error(resp) 29 | } 30 | 31 | if len(resp.Moderation) < 1 || resp.Moderation[0].Kind != "manual" { 32 | t.Error(resp) 33 | } 34 | } 35 | 36 | func TestUploader_Edit(t *testing.T) { 37 | cldtest.UploadTestAsset(t, cldtest.PublicID) 38 | 39 | params := uploader.RenameParams{ 40 | FromPublicID: cldtest.PublicID, 41 | ToPublicID: cldtest.PublicID2, 42 | Overwrite: api.Bool(true), 43 | } 44 | 45 | resp, err := uploadAPI.Rename(ctx, params) 46 | 47 | if err != nil { 48 | t.Error(err) 49 | } 50 | 51 | if resp == nil || resp.PublicID != cldtest.PublicID2 { 52 | t.Error(resp) 53 | } 54 | 55 | dParams := uploader.DestroyParams{ 56 | PublicID: cldtest.PublicID2, 57 | } 58 | 59 | dResp, err := uploadAPI.Destroy(ctx, dParams) 60 | 61 | if err != nil { 62 | t.Error(err) 63 | } 64 | 65 | if resp == nil || dResp.Result != "ok" { 66 | t.Error(resp) 67 | } 68 | } 69 | 70 | func TestUploader_UpdateMetadata(t *testing.T) { 71 | externalID := cldtest.CreateStringMetadataField(t, "update_metadata_field_") 72 | externalID2 := cldtest.CreateStringMetadataField(t, "update_metadata_field2_") 73 | pID1 := cldtest.UniqueID(cldtest.PublicID + "_metadata") 74 | pID2 := cldtest.UniqueID(cldtest.PublicID2 + "_metadata") 75 | cldtest.UploadTestAsset(t, pID1) 76 | cldtest.UploadTestAsset(t, pID2) 77 | 78 | params := uploader.UpdateMetadataParams{ 79 | PublicIDs: []string{pID1, pID2}, 80 | Metadata: api.CldAPIMap{externalID: "upd1", externalID2: "upd2"}, 81 | } 82 | 83 | resp, err := uploadAPI.UpdateMetadata(ctx, params) 84 | 85 | // FIXME: use setUp/tearDown 86 | cldtest.DeleteTestMetadataField(t, externalID) 87 | cldtest.DeleteTestMetadataField(t, externalID2) 88 | 89 | if err != nil { 90 | t.Error(err) 91 | } 92 | 93 | assert.Equal(t, 2, len(resp.PublicIDs)) 94 | } 95 | -------------------------------------------------------------------------------- /api/uploader/tag.go: -------------------------------------------------------------------------------- 1 | package uploader 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/cloudinary/cloudinary-go/v2/api" 7 | ) 8 | 9 | const ( 10 | tags api.EndPoint = "tags" 11 | ) 12 | 13 | type tagCommand string 14 | 15 | const ( 16 | addTag tagCommand = "add" 17 | removeTag tagCommand = "remove" 18 | replaceTag tagCommand = "replace" 19 | removeAllTags tagCommand = "remove_all" 20 | ) 21 | 22 | // AddTagParams are the parameters for 23 | type AddTagParams struct { 24 | Tag string `json:"tag,omitempty"` // The name of the tag to add. 25 | PublicIDs []string `json:"public_ids"` // The public IDs of the assets to add the tag to. 26 | Type string `json:"type,omitempty"` 27 | ResourceType string `json:"-"` 28 | } 29 | 30 | // AddTag adds a tag to the assets specified. 31 | // 32 | // https://cloudinary.com/documentation/image_upload_api_reference#tags_method 33 | func (u *API) AddTag(ctx context.Context, params AddTagParams) (*AddTagResult, error) { 34 | res := &AddTagResult{} 35 | err := u.callTagsAPI(ctx, addTag, params, res) 36 | 37 | return res, err 38 | } 39 | 40 | // AddTagResult is the result of AddTag. 41 | type AddTagResult struct { 42 | TagResult 43 | } 44 | 45 | // RemoveTagParams are the parameters for RemoveTag. 46 | type RemoveTagParams struct { 47 | Tag string `json:"tag,omitempty"` // The name of the tag to remove. 48 | PublicIDs []string `json:"public_ids"` // The public IDs of the assets to remove the tags from. 49 | Type string `json:"type,omitempty"` 50 | ResourceType string `json:"-"` 51 | } 52 | 53 | // RemoveTag removes a tag from the assets specified. 54 | // 55 | // https://cloudinary.com/documentation/image_upload_api_reference#tags_method 56 | func (u *API) RemoveTag(ctx context.Context, params RemoveTagParams) (*RemoveTagResult, error) { 57 | res := &RemoveTagResult{} 58 | err := u.callTagsAPI(ctx, removeTag, params, res) 59 | 60 | return res, err 61 | } 62 | 63 | // RemoveTagResult is the result of RemoveTag. 64 | type RemoveTagResult struct { 65 | TagResult 66 | } 67 | 68 | // RemoveAllTagsParams are the parameters for RemoveAllTags. 69 | type RemoveAllTagsParams struct { 70 | PublicIDs []string `json:"public_ids"` // The public IDs of the assets to remove all tags from. 71 | Type string `json:"type,omitempty"` 72 | ResourceType string `json:"-"` 73 | } 74 | 75 | // RemoveAllTags removes all tags from the assets specified. 76 | // 77 | // https://cloudinary.com/documentation/image_upload_api_reference#tags_method 78 | func (u *API) RemoveAllTags(ctx context.Context, params RemoveAllTagsParams) (*RemoveAllTagsResult, error) { 79 | res := &RemoveAllTagsResult{} 80 | err := u.callTagsAPI(ctx, removeAllTags, params, res) 81 | 82 | return res, err 83 | } 84 | 85 | // RemoveAllTagsResult is the result of RemoveAllTags. 86 | type RemoveAllTagsResult struct { 87 | TagResult 88 | } 89 | 90 | // ReplaceTagParams are the parameters for ReplaceTag. 91 | type ReplaceTagParams struct { 92 | Tag string `json:"tag"` // The new tag with which to replace the existing tags. 93 | PublicIDs []string `json:"public_ids"` // The public IDs of the assets to replace the tags of. 94 | Type string `json:"type,omitempty"` 95 | ResourceType string `json:"-"` 96 | } 97 | 98 | // ReplaceTag replaces all existing tags on the assets specified with the tag specified. 99 | // 100 | // https://cloudinary.com/documentation/image_upload_api_reference#tags_method 101 | func (u *API) ReplaceTag(ctx context.Context, params ReplaceTagParams) (*ReplaceTagResult, error) { 102 | res := &ReplaceTagResult{} 103 | err := u.callTagsAPI(ctx, replaceTag, params, res) 104 | 105 | return res, err 106 | } 107 | 108 | // ReplaceTagResult is the result of ReplaceTag. 109 | type ReplaceTagResult struct { 110 | TagResult 111 | } 112 | 113 | // TagResult represents the tag result. 114 | type TagResult struct { 115 | PublicIDs []string `json:"public_ids"` // The public IDs of the assets that were affected. 116 | Error api.ErrorResp `json:"error,omitempty"` 117 | Response interface{} 118 | } 119 | 120 | // callTagsAPI is an internal method that is used to call to the tags API. 121 | func (u *API) callTagsAPI(ctx context.Context, command tagCommand, requestParams interface{}, result interface{}) error { 122 | formParams, err := api.StructToParams(requestParams) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | formParams.Add("command", fmt.Sprintf("%v", command)) 128 | 129 | return u.callUploadAPIWithParams(ctx, api.BuildPath(getAssetType(requestParams), tags), formParams, result) 130 | } 131 | -------------------------------------------------------------------------------- /api/uploader/tag_test.go: -------------------------------------------------------------------------------- 1 | package uploader_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cloudinary/cloudinary-go/v2/api" 7 | "github.com/cloudinary/cloudinary-go/v2/api/uploader" 8 | "github.com/cloudinary/cloudinary-go/v2/internal/cldtest" 9 | ) 10 | 11 | func TestUploader_Tag(t *testing.T) { 12 | cldtest.UploadTestAsset(t, cldtest.PublicID) 13 | cldtest.UploadTestAsset(t, cldtest.PublicID2) 14 | 15 | params := uploader.AddTagParams{ 16 | PublicIDs: []string{cldtest.PublicID, cldtest.PublicID2}, 17 | Tag: cldtest.Tag1, 18 | } 19 | 20 | resp, err := uploadAPI.AddTag(ctx, params) 21 | 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | 26 | if resp == nil || len(resp.PublicIDs) != 2 || resp.PublicIDs[0] != cldtest.PublicID { 27 | t.Error(resp) 28 | } 29 | 30 | rParams := uploader.RemoveTagParams{ 31 | PublicIDs: []string{cldtest.PublicID, cldtest.PublicID2}, 32 | Tag: cldtest.Tag1, 33 | } 34 | 35 | rResp, err := uploadAPI.RemoveTag(ctx, rParams) 36 | 37 | if err != nil { 38 | t.Error(err) 39 | } 40 | 41 | if rResp == nil || len(rResp.PublicIDs) != 2 || rResp.PublicIDs[0] != cldtest.PublicID { 42 | t.Error(resp) 43 | } 44 | // FIXME: add some tags :) before removing 45 | raParams := uploader.RemoveAllTagsParams{ 46 | PublicIDs: api.CldAPIArray{cldtest.PublicID, cldtest.PublicID2}, 47 | } 48 | 49 | raResp, err := uploadAPI.RemoveAllTags(ctx, raParams) 50 | 51 | if err != nil { 52 | t.Error(err) 53 | } 54 | 55 | if raResp == nil || len(raResp.PublicIDs) != 2 || raResp.PublicIDs[0] != cldtest.PublicID { 56 | t.Error(resp) 57 | } 58 | 59 | reParams := uploader.ReplaceTagParams{ 60 | PublicIDs: api.CldAPIArray{cldtest.PublicID, cldtest.PublicID2}, 61 | Tag: cldtest.Tag2, 62 | } 63 | 64 | reResp, err := uploadAPI.ReplaceTag(ctx, reParams) 65 | 66 | if err != nil { 67 | t.Error(err) 68 | } 69 | 70 | if reResp == nil || len(reResp.PublicIDs) != 2 || reResp.PublicIDs[0] != cldtest.PublicID { 71 | t.Error(resp) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /asset/analytics.go: -------------------------------------------------------------------------------- 1 | package asset 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/cloudinary/cloudinary-go/v2/api" 7 | "runtime" 8 | "strconv" 9 | "strings" 10 | "sync" 11 | ) 12 | 13 | const queryString = "_a" 14 | const algoVersion = "A" // The version of the algorithm 15 | const sdkCode = "Q" // Cloudinary Go SDK 16 | 17 | var sdkVersion = api.Version 18 | var techVersion = strings.Join(strings.Split(strings.TrimPrefix(runtime.Version(), "go"), ".")[:2], ".") 19 | 20 | const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" 21 | const binaryPadSize = 6 22 | 23 | var charCodes = map[string]string{} 24 | var initialize sync.Once 25 | 26 | var analyticsSignature = "" 27 | 28 | func sdkAnalyticsSignature() string { 29 | if analyticsSignature != "" { 30 | return analyticsSignature 31 | } 32 | sdkVersionStr, err := encodeVersion(sdkVersion) 33 | if err != nil { 34 | analyticsSignature = "E" 35 | 36 | return analyticsSignature 37 | } 38 | 39 | techVersionStr, err := encodeVersion(techVersion) 40 | if err != nil { 41 | analyticsSignature = "E" 42 | 43 | return analyticsSignature 44 | } 45 | 46 | analyticsSignature = fmt.Sprintf("%s%s%s%s", algoVersion, sdkCode, sdkVersionStr, techVersionStr) 47 | 48 | return analyticsSignature 49 | } 50 | 51 | func encodeVersion(version string) (string, error) { 52 | 53 | parts := strings.Split(version, ".") 54 | 55 | paddedParts := make([]string, len(parts)) 56 | for i, v := range parts { 57 | vInt, _ := strconv.Atoi(v) 58 | paddedParts[i] = fmt.Sprintf("%02d", vInt) 59 | } 60 | 61 | // reverse (in this case swap first and last elements) 62 | paddedParts[0], paddedParts[len(paddedParts)-1] = paddedParts[len(paddedParts)-1], paddedParts[0] 63 | 64 | num, _ := strconv.Atoi(strings.Join(paddedParts, "")) 65 | paddedBinary := intToPaddedBin(num, len(parts)*binaryPadSize) 66 | 67 | if len(paddedBinary)%binaryPadSize != 0 { 68 | return "", errors.New("version must be smaller than 43.21.26") 69 | } 70 | 71 | encodedChars := make([]string, len(parts)) 72 | 73 | for i := 0; i < len(parts); i++ { 74 | encodedChars[i] = getKey(paddedBinary[i*binaryPadSize : (i+1)*binaryPadSize]) 75 | } 76 | 77 | return strings.Join(encodedChars, ""), nil 78 | } 79 | 80 | func initializeCharCodes() { 81 | for i, char := range chars { 82 | charCodes[intToPaddedBin(i, binaryPadSize)] = string(char) 83 | } 84 | } 85 | 86 | func getKey(binaryValue string) string { 87 | initialize.Do(initializeCharCodes) 88 | 89 | return charCodes[binaryValue] 90 | } 91 | 92 | func intToPaddedBin(integer int, padNum int) string { 93 | return fmt.Sprintf("%0*b", padNum, integer) 94 | } 95 | -------------------------------------------------------------------------------- /asset/analytics_test.go: -------------------------------------------------------------------------------- 1 | package asset 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAsset_Analytics_EncodeVersion(t *testing.T) { 8 | s, err := encodeVersion("1.24.0") 9 | if err != nil || s != "Alh" { 10 | t.Fatal("Invalid signature") 11 | } 12 | 13 | s, err = encodeVersion("12.0") 14 | if err != nil || s != "AM" { 15 | t.Fatal("Invalid signature") 16 | } 17 | 18 | s, err = encodeVersion("43.21.26") 19 | if err != nil || s != "///" { 20 | t.Fatal("Invalid signature") 21 | } 22 | 23 | s, err = encodeVersion("0.0.0") 24 | if err != nil || s != "AAA" { 25 | t.Fatal("Invalid signature") 26 | } 27 | } 28 | 29 | func TestAsset_Analytics_InvalidVersion(t *testing.T) { 30 | _, err := encodeVersion("44.45.46") 31 | if err == nil || err.Error() != "version must be smaller than 43.21.26" { 32 | t.Fatal("Error expected") 33 | } 34 | } 35 | 36 | func TestAsset_Analytics_Signature(t *testing.T) { 37 | // FIXME: use setUp/tearDown 38 | sdkVersionBackup := sdkVersion 39 | techVersionBackup := techVersion 40 | 41 | sdkVersion = "2.3.4" 42 | techVersion = "8.0" 43 | 44 | analyticsSignature = sdkAnalyticsSignature() 45 | 46 | // FIXME: use setUp/tearDown 47 | sdkVersion = sdkVersionBackup 48 | techVersion = techVersionBackup 49 | 50 | if analyticsSignature != "AQJ1uAI" { 51 | t.Fatal("Invalid signature", analyticsSignature) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /asset/asset_test.go: -------------------------------------------------------------------------------- 1 | package asset_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/cloudinary/cloudinary-go/v2/api" 6 | "github.com/cloudinary/cloudinary-go/v2/asset" 7 | "github.com/cloudinary/cloudinary-go/v2/internal/cldtest" 8 | "github.com/stretchr/testify/assert" 9 | "testing" 10 | ) 11 | 12 | func TestAsset_Signature(t *testing.T) { 13 | i := getTestImage(t) 14 | 15 | i.DeliveryType = api.Authenticated 16 | i.Config.URL.SignURL = true 17 | 18 | assert.Regexp(t, "s--.{8}--", getAssetUrl(t, i)) 19 | } 20 | 21 | func TestAsset_LongURLSignature(t *testing.T) { 22 | i := getTestImage(t) 23 | i.DeliveryType = api.Authenticated 24 | i.Config.URL.SignURL = true 25 | i.Config.URL.LongURLSignature = true 26 | 27 | assert.Regexp(t, "s--.{32}--", getAssetUrl(t, i)) 28 | } 29 | 30 | func TestAsset_WithAuthToken(t *testing.T) { 31 | i, _ := asset.Image(authTokenTestImage, nil) 32 | i.DeliveryType = api.Authenticated 33 | i.Version = 1486020273 34 | 35 | i.Config.URL.SignURL = true 36 | 37 | localTokenConfig := authTokenConfig 38 | i.AuthToken.Config = &localTokenConfig 39 | 40 | i.AuthToken.Config.StartTime = 1111111111 41 | 42 | assert.Contains(t, getAssetUrl(t, i), "1751370bcc6cfe9e03f30dd1a9722ba0f2cdca283fa3e6df3342a00a7528cc51") 43 | 44 | assert.NotContains(t, getAssetUrl(t, i), "s--") // no simple signature 45 | assert.NotContains(t, getAssetUrl(t, i), "_a=") // no analytics 46 | 47 | i.AuthToken.Config.ACL = "" 48 | i.AuthToken.Config.StartTime = startTime 49 | 50 | i.Config.URL.PrivateCDN = true 51 | 52 | assert.Contains(t, getAssetUrl(t, i), "8db0d753ee7bbb9e2eaf8698ca3797436ba4c20e31f44527e43b6a6e995cfdb3") 53 | } 54 | 55 | func TestAsset_ForceVersion(t *testing.T) { 56 | i, err := asset.Image(cldtest.ImageInFolder, nil) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | assert.Contains(t, getAssetUrl(t, i), "v1") 62 | 63 | i.Config.URL.ForceVersion = false 64 | 65 | assert.NotContains(t, getAssetUrl(t, i), "v1") 66 | } 67 | 68 | func TestAsset_Video(t *testing.T) { 69 | v, err := asset.Video(cldtest.VideoPublicID, nil) 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | assert.Contains(t, getAssetUrl(t, v), fmt.Sprintf("video/upload/%s", cldtest.VideoPublicID)) 74 | } 75 | 76 | func TestAsset_VideoSEO(t *testing.T) { 77 | f, err := asset.Video(cldtest.VideoPublicID+cldtest.VideoExt, nil) 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | f.Suffix = "my_favorite_video" 82 | 83 | assert.Contains(t, getAssetUrl(t, f), fmt.Sprintf("videos/%s/%s%s", cldtest.VideoPublicID, f.Suffix, cldtest.VideoExt)) 84 | } 85 | 86 | func TestAsset_File(t *testing.T) { 87 | f, err := asset.File(cldtest.PublicID, nil) 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | assert.Contains(t, getAssetUrl(t, f), fmt.Sprintf("raw/upload/%s", cldtest.PublicID)) 92 | } 93 | 94 | func TestAsset_FileSEO(t *testing.T) { 95 | f, err := asset.File(cldtest.PublicID+cldtest.FileExt, nil) 96 | if err != nil { 97 | t.Fatal(err) 98 | } 99 | f.Suffix = "my_favorite_sample" 100 | 101 | assert.Contains(t, getAssetUrl(t, f), fmt.Sprintf("files/%s/%s%s", cldtest.PublicID, f.Suffix, cldtest.FileExt)) 102 | } 103 | 104 | func TestAsset_Media(t *testing.T) { 105 | m, err := asset.Media(cldtest.PublicID, nil) 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | assert.Contains(t, getAssetUrl(t, m), fmt.Sprintf("image/upload/%s", cldtest.PublicID)) 110 | } 111 | -------------------------------------------------------------------------------- /asset/auth_token.go: -------------------------------------------------------------------------------- 1 | package asset 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "github.com/cloudinary/cloudinary-go/v2/config" 8 | "regexp" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | const unsafeAuthTokenChars = " \"#%&'/:;<=>?@\\[\\]^`{\\|}~\\" 14 | 15 | const authTokenName = "__cld_token__" 16 | const authTokenSeparator = "~" 17 | const authTokenInnerSeparator = "=" 18 | 19 | // AuthToken is the Authentication Token struct. 20 | type AuthToken struct { 21 | Config *config.AuthToken 22 | } 23 | 24 | func (a AuthToken) isEnabled() bool { 25 | return a.Config.Key != "" 26 | } 27 | 28 | // Generate generates the authentication token. 29 | func (a AuthToken) Generate(path string) string { 30 | if !a.isEnabled() { 31 | return "" 32 | } 33 | 34 | start, expiration := a.handleLifetime() 35 | 36 | if path == "" && a.Config.ACL == "" { 37 | panic("AuthToken must contain either ACL or URL property") 38 | } 39 | 40 | var tokenParts []interface{} 41 | 42 | if a.Config.IP != "" { 43 | tokenParts = append(tokenParts, "ip="+a.Config.IP) 44 | } 45 | if start != 0 { 46 | tokenParts = append(tokenParts, "st="+strconv.FormatInt(start, 10)) 47 | } 48 | if expiration != 0 { 49 | tokenParts = append(tokenParts, "exp="+strconv.FormatInt(expiration, 10)) 50 | } 51 | if a.Config.ACL != "" { 52 | tokenParts = append(tokenParts, "acl="+escapeToLower(a.Config.ACL)) 53 | } 54 | 55 | toSign := tokenParts 56 | 57 | if path != "" && a.Config.ACL == "" { 58 | toSign = append(tokenParts, "url="+escapeToLower(path)) 59 | } 60 | 61 | auth := a.digest(joinNonEmpty(toSign, authTokenSeparator)) 62 | 63 | tokenParts = append(tokenParts, "hmac="+auth) 64 | 65 | return joinNonEmpty([]interface{}{authTokenName, joinNonEmpty(tokenParts, authTokenSeparator)}, authTokenInnerSeparator) 66 | } 67 | 68 | func (a AuthToken) handleLifetime() (int64, int64) { 69 | expiration := a.Config.Expiration 70 | 71 | if expiration == 0 { 72 | if a.Config.Duration != 0 { 73 | start := a.Config.StartTime 74 | if start == 0 { 75 | start = time.Now().Unix() 76 | } 77 | expiration = start + a.Config.Duration 78 | } else { 79 | panic("must provide Expiration or Duration") 80 | } 81 | } 82 | 83 | return a.Config.StartTime, expiration 84 | } 85 | 86 | func escapeToLower(str string) string { 87 | re := regexp.MustCompile("([" + regexp.QuoteMeta(unsafeAuthTokenChars) + "])") 88 | 89 | return re.ReplaceAllStringFunc(str, func(s string) string { 90 | return "%" + hex.EncodeToString([]byte(s)) 91 | }) 92 | } 93 | 94 | func (a AuthToken) digest(message string) string { 95 | key, err := hex.DecodeString(a.Config.Key) // H* +`? 96 | if err != nil { 97 | panic(err.Error()) 98 | } 99 | h := hmac.New(sha256.New, key) 100 | h.Write([]byte(message)) 101 | 102 | return hex.EncodeToString(h.Sum(nil)) 103 | } 104 | -------------------------------------------------------------------------------- /asset/auth_token_test.go: -------------------------------------------------------------------------------- 1 | package asset_test 2 | 3 | import ( 4 | "github.com/cloudinary/cloudinary-go/v2/asset" 5 | "github.com/cloudinary/cloudinary-go/v2/config" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | const authTokenKey = "00112233FF99" 11 | const authTokenAltKey = "CCBB2233FF00" 12 | 13 | const duration = 300 14 | const startTime = 11111111 15 | 16 | const authTokenTestImage = "sample.jpg" 17 | const authTokenTestConfigACL = "/*/t_foobar" 18 | const authTokenTestPath = "http://res.cloudinary.com/test123/image/upload/v1486020273/sample.jpg" 19 | 20 | var authTokenConfig = config.AuthToken{ 21 | Duration: duration, 22 | StartTime: startTime, 23 | Key: authTokenKey, 24 | ACL: "/image/*", 25 | } 26 | 27 | func TestAsset_AuthToken_GenerateWithStartTimeAndDuration(t *testing.T) { 28 | newConfig := authTokenConfig 29 | a := asset.AuthToken{Config: &newConfig} 30 | a.Config.StartTime = 1111111111 31 | 32 | expectedToken := "__cld_token__=st=1111111111~exp=1111111411~acl=%2fimage%2f*" + 33 | "~hmac=1751370bcc6cfe9e03f30dd1a9722ba0f2cdca283fa3e6df3342a00a7528cc51" 34 | 35 | assert.Equal(t, a.Generate(""), expectedToken) 36 | } 37 | 38 | func TestAsset_AuthToken_MustProvideExpirationOrDuration(t *testing.T) { 39 | a := asset.AuthToken{Config: &config.AuthToken{Key: authTokenKey}} 40 | 41 | assert.Panics(t, func() { a.Generate("") }) 42 | } 43 | 44 | func TestAsset_AuthToken_NoStartTimeRequired(t *testing.T) { 45 | a := asset.AuthToken{Config: &config.AuthToken{Key: authTokenKey, Expiration: startTime + duration}} 46 | 47 | expectedToken := "__cld_token__=exp=11111411~hmac=470d32e3ee9b872d64bd00d974c559d96892398c8542ff33ce2f2647ee1bf7a4" 48 | assert.Equal(t, expectedToken, a.Generate(authTokenTestImage)) 49 | } 50 | 51 | func TestAsset_AuthToken_ShouldIgnoreUrlIfAclIsProvided(t *testing.T) { 52 | a := asset.AuthToken{Config: &authTokenConfig} 53 | aclToken := a.Generate("") 54 | aclTokenUrlIgnored := a.Generate(authTokenTestImage) 55 | 56 | a.Config.ACL = "" 57 | 58 | urlToken := a.Generate(authTokenTestImage) 59 | 60 | assert.NotEqual(t, aclToken, urlToken) 61 | assert.Equal(t, aclToken, aclTokenUrlIgnored) 62 | } 63 | 64 | func TestAsset_AuthToken_EscapeToLower(t *testing.T) { 65 | a := asset.AuthToken{Config: &authTokenConfig} 66 | a.Config.ACL = "" 67 | 68 | expected := "__cld_token__=st=11111111~exp=11111411~hmac=7ffc0fd1f3ee2622082689f64a65454da39d94c297bcf498b682aa65a0d2ce0a" 69 | 70 | assert.Equal(t, expected, a.Generate("Encode these :~@#%^&{}[]\\\"';/\", but not those $!()_.*")) 71 | } 72 | -------------------------------------------------------------------------------- /asset/distribution_test.go: -------------------------------------------------------------------------------- 1 | package asset_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/cloudinary/cloudinary-go/v2/asset" 6 | "github.com/cloudinary/cloudinary-go/v2/config" 7 | "github.com/cloudinary/cloudinary-go/v2/internal/cldtest" 8 | "github.com/stretchr/testify/assert" 9 | "testing" 10 | ) 11 | 12 | func TestAsset_SecureDistribution(t *testing.T) { 13 | i := getTestImage(t) 14 | 15 | assertURLProtocol(t, i, "https") 16 | 17 | i.Config.URL.Secure = false 18 | 19 | assertURLProtocol(t, i, "http") 20 | } 21 | 22 | func TestAsset_SecureDistributionFromConfig(t *testing.T) { 23 | i, err := asset.Image(cldtest.PublicID, &config.Configuration{URL: config.URL{Secure: false}}) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | assertURLProtocol(t, i, "http") 29 | } 30 | 31 | func TestAsset_SecureCNameOverwrite(t *testing.T) { 32 | host := "something.else.com" 33 | 34 | i := getTestImage(t) 35 | 36 | i.Config.URL.SecureCName = host 37 | 38 | assert.Contains(t, getAssetUrl(t, i), host) 39 | } 40 | 41 | func TestAsset_SecureAkamai(t *testing.T) { 42 | i := getTestImage(t) 43 | i.Config.URL.PrivateCDN = true 44 | 45 | assert.Contains(t, getAssetUrl(t, i), fmt.Sprintf("%s-%s", i.Config.Cloud.CloudName, i.Config.URL.SharedHost)) 46 | assert.NotContains(t, getAssetUrl(t, i), fmt.Sprintf("/%s/", i.Config.Cloud.CloudName)) 47 | } 48 | 49 | func TestAsset_SecureNonAkamai(t *testing.T) { 50 | host := "something.cloudfront.net" 51 | 52 | i := getTestImage(t) 53 | i.Config.URL.PrivateCDN = true 54 | i.Config.URL.SecureCName = host 55 | 56 | assert.Contains(t, getAssetUrl(t, i), fmt.Sprintf("%s://%s/image/upload/", i.Config.URL.Protocol(), host)) 57 | } 58 | 59 | func TestAsset_HTTPPrivateCDN(t *testing.T) { 60 | i := getTestImage(t) 61 | i.Config.URL.PrivateCDN = true 62 | i.Config.URL.Secure = false 63 | 64 | assert.Contains(t, getAssetUrl(t, i), fmt.Sprintf("%s-%s", i.Config.Cloud.CloudName, i.Config.URL.SharedHost)) 65 | } 66 | 67 | func TestAsset_SecureSharedSubDomain(t *testing.T) { 68 | i := getTestImage(t) 69 | i.Config.URL.CDNSubDomain = true 70 | 71 | assert.Contains(t, getAssetUrl(t, i), fmt.Sprintf("%s-3.%s", i.Config.URL.SubDomain, i.Config.URL.Domain)) 72 | } 73 | 74 | func TestAsset_SecureSubDomainTrue(t *testing.T) { 75 | i := getTestImage(t) 76 | i.Config.URL.CDNSubDomain = true 77 | i.Config.URL.SecureCDNSubDomain = true 78 | i.Config.URL.PrivateCDN = true 79 | 80 | assert.Contains(t, getAssetUrl(t, i), fmt.Sprintf("%s-%s-3.%s", i.Config.Cloud.CloudName, i.Config.URL.SubDomain, i.Config.URL.Domain)) 81 | } 82 | 83 | func TestAsset_CName(t *testing.T) { 84 | host := "hello.com" 85 | 86 | i := getTestImage(t) 87 | i.Config.URL.Secure = false 88 | i.Config.URL.CName = host 89 | 90 | assert.Contains(t, getAssetUrl(t, i), fmt.Sprintf("%s://%s/%s/", i.Config.URL.Protocol(), host, i.Config.Cloud.CloudName)) 91 | } 92 | 93 | func TestAsset_CNameSubDomain(t *testing.T) { 94 | host := "hello.com" 95 | 96 | i := getTestImage(t) 97 | i.Config.URL.Secure = false 98 | i.Config.URL.CName = host 99 | i.Config.URL.CDNSubDomain = true 100 | 101 | assert.Contains(t, getAssetUrl(t, i), fmt.Sprintf("a3.%s/%s/", host, i.Config.Cloud.CloudName)) 102 | } 103 | 104 | func assertURLProtocol(t *testing.T, a *asset.Asset, protocol string) { 105 | url := getAssetUrl(t, a) 106 | 107 | assert.Regexp(t, fmt.Sprintf("^%s://", protocol), url) 108 | } 109 | 110 | type AssetInterface interface { 111 | String() (string, error) 112 | } 113 | 114 | func getAssetUrl(t *testing.T, a AssetInterface) string { 115 | url, err := a.String() 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | 120 | return url 121 | } 122 | 123 | func getTestImage(t *testing.T) *asset.Asset { 124 | i, err := asset.Image(cldtest.PublicID, nil) 125 | if err != nil { 126 | t.Fatal(err) 127 | } 128 | 129 | return i 130 | } 131 | -------------------------------------------------------------------------------- /asset/image_test.go: -------------------------------------------------------------------------------- 1 | package asset_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/cloudinary/cloudinary-go/v2/api" 6 | "github.com/cloudinary/cloudinary-go/v2/asset" 7 | "github.com/cloudinary/cloudinary-go/v2/internal/cldtest" 8 | "github.com/stretchr/testify/assert" 9 | "testing" 10 | ) 11 | 12 | func TestAsset_SimpleImage(t *testing.T) { 13 | i := getTestImage(t) 14 | 15 | assert.Contains(t, getAssetUrl(t, i), fmt.Sprintf("%s://%s/%s/image/upload/%s", i.Config.URL.Protocol(), i.Config.URL.SharedHost, i.Config.Cloud.CloudName, cldtest.PublicID)) 16 | } 17 | 18 | func TestAsset_FetchImage(t *testing.T) { 19 | i, err := asset.Image(cldtest.LogoURL, nil) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | i.DeliveryType = api.Fetch 25 | 26 | assert.Contains(t, getAssetUrl(t, i), fmt.Sprintf("image/fetch/%s", cldtest.LogoURL)) 27 | } 28 | 29 | func TestAsset_ImageSEO(t *testing.T) { 30 | i, err := asset.Image(cldtest.PublicID+cldtest.ImgExt, nil) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | 35 | i.Suffix = cldtest.SEOName 36 | 37 | assert.Contains(t, getAssetUrl(t, i), fmt.Sprintf("images/%s/%s", cldtest.PublicID, cldtest.SEOName+cldtest.ImgExt)) 38 | 39 | i.Config.URL.Shorten = true 40 | 41 | assert.Contains(t, getAssetUrl(t, i), fmt.Sprintf("iu/%s/%s", cldtest.PublicID, cldtest.SEOName+cldtest.ImgExt)) 42 | 43 | i.Config.URL.UseRootPath = true 44 | 45 | assert.Contains(t, getAssetUrl(t, i), fmt.Sprintf("%s/%s/%s", i.Config.Cloud.CloudName, cldtest.PublicID, cldtest.SEOName+cldtest.ImgExt)) 46 | 47 | i.PublicID = cldtest.PublicID 48 | 49 | assert.Contains(t, getAssetUrl(t, i), fmt.Sprintf("%s/%s/%s", i.Config.Cloud.CloudName, cldtest.PublicID, cldtest.SEOName)) 50 | } 51 | 52 | func TestAsset_ImageSuffixForNotSupportedDeliveryType(t *testing.T) { 53 | i := getTestImage(t) 54 | 55 | i.Suffix = cldtest.SEOName 56 | i.DeliveryType = api.Public 57 | 58 | _, err := i.String() 59 | 60 | if err == nil || err.Error() != "failed to build URL: URL Suffix is not supported for image/public" { 61 | t.Fatal(err) 62 | } 63 | } 64 | 65 | func TestAsset_ImageTransformation(t *testing.T) { 66 | i := getTestImage(t) 67 | 68 | i.Transformation = cldtest.Transformation 69 | 70 | assert.Contains(t, getAssetUrl(t, i), fmt.Sprintf("image/upload/%s/%s", cldtest.Transformation, cldtest.PublicID)) 71 | } 72 | 73 | func TestAsset_ImageAnalytics(t *testing.T) { 74 | i := getTestImage(t) 75 | 76 | assert.Contains(t, getAssetUrl(t, i), "?_a=") 77 | 78 | i.Config.URL.Analytics = false 79 | 80 | assert.NotContains(t, getAssetUrl(t, i), "?_a=") 81 | } 82 | -------------------------------------------------------------------------------- /asset/search_url_asset.go: -------------------------------------------------------------------------------- 1 | package asset 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/hex" 6 | "errors" 7 | "fmt" 8 | "github.com/cloudinary/cloudinary-go/v2/api" 9 | "github.com/cloudinary/cloudinary-go/v2/api/admin/search" 10 | "github.com/cloudinary/cloudinary-go/v2/config" 11 | "github.com/cloudinary/cloudinary-go/v2/internal/signature" 12 | "github.com/cloudinary/cloudinary-go/v2/logger" 13 | "strconv" 14 | ) 15 | 16 | const searchAssetType = "search" 17 | const searchAssetURLTTL = 300 18 | 19 | // SearchURLAsset is the SearchURLAsset struct. 20 | type SearchURLAsset struct { 21 | SearchQuery search.Query 22 | TTL int 23 | NextCursor string 24 | Config config.Configuration 25 | logger logger.Logger 26 | } 27 | 28 | // SearchURL returns a new SearchURLAsset instance from the provided query and configuration. 29 | func SearchURL(query search.Query, conf *config.Configuration) (*SearchURLAsset, error) { 30 | if conf == nil { 31 | var err error 32 | conf, err = config.New() 33 | if err != nil { 34 | return nil, err 35 | } 36 | } 37 | 38 | asset := SearchURLAsset{SearchQuery: query, Config: *conf} 39 | asset.setDefaults() 40 | 41 | return &asset, nil 42 | } 43 | 44 | // setDefaults sets the default values. 45 | func (sa *SearchURLAsset) setDefaults() { 46 | sa.TTL = searchAssetURLTTL 47 | } 48 | 49 | // String serializes SearchURLAsset to string. 50 | func (sa SearchURLAsset) String() (result string, err error) { 51 | return sa.ToURL(0, "") 52 | } 53 | 54 | // ToURLWithNextCursor serializes SearchURLAsset to string. 55 | func (sa SearchURLAsset) ToURLWithNextCursor(nextCursor string) (result string, err error) { 56 | return sa.ToURL(0, nextCursor) 57 | } 58 | 59 | // ToURL serializes SearchURLAsset to string. 60 | func (sa SearchURLAsset) ToURL(ttl int, nextCursor string) (result string, err error) { 61 | defer func() { 62 | if r := recover(); r != nil { 63 | msg := fmt.Sprintf("failed to build URL: %v", r) 64 | sa.logger.Error(msg) 65 | result = "" 66 | err = errors.New(msg) 67 | } 68 | }() 69 | 70 | path, err := sa.path(ttl, nextCursor) 71 | if err != nil { 72 | return "", err 73 | } 74 | 75 | assetURL := joinURL([]interface{}{distribution("", sa.Config), path}) 76 | 77 | query := sa.query() 78 | 79 | return joinNonEmpty([]interface{}{assetURL, query}, "?"), nil 80 | } 81 | 82 | // path builds the URL path. 83 | func (sa SearchURLAsset) path(ttl int, nextCursor string) (result string, err error) { 84 | if ttl == 0 { 85 | ttl = sa.TTL 86 | } 87 | 88 | if nextCursor == "" { 89 | nextCursor = sa.SearchQuery.NextCursor 90 | } 91 | 92 | b64Query, err := sa.b64SearchQuery() 93 | if err != nil { 94 | return "", err 95 | } 96 | 97 | toSign := strconv.Itoa(ttl) + b64Query 98 | sig, err := sa.signature(toSign) 99 | if err != nil { 100 | return "", err 101 | } 102 | 103 | return joinURL([]interface{}{searchAssetType, sig, ttl, b64Query, nextCursor}), nil 104 | } 105 | 106 | // query builds the URL query string. 107 | func (sa SearchURLAsset) query() string { 108 | if !sa.Config.URL.Analytics { 109 | return "" 110 | } 111 | 112 | return fmt.Sprintf("%s=%s", queryString, sdkAnalyticsSignature()) 113 | } 114 | 115 | // signature builds the signature. 116 | func (sa SearchURLAsset) signature(toSign string) (result string, err error) { 117 | rawSignature, err := signature.Sign(toSign, sa.Config.Cloud.APISecret, signature.SHA256) 118 | if err != nil { 119 | return "", err 120 | } 121 | return hex.EncodeToString(rawSignature), nil 122 | } 123 | 124 | // b64SearchQuery encodes the search query using base64 encoding. 125 | func (sa SearchURLAsset) b64SearchQuery() (result string, err error) { 126 | query := sa.SearchQuery 127 | query.NextCursor = "" // drop next_cursor 128 | jsonBytes, err := api.MarshalJSONRaw(query) 129 | if err != nil { 130 | return "", err 131 | } 132 | 133 | jsonBytes, err = api.ReMarshalJSON(jsonBytes) 134 | if err != nil { 135 | return "", err 136 | } 137 | return base64.StdEncoding.EncodeToString(jsonBytes), nil 138 | } 139 | -------------------------------------------------------------------------------- /asset/search_url_asset_test.go: -------------------------------------------------------------------------------- 1 | package asset_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/cloudinary/cloudinary-go/v2" 6 | "github.com/cloudinary/cloudinary-go/v2/api/admin/search" 7 | "github.com/cloudinary/cloudinary-go/v2/asset" 8 | "github.com/cloudinary/cloudinary-go/v2/internal/cldtest" 9 | "github.com/stretchr/testify/assert" 10 | "testing" 11 | ) 12 | 13 | var query = search.Query{ 14 | Expression: "resource_type:image AND tags=kitten AND uploaded_at>1d AND bytes>1m", 15 | SortBy: []search.SortByField{{"public_id": search.Descending}}, 16 | MaxResults: 30, 17 | } 18 | var b64Query = "eyJleHByZXNzaW9uIjoicmVzb3VyY2VfdHlwZTppbWFnZSBBTkQgdGFncz1raXR0ZW4gQU5EIHVwbG9hZGVkX2F0" + 19 | "PjFkIEFORCBieXRlcz4xbSIsIm1heF9yZXN1bHRzIjozMCwic29ydF9ieSI6W3sicHVibGljX2lkIjoiZGVzYyJ9XX0=" 20 | 21 | var ttl300Sig = "431454b74cefa342e2f03e2d589b2e901babb8db6e6b149abf25bc0dd7ab20b7" 22 | var ttl1000Sig = "25b91426a37d4f633a9b34383c63889ff8952e7ffecef29a17d600eeb3db0db7" 23 | 24 | var c, _ = cloudinary.NewFromURL(cldtest.CldURL) 25 | 26 | var searchEndpoint = fmt.Sprintf("%s://%s/%s/search", c.Config.URL.Protocol(), c.Config.URL.SharedHost, c.Config.Cloud.CloudName) 27 | 28 | func getSearchURL(t *testing.T) *asset.SearchURLAsset { 29 | su, err := c.SearchURL(query) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | return su 35 | } 36 | func TestSearchURL_Default(t *testing.T) { 37 | su := getSearchURL(t) 38 | 39 | assert.Contains(t, getAssetUrl(t, su), fmt.Sprintf("%s/%s/%d/%s", searchEndpoint, ttl300Sig, 300, b64Query)) 40 | } 41 | 42 | func TestSearchURL_WithNextCursor(t *testing.T) { 43 | su := getSearchURL(t) 44 | url, err := su.ToURLWithNextCursor(cldtest.NextCursor) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | assert.Contains(t, url, fmt.Sprintf("%s/%s/%d/%s/%s", searchEndpoint, ttl300Sig, 300, b64Query, cldtest.NextCursor)) 50 | } 51 | 52 | func TestSearchURL_WithCustomTTLAndNextCursor(t *testing.T) { 53 | su := getSearchURL(t) 54 | url, err := su.ToURL(1000, cldtest.NextCursor) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | 59 | assert.Contains(t, url, fmt.Sprintf("%s/%s/%d/%s/%s", searchEndpoint, ttl1000Sig, 1000, b64Query, cldtest.NextCursor)) 60 | } 61 | 62 | func TestSearchURL_TTLAndNextCursorSetOnStruct(t *testing.T) { 63 | su := getSearchURL(t) 64 | su.TTL = 1000 65 | su.SearchQuery.NextCursor = cldtest.NextCursor 66 | 67 | assert.Contains(t, getAssetUrl(t, su), fmt.Sprintf("%s/%s/%d/%s/%s", searchEndpoint, ttl1000Sig, 1000, b64Query, cldtest.NextCursor)) 68 | } 69 | 70 | func TestSearchURL_PrivateCDN(t *testing.T) { 71 | su := getSearchURL(t) 72 | su.Config.URL.PrivateCDN = true 73 | 74 | privateSearchEndpoint := fmt.Sprintf("%s://%s-%s/search", c.Config.URL.Protocol(), c.Config.Cloud.CloudName, c.Config.URL.SharedHost) 75 | 76 | assert.Contains(t, getAssetUrl(t, su), fmt.Sprintf("%s/%s/%d/%s", privateSearchEndpoint, ttl300Sig, 300, b64Query)) 77 | } 78 | 79 | func TestSearchURL_NoAnalytics(t *testing.T) { 80 | su := getSearchURL(t) 81 | 82 | assert.Contains(t, getAssetUrl(t, su), fmt.Sprintf("%s/%s/%d/%s", searchEndpoint, ttl300Sig, 300, b64Query)) 83 | assert.Contains(t, getAssetUrl(t, su), "?_a=") 84 | 85 | su.Config.URL.Analytics = false 86 | 87 | assert.Contains(t, getAssetUrl(t, su), fmt.Sprintf("%s/%s/%d/%s", searchEndpoint, ttl300Sig, 300, b64Query)) 88 | assert.NotContains(t, getAssetUrl(t, su), "?_a=") 89 | } 90 | -------------------------------------------------------------------------------- /cloudinary.go: -------------------------------------------------------------------------------- 1 | package cloudinary 2 | 3 | import ( 4 | "github.com/cloudinary/cloudinary-go/v2/api/admin" 5 | "github.com/cloudinary/cloudinary-go/v2/api/admin/search" 6 | "github.com/cloudinary/cloudinary-go/v2/api/uploader" 7 | "github.com/cloudinary/cloudinary-go/v2/asset" 8 | "github.com/cloudinary/cloudinary-go/v2/config" 9 | "github.com/cloudinary/cloudinary-go/v2/logger" 10 | ) 11 | 12 | // Cloudinary main struct 13 | type Cloudinary struct { 14 | Config config.Configuration 15 | Admin admin.API 16 | Upload uploader.API 17 | Logger *logger.Logger 18 | } 19 | 20 | // New returns a new Cloudinary instance from environment variable. 21 | func New() (*Cloudinary, error) { 22 | c, err := config.New() 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | return NewFromConfiguration(*c) 28 | } 29 | 30 | // NewFromURL returns a new Cloudinary instance from a cloudinary url. 31 | func NewFromURL(cloudinaryURL string) (*Cloudinary, error) { 32 | c, err := config.NewFromURL(cloudinaryURL) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return NewFromConfiguration(*c) 37 | } 38 | 39 | // NewFromParams returns a new Cloudinary instance from the provided parameters. 40 | func NewFromParams(cloud string, key string, secret string) (*Cloudinary, error) { 41 | c, err := config.NewFromParams(cloud, key, secret) 42 | if err != nil { 43 | return nil, err 44 | } 45 | return NewFromConfiguration(*c) 46 | } 47 | 48 | // NewFromOAuthToken returns a new Cloudinary instance from the provided cloud name and OAuth token. 49 | func NewFromOAuthToken(cloud string, oAuthToken string) (*Cloudinary, error) { 50 | c, err := config.NewFromOAuthToken(cloud, oAuthToken) 51 | if err != nil { 52 | return nil, err 53 | } 54 | return NewFromConfiguration(*c) 55 | } 56 | 57 | // NewFromConfiguration returns a new Cloudinary instance from the provided configuration. 58 | func NewFromConfiguration(configuration config.Configuration) (*Cloudinary, error) { 59 | log := logger.New() 60 | 61 | return &Cloudinary{ 62 | Config: configuration, 63 | Admin: admin.API{ 64 | Config: configuration, 65 | Logger: log, 66 | }, 67 | Upload: uploader.API{ 68 | Config: configuration, 69 | Logger: log, 70 | }, 71 | Logger: log, 72 | }, nil 73 | } 74 | 75 | // Image creates a new asset.Image instance. 76 | func (c Cloudinary) Image(publicID string) (*asset.Asset, error) { 77 | return asset.Image(publicID, &c.Config) 78 | } 79 | 80 | // Video creates a new asset.Video instance. 81 | func (c Cloudinary) Video(publicID string) (*asset.Asset, error) { 82 | return asset.Video(publicID, &c.Config) 83 | } 84 | 85 | // File creates a new asset.File instance. 86 | func (c Cloudinary) File(publicID string) (*asset.Asset, error) { 87 | return asset.File(publicID, &c.Config) 88 | } 89 | 90 | // Media creates a new asset.Media instance. 91 | func (c Cloudinary) Media(publicID string) (*asset.Asset, error) { 92 | return asset.Media(publicID, &c.Config) 93 | } 94 | 95 | // SearchURL creates a new asset.SearchURL instance. 96 | func (c Cloudinary) SearchURL(query search.Query) (*asset.SearchURLAsset, error) { 97 | return asset.SearchURL(query, &c.Config) 98 | } 99 | -------------------------------------------------------------------------------- /cloudinary_test.go: -------------------------------------------------------------------------------- 1 | package cloudinary 2 | 3 | import ( 4 | "context" 5 | "github.com/cloudinary/cloudinary-go/v2/api" 6 | "github.com/cloudinary/cloudinary-go/v2/internal/cldtest" 7 | "log" 8 | "testing" 9 | 10 | "github.com/cloudinary/cloudinary-go/v2/api/uploader" 11 | ) 12 | 13 | var c, _ = New() 14 | var ctx = context.Background() 15 | 16 | func TestCloudinary_CreateInstance(t *testing.T) { 17 | c, _ := New() 18 | 19 | if c.Config.Cloud.CloudName == "" { 20 | t.Error("Please set up CLOUDINARY_URL environment variable to run the test.") 21 | } 22 | 23 | c, _ = NewFromURL(cldtest.CldURL) 24 | 25 | if c.Config.Cloud.CloudName != cldtest.CloudName { 26 | t.Error("Failed creating Cloudinary instance from Cloudinary URL.") 27 | } 28 | 29 | c, err := NewFromURL("") 30 | if err == nil || err.Error() != "must provide CLOUDINARY_URL" { 31 | t.Error("Error expected, got: ", err) 32 | } 33 | 34 | c, _ = NewFromParams(cldtest.CloudName, cldtest.APIKey, cldtest.APISecret) 35 | 36 | if c.Config.Cloud.CloudName != cldtest.CloudName { 37 | t.Error("Failed creating Cloudinary instance from parameters.") 38 | } 39 | 40 | c, _ = NewFromOAuthToken(cldtest.CloudName, "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4") 41 | 42 | if c.Config.Cloud.CloudName != cldtest.CloudName { 43 | t.Error("Failed creating Cloudinary instance from OAuth token.") 44 | } 45 | } 46 | 47 | func TestCloudinary_Upload(t *testing.T) { 48 | params := uploader.UploadParams{ 49 | PublicID: "test_image", 50 | Eager: "w_500,h_500", 51 | UniqueFilename: api.Bool(false), 52 | UseFilename: api.Bool(true), 53 | Overwrite: api.Bool(true), 54 | } 55 | 56 | resp, err := c.Upload.Upload(ctx, cldtest.LogoURL, params) 57 | 58 | if err != nil { 59 | t.Error("Uploader failed: ", err) 60 | } 61 | 62 | if resp == nil || resp.PublicID != "test_image" { 63 | t.Error("Uploader failed: ", resp) 64 | } 65 | } 66 | 67 | func TestCloudinary_Admin(t *testing.T) { 68 | resp, err := c.Admin.Ping(ctx) 69 | 70 | if err != nil { 71 | t.Error("Admin API failed: ", err) 72 | } 73 | 74 | if resp == nil || resp.Status != "ok" { 75 | t.Error("Admin API failed: ", resp) 76 | } 77 | } 78 | 79 | func TestCloudinary_Asset(t *testing.T) { 80 | c.Config.URL.Secure = true 81 | c.Config.URL.SecureCDNSubDomain = true 82 | 83 | i, err := c.Image(cldtest.PublicID) 84 | if err != nil { 85 | t.Fatal(err) 86 | } 87 | i.DeliveryType = "fetch" 88 | i.Version = 123 89 | log.Println(i.String()) 90 | 91 | v, err := c.Video(cldtest.VideoPublicID) 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | log.Println(v.String()) 96 | 97 | f, err := c.File("sample_file") 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | log.Println(f.String()) 102 | 103 | m, err := c.Media(cldtest.PublicID) 104 | if err != nil { 105 | t.Fatal(err) 106 | } 107 | log.Println(m.String()) 108 | } 109 | -------------------------------------------------------------------------------- /config/api.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // API defines the configuration for making requests to the Cloudinary API. 4 | type API struct { 5 | UploadPrefix string `schema:"upload_prefix" default:"https://api.cloudinary.com"` 6 | Timeout int64 `schema:"timeout" default:"60"` // seconds 7 | UploadTimeout int64 `schema:"upload_timeout"` 8 | ChunkSize int64 `schema:"chunk_size" default:"20000000"` // bytes 9 | } 10 | -------------------------------------------------------------------------------- /config/auth_token.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // AuthToken defines the configuration for delivering token-based authenticated media assets. 4 | // 5 | // https://cloudinary.com/documentation/control_access_to_media#delivering_token_based_authenticated_media_assets 6 | type AuthToken struct { 7 | Key string `schema:"key"` 8 | IP string `schema:"ip"` 9 | ACL string `schema:"acl"` 10 | StartTime int64 `schema:"start_time"` 11 | Expiration int64 `schema:"expiration"` 12 | Duration int64 `schema:"duration"` 13 | } 14 | -------------------------------------------------------------------------------- /config/cloud.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/cloudinary/cloudinary-go/v2/internal/signature" 4 | 5 | // Cloud defines the cloud configuration required to connect your application to Cloudinary. 6 | // 7 | // https://cloudinary.com/documentation/how_to_integrate_cloudinary#get_familiar_with_the_cloudinary_console 8 | type Cloud struct { 9 | CloudName string `schema:"-"` 10 | APIKey string `schema:"-"` 11 | APISecret string `schema:"-"` 12 | OAuthToken string `schema:"oauth_token"` 13 | SignatureAlgorithm string `schema:"signature_algorithm"` 14 | } 15 | 16 | // GetSignatureAlgorithm returns the signature algorithm. 17 | func (c Cloud) GetSignatureAlgorithm() string { 18 | if c.SignatureAlgorithm == "" { 19 | return signature.SHA1 20 | } 21 | 22 | return c.SignatureAlgorithm 23 | } 24 | -------------------------------------------------------------------------------- /config/configuration.go: -------------------------------------------------------------------------------- 1 | // Package config defines the Cloudinary configuration. 2 | package config 3 | 4 | import ( 5 | "errors" 6 | "net/url" 7 | "os" 8 | 9 | "github.com/creasty/defaults" 10 | "github.com/gorilla/schema" 11 | ) 12 | 13 | // Configuration is the main configuration struct. 14 | type Configuration struct { 15 | Cloud Cloud 16 | API API 17 | URL URL 18 | AuthToken AuthToken 19 | } 20 | 21 | var decoder = schema.NewDecoder() 22 | 23 | // New returns a new Configuration instance from the environment variable 24 | func New() (*Configuration, error) { 25 | return NewFromURL(os.Getenv("CLOUDINARY_URL")) 26 | } 27 | 28 | // NewFromURL returns a new Configuration instance from a cloudinary url. 29 | func NewFromURL(cldURLStr string) (*Configuration, error) { 30 | if cldURLStr == "" { 31 | return nil, errors.New("must provide CLOUDINARY_URL") 32 | } 33 | 34 | cldURL, err := url.Parse(cldURLStr) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | pass, _ := cldURL.User.Password() 40 | params := cldURL.Query() 41 | conf, err := NewFromQueryParams(cldURL.Host, cldURL.User.Username(), pass, params) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return conf, err 47 | } 48 | 49 | // NewFromParams returns a new Configuration instance from the provided parameters. 50 | func NewFromParams(cloud string, key string, secret string) (*Configuration, error) { 51 | return NewFromQueryParams(cloud, key, secret, map[string][]string{}) 52 | } 53 | 54 | // NewFromOAuthToken returns a new Configuration instance from the provided cloud name and OAuth token. 55 | func NewFromOAuthToken(cloud string, oAuthToken string) (*Configuration, error) { 56 | return NewFromQueryParams(cloud, "", "", map[string][]string{"oauth_token": {oAuthToken}}) 57 | } 58 | 59 | // NewFromQueryParams returns a new Configuration instance from the provided url query parameters. 60 | func NewFromQueryParams(cloud string, key string, secret string, params map[string][]string) (*Configuration, error) { 61 | cloudConf := Cloud{ 62 | CloudName: cloud, 63 | APIKey: key, 64 | APISecret: secret, 65 | } 66 | 67 | conf := &Configuration{ 68 | Cloud: cloudConf, 69 | API: API{}, 70 | URL: URL{}, 71 | AuthToken: AuthToken{}, 72 | } 73 | 74 | if err := defaults.Set(conf); err != nil { 75 | return nil, err 76 | } 77 | 78 | // import configuration keys from parameters 79 | 80 | decoder.IgnoreUnknownKeys(true) 81 | 82 | err := decoder.Decode(&conf.Cloud, params) 83 | if err != nil { 84 | return nil, err 85 | } 86 | err = decoder.Decode(&conf.API, params) 87 | if err != nil { 88 | return nil, err 89 | } 90 | err = decoder.Decode(&conf.URL, params) 91 | if err != nil { 92 | return nil, err 93 | } 94 | err = decoder.Decode(&conf.AuthToken, params) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | return conf, nil 100 | } 101 | -------------------------------------------------------------------------------- /config/configuration_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "github.com/cloudinary/cloudinary-go/v2/internal/cldtest" 5 | "testing" 6 | 7 | "github.com/cloudinary/cloudinary-go/v2/config" 8 | "github.com/cloudinary/cloudinary-go/v2/internal/signature" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | 13 | var fakeOAuthToken = "MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4" 14 | 15 | func TestConfiguration_CreateInstance(t *testing.T) { 16 | c, _ := config.New() 17 | 18 | if c.Cloud.CloudName == "" { 19 | t.Error("Please set up CLOUDINARY_URL environment variable to run the test.") 20 | } 21 | 22 | c, err := config.NewFromURL(cldtest.CldURL + "?signature_algorithm=sha256") 23 | if err != nil { 24 | t.Error("Error: ", err) 25 | } 26 | 27 | assert.Equal(t, cldtest.CloudName, c.Cloud.CloudName) 28 | assert.Equal(t, signature.SHA256, c.Cloud.SignatureAlgorithm) 29 | 30 | c, err = config.NewFromURL("") 31 | if err == nil || err.Error() != "must provide CLOUDINARY_URL" { 32 | t.Error("Error expected, got: ", err) 33 | } 34 | 35 | c, _ = config.NewFromParams(cldtest.CloudName, cldtest.APIKey, cldtest.APISecret) 36 | 37 | assert.Equal(t, cldtest.CloudName, c.Cloud.CloudName) 38 | assert.Equal(t, cldtest.APIKey, c.Cloud.APIKey) 39 | assert.Equal(t, cldtest.APISecret, c.Cloud.APISecret) 40 | 41 | c, _ = config.NewFromOAuthToken(cldtest.CloudName, fakeOAuthToken) 42 | 43 | assert.Equal(t, cldtest.CloudName, c.Cloud.CloudName) 44 | assert.Equal(t, fakeOAuthToken, c.Cloud.OAuthToken) 45 | assert.Equal(t, "", c.Cloud.APIKey) 46 | assert.Equal(t, "", c.Cloud.APISecret) 47 | 48 | // check a few default values 49 | assert.EqualValues(t, signature.SHA1, c.Cloud.GetSignatureAlgorithm()) 50 | assert.EqualValues(t, 60, c.API.Timeout) 51 | assert.EqualValues(t, true, c.URL.Secure) 52 | } 53 | 54 | func TestConfiguration_API(t *testing.T) { 55 | c, err := config.NewFromURL(cldtest.CldURL + 56 | "?upload_prefix=https://test.prefix.com&timeout=59&upload_timeout=59&chunk_size=7357") 57 | if err != nil { 58 | t.Error("Error: ", err) 59 | } 60 | 61 | assert.Equal(t, cldtest.CloudName, c.Cloud.CloudName) 62 | 63 | assert.Equal(t, "https://test.prefix.com", c.API.UploadPrefix) 64 | assert.EqualValues(t, 59, c.API.Timeout) 65 | assert.EqualValues(t, 59, c.API.UploadTimeout) 66 | assert.EqualValues(t, 7357, c.API.ChunkSize) 67 | } 68 | 69 | func TestConfiguration_URL(t *testing.T) { 70 | c, err := config.NewFromURL(cldtest.CldURL + 71 | "?cname=cname.com&secure_cname=secure.cname.com&secure=false&cdn_sub_domain=true" + 72 | "&secure_cdn_sub_domain=true&private_cdn=true&sign_url=true&long_url_signature=true" + 73 | "&shorten=true&use_root_path=true&force_version=false&analytics=false") 74 | if err != nil { 75 | t.Error("Error: ", err) 76 | } 77 | 78 | assert.Equal(t, cldtest.CloudName, c.Cloud.CloudName) 79 | 80 | assert.Equal(t, "cname.com", c.URL.CName) 81 | assert.Equal(t, "secure.cname.com", c.URL.SecureCName) 82 | assert.Equal(t, false, c.URL.Secure) 83 | assert.Equal(t, true, c.URL.CDNSubDomain) 84 | assert.Equal(t, true, c.URL.SecureCDNSubDomain) 85 | assert.Equal(t, true, c.URL.PrivateCDN) 86 | assert.Equal(t, true, c.URL.SignURL) 87 | assert.Equal(t, true, c.URL.LongURLSignature) 88 | assert.Equal(t, true, c.URL.Shorten) 89 | assert.Equal(t, true, c.URL.UseRootPath) 90 | assert.Equal(t, false, c.URL.ForceVersion) 91 | assert.Equal(t, false, c.URL.Analytics) 92 | } 93 | 94 | func TestConfiguration_AuthToken(t *testing.T) { 95 | c, err := config.NewFromURL(cldtest.CldURL + 96 | "?key=key&ip=127.0.0.1&acl=*&start_time=1&expiration=3&duration=2") 97 | if err != nil { 98 | t.Error("Error: ", err) 99 | } 100 | 101 | assert.Equal(t, cldtest.CloudName, c.Cloud.CloudName) 102 | 103 | assert.Equal(t, "key", c.AuthToken.Key) 104 | assert.Equal(t, "127.0.0.1", c.AuthToken.IP) 105 | assert.Equal(t, "*", c.AuthToken.ACL) 106 | assert.EqualValues(t, 1, c.AuthToken.StartTime) 107 | assert.EqualValues(t, 3, c.AuthToken.Expiration) 108 | assert.EqualValues(t, 2, c.AuthToken.Duration) 109 | } 110 | -------------------------------------------------------------------------------- /config/url.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/cloudinary/cloudinary-go/v2/internal/signature" 4 | 5 | // URL defines the configuration applied when generating Cloudinary URLs. 6 | // 7 | // https://cloudinary.com/documentation/how_to_integrate_cloudinary#get_familiar_with_the_cloudinary_console 8 | type URL struct { 9 | Domain string `schema:"-" default:"cloudinary.com"` 10 | SubDomain string `schema:"-" default:"res"` 11 | SharedHost string `schema:"-" default:"res.cloudinary.com"` 12 | CName string `schema:"cname"` 13 | SecureCName string `schema:"secure_cname"` 14 | Secure bool `schema:"secure" default:"true"` 15 | CDNSubDomain bool `schema:"cdn_sub_domain"` 16 | SecureCDNSubDomain bool `schema:"secure_cdn_sub_domain"` 17 | PrivateCDN bool `schema:"private_cdn"` 18 | SignURL bool `schema:"sign_url"` 19 | LongURLSignature bool `schema:"long_url_signature"` 20 | Shorten bool `schema:"shorten"` 21 | UseRootPath bool `schema:"use_root_path"` 22 | ForceVersion bool `schema:"force_version" default:"true"` 23 | Analytics bool `schema:"analytics" default:"true"` 24 | } 25 | 26 | // Protocol returns URL protocol (http or https). 27 | func (uc URL) Protocol() string { 28 | if uc.Secure { 29 | return "https" 30 | } 31 | 32 | return "http" 33 | } 34 | 35 | // GetSignatureLength returns the length of the URL signature. 36 | func (uc URL) GetSignatureLength() uint8 { 37 | if uc.LongURLSignature { 38 | return signature.Long 39 | } 40 | 41 | return signature.Short 42 | } 43 | -------------------------------------------------------------------------------- /example/.env.example: -------------------------------------------------------------------------------- 1 | CLOUDINARY_URL=cloudinary://:@ -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env -------------------------------------------------------------------------------- /example/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cloudinary/cloudinary-go/v2/example 2 | 3 | replace github.com/cloudinary/cloudinary-go/v2 => ../ 4 | 5 | go 1.21 6 | 7 | toolchain go1.21.6 8 | 9 | require github.com/cloudinary/cloudinary-go/v2 v2.7.0 10 | 11 | require ( 12 | github.com/creasty/defaults v1.7.0 // indirect 13 | github.com/google/uuid v1.5.0 // indirect 14 | github.com/gorilla/schema v1.4.1 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /example/go.sum: -------------------------------------------------------------------------------- 1 | github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA= 2 | github.com/creasty/defaults v1.7.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= 6 | github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 7 | github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= 8 | github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 12 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 13 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 14 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /gen/generate_setters/README.md: -------------------------------------------------------------------------------- 1 | # Typed setters code generation for structs 2 | 3 | ### Example: 4 | 5 | Assuming we have a struct `TestType` with a couple of fields that could store a value with different types. 6 | 7 | ```go 8 | type TestType struct { 9 | field1 interface{} 10 | field2 interface{} 11 | } 12 | ``` 13 | 14 | We want to limit available types with typed setters and add some fluent-style interface for it. 15 | 16 | **The final interface should be like this:** 17 | ```go 18 | t := TestType{} 19 | t.Field1(1).Field2Percent(0.85) 20 | 21 | t1 := TestType{} 22 | t1.Field2Expr("Test") 23 | ``` 24 | 25 | `field1` should be a type of int, `field2` should be a type of `float32` (when we want to set it to percent) or `string` (if we want to pass an expression here). 26 | For our interface to be clear it'd be great to have specific suffixes for named values like `percent` for `float32` value or `expression` for `string`. 27 | 28 | **We could create typed setters by ourselves:** 29 | ```go 30 | func (t *TestType) Field1(field1 int) *TestType { 31 | t.field1 = field1 32 | 33 | return t 34 | } 35 | 36 | func (t *TestType) Field2Percent(field2 float32) *TestType { 37 | t.field2 = field2 38 | 39 | return t 40 | } 41 | 42 | func (t *TestType) Field2Expr(field2 string) *TestType { 43 | t.field2 = field2 44 | 45 | return t 46 | } 47 | ``` 48 | 49 | But what if we need this type of setters for multiple structs? 50 | Golang provides type embedding for these needs, but this approach does not work with "fluent" style interfaces as the return type of setter would be an embed struct, not a called struct. 51 | 52 | To avoid this issue we need to create these setters for every struct. 53 | 54 | **Let's use the code generator for it!** 55 | 56 | 1. Add annotation for the struct's fields 57 | ```go 58 | type TestType struct { 59 | field1 interface{} `setters:"int"` 60 | field2 interface{} `setters:"float32:Percent,string:Expr"` 61 | } 62 | ``` 63 | 2. Run `make generate` from the root of the SDK. 64 | 65 | `testtype_setters.go` would be generated and put near the original file. 66 | Now `TestType` has all needed setters with suffixes. 67 | 68 | What's the magic? Code generation. 69 | 70 | ### Code generator 71 | 72 | `gen/generate_setters/main.go` walks through all `*.go` files and searches for `setters` annotations in the structs. 73 | - All structs are placed in Directed Acyclic Graph to calculate its root elements. 74 | - Setters are generated only for root elements. 75 | - Setters would be generated even for cross-module embedding 76 | - When cross-module embedding only exported fields should be used for setters 77 | 78 | #### Example 79 | 80 | ```go 81 | type TestType struct { 82 | field1 interface{} `setters:"int"` 83 | field2 interface{} `setters:"float32:Percent,string:Expr"` 84 | } 85 | 86 | type RootType struct { 87 | TestType 88 | } 89 | ``` 90 | 91 | In this case setters file would be generated only for the `RootType`. 92 | 93 | 94 | ### Annotation structure 95 | 96 | `setters:"type:suffix"` 97 | Types should be divided with comma. 98 | When suffix is provided it would be used in the function name. 99 | If suffix is not set function name would contain only parameter name without any suffix. 100 | -------------------------------------------------------------------------------- /gen/generate_setters/ast.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/heimdalr/dag" 6 | "go/ast" 7 | "reflect" 8 | ) 9 | 10 | // Parse reflected field. Returns currentPackage if field name does not contain other package name. 11 | func getPackageAndStructNamesByAstField(currentPackage string, field *ast.Field) (string, string) { 12 | var embedStructName string 13 | var embedPackageName string 14 | 15 | if embedStruct, ok := field.Type.(*ast.Ident); ok { 16 | embedStructName = embedStruct.Name 17 | embedPackageName = currentPackage 18 | } 19 | 20 | if selector, ok := field.Type.(*ast.SelectorExpr); ok { 21 | if embedPackage, ok := selector.X.(*ast.Ident); ok { 22 | embedPackageName = embedPackage.Name 23 | } 24 | 25 | embedStructName = selector.Sel.Name 26 | } 27 | 28 | return embedPackageName, embedStructName 29 | } 30 | 31 | // Parses a given AST file and adds all found types to the given graph 32 | func searchTypesInAstFile(file *ast.File, filename string, graph *dag.DAG) { 33 | var currentTypeName string 34 | var currentPackage string 35 | 36 | ast.Inspect(file, func(x ast.Node) bool { 37 | if pkg, ok := x.(*ast.File); ok { 38 | currentPackage = pkg.Name.String() 39 | } 40 | 41 | if ss, ok := x.(*ast.TypeSpec); ok { 42 | currentTypeName = ss.Name.String() 43 | } 44 | 45 | s, ok := x.(*ast.StructType) 46 | if !ok { 47 | return true 48 | } 49 | 50 | structVertexID := fmt.Sprintf("%s.%s", currentPackage, currentTypeName) 51 | cldType := findOrCreateGraphNode(structVertexID, currentPackage, currentTypeName, filename, graph) 52 | 53 | for _, field := range s.Fields.List { 54 | if field.Names == nil { 55 | // Embed struct 56 | embedPackageName, embedStructName := getPackageAndStructNamesByAstField(currentPackage, field) 57 | embedVertexID := fmt.Sprintf("%s.%s", embedPackageName, embedStructName) 58 | findOrCreateGraphNode(embedVertexID, embedPackageName, embedStructName, filename, graph) 59 | if err := graph.AddEdge(structVertexID, embedVertexID); err != nil { 60 | panic(err) 61 | } 62 | } else if field.Names != nil && field.Tag != nil { 63 | // Just a field 64 | tag := reflect.StructTag(field.Tag.Value[1 : len(field.Tag.Value)-1]) 65 | if tag.Get("setters") != "" { 66 | cldType.setters = append(cldType.setters, getSettersFromAnnotation(tag.Get("setters"), field.Names[0].Name)...) 67 | } 68 | } 69 | } 70 | 71 | return false 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /gen/generate_setters/ast_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/heimdalr/dag" 5 | "go/ast" 6 | "go/parser" 7 | "go/token" 8 | "testing" 9 | ) 10 | 11 | func TestAst_GetPackageAndStructNamesByAstField(t *testing.T) { 12 | type args struct { 13 | currentPackage string 14 | field *ast.Field 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | want string 20 | want1 string 21 | }{ 22 | { 23 | name: "Struct name field", 24 | args: args{ 25 | currentPackage: "main", 26 | field: &ast.Field{ 27 | Type: &ast.Ident{ 28 | Name: "test", 29 | }, 30 | }, 31 | }, 32 | want: "main", 33 | want1: "test", 34 | }, 35 | { 36 | name: "Struct from another package", 37 | args: args{ 38 | currentPackage: "main", 39 | field: &ast.Field{ 40 | Type: &ast.SelectorExpr{ 41 | X: &ast.Ident{ 42 | Name: "not_main", 43 | }, 44 | Sel: &ast.Ident{ 45 | Name: "test1", 46 | }, 47 | }, 48 | }, 49 | }, 50 | want: "not_main", 51 | want1: "test1", 52 | }, 53 | { 54 | name: "Unknown field type", 55 | args: args{ 56 | currentPackage: "main2", 57 | field: &ast.Field{ 58 | Type: &ast.ArrayType{}, 59 | }, 60 | }, 61 | want: "", 62 | want1: "", 63 | }, 64 | } 65 | 66 | for _, tt := range tests { 67 | t.Run(tt.name, func(t *testing.T) { 68 | got, got1 := getPackageAndStructNamesByAstField(tt.args.currentPackage, tt.args.field) 69 | if got != tt.want { 70 | t.Errorf("getPackageAndStructNamesByAstField() got = %v, want %v", got, tt.want) 71 | } 72 | if got1 != tt.want1 { 73 | t.Errorf("getPackageAndStructNamesByAstField() got1 = %v, want %v", got1, tt.want1) 74 | } 75 | }) 76 | } 77 | } 78 | 79 | func Test_searchTypesInAstFile(t *testing.T) { 80 | type file struct { 81 | file *ast.File 82 | filename string 83 | } 84 | 85 | type args struct { 86 | files []file 87 | } 88 | 89 | type expected struct { 90 | vertices []string 91 | edges map[string][]string 92 | } 93 | 94 | type test struct { 95 | name string 96 | args args 97 | expected expected 98 | } 99 | 100 | var tests []test 101 | 102 | tests = append(tests, test{ 103 | name: "Empty files causes empty graph", 104 | args: args{ 105 | files: nil, 106 | }, 107 | expected: expected{ 108 | vertices: nil, 109 | edges: nil, 110 | }, 111 | }) 112 | 113 | test2 := test{ 114 | name: "One structure without embeding", 115 | args: args{ 116 | files: []file{}, 117 | }, 118 | expected: expected{ 119 | vertices: []string{"testdata.struct1"}, 120 | edges: nil, 121 | }, 122 | } 123 | 124 | for _, filename := range []string{"./testdata/struct1.go"} { 125 | fset := token.NewFileSet() 126 | parsedFile, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) 127 | if err != nil { 128 | panic(err) 129 | } 130 | test2.args.files = append(test2.args.files, file{parsedFile, filename}) 131 | } 132 | 133 | tests = append(tests, test2) 134 | 135 | test3 := test{ 136 | name: "Two structures without embeding", 137 | args: args{ 138 | files: []file{}, 139 | }, 140 | expected: expected{ 141 | vertices: []string{"testdata.struct1", "testdata.struct2"}, 142 | edges: nil, 143 | }, 144 | } 145 | 146 | for _, filename := range []string{"./testdata/struct1.go", "./testdata/struct2.go"} { 147 | fset := token.NewFileSet() 148 | parsedFile, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) 149 | if err != nil { 150 | panic(err) 151 | } 152 | test3.args.files = append(test3.args.files, file{parsedFile, filename}) 153 | } 154 | 155 | tests = append(tests, test3) 156 | 157 | test4 := test{ 158 | name: "Two structures, one from different package", 159 | args: args{ 160 | files: []file{}, 161 | }, 162 | expected: expected{ 163 | vertices: []string{"testdata.struct1", "anotherpackage.Struct3"}, 164 | edges: nil, 165 | }, 166 | } 167 | 168 | for _, filename := range []string{"./testdata/struct1.go", "./testdata/anotherpackage/struct3.go"} { 169 | fset := token.NewFileSet() 170 | parsedFile, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) 171 | if err != nil { 172 | panic(err) 173 | } 174 | test4.args.files = append(test4.args.files, file{parsedFile, filename}) 175 | } 176 | 177 | tests = append(tests, test4) 178 | 179 | test5 := test{ 180 | name: "Cases file", 181 | args: args{ 182 | files: []file{}, 183 | }, 184 | expected: expected{ 185 | vertices: []string{"testdata.struct1", "testdata.struct2", "anotherpackage.Struct3", "testdata.case1", "testdata.case2", "testdata.case3"}, 186 | edges: map[string][]string{ 187 | "testdata.case1": {"testdata.struct1"}, 188 | "testdata.case2": {"testdata.struct1", "testdata.struct2"}, 189 | "testdata.case3": {"testdata.struct1", "anotherpackage.Struct3"}, 190 | }, 191 | }, 192 | } 193 | 194 | for _, filename := range []string{"./testdata/cases.go", "./testdata/struct1.go", "./testdata/anotherpackage/struct3.go", "./testdata/struct2.go"} { 195 | fset := token.NewFileSet() 196 | parsedFile, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) 197 | if err != nil { 198 | panic(err) 199 | } 200 | test5.args.files = append(test5.args.files, file{parsedFile, filename}) 201 | } 202 | 203 | tests = append(tests, test5) 204 | 205 | for _, tt := range tests { 206 | t.Run(tt.name, func(t *testing.T) { 207 | graph := dag.NewDAG() 208 | for _, file := range tt.args.files { 209 | searchTypesInAstFile(file.file, file.filename, graph) 210 | } 211 | 212 | if graph.GetOrder() != len(tt.expected.vertices) { 213 | t.Errorf("Expected result graph to have order %d. %d given", len(tt.expected.vertices), graph.GetOrder()) 214 | } 215 | 216 | for _, id := range tt.expected.vertices { 217 | if _, err := graph.GetVertex(id); err != nil { 218 | t.Errorf("Vertex %s is not found in the result graph", id) 219 | } 220 | } 221 | 222 | expectedSize := 0 223 | 224 | for src, dests := range tt.expected.edges { 225 | for _, dest := range dests { 226 | expectedSize++ 227 | if isEdge, err := graph.IsEdge(src, dest); err != nil { 228 | t.Errorf("Unexpected error %v during edge check", err) 229 | } else if !isEdge { 230 | t.Errorf("Edge %s->%s is not found in the result graph", src, dest) 231 | } 232 | } 233 | } 234 | 235 | if graph.GetSize() != expectedSize { 236 | t.Errorf("Expected result graph to have size %d. %d given", expectedSize, graph.GetSize()) 237 | } 238 | }) 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /gen/generate_setters/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/heimdalr/dag" 7 | "go/parser" 8 | "go/token" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | ) 13 | 14 | func main() { 15 | excludePathsString := flag.String("e", "", "exclude paths") 16 | flag.Parse() 17 | 18 | excludePaths := strings.Split(*excludePathsString, ",") 19 | 20 | curDir, _ := os.Getwd() 21 | graph := dag.NewDAG() 22 | 23 | err := filepath.Walk(".", 24 | func(path string, info os.FileInfo, err error) error { 25 | if err != nil { 26 | return err 27 | } 28 | 29 | if !info.IsDir() && filepath.Ext(path) == ".go" && !contains(excludePaths, filepath.Dir(path)) { 30 | fset := token.NewFileSet() 31 | file, err := parser.ParseFile(fset, path, nil, parser.ParseComments) 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | searchTypesInAstFile(file, path, graph) 37 | } 38 | return nil 39 | }, 40 | ) 41 | 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | for id, cldType := range graph.GetRoots() { 47 | setters := getSettersForType(id, graph) 48 | if len(setters) > 0 { 49 | t, ok := cldType.(*CldType) 50 | if !ok { 51 | panic("Graph node can not be converted to CldType") 52 | } 53 | 54 | fmt.Printf("Generating setters file for struct %s with %d setters\n", id, len(setters)) 55 | tmpl := generateTemplateForSettersFile(setters) 56 | 57 | fileName := fmt.Sprintf("%s/%s/%s_setters.go", curDir, t.filePath, strings.ToLower(t.structName)) 58 | file, _ := os.Create(fileName) 59 | if err = writeTemplateToFile(file, tmpl, t); err != nil { 60 | panic(err) 61 | } 62 | 63 | if err := file.Close(); err != nil { 64 | panic(err) 65 | } 66 | fmt.Printf("Done. File: %s\n", fileName) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /gen/generate_setters/misc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/heimdalr/dag" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | // CldSetter is the setter. 10 | type CldSetter struct { 11 | Type string 12 | Suffix string 13 | Param string 14 | } 15 | 16 | // CldType is the Cloudinary node type. 17 | type CldType struct { 18 | packageName string 19 | structName string 20 | id string 21 | setters []CldSetter 22 | filePath string 23 | } 24 | 25 | // ID returns the ID of the node. 26 | func (t *CldType) ID() string { 27 | return t.id 28 | } 29 | 30 | // Get all setters for type based on embedding graph 31 | func getSettersForType(id string, graph *dag.DAG) []CldSetter { 32 | vertex, _ := graph.GetVertex(id) 33 | setters := vertex.(*CldType).setters 34 | 35 | if child, err := graph.GetChildren(id); err == nil { 36 | for id := range child { 37 | setters = append(setters, getSettersForType(id, graph)...) 38 | } 39 | } 40 | 41 | return setters 42 | } 43 | 44 | // Search in graph for the node by id. Creates new node if not found. 45 | func findOrCreateGraphNode(id string, packageName string, typeName string, filename string, graph *dag.DAG) *CldType { 46 | var cldType *CldType 47 | if _, err := graph.GetVertex(id); err != nil { 48 | cldType = &CldType{ 49 | id: id, 50 | packageName: packageName, 51 | structName: typeName, 52 | filePath: filepath.Dir(filename), 53 | } 54 | 55 | if _, err := graph.AddVertex(cldType); err != nil { 56 | panic(err) 57 | } 58 | } else { 59 | t, _ := graph.GetVertex(id) 60 | cldType = t.(*CldType) 61 | } 62 | 63 | return cldType 64 | } 65 | 66 | // Parse annotation and get CldSetters from it 67 | func getSettersFromAnnotation(annotation string, paramName string) []CldSetter { 68 | var res []CldSetter 69 | types := strings.Split(annotation, ",") 70 | 71 | for _, t := range types { 72 | s := strings.Split(t, ":") 73 | if len(s) > 1 { 74 | res = append(res, CldSetter{s[0], s[1], paramName}) 75 | } else { 76 | res = append(res, CldSetter{t, "", paramName}) 77 | } 78 | } 79 | 80 | return res 81 | } 82 | 83 | func contains(s []string, str string) bool { 84 | for _, v := range s { 85 | if v == str { 86 | return true 87 | } 88 | } 89 | 90 | return false 91 | } 92 | -------------------------------------------------------------------------------- /gen/generate_setters/template.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "strings" 9 | "text/template" 10 | ) 11 | 12 | // Generate template for setters file 13 | func generateTemplateForSettersFile(setters []CldSetter) string { 14 | stringTemplate := "package {{ .PackageName}}\n" 15 | functionTemplate := ` 16 | func ({{ .Receiver}} *{{ .StructName}}) <>(<> <>) *{{ .StructName}} { 17 | {{ .Receiver}}.<> = <> 18 | 19 | return {{ .Receiver}} 20 | } 21 | ` 22 | templateData := struct { 23 | FuncName map[int]string 24 | ParamName map[int]string 25 | Type map[int]string 26 | }{ 27 | map[int]string{}, 28 | map[int]string{}, 29 | map[int]string{}, 30 | } 31 | 32 | for i, t := range setters { 33 | stringTemplate += fmt.Sprintf(functionTemplate, i, i, i, i, i) 34 | templateData.FuncName[i] = strings.Title(t.Param) + t.Suffix 35 | templateData.Type[i] = t.Type 36 | templateData.ParamName[i] = t.Param 37 | } 38 | 39 | buf := new(bytes.Buffer) 40 | w := bufio.NewWriter(buf) 41 | tmpl := template.Must(template.New("test").Delims("<<", ">>").Parse(stringTemplate)) 42 | if err := tmpl.Execute(w, templateData); err != nil { 43 | panic(err) 44 | } 45 | 46 | if err := w.Flush(); err != nil { 47 | panic(err) 48 | } 49 | 50 | return buf.String() 51 | } 52 | 53 | // Write template to file filling it with type data 54 | func writeTemplateToFile(file io.Writer, templateString string, t *CldType) error { 55 | tmpl := template.Must(template.New("setters").Parse(templateString)) 56 | 57 | mixinData := struct { 58 | PackageName string 59 | StructName string 60 | Receiver string 61 | }{ 62 | t.packageName, 63 | t.structName, 64 | strings.ToLower(t.structName)[0:1], 65 | } 66 | 67 | w := bufio.NewWriter(file) 68 | if err := tmpl.Execute(w, mixinData); err != nil { 69 | return err 70 | } 71 | 72 | if err := w.Flush(); err != nil { 73 | return err 74 | } 75 | 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /gen/generate_setters/template_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "testing" 7 | ) 8 | 9 | func TestTemplate_GenerateTemplateForSettersFile(t *testing.T) { 10 | type args struct { 11 | setters []CldSetter 12 | } 13 | tests := []struct { 14 | name string 15 | args args 16 | want string 17 | }{ 18 | { 19 | name: "One setter without suffix", 20 | args: args{ 21 | setters: []CldSetter{{ 22 | Type: "int", 23 | Suffix: "", 24 | Param: "param1", 25 | }}, 26 | }, 27 | want: `package {{ .PackageName}} 28 | 29 | func ({{ .Receiver}} *{{ .StructName}}) Param1(param1 int) *{{ .StructName}} { 30 | {{ .Receiver}}.param1 = param1 31 | 32 | return {{ .Receiver}} 33 | } 34 | `, 35 | }, 36 | { 37 | name: "One setter with suffix", 38 | args: args{ 39 | setters: []CldSetter{{ 40 | Type: "int", 41 | Suffix: "Suffix", 42 | Param: "param1", 43 | }}, 44 | }, 45 | want: `package {{ .PackageName}} 46 | 47 | func ({{ .Receiver}} *{{ .StructName}}) Param1Suffix(param1 int) *{{ .StructName}} { 48 | {{ .Receiver}}.param1 = param1 49 | 50 | return {{ .Receiver}} 51 | } 52 | `, 53 | }, 54 | { 55 | name: "Multiple mixed setters", 56 | args: args{ 57 | setters: []CldSetter{ 58 | { 59 | Type: "int", 60 | Suffix: "", 61 | Param: "param1", 62 | }, 63 | { 64 | Type: "string", 65 | Suffix: "Suffix", 66 | Param: "param1", 67 | }, 68 | { 69 | Type: "float32", 70 | Suffix: "", 71 | Param: "param2", 72 | }, 73 | }, 74 | }, 75 | want: `package {{ .PackageName}} 76 | 77 | func ({{ .Receiver}} *{{ .StructName}}) Param1(param1 int) *{{ .StructName}} { 78 | {{ .Receiver}}.param1 = param1 79 | 80 | return {{ .Receiver}} 81 | } 82 | 83 | func ({{ .Receiver}} *{{ .StructName}}) Param1Suffix(param1 string) *{{ .StructName}} { 84 | {{ .Receiver}}.param1 = param1 85 | 86 | return {{ .Receiver}} 87 | } 88 | 89 | func ({{ .Receiver}} *{{ .StructName}}) Param2(param2 float32) *{{ .StructName}} { 90 | {{ .Receiver}}.param2 = param2 91 | 92 | return {{ .Receiver}} 93 | } 94 | `, 95 | }, 96 | { 97 | name: "Nil slice input", 98 | args: args{ 99 | setters: nil, 100 | }, 101 | want: "package {{ .PackageName}}\n", 102 | }, 103 | } 104 | for _, tt := range tests { 105 | t.Run(tt.name, func(t *testing.T) { 106 | if got := generateTemplateForSettersFile(tt.args.setters); got != tt.want { 107 | t.Errorf("generateTemplateForSettersFile() = %v, want %v", got, tt.want) 108 | } 109 | }) 110 | } 111 | } 112 | 113 | func TestTemplate_WriteTemplateToFile(t *testing.T) { 114 | cldType := CldType{ 115 | packageName: "package", 116 | structName: "struct", 117 | id: "id", 118 | setters: []CldSetter{ 119 | { 120 | Type: "int", 121 | Suffix: "suffix", 122 | Param: "param", 123 | }, 124 | }, 125 | filePath: "test1/test2/test3", 126 | } 127 | 128 | type args struct { 129 | buffer bytes.Buffer 130 | templateString string 131 | t *CldType 132 | } 133 | tests := []struct { 134 | name string 135 | args args 136 | wantErr bool 137 | wantString string 138 | }{ 139 | { 140 | name: "Empty template = empty buffer", 141 | args: args{ 142 | buffer: bytes.Buffer{}, 143 | templateString: "", 144 | t: &cldType, 145 | }, 146 | wantErr: false, 147 | wantString: "", 148 | }, 149 | { 150 | name: "Receiver in template subsitutes with small first letter of the struct name", 151 | args: args{ 152 | buffer: bytes.Buffer{}, 153 | templateString: "{{ .Receiver}}", 154 | t: &cldType, 155 | }, 156 | wantErr: false, 157 | wantString: "s", 158 | }, 159 | { 160 | name: "Package name in template subsitutes with package name from struct", 161 | args: args{ 162 | buffer: bytes.Buffer{}, 163 | templateString: "{{ .PackageName}}", 164 | t: &cldType, 165 | }, 166 | wantErr: false, 167 | wantString: "package", 168 | }, 169 | { 170 | name: "Struct name in template subsitutes with struct name", 171 | args: args{ 172 | buffer: bytes.Buffer{}, 173 | templateString: "{{ .StructName}}", 174 | t: &cldType, 175 | }, 176 | wantErr: false, 177 | wantString: "struct", 178 | }, 179 | } 180 | 181 | for _, tt := range tests { 182 | t.Run(tt.name, func(t *testing.T) { 183 | writer := bufio.NewWriter(&tt.args.buffer) 184 | if err := writeTemplateToFile(writer, tt.args.templateString, tt.args.t); (err != nil) != tt.wantErr { 185 | t.Errorf("writeTemplateToFile() error = %v, wantErr %v", err, tt.wantErr) 186 | } 187 | 188 | if tt.args.buffer.String() != tt.wantString { 189 | t.Errorf("writeTemplateToFile should write %v to given buffer. %v given", tt.wantString, tt.args.buffer.String()) 190 | } 191 | }) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /gen/generate_setters/testdata/anotherpackage/struct3.go: -------------------------------------------------------------------------------- 1 | package anotherpackage 2 | 3 | // Struct3 test struct 4 | type Struct3 struct { 5 | Param3 interface{} `setters:"string"` 6 | } 7 | -------------------------------------------------------------------------------- /gen/generate_setters/testdata/cases.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import "github.com/cloudinary/cloudinary-go/v2/gen/generate_setters/testdata/anotherpackage" 4 | 5 | type case1 struct { 6 | struct1 7 | } 8 | 9 | type case2 struct { 10 | struct1 11 | struct2 12 | } 13 | 14 | type case3 struct { 15 | struct1 16 | anotherpackage.Struct3 17 | } 18 | -------------------------------------------------------------------------------- /gen/generate_setters/testdata/struct1.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | type struct1 struct { 4 | param1 interface{} `setters:"string"` 5 | } 6 | -------------------------------------------------------------------------------- /gen/generate_setters/testdata/struct2.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | type struct2 struct { 4 | param2 interface{} `setters:"string"` 5 | } 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cloudinary/cloudinary-go/v2 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/creasty/defaults v1.7.0 7 | github.com/google/uuid v1.5.0 8 | github.com/gorilla/schema v1.4.1 9 | github.com/heimdalr/dag v1.4.0 10 | github.com/stretchr/testify v1.8.4 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/emirpasic/gods v1.18.1 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | gopkg.in/yaml.v3 v3.0.1 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA= 2 | github.com/creasty/defaults v1.7.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 6 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 7 | github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= 8 | github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 9 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 10 | github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= 11 | github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 12 | github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= 13 | github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= 14 | github.com/heimdalr/dag v1.4.0 h1:zG3JA4RDVLc55k3AXAgfwa+EgBNZ0TkfOO3C29Ucpmg= 15 | github.com/heimdalr/dag v1.4.0/go.mod h1:OCh6ghKmU0hPjtwMqWBoNxPmtRioKd1xSu7Zs4sbIqM= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 19 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 22 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 23 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 24 | -------------------------------------------------------------------------------- /internal/cldtest/testdata/cloudinary_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudinary/cloudinary-go/60a591dd68482f3dd233912d55cd8d1818fa727b/internal/cldtest/testdata/cloudinary_logo.png -------------------------------------------------------------------------------- /internal/cldtest/testdata/movie.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudinary/cloudinary-go/60a591dd68482f3dd233912d55cd8d1818fa727b/internal/cldtest/testdata/movie.mp4 -------------------------------------------------------------------------------- /internal/signature/signature.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | import ( 4 | "crypto/sha1" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "errors" 8 | "fmt" 9 | "hash" 10 | ) 11 | 12 | // Length represents the length of the signature. 13 | type Length = uint8 14 | 15 | // Algo represent the algorithm of the signature. 16 | type Algo = string 17 | 18 | const ( 19 | // Short signature length 20 | Short Length = 8 21 | // Long signature length 22 | Long Length = 32 23 | ) 24 | 25 | const ( 26 | // SHA1 algorithm. 27 | SHA1 Algo = "sha1" 28 | // SHA256 algorithm. 29 | SHA256 Algo = "sha256" 30 | ) 31 | 32 | // Sign signs the content with the provided signature. 33 | func Sign(content string, secret string, algo Algo) ([]byte, error) { 34 | if len(secret) < 1 { 35 | return nil, errors.New("must supply api_secret") 36 | } 37 | var hashFunc hash.Hash 38 | switch algo { 39 | case SHA1: 40 | hashFunc = sha1.New() 41 | case SHA256: 42 | hashFunc = sha256.New() 43 | default: 44 | return nil, errors.New("unsupported signature algorithm") 45 | } 46 | 47 | hashFunc.Write([]byte(content + secret)) 48 | 49 | return hashFunc.Sum(nil), nil 50 | } 51 | 52 | // SignURL returns the URL signature. 53 | func SignURL(content string, secret string, algo Algo, length uint8) string { 54 | rawSignature, _ := Sign(content, secret, algo) 55 | signature := base64.RawURLEncoding.EncodeToString(rawSignature) 56 | 57 | return fmt.Sprintf("s--%s--", signature[:length]) 58 | } 59 | -------------------------------------------------------------------------------- /logger/README.md: -------------------------------------------------------------------------------- 1 | ### Logging in Cloudinary SDK 2 | The default logger in Cloudinary Go SDK is `go log`. 3 | 4 | You can use any log library by overwriting the standard SDK logging functions. 5 | 6 | #### Using logrus with the SDK 7 | ```go 8 | package main 9 | 10 | import ( 11 | "github.com/cloudinary/cloudinary-go" 12 | "github.com/sirupsen/logrus" 13 | "log" 14 | ) 15 | 16 | func main() { 17 | // Start by creating a new instance of Cloudinary using CLOUDINARY_URL environment variable. 18 | // Alternatively you can use cloudinary.NewFromParams() or cloudinary.NewFromURL(). 19 | var cld, err = cloudinary.New() 20 | if err != nil { 21 | log.Fatalf("Failed to intialize Cloudinary, %v", err) 22 | } 23 | 24 | // Initialize your logger somewhere in your code. 25 | // Set cloudinary.Logger.Writer with logrus instance 26 | var logger = logrus.New() 27 | cld.Logger.Writer = logger.WithField("source", "cloudinary") 28 | } 29 | ``` 30 | 31 | #### Using Zap with the SDK 32 | ```go 33 | package main 34 | 35 | import ( 36 | "github.com/cloudinary/cloudinary-go" 37 | "go.uber.org/zap" 38 | "log" 39 | ) 40 | 41 | func main() { 42 | // Start by creating a new instance of Cloudinary using CLOUDINARY_URL environment variable. 43 | // Alternatively you can use cloudinary.NewFromParams() or cloudinary.NewFromURL(). 44 | var cld, err = cloudinary.New() 45 | if err != nil { 46 | log.Fatalf("Failed to intialize Cloudinary, %v", err) 47 | } 48 | 49 | // Initialize your logger somewhere in your code. 50 | // Set cloudinary.Logger.Writer with zap.SugaredLogger instance 51 | var zapLogger, _ = zap.NewDevelopment() 52 | cld.Logger.Writer = zapLogger.Sugar().With("source", "cloudinary") 53 | } 54 | ``` 55 | 56 | #### Logging level 57 | 58 | You can change logging level with the `Logger.SetLevel()` function. 59 | 60 | Possible values: 61 | - `logger.NONE` - disabling logging from the SDK 62 | - `logger.ERROR` - enable logging only for error messages 63 | - `logger.DEBUG` - enable debug logs 64 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | // Package logger defines the Cloudinary Logger 2 | package logger 3 | 4 | import ( 5 | "log" 6 | ) 7 | 8 | // Level represents the level of the logger. 9 | type Level int8 10 | 11 | // NONE level of the logger. 12 | const NONE Level = 0 13 | 14 | // ERROR level of the logger. 15 | const ERROR Level = 1 16 | 17 | // DEBUG level of the logger. 18 | const DEBUG Level = 2 19 | 20 | // LogWriter is an interface for a log writer. 21 | type LogWriter interface { 22 | Debug(v ...interface{}) 23 | Error(v ...interface{}) 24 | } 25 | 26 | // GoLog is a struct for go log writer. 27 | type GoLog struct{} 28 | 29 | // Debug prints debug messages. 30 | func (g *GoLog) Debug(v ...interface{}) { 31 | log.Println("cloudinary debug", v) 32 | } 33 | 34 | // Error prints error messages. 35 | func (g *GoLog) Error(v ...interface{}) { 36 | log.Println("cloudinary error", v) 37 | } 38 | 39 | // Logger is the logger struct. 40 | type Logger struct { 41 | Writer LogWriter 42 | level Level 43 | } 44 | 45 | // SetLevel sets the logger level. 46 | func (l *Logger) SetLevel(level Level) { 47 | l.level = level 48 | } 49 | 50 | // Debug writes error messages. 51 | func (l *Logger) Error(v ...interface{}) { 52 | if l.level >= ERROR { 53 | l.Writer.Error(v...) 54 | } 55 | } 56 | 57 | // Debug writes debug messages. 58 | func (l *Logger) Debug(v ...interface{}) { 59 | if l.level == DEBUG { 60 | l.Writer.Debug(v...) 61 | } 62 | } 63 | 64 | // New returns a new logger instance. 65 | func New() *Logger { 66 | return &Logger{ 67 | Writer: &GoLog{}, 68 | level: ERROR, 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /logger/logger_test.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | type MockLogger struct { 9 | ErrorMessages map[int][]string 10 | DebugMessages map[int][]string 11 | } 12 | 13 | func (m MockLogger) Debug(v ...interface{}) { 14 | m.DebugMessages[len(m.DebugMessages)] = toStringsSlice(v...) 15 | } 16 | 17 | func (m MockLogger) Error(v ...interface{}) { 18 | m.ErrorMessages[len(m.ErrorMessages)] = toStringsSlice(v...) 19 | } 20 | 21 | func TestLogger_LogLevel_Error(t *testing.T) { 22 | mock := MockLogger{ 23 | ErrorMessages: map[int][]string{}, 24 | DebugMessages: map[int][]string{}, 25 | } 26 | log := Logger{Writer: mock} 27 | log.SetLevel(ERROR) 28 | 29 | log.Error("Test error") 30 | log.Debug("Test debug") 31 | 32 | if len(mock.ErrorMessages) != 1 { 33 | t.Error("Logger should log error messages with level ERROR") 34 | } 35 | 36 | if mock.ErrorMessages[0][0] != "Test error" { 37 | t.Errorf("Error log message should be \"Test error\", %v given", mock.ErrorMessages[0][0]) 38 | } 39 | 40 | if len(mock.DebugMessages) > 0 { 41 | t.Error("Logger should not log debug messages with level ERROR") 42 | } 43 | } 44 | 45 | func TestLogger_LogLevel_Debug(t *testing.T) { 46 | mock := MockLogger{ 47 | ErrorMessages: map[int][]string{}, 48 | DebugMessages: map[int][]string{}, 49 | } 50 | log := Logger{Writer: mock} 51 | log.SetLevel(DEBUG) 52 | 53 | log.Error("Test error") 54 | log.Debug("Test debug") 55 | 56 | if len(mock.ErrorMessages) != 1 { 57 | t.Error("Logger should log error messages with level DEBUG") 58 | } 59 | 60 | if mock.ErrorMessages[0][0] != "Test error" { 61 | t.Errorf("Error log message should be \"Test error\", %v given", mock.ErrorMessages[0][0]) 62 | } 63 | 64 | if len(mock.DebugMessages) != 1 { 65 | t.Error("Logger should log debug messages with level DEBUG") 66 | } 67 | 68 | if mock.DebugMessages[0][0] != "Test debug" { 69 | t.Errorf("Error log message should be \"Test debug\", %v given", mock.DebugMessages[0][0]) 70 | } 71 | } 72 | 73 | func TestLogger_LogLevel_None(t *testing.T) { 74 | mock := MockLogger{ 75 | ErrorMessages: map[int][]string{}, 76 | DebugMessages: map[int][]string{}, 77 | } 78 | log := Logger{Writer: mock} 79 | log.SetLevel(NONE) 80 | 81 | log.Error("Test error") 82 | log.Debug("Test debug") 83 | 84 | if len(mock.ErrorMessages) > 0 { 85 | t.Error("Logger should not log error messages with level NONE") 86 | } 87 | 88 | if len(mock.DebugMessages) > 0 { 89 | t.Error("Logger should not log debug messages with level NONE") 90 | } 91 | } 92 | 93 | func toStringsSlice(v ...interface{}) []string { 94 | var res []string 95 | 96 | for _, value := range v { 97 | res = append(res, fmt.Sprintf("%v", value)) 98 | } 99 | 100 | return res 101 | } 102 | -------------------------------------------------------------------------------- /scripts/allocate_test_cloud.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | ) 12 | 13 | const apiEndpoint = "https://sub-account-testing.cloudinary.com/create_sub_account" 14 | 15 | func main() { 16 | if len(os.Args) < 2 { 17 | log.Fatal("Please specify prefix") 18 | return 19 | } 20 | var resp, err = http.PostForm(apiEndpoint, url.Values{"prefix": {os.Args[1]}}) 21 | 22 | if nil != err { 23 | log.Fatal("error happened getting the response", err) 24 | return 25 | } 26 | 27 | defer resp.Body.Close() 28 | 29 | body, err := ioutil.ReadAll(resp.Body) 30 | 31 | if nil != err { 32 | log.Fatal("error happened reading the body", err) 33 | } 34 | 35 | res := &cloudAllocationResult{} 36 | err = json.Unmarshal(body, res) 37 | 38 | if err != nil { 39 | log.Fatal("error happened reading the body", err) 40 | return 41 | } 42 | 43 | if res.Status != "success" { 44 | log.Fatal("error happened: ", res.Status, res.ErrMsg) 45 | } 46 | 47 | c := res.Payload 48 | 49 | fmt.Printf("cloudinary://%v:%v@%v\n", c.CloudAPIKey, c.CloudAPISecret, c.CloudName) 50 | } 51 | 52 | type cloudAllocationResult struct { 53 | Payload struct { 54 | CloudAPIKey string `json:"cloudApiKey"` 55 | CloudAPISecret string `json:"cloudApiSecret"` 56 | CloudName string `json:"cloudName"` 57 | ID string `json:"id"` 58 | } `json:"payload"` 59 | ErrMsg string `json:"errMsg"` 60 | Operation string `json:"operation"` 61 | Status string `json:"status"` 62 | } 63 | -------------------------------------------------------------------------------- /scripts/get_test_cloud.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 4 | 5 | GO_VER=$(go version | head -n 1 | cut -d ' ' -f 3| cut -c 3-); 6 | SDK_VER=$(grep -oiP '(?<=Version \= \")([a-zA-Z0-9\-.]+)(?=")' "${DIR}"/../api/api.go) 7 | 8 | go run "${DIR}"/allocate_test_cloud.go "GO ${GO_VER} SDK ${SDK_VER}" 9 | -------------------------------------------------------------------------------- /scripts/update_version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Update version number and prepare for publishing the new version 4 | 5 | set -e 6 | 7 | # Empty to run the rest of the line and "echo" for a dry run 8 | CMD_PREFIX= 9 | 10 | # Add a quote if this is a dry run 11 | QUOTE= 12 | 13 | NEW_VERSION= 14 | 15 | UPDATE_ONLY=false 16 | 17 | function echo_err 18 | { 19 | echo "$@" 1>&2; 20 | } 21 | 22 | function usage 23 | { 24 | echo "Usage: $0 [parameters]" 25 | echo " -v | --version " 26 | echo " -d | --dry-run print the commands without executing them" 27 | echo " -u | --update-only only update the version" 28 | echo " -h | --help print this information and exit" 29 | echo 30 | echo "For example: $0 -v 1.2.3" 31 | } 32 | 33 | function process_arguments 34 | { 35 | while [[ "$1" != "" ]]; do 36 | case $1 in 37 | -v | --version ) 38 | shift 39 | NEW_VERSION=${1:-} 40 | if ! [[ "${NEW_VERSION}" =~ [0-9]+\.[0-9]+\.[0-9]+(\-.+)? ]]; then 41 | echo_err "You must supply a new version after -v or --version" 42 | echo_err "For example:" 43 | echo_err " 1.2.3" 44 | echo_err " 1.2.3-rc1" 45 | echo_err "" 46 | usage; return 1 47 | fi 48 | ;; 49 | -d | --dry-run ) 50 | CMD_PREFIX=echo 51 | echo "Dry Run" 52 | echo "" 53 | ;; 54 | -u | --update-only ) 55 | UPDATE_ONLY=true 56 | echo "Only update version" 57 | echo "" 58 | ;; 59 | -h | --help ) 60 | usage; return 0 61 | ;; 62 | * ) 63 | usage; return 1 64 | esac 65 | shift || true 66 | done 67 | } 68 | 69 | # Intentionally make pushd silent 70 | function pushd 71 | { 72 | command pushd "$@" > /dev/null 73 | } 74 | 75 | # Intentionally make popd silent 76 | function popd 77 | { 78 | command popd > /dev/null 79 | } 80 | 81 | # Check if one version is less than or equal than other 82 | # Example: 83 | # ver_lte 1.2.3 1.2.3 && echo "yes" || echo "no" # yes 84 | # ver_lte 1.2.3 1.2.4 && echo "yes" || echo "no" # yes 85 | # ver_lte 1.2.4 1.2.3 && echo "yes" || echo "no" # no 86 | function ver_lte 87 | { 88 | [[ "$1" = "`echo -e "$1\n$2" | sort -V | head -n1`" ]] 89 | } 90 | 91 | # Extract the last entry or entry for a given version 92 | # The function is not currently used in this file. 93 | # Examples: 94 | # changelog_last_entry 95 | # changelog_last_entry 1.10.0 96 | # 97 | function changelog_last_entry 98 | { 99 | sed -e "1,/^${1}/d" -e '/^=/d' -e '/^$/d' -e '/^[0-9]/,$d' CHANGELOG.md 100 | } 101 | 102 | function verify_dependencies 103 | { 104 | # Test if the gnu grep is installed 105 | if ! grep --version | grep -q GNU 106 | then 107 | echo_err "GNU grep is required for this script" 108 | echo_err "You can install it using the following command:" 109 | echo_err "" 110 | echo_err "brew install grep --with-default-names" 111 | return 1 112 | fi 113 | 114 | if [[ "${UPDATE_ONLY}" = true ]]; then 115 | return 0; 116 | fi 117 | 118 | if [[ -z "$(type -t git-changelog)" ]] 119 | then 120 | echo_err "git-extras packages is not installed." 121 | echo_err "You can install it using the following command:" 122 | echo_err "" 123 | echo_err "brew install git-extras" 124 | return 1 125 | fi 126 | } 127 | 128 | # Replace old string only if it is present in the file, otherwise return 1 129 | function safe_replace 130 | { 131 | local old=$1 132 | local new=$2 133 | local file=$3 134 | 135 | grep -q "${old}" "${file}" || { echo_err "${old} was not found in ${file}"; return 1; } 136 | 137 | ${CMD_PREFIX} sed -i.bak -e "${QUOTE}s/${old}/${new}/${QUOTE}" -- "${file}" && rm -- "${file}.bak" 138 | } 139 | 140 | function update_version 141 | { 142 | if [[ -z "${NEW_VERSION}" ]]; then 143 | usage; return 1 144 | fi 145 | 146 | # Enter git root 147 | pushd $(git rev-parse --show-toplevel) 148 | 149 | local current_version 150 | current_version=$(grep -oiP '(?<=const Version = ")([a-zA-Z0-9\-.]+)(?=")' api/api.go) 151 | 152 | if [[ -z "${current_version}" ]]; then 153 | echo_err "Failed getting current version, please check directory structure and/or contact developer" 154 | return 1 155 | fi 156 | 157 | # Use literal dot character in regular expression 158 | local current_version_re=${current_version//./\\.} 159 | 160 | echo "# Current version is: ${current_version}" 161 | echo "# New version is: ${NEW_VERSION}" 162 | 163 | ver_lte "${NEW_VERSION}" "${current_version}" && { echo_err "New version is not greater than current version"; return 1; } 164 | 165 | # Add a quote if this is a dry run 166 | QUOTE=${CMD_PREFIX:+"'"} 167 | 168 | safe_replace "const Version = \"${current_version_re}\""\ 169 | "const Version = \"${NEW_VERSION}\""\ 170 | api/api.go\ 171 | || return 1 172 | 173 | if [[ "${UPDATE_ONLY}" = true ]]; then 174 | popd; 175 | return 0; 176 | fi 177 | 178 | ${CMD_PREFIX} git changelog -t "${NEW_VERSION}" || true 179 | 180 | echo "" 181 | echo "# After editing CHANGELOG.md, optionally review changes and issue these commands:" 182 | echo git add api/api.go CHANGELOG.md 183 | echo git commit -m "\"Version ${NEW_VERSION}\"" 184 | echo sed -e "'1,/^${NEW_VERSION//./\\.}/d'" \ 185 | -e "'/^=/d'" \ 186 | -e "'/^$/d'" \ 187 | -e "'/^[0-9]/,\$d'" \ 188 | CHANGELOG.md \ 189 | \| git tag -a "'v${NEW_VERSION}'" --file=- 190 | 191 | # Don't run those commands on dry run 192 | [[ -n "${CMD_PREFIX}" ]] && { popd; return 0; } 193 | 194 | echo "" 195 | read -p "Run the above commands automatically? (y/N): " confirm && [[ ${confirm} == [yY] || ${confirm} == [yY][eE][sS] ]] || { popd; return 0; } 196 | 197 | git add api/api.go CHANGELOG.md 198 | git commit -m "Version ${NEW_VERSION}" 199 | sed -e "1,/^${NEW_VERSION//./\\.}/d" \ 200 | -e "/^=/d" \ 201 | -e "/^$/d" \ 202 | -e "/^[0-9]/,\$d" \ 203 | CHANGELOG.md \ 204 | | git tag -a "v${NEW_VERSION}" --file=- 205 | 206 | popd 207 | } 208 | 209 | process_arguments "$@" 210 | verify_dependencies 211 | update_version 212 | -------------------------------------------------------------------------------- /transformation/transformation.go: -------------------------------------------------------------------------------- 1 | // Package transformation defines Cloudinary Transformation. 2 | package transformation 3 | 4 | // This is a placeholder for a Transformation struct. 5 | 6 | // Action represents a single transformation action. Consist of qualifiers. 7 | type Action = map[string]interface{} 8 | 9 | // Transformation is the asset transformation. Consists of Actions. 10 | type Transformation = []Action 11 | 12 | // RawTransformation is the raw (free form) transformation string. 13 | type RawTransformation = string 14 | --------------------------------------------------------------------------------