├── .editorconfig ├── .gitignore ├── AUTHORS ├── LICENSE ├── README.rst ├── admin.go ├── cloudinary ├── main.go └── vars.go ├── glide.lock ├── glide.yaml ├── service.go ├── service_test.go └── tools.go /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = LF 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | indent_size = 2 10 | 11 | [*.go] 12 | indent_size = 4 13 | indent_style = tab 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cloudinary/cloudinary 2 | 3 | /.idea 4 | /vendor 5 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Anthony Baillard 2 | Mathias Monnerville 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 GoTsunami 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | go-cloudinary 2 | ============= 3 | 4 | A Go client library and CLI tool to upload static assets to the `Cloudinary`_ service. 5 | 6 | .. _Cloudinary: http://www.cloudinary.com 7 | 8 | Installation 9 | ------------ 10 | 11 | Install the CLI tool and the library with:: 12 | 13 | go get github.com/gotsunami/go-cloudinary/cloudinary 14 | 15 | Usage 16 | ----- 17 | 18 | Usage:: 19 | 20 | cloudinary [options] action settings.conf 21 | 22 | where action is one of ``ls``, ``rm``, ``up`` or ``url``. 23 | 24 | Create a config file ``settings.conf`` with a ``[cloudinary]`` section:: 25 | 26 | [cloudinary] 27 | uri=cloudinary://api_key:api_secret@cloud_name 28 | 29 | Type ``cloudinary`` in the terminal to get some help. 30 | 31 | Uploading files 32 | ~~~~~~~~~~~~~~~ 33 | 34 | Use the ``up`` action to upload an image (or a directory of images) to Cloudinary with ``-i`` option:: 35 | 36 | $ cloudinary -i /path/to/img.png up settings.conf 37 | $ cloudinary -i /path/to/images/ up settings.conf 38 | 39 | In order to upload raw files, use the ``-r`` option. For example, a CSS file can be upload with:: 40 | 41 | $ cloudinary -r /path/to/default.css up settings.conf 42 | 43 | Raw files can be of any type (css, js, pdf etc.), even images if you don't 44 | care about not using Cloudinary's image processing features. 45 | 46 | List Remote Resources 47 | ~~~~~~~~~~~~~~~~~~~~~ 48 | 49 | Using the ``ls`` action will list all uploaded images and raw files:: 50 | 51 | $ cloudinary ls settings.conf 52 | 53 | Delete Remote Resources 54 | ~~~~~~~~~~~~~~~~~~~~~~~ 55 | 56 | Use the ``rm`` action to delete resources and give the ``-i`` or ``-r`` ``public_id`` for the resource:: 57 | 58 | $ cloudinary -i img/home rm settings.conf 59 | $ cloudinary -r media/js/jquery-min.js rm settings.conf 60 | 61 | Delete all remote resources(!) with:: 62 | 63 | $ cloudinary -a rm settings.conf 64 | 65 | In any case, you can always use the ``-s`` flag to simulate an action and see what result to expect. 66 | i 67 | -------------------------------------------------------------------------------- /admin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Mathias Monnerville and Anthony Baillard. 2 | // All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package cloudinary 7 | 8 | import ( 9 | "encoding/json" 10 | "fmt" 11 | "io" 12 | "net/http" 13 | "net/url" 14 | "strconv" 15 | "github.com/qiscus/qiscus-sdk-api/api/admin/v1" 16 | ) 17 | 18 | const ( 19 | baseAdminUrl = "https://api.cloudinary.com/v1_1" 20 | ) 21 | 22 | const ( 23 | pathListAllImages = "/resources/image" 24 | pathListAllRaws = "/resources/raw" 25 | pathListSingleImage = "/resources/image/upload/" 26 | pathListAllVideos = "/resources/video" 27 | ) 28 | 29 | const ( 30 | maxResults = 2048 31 | ) 32 | 33 | func (s *Service) dropAllResources(rtype ResourceType, w io.Writer) error { 34 | qs := url.Values{ 35 | "max_results": []string{strconv.FormatInt(maxResults, 10)}, 36 | } 37 | path := pathListAllImages 38 | if rtype == RawType { 39 | path = pathListAllRaws 40 | } 41 | for { 42 | resp, err := http.Get(fmt.Sprintf("%s%s?%s", s.adminURI, path, qs.Encode())) 43 | m, err := handleHttpResponse(resp) 44 | if err != nil { 45 | return err 46 | } 47 | for _, v := range m["resources"].([]interface{}) { 48 | publicId := v.(map[string]interface{})["public_id"].(string) 49 | if w != nil { 50 | fmt.Fprintf(w, "Deleting %s ... ", publicId) 51 | } 52 | if err := s.Delete(publicId, "", rtype); err != nil { 53 | // Do not return. Report the error but continue through the list. 54 | fmt.Fprintf(w, "Error: %s: %s\n", publicId, err.Error()) 55 | } 56 | } 57 | if e, ok := m["next_cursor"]; ok { 58 | qs.Set("next_cursor", e.(string)) 59 | } else { 60 | break 61 | } 62 | } 63 | 64 | return nil 65 | } 66 | 67 | // DropAllImages deletes all remote images from Cloudinary. File names are 68 | // written to io.Writer if available. 69 | func (s *Service) DropAllImages(w io.Writer) error { 70 | return s.dropAllResources(ImageType, w) 71 | } 72 | 73 | // DropAllRaws deletes all remote raw files from Cloudinary. File names are 74 | // written to io.Writer if available. 75 | func (s *Service) DropAllRaws(w io.Writer) error { 76 | return s.dropAllResources(RawType, w) 77 | } 78 | 79 | // DropAll deletes all remote resources (both images and raw files) from Cloudinary. 80 | // File names are written to io.Writer if available. 81 | func (s *Service) DropAll(w io.Writer) error { 82 | if err := s.DropAllImages(w); err != nil { 83 | return err 84 | } 85 | if err := s.DropAllRaws(w); err != nil { 86 | return err 87 | } 88 | return nil 89 | } 90 | 91 | func (s *Service) doGetResources(rtype ResourceType) ([]*Resource, error) { 92 | qs := url.Values{ 93 | "max_results": []string{strconv.FormatInt(maxResults, 10)}, 94 | } 95 | path := pathListAllImages 96 | if rtype == RawType { 97 | path = pathListAllRaws 98 | } else if rtype == VideoType { 99 | path = pathListAllVideos 100 | } 101 | 102 | allres := make([]*Resource, 0) 103 | for { 104 | resp, err := http.Get(fmt.Sprintf("%s%s?%s", s.adminURI, path, qs.Encode())) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | rs := new(resourceList) 110 | dec := json.NewDecoder(resp.Body) 111 | if err := dec.Decode(rs); err != nil { 112 | return nil, err 113 | } 114 | for _, res := range rs.Resources { 115 | allres = append(allres, res) 116 | } 117 | if rs.NextCursor > 0 { 118 | qs.Set("next_cursor", strconv.FormatInt(rs.NextCursor, 10)) 119 | } else { 120 | break 121 | } 122 | } 123 | return allres, nil 124 | } 125 | 126 | func (s *Service) doGetResourceDetails(publicId string) (*ResourceDetails, error) { 127 | path := pathListSingleImage 128 | 129 | resp, err := http.Get(fmt.Sprintf("%s%s%s", s.adminURI, path, publicId)) 130 | if err != nil { 131 | return nil, err 132 | } 133 | details := new(ResourceDetails) 134 | dec := json.NewDecoder(resp.Body) 135 | if err := dec.Decode(details); err != nil { 136 | return nil, err 137 | } 138 | return details, nil 139 | } 140 | 141 | // Resources returns a list of all uploaded resources. They can be 142 | // images or raw files, depending on the resource type passed in rtype. 143 | // Cloudinary can return a limited set of results. Pagination is supported, 144 | // so the full set of results is returned. 145 | func (s *Service) Resources(rtype ResourceType) ([]*Resource, error) { 146 | return s.doGetResources(rtype) 147 | } 148 | 149 | // GetResourceDetails gets the details of a single resource that is specified by publicId. 150 | func (s *Service) ResourceDetails(publicId string) (*ResourceDetails, error) { 151 | return s.doGetResourceDetails(publicId) 152 | } 153 | -------------------------------------------------------------------------------- /cloudinary/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Mathias Monnerville and Anthony Baillard. 2 | // All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package main 7 | 8 | import ( 9 | "errors" 10 | "flag" 11 | "fmt" 12 | "net/url" 13 | "os" 14 | "strings" 15 | 16 | "github.com/gotsunami/go-cloudinary" 17 | "github.com/outofpluto/goconfig/config" 18 | ) 19 | 20 | type Config struct { 21 | // Url to the CLoudinary service. 22 | CloudinaryURI *url.URL 23 | // Url to a MongoDB instance, used to track files and upload 24 | // only changed. Optional. 25 | MongoURI *url.URL 26 | // Regexp pattern to prevent remote file deletion. 27 | KeepFilesPattern string 28 | // An optional remote prepend path, used to generate a unique 29 | // data path to a remote resource. This can be useful if public 30 | // ids are not random (i.e provided as request arguments) to solve 31 | // any caching issue: a different prepend path generates a new path 32 | // to the remote resource. 33 | PrependPath string 34 | // ProdTag is an alias to PrependPath. If PrependPath is empty but 35 | // ProdTag is set (with at prodtag= line in the [global] section of 36 | // the config file), PrependPath is set to ProdTag. For example, it 37 | // can be used with a DVCS commit tag to force new remote data paths 38 | // to remote resources. 39 | ProdTag string 40 | } 41 | 42 | var service *cloudinary.Service 43 | 44 | // Parses all structure fields values, looks for any 45 | // variable defined as ${VARNAME} and substitute it by 46 | // calling os.Getenv(). 47 | // 48 | // The reflect package is not used here since we cannot 49 | // set a private field (not exported) within a struct using 50 | // reflection. 51 | func (c *Config) handleEnvVars() error { 52 | // [cloudinary] 53 | if c.CloudinaryURI != nil { 54 | curi, err := handleQuery(c.CloudinaryURI) 55 | if err != nil { 56 | return err 57 | } 58 | c.CloudinaryURI = curi 59 | } 60 | if len(c.PrependPath) == 0 { 61 | // [global] 62 | if len(c.ProdTag) > 0 { 63 | ptag, err := replaceEnvVars(c.ProdTag) 64 | if err != nil { 65 | return err 66 | } 67 | c.PrependPath = cloudinary.EnsureTrailingSlash(ptag) 68 | } 69 | } 70 | 71 | // [database] 72 | if c.MongoURI != nil { 73 | muri, err := handleQuery(c.MongoURI) 74 | if err != nil { 75 | return err 76 | } 77 | c.MongoURI = muri 78 | } 79 | return nil 80 | } 81 | 82 | // LoadConfig parses a config file and sets global settings 83 | // variables to be used at runtime. Note that returning an error 84 | // will cause the application to exit with code error 1. 85 | func LoadConfig(path string) (*Config, error) { 86 | settings := &Config{} 87 | 88 | c, err := config.ReadDefault(path) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | // Cloudinary settings 94 | var cURI *url.URL 95 | var uri string 96 | 97 | if uri, err = c.String("cloudinary", "uri"); err != nil { 98 | return nil, err 99 | } 100 | if cURI, err = url.Parse(uri); err != nil { 101 | return nil, errors.New(fmt.Sprint("cloudinary URI: ", err.Error())) 102 | } 103 | settings.CloudinaryURI = cURI 104 | 105 | // An optional remote prepend path 106 | if prepend, err := c.String("cloudinary", "prepend"); err == nil { 107 | settings.PrependPath = cloudinary.EnsureTrailingSlash(prepend) 108 | } 109 | settings.ProdTag, _ = c.String("global", "prodtag") 110 | 111 | // Keep files regexp? (optional) 112 | var pattern string 113 | pattern, _ = c.String("cloudinary", "keepfiles") 114 | if pattern != "" { 115 | settings.KeepFilesPattern = pattern 116 | } 117 | 118 | // mongodb section (optional) 119 | uri, _ = c.String("database", "uri") 120 | if uri != "" { 121 | var mURI *url.URL 122 | if mURI, err = url.Parse(uri); err != nil { 123 | return nil, errors.New(fmt.Sprint("mongoDB URI: ", err.Error())) 124 | } 125 | settings.MongoURI = mURI 126 | } else { 127 | fmt.Fprintf(os.Stderr, "Warning: database not set (upload sync disabled)\n") 128 | } 129 | 130 | // Looks for env variables, perform substitutions if needed 131 | if err := settings.handleEnvVars(); err != nil { 132 | return nil, err 133 | } 134 | return settings, nil 135 | } 136 | 137 | func fail(msg string) { 138 | fmt.Fprintf(os.Stderr, "Error: %s\n", msg) 139 | os.Exit(1) 140 | } 141 | 142 | func printResources(res []*cloudinary.Resource, err error) { 143 | if err != nil { 144 | fail(err.Error()) 145 | } 146 | if len(res) == 0 { 147 | fmt.Println("No resource found.") 148 | return 149 | } 150 | fmt.Printf("%-30s %-10s %-5s %s\n", "public_id", "Version", "Type", "Size") 151 | fmt.Println(strings.Repeat("-", 70)) 152 | for _, r := range res { 153 | fmt.Printf("%-30s %d %s %10d\n", r.PublicId, r.Version, r.ResourceType, r.Size) 154 | } 155 | } 156 | 157 | func printResourceDetails(res *cloudinary.ResourceDetails, err error) { 158 | if err != nil { 159 | fail(err.Error()) 160 | } 161 | if res == nil || len(res.PublicId) == 0 { 162 | fmt.Println("No resource details found.") 163 | return 164 | } 165 | fmt.Printf("%-30s %-6s %-10s %-5s %-8s %-6s %-6s %-s\n", "public_id", "Format", "Version", "Type", "Size", "Width", "Height", "Url") 166 | fmt.Printf("%-30s %-6s %-10d %-5s %-8d %-6d %-6d %-s\n", res.PublicId, res.Format, res.Version, res.ResourceType, res.Size, res.Width, res.Height, res.Url) 167 | 168 | fmt.Println() 169 | 170 | for i, d := range res.Derived { 171 | if i == 0 { 172 | fmt.Printf("%-25s %-8s %-s\n", "transformation", "Size", "Url") 173 | } 174 | fmt.Printf("%-25s %-8d %-s\n", d.Transformation, d.Size, d.Url) 175 | } 176 | } 177 | 178 | func perror(err error) { 179 | fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error()) 180 | os.Exit(1) 181 | } 182 | 183 | func step(caption string) { 184 | fmt.Printf("==> %s\n", caption) 185 | } 186 | 187 | func main() { 188 | flag.Usage = func() { 189 | fmt.Fprintf(os.Stderr, fmt.Sprintf("Usage: %s [options] action settings.conf \n", os.Args[0])) 190 | fmt.Fprintf(os.Stderr, ` 191 | Actions: 192 | ls list all remote resources 193 | rm delete a remote resource 194 | up upload a local resource 195 | url get the URL of of a remote resource 196 | 197 | The config file is a plain text file with a [cloudinary] section, e.g 198 | [cloudinary] 199 | uri=cloudinary://api_key:api_secret@cloud_name 200 | `) 201 | fmt.Fprintf(os.Stderr, "\nOptions:\n") 202 | flag.PrintDefaults() 203 | os.Exit(2) 204 | } 205 | 206 | optRaw := flag.String("r", "", "raw filename or public id") 207 | optImg := flag.String("i", "", "image filename or public id") 208 | optVerbose := flag.Bool("v", false, "verbose output") 209 | optSimulate := flag.Bool("s", false, "simulate, do nothing (dry run)") 210 | optAll := flag.Bool("a", false, "applies to all resource files") 211 | flag.Parse() 212 | 213 | if len(flag.Args()) != 2 { 214 | flag.Usage() 215 | } 216 | 217 | action := flag.Arg(0) 218 | supportedAction := func(act string) bool { 219 | switch act { 220 | case "ls", "rm", "up", "url": 221 | return true 222 | } 223 | return false 224 | }(action) 225 | if !supportedAction { 226 | fmt.Fprintf(os.Stderr, "Unknown action '%s'\n", action) 227 | flag.Usage() 228 | } 229 | 230 | var err error 231 | settings, err := LoadConfig(flag.Arg(1)) 232 | if err != nil { 233 | fmt.Fprintf(os.Stderr, "%s: %s\n", flag.Arg(1), err.Error()) 234 | os.Exit(1) 235 | } 236 | 237 | service, err = cloudinary.Dial(settings.CloudinaryURI.String()) 238 | service.Verbose(*optVerbose) 239 | service.Simulate(*optSimulate) 240 | service.KeepFiles(settings.KeepFilesPattern) 241 | if settings.MongoURI != nil { 242 | if err := service.UseDatabase(settings.MongoURI.String()); err != nil { 243 | fmt.Fprintf(os.Stderr, "Error connecting to mongoDB: %s\n", err.Error()) 244 | os.Exit(1) 245 | } 246 | } 247 | 248 | if err != nil { 249 | fail(err.Error()) 250 | } 251 | 252 | if *optSimulate { 253 | fmt.Println("*** DRY RUN MODE ***") 254 | } 255 | 256 | if len(settings.PrependPath) > 0 { 257 | fmt.Println("/!\\ Remote prepend path set to: ", settings.PrependPath) 258 | } else { 259 | fmt.Println("/!\\ No remote prepend path set") 260 | } 261 | 262 | switch action { 263 | case "up": 264 | if *optRaw == "" && *optImg == "" { 265 | fail("Missing -i or -r option.") 266 | } 267 | if *optRaw != "" { 268 | step("Uploading as raw data") 269 | if _, err := service.UploadStaticRaw(*optRaw, nil, settings.PrependPath); err != nil { 270 | perror(err) 271 | } 272 | } else { 273 | step("Uploading as images") 274 | if _, err := service.UploadStaticImage(*optImg, nil, settings.PrependPath); err != nil { 275 | perror(err) 276 | } 277 | } 278 | break 279 | 280 | case "rm": 281 | if *optAll { 282 | step(fmt.Sprintf("Deleting all resources...")) 283 | if err := service.DropAll(os.Stdout); err != nil { 284 | perror(err) 285 | } 286 | } else { 287 | if *optRaw == "" && *optImg == "" { 288 | fail("Missing -i or -r option.") 289 | } 290 | if *optRaw != "" { 291 | step(fmt.Sprintf("Deleting raw file %s", *optRaw)) 292 | if err := service.Delete(*optRaw, settings.PrependPath, cloudinary.RawType); err != nil { 293 | perror(err) 294 | } 295 | } else { 296 | step(fmt.Sprintf("Deleting image %s", *optImg)) 297 | if err := service.Delete(*optImg, settings.PrependPath, cloudinary.ImageType); err != nil { 298 | perror(err) 299 | } 300 | } 301 | } 302 | 303 | case "ls": 304 | if *optImg != "" { 305 | fmt.Println("==> Image Details:") 306 | printResourceDetails(service.ResourceDetails(*optImg)) 307 | } else { 308 | fmt.Println("==> Raw resources:") 309 | printResources(service.Resources(cloudinary.RawType)) 310 | fmt.Println("==> Images:") 311 | printResources(service.Resources(cloudinary.ImageType)) 312 | } 313 | 314 | case "url": 315 | if *optRaw == "" && *optImg == "" { 316 | fail("Missing -i or -r option.") 317 | } 318 | if *optRaw != "" { 319 | fmt.Println(service.Url(*optRaw, cloudinary.RawType)) 320 | } else { 321 | fmt.Println(service.Url(*optImg, cloudinary.ImageType)) 322 | } 323 | } 324 | 325 | fmt.Println("") 326 | if err != nil { 327 | fail(err.Error()) 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /cloudinary/vars.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | "regexp" 9 | "strings" 10 | ) 11 | 12 | // replaceEnvVars replaces all ${VARNAME} with their value 13 | // using os.Getenv(). 14 | func replaceEnvVars(src string) (string, error) { 15 | r, err := regexp.Compile(`\${([A-Z_]+)}`) 16 | if err != nil { 17 | return "", err 18 | } 19 | envs := r.FindAllString(src, -1) 20 | for _, varname := range envs { 21 | evar := os.Getenv(varname[2 : len(varname)-1]) 22 | if evar == "" { 23 | return "", errors.New(fmt.Sprintf("error: env var %s not defined", varname)) 24 | } 25 | src = strings.Replace(src, varname, evar, -1) 26 | } 27 | return src, nil 28 | } 29 | 30 | func handleQuery(uri *url.URL) (*url.URL, error) { 31 | qs, err := url.QueryUnescape(uri.String()) 32 | if err != nil { 33 | return nil, err 34 | } 35 | r, err := replaceEnvVars(qs) 36 | if err != nil { 37 | return nil, err 38 | } 39 | wuri, err := url.Parse(r) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return wuri, nil 44 | } 45 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: 77f7a7ef626045dd80059228fddb55782e5493a752231fe9fd925d30a895adfc 2 | updated: 2017-02-22T16:39:26.77072092+07:00 3 | imports: 4 | - name: github.com/gotsunami/go-cloudinary 5 | version: 1216300b6d57f89bfa40f47236e9f661f71a84f2 6 | - name: github.com/outofpluto/goconfig 7 | version: 0b3e87a7e8f2b425d87a1d97b2975fb421df1a55 8 | subpackages: 9 | - config 10 | - name: gopkg.in/mgo.v2 11 | version: 3f83fa5005286a7fe593b055f0d7771a7dce4655 12 | subpackages: 13 | - bson 14 | - internal/json 15 | - internal/sasl 16 | - internal/scram 17 | testImports: [] 18 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/qiscus/go-cloudinary 2 | import: 3 | - package: github.com/gotsunami/go-cloudinary 4 | - package: github.com/outofpluto/goconfig 5 | subpackages: 6 | - config 7 | - package: gopkg.in/mgo.v2 8 | subpackages: 9 | - bson 10 | -------------------------------------------------------------------------------- /service.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Mathias Monnerville and Anthony Baillard. 2 | // All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | // Package cloudinary provides support for managing static assets 7 | // on the Cloudinary service. 8 | // 9 | // The Cloudinary service allows image and raw files management in 10 | // the cloud. 11 | package cloudinary 12 | 13 | import ( 14 | "bytes" 15 | "crypto/sha1" 16 | "encoding/json" 17 | "errors" 18 | "fmt" 19 | "io" 20 | "io/ioutil" 21 | "log" 22 | "mime/multipart" 23 | "net/http" 24 | "net/url" 25 | "os" 26 | "path/filepath" 27 | "regexp" 28 | "strconv" 29 | "strings" 30 | "time" 31 | 32 | "gopkg.in/mgo.v2" 33 | "gopkg.in/mgo.v2/bson" 34 | ) 35 | 36 | const ( 37 | baseUploadUrl = "https://api.cloudinary.com/v1_1" 38 | baseResourceUrl = "https://res.cloudinary.com" 39 | imageType = "image" 40 | videoType = "video" 41 | pdfType = "image" 42 | rawType = "raw" 43 | ) 44 | 45 | type ResourceType int 46 | 47 | const ( 48 | ImageType ResourceType = iota 49 | PdfType 50 | VideoType 51 | RawType 52 | ) 53 | 54 | type Service struct { 55 | cloudName string 56 | apiKey string 57 | apiSecret string 58 | uploadURI *url.URL // To upload resources 59 | adminURI *url.URL // To use the admin API 60 | uploadResType ResourceType // Upload resource type 61 | basePathDir string // Base path directory 62 | prependPath string // Remote prepend path 63 | verbose bool 64 | simulate bool // Dry run (NOP) 65 | keepFilesPattern *regexp.Regexp 66 | 67 | mongoDbURI *url.URL // Can be nil: checksum checks are disabled 68 | dbSession *mgo.Session 69 | col *mgo.Collection 70 | } 71 | 72 | // Resource holds information about an image or a raw file. 73 | type Resource struct { 74 | PublicId string `json:"public_id"` 75 | Version int `json:"version"` 76 | ResourceType string `json:"resource_type"` // image or raw 77 | Size int `json:"bytes"` // In bytes 78 | Url string `json:"url"` // Remote url 79 | SecureUrl string `json:"secure_url"` // Over https 80 | } 81 | 82 | type pagination struct { 83 | NextCursor int64 `json: "next_cursor"` 84 | } 85 | 86 | type resourceList struct { 87 | pagination 88 | Resources []*Resource `json: "resources"` 89 | } 90 | 91 | type ResourceDetails struct { 92 | PublicId string `json:"public_id"` 93 | Format string `json:"format"` 94 | Version int `json:"version"` 95 | ResourceType string `json:"resource_type"` // image or raw 96 | Size int `json:"bytes"` // In bytes 97 | Width int `json:"width"` // Width 98 | Height int `json:"height"` // Height 99 | Url string `json:"url"` // Remote url 100 | SecureUrl string `json:"secure_url"` // Over https 101 | Derived []*Derived `json:"derived"` // Derived 102 | } 103 | 104 | type Derived struct { 105 | Transformation string `json:"transformation"` // Transformation 106 | Size int `json:"bytes"` // In bytes 107 | Url string `json:"url"` // Remote url 108 | } 109 | 110 | // Upload response after uploading a file. 111 | type uploadResponse struct { 112 | Id string `bson:"_id"` 113 | PublicId string `json:"public_id"` 114 | Version uint `json:"version"` 115 | Format string `json:"format"` 116 | ResourceType string `json:"resource_type"` // "image" or "raw" 117 | Size int `json:"bytes"` // In bytes 118 | Checksum string // SHA1 Checksum 119 | } 120 | 121 | // Dial will use the url to connect to the Cloudinary service. 122 | // The uri parameter must be a valid URI with the cloudinary:// scheme, 123 | // e.g. 124 | // cloudinary://api_key:api_secret@cloud_name 125 | func Dial(uri string) (*Service, error) { 126 | u, err := url.Parse(uri) 127 | if err != nil { 128 | return nil, err 129 | } 130 | if u.Scheme != "cloudinary" { 131 | return nil, errors.New("Missing cloudinary:// scheme in URI") 132 | } 133 | secret, exists := u.User.Password() 134 | if !exists { 135 | return nil, errors.New("No API secret provided in URI.") 136 | } 137 | s := &Service{ 138 | cloudName: u.Host, 139 | apiKey: u.User.Username(), 140 | apiSecret: secret, 141 | uploadResType: ImageType, 142 | simulate: false, 143 | verbose: false, 144 | } 145 | // Default upload URI to the service. Can change at runtime in the 146 | // Upload() function for raw file uploading. 147 | up, err := url.Parse(fmt.Sprintf("%s/%s/image/upload/", baseUploadUrl, s.cloudName)) 148 | if err != nil { 149 | return nil, err 150 | } 151 | s.uploadURI = up 152 | 153 | // Admin API url 154 | adm, err := url.Parse(fmt.Sprintf("%s/%s", baseAdminUrl, s.cloudName)) 155 | if err != nil { 156 | return nil, err 157 | } 158 | adm.User = url.UserPassword(s.apiKey, s.apiSecret) 159 | s.adminURI = adm 160 | return s, nil 161 | } 162 | 163 | // Verbose activate/desactivate debugging information on standard output. 164 | func (s *Service) Verbose(v bool) { 165 | s.verbose = v 166 | } 167 | 168 | // Simulate show what would occur but actualy don't do anything. This is a dry-run. 169 | func (s *Service) Simulate(v bool) { 170 | s.simulate = v 171 | } 172 | 173 | // KeepFiles sets a regex pattern of remote public ids that won't be deleted 174 | // by any Delete() command. This can be useful to forbid deletion of some 175 | // remote resources. This regexp pattern applies to both image and raw data 176 | // types. 177 | func (s *Service) KeepFiles(pattern string) error { 178 | if len(strings.TrimSpace(pattern)) == 0 { 179 | return nil 180 | } 181 | re, err := regexp.Compile(pattern) 182 | if err != nil { 183 | return err 184 | } 185 | s.keepFilesPattern = re 186 | return nil 187 | } 188 | 189 | // UseDatabase connects to a mongoDB database and stores upload JSON 190 | // responses, along with a source file checksum to prevent uploading 191 | // the same file twice. Stored information is used by Url() to build 192 | // a public URL for accessing the uploaded resource. 193 | func (s *Service) UseDatabase(mongoDbURI string) error { 194 | u, err := url.Parse(mongoDbURI) 195 | if err != nil { 196 | return err 197 | } 198 | if u.Scheme != "mongodb" { 199 | return errors.New("Missing mongodb:// scheme in URI") 200 | } 201 | s.mongoDbURI = u 202 | 203 | if s.verbose { 204 | log.Printf("Connecting to database %s/%s ... ", u.Host, u.Path[1:]) 205 | } 206 | dbSession, err := mgo.Dial(mongoDbURI) 207 | if err != nil { 208 | return err 209 | } 210 | if s.verbose { 211 | log.Println("Connected") 212 | } 213 | s.dbSession = dbSession 214 | s.col = s.dbSession.DB(s.mongoDbURI.Path[1:]).C("sync") 215 | return nil 216 | } 217 | 218 | // CloudName returns the cloud name used to access the Cloudinary service. 219 | func (s *Service) CloudName() string { 220 | return s.cloudName 221 | } 222 | 223 | // ApiKey returns the API key used to access the Cloudinary service. 224 | func (s *Service) ApiKey() string { 225 | return s.apiKey 226 | } 227 | 228 | // DefaultUploadURI returns the default URI used to upload images to the Cloudinary service. 229 | func (s *Service) DefaultUploadURI() *url.URL { 230 | return s.uploadURI 231 | } 232 | 233 | // cleanAssetName returns an asset name from the parent dirname and 234 | // the file name without extension. 235 | // The combination 236 | // path=/tmp/css/default.css 237 | // basePath=/tmp/ 238 | // prependPath=new/ 239 | // will return 240 | // new/css/default 241 | func cleanAssetName(path, basePath, prependPath string) string { 242 | var name string 243 | path, basePath, prependPath = strings.TrimSpace(path), strings.TrimSpace(basePath), strings.TrimSpace(prependPath) 244 | basePath, err := filepath.Abs(basePath) 245 | if err != nil { 246 | basePath = "" 247 | } 248 | apath, err := filepath.Abs(path) 249 | if err == nil { 250 | path = apath 251 | } 252 | if basePath == "" { 253 | idx := strings.LastIndex(path, string(os.PathSeparator)) 254 | if idx != -1 { 255 | idx = strings.LastIndex(path[:idx], string(os.PathSeparator)) 256 | } 257 | name = path[idx+1:] 258 | } else { 259 | // Directory 260 | name = strings.Replace(path, basePath, "", 1) 261 | if name[0] == os.PathSeparator { 262 | name = name[1:] 263 | } 264 | } 265 | if prependPath != "" { 266 | if prependPath[0] == os.PathSeparator { 267 | prependPath = prependPath[1:] 268 | } 269 | prependPath = EnsureTrailingSlash(prependPath) 270 | } 271 | r := prependPath + name[:len(name)-len(filepath.Ext(name))] 272 | return strings.Replace(r, string(os.PathSeparator), "/", -1) 273 | } 274 | 275 | // EnsureTrailingSlash adds a missing trailing / at the end 276 | // of a directory name. 277 | func EnsureTrailingSlash(dirname string) string { 278 | if !strings.HasSuffix(dirname, "/") { 279 | dirname += "/" 280 | } 281 | return dirname 282 | } 283 | 284 | func (s *Service) walkIt(path string, info os.FileInfo, err error) error { 285 | if info.IsDir() { 286 | return nil 287 | } 288 | if _, err := s.uploadFile(path, nil, false); err != nil { 289 | return err 290 | } 291 | return nil 292 | } 293 | 294 | // Upload file to the service. When using a mongoDB database for storing 295 | // file information (such as checksums), the database is updated after 296 | // any successful upload. 297 | func (s *Service) uploadFile(fullPath string, data io.Reader, randomPublicId bool) (string, error) { 298 | // Do not upload empty files 299 | fi, err := os.Stat(fullPath) 300 | if err == nil && fi.Size() == 0 { 301 | return fullPath, nil 302 | if s.verbose { 303 | fmt.Println("Not uploading empty file: ", fullPath) 304 | } 305 | } 306 | // First check we have no match before sending an HTTP query 307 | changedLocally := false 308 | if s.dbSession != nil { 309 | publicId := cleanAssetName(fullPath, s.basePathDir, s.prependPath) 310 | ext := filepath.Ext(fullPath) 311 | match := &uploadResponse{} 312 | err := s.col.Find(bson.M{"$or": []bson.M{bson.M{"_id": publicId}, bson.M{"_id": publicId + ext}}}).One(&match) 313 | if err == nil { 314 | // Current file checksum 315 | chk, err := fileChecksum(fullPath) 316 | if err != nil { 317 | return fullPath, err 318 | } 319 | if chk == match.Checksum { 320 | if s.verbose { 321 | fmt.Printf("%s: no local changes\n", fullPath) 322 | } else { 323 | fmt.Printf(".") 324 | } 325 | return fullPath, nil 326 | } else { 327 | if s.verbose { 328 | fmt.Println("File has changed locally, needs upload") 329 | } else { 330 | fmt.Printf("U") 331 | } 332 | changedLocally = true 333 | } 334 | } 335 | } 336 | buf := new(bytes.Buffer) 337 | w := multipart.NewWriter(buf) 338 | 339 | // Write public ID 340 | var publicId string 341 | if !randomPublicId { 342 | publicId = cleanAssetName(fullPath, s.basePathDir, s.prependPath) 343 | pi, err := w.CreateFormField("public_id") 344 | if err != nil { 345 | return fullPath, err 346 | } 347 | pi.Write([]byte(publicId)) 348 | } 349 | // Write API key 350 | ak, err := w.CreateFormField("api_key") 351 | if err != nil { 352 | return fullPath, err 353 | } 354 | ak.Write([]byte(s.apiKey)) 355 | 356 | // Write timestamp 357 | timestamp := strconv.FormatInt(time.Now().Unix(), 10) 358 | ts, err := w.CreateFormField("timestamp") 359 | if err != nil { 360 | return fullPath, err 361 | } 362 | ts.Write([]byte(timestamp)) 363 | 364 | // Write signature 365 | hash := sha1.New() 366 | part := fmt.Sprintf("timestamp=%s%s", timestamp, s.apiSecret) 367 | if !randomPublicId { 368 | part = fmt.Sprintf("public_id=%s&%s", publicId, part) 369 | } 370 | io.WriteString(hash, part) 371 | signature := fmt.Sprintf("%x", hash.Sum(nil)) 372 | 373 | si, err := w.CreateFormField("signature") 374 | if err != nil { 375 | return fullPath, err 376 | } 377 | si.Write([]byte(signature)) 378 | 379 | // Write file field 380 | fw, err := w.CreateFormFile("file", fullPath) 381 | if err != nil { 382 | return fullPath, err 383 | } 384 | if data != nil { // file descriptor given 385 | tmp, err := ioutil.ReadAll(data) 386 | if err != nil { 387 | return fullPath, err 388 | } 389 | fw.Write(tmp) 390 | } else { // no file descriptor, try opening the file 391 | fd, err := os.Open(fullPath) 392 | if err != nil { 393 | return fullPath, err 394 | } 395 | defer fd.Close() 396 | 397 | _, err = io.Copy(fw, fd) 398 | if err != nil { 399 | return fullPath, err 400 | } 401 | log.Printf("Uploading %s\n", fullPath) 402 | } 403 | // Don't forget to close the multipart writer to get a terminating boundary 404 | w.Close() 405 | if s.simulate { 406 | return fullPath, nil 407 | } 408 | 409 | upURI := s.uploadURI.String() 410 | 411 | if s.uploadResType == PdfType { 412 | upURI = strings.Replace(upURI, imageType, pdfType, 1) 413 | } else if s.uploadResType == VideoType { 414 | upURI = strings.Replace(upURI, imageType, videoType, 1) 415 | } else if s.uploadResType == RawType { 416 | upURI = strings.Replace(upURI, imageType, rawType, 1) 417 | } 418 | req, err := http.NewRequest("POST", upURI, buf) 419 | if err != nil { 420 | return fullPath, err 421 | } 422 | req.Header.Set("Content-Type", w.FormDataContentType()) 423 | resp, err := http.DefaultClient.Do(req) 424 | 425 | if err != nil { 426 | return fullPath, err 427 | } 428 | defer resp.Body.Close() 429 | 430 | if resp.StatusCode == http.StatusOK { 431 | // Body is JSON data and looks like: 432 | // {"public_id":"Downloads/file","version":1369431906,"format":"png","resource_type":"image"} 433 | dec := json.NewDecoder(resp.Body) 434 | upInfo := new(uploadResponse) 435 | if err := dec.Decode(upInfo); err != nil { 436 | return fullPath, err 437 | } 438 | // Write info to db 439 | if s.dbSession != nil { 440 | // Compute file's checksum 441 | chk, err := fileChecksum(fullPath) 442 | if err != nil { 443 | return fullPath, err 444 | } 445 | upInfo.Id = upInfo.PublicId // Force document id 446 | upInfo.Checksum = chk 447 | if changedLocally { 448 | if err := s.col.Update(bson.M{"_id": upInfo.PublicId}, upInfo); err != nil { 449 | return fullPath, err 450 | } 451 | } else { 452 | if err := s.col.Insert(upInfo); err != nil { 453 | return fullPath, err 454 | } 455 | } 456 | } 457 | return upInfo.PublicId, nil 458 | } else { 459 | return fullPath, errors.New("Request error: " + resp.Status) 460 | } 461 | } 462 | 463 | // helpers 464 | func (s *Service) UploadStaticRaw(path string, data io.Reader, prepend string) (string, error) { 465 | return s.Upload(path, data, prepend, false, RawType) 466 | } 467 | 468 | func (s *Service) UploadStaticImage(path string, data io.Reader, prepend string) (string, error) { 469 | return s.Upload(path, data, prepend, false, ImageType) 470 | } 471 | 472 | func (s *Service) UploadRaw(path string, data io.Reader, prepend string) (string, error) { 473 | return s.Upload(path, data, prepend, false, RawType) 474 | } 475 | 476 | func (s *Service) UploadImage(path string, data io.Reader, prepend string) (string, error) { 477 | return s.Upload(path, data, prepend, false, ImageType) 478 | } 479 | 480 | func (s *Service) UploadVideo(path string, data io.Reader, prepend string) (string, error) { 481 | return s.Upload(path, data, prepend, false, VideoType) 482 | } 483 | 484 | func (s *Service) UploadPdf(path string, data io.Reader, prepend string) (string, error) { 485 | return s.Upload(path, data, prepend, false, PdfType) 486 | } 487 | 488 | // Upload a file or a set of files to the cloud. The path parameter is 489 | // a file location or a directory. If the source path is a directory, 490 | // all files are recursively uploaded to Cloudinary. 491 | // 492 | // In order to upload content, path is always required (used to get the 493 | // directory name or resource name if randomPublicId is false) but data 494 | // can be nil. If data is non-nil the content of the file will be read 495 | // from it. If data is nil, the function will try to open filename(s) 496 | // specified by path. 497 | // 498 | // If ramdomPublicId is true, the service generates a unique random public 499 | // id. Otherwise, the resource's public id is computed using the absolute 500 | // path of the file. 501 | // 502 | // Set rtype to the target resource type, e.g. image or raw file. 503 | // 504 | // For example, a raw file /tmp/css/default.css will be stored with a public 505 | // name of css/default.css (raw file keeps its extension), but an image file 506 | // /tmp/images/logo.png will be stored as images/logo. 507 | // 508 | // The function returns the public identifier of the resource. 509 | func (s *Service) Upload(path string, data io.Reader, prepend string, randomPublicId bool, rtype ResourceType) (string, error) { 510 | s.uploadResType = rtype 511 | s.basePathDir = "" 512 | s.prependPath = prepend 513 | if data == nil { 514 | info, err := os.Stat(path) 515 | if err != nil { 516 | return path, err 517 | } 518 | 519 | if info.IsDir() { 520 | s.basePathDir = path 521 | if err := filepath.Walk(path, s.walkIt); err != nil { 522 | return path, err 523 | } 524 | } else { 525 | return s.uploadFile(path, nil, randomPublicId) 526 | } 527 | } else { 528 | return s.uploadFile(path, data, randomPublicId) 529 | } 530 | return path, nil 531 | } 532 | 533 | // Url returns the complete access path in the cloud to the 534 | // resource designed by publicId or the empty string if 535 | // no match. 536 | func (s *Service) Url(publicId string, rtype ResourceType) string { 537 | path := imageType 538 | if rtype == PdfType { 539 | path = pdfType 540 | } else if rtype == VideoType { 541 | path = videoType 542 | } else if rtype == RawType { 543 | path = rawType 544 | } 545 | return fmt.Sprintf("%s/%s/%s/upload/%s", baseResourceUrl, s.cloudName, path, publicId) 546 | } 547 | 548 | func handleHttpResponse(resp *http.Response) (map[string]interface{}, error) { 549 | if resp == nil { 550 | return nil, errors.New("nil http response") 551 | } 552 | dec := json.NewDecoder(resp.Body) 553 | var msg interface{} 554 | if err := dec.Decode(&msg); err != nil { 555 | return nil, err 556 | } 557 | m := msg.(map[string]interface{}) 558 | if resp.StatusCode != http.StatusOK { 559 | // JSON error looks like {"error":{"message":"Missing required parameter - public_id"}} 560 | if e, ok := m["error"]; ok { 561 | return nil, errors.New(e.(map[string]interface{})["message"].(string)) 562 | } 563 | return nil, errors.New(resp.Status) 564 | } 565 | return m, nil 566 | } 567 | 568 | // Delete deletes a resource uploaded to Cloudinary. 569 | func (s *Service) Delete(publicId, prepend string, rtype ResourceType) error { 570 | // TODO: also delete resource entry from database (if used) 571 | timestamp := strconv.FormatInt(time.Now().Unix(), 10) 572 | data := url.Values{ 573 | "api_key": []string{s.apiKey}, 574 | "public_id": []string{prepend + publicId}, 575 | "timestamp": []string{timestamp}, 576 | } 577 | if s.keepFilesPattern != nil { 578 | if s.keepFilesPattern.MatchString(prepend + publicId) { 579 | fmt.Println("keep") 580 | return nil 581 | } 582 | } 583 | if s.simulate { 584 | fmt.Println("ok") 585 | return nil 586 | } 587 | 588 | // Signature 589 | hash := sha1.New() 590 | part := fmt.Sprintf("public_id=%s×tamp=%s%s", prepend+publicId, timestamp, s.apiSecret) 591 | io.WriteString(hash, part) 592 | data.Set("signature", fmt.Sprintf("%x", hash.Sum(nil))) 593 | 594 | rt := imageType 595 | if rtype == RawType { 596 | rt = rawType 597 | } 598 | resp, err := http.PostForm(fmt.Sprintf("%s/%s/%s/destroy/", baseUploadUrl, s.cloudName, rt), data) 599 | if err != nil { 600 | return err 601 | } 602 | 603 | m, err := handleHttpResponse(resp) 604 | if err != nil { 605 | return err 606 | } 607 | if e, ok := m["result"]; ok { 608 | fmt.Println(e.(string)) 609 | } 610 | // Remove DB entry 611 | if s.dbSession != nil { 612 | if err := s.col.Remove(bson.M{"_id": prepend + publicId}); err != nil { 613 | return errors.New("can't remove entry from DB: " + err.Error()) 614 | } 615 | } 616 | return nil 617 | } 618 | 619 | func (s *Service) Rename(publicID, toPublicID, prepend string, rtype ResourceType) error { 620 | publicID = strings.TrimPrefix(publicID, "/") 621 | toPublicID = strings.TrimPrefix(toPublicID, "/") 622 | timestamp := fmt.Sprintf(`%d`, time.Now().Unix()) 623 | data := url.Values{ 624 | "api_key": []string{s.apiKey}, 625 | "from_public_id": []string{prepend + publicID}, 626 | "timestamp": []string{timestamp}, 627 | "to_public_id": []string{prepend + toPublicID}, 628 | } 629 | // Signature 630 | hash := sha1.New() 631 | part := fmt.Sprintf("from_public_id=%s×tamp=%s&to_public_id=%s%s", prepend+publicID, timestamp, toPublicID, s.apiSecret) 632 | io.WriteString(hash, part) 633 | data.Set("signature", fmt.Sprintf("%x", hash.Sum(nil))) 634 | 635 | rt := imageType 636 | if rtype == RawType { 637 | rt = rawType 638 | } 639 | resp, err := http.PostForm(fmt.Sprintf("%s/%s/%s/rename", baseUploadUrl, s.cloudName, rt), data) 640 | if err != nil { 641 | return err 642 | } 643 | defer resp.Body.Close() 644 | 645 | if resp.StatusCode != http.StatusOK { 646 | body, _ := ioutil.ReadAll(resp.Body) 647 | return errors.New(string(body)) 648 | } 649 | return nil 650 | } 651 | -------------------------------------------------------------------------------- /service_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Mathias Monnerville and Anthony Baillard. 2 | // All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | package cloudinary 6 | 7 | import ( 8 | "fmt" 9 | "testing" 10 | ) 11 | 12 | func TestDial(t *testing.T) { 13 | if _, err := Dial("baduri::"); err == nil { 14 | t.Error("should fail on bad uri") 15 | } 16 | 17 | // Not a cloudinary:// URL scheme 18 | if _, err := Dial("http://localhost"); err == nil { 19 | t.Error("should fail if URL scheme different from cloudinary://") 20 | } 21 | 22 | // Missing API secret (password)? 23 | if _, err := Dial("cloudinary://login@cloudname"); err == nil { 24 | t.Error("should fail when no API secret is provided") 25 | } 26 | 27 | k := &Service{ 28 | cloudName: "cloudname", 29 | apiKey: "login", 30 | apiSecret: "secret", 31 | } 32 | s, err := Dial(fmt.Sprintf("cloudinary://%s:%s@%s", k.apiKey, k.apiSecret, k.cloudName)) 33 | if err != nil { 34 | t.Error("expect a working service at this stage but got an error.") 35 | } 36 | if s.cloudName != k.cloudName || s.apiKey != k.apiKey || s.apiSecret != k.apiSecret { 37 | t.Errorf("wrong service instance. Expect %v, got %v", k, s) 38 | } 39 | uexp := fmt.Sprintf("%s/%s/image/upload/", baseUploadUrl, s.cloudName) 40 | if s.uploadURI.String() != uexp { 41 | t.Errorf("wrong upload URI. Expect %s, got %s", uexp, s.uploadURI.String()) 42 | } 43 | 44 | } 45 | 46 | func TestVerbose(t *testing.T) { 47 | s := new(Service) 48 | s.Verbose(true) 49 | if !s.verbose { 50 | t.Errorf("wrong verbose attribute. Expect %v, got %v", true, s.verbose) 51 | } 52 | } 53 | 54 | func TestSimulate(t *testing.T) { 55 | s := new(Service) 56 | s.Simulate(true) 57 | if !s.simulate { 58 | t.Errorf("wrong simulate attribute. Expect %v, got %v", true, s.simulate) 59 | } 60 | } 61 | 62 | func TestKeepFiles(t *testing.T) { 63 | s := new(Service) 64 | if err := s.KeepFiles(""); err != nil { 65 | t.Error("empty pattern should not raise an error") 66 | } 67 | pat := "[[;" 68 | if err := s.KeepFiles(pat); err == nil { 69 | t.Errorf("wrong pattern %s should raise an error", pat) 70 | } 71 | pat = "images/\\.jpg$" 72 | err := s.KeepFiles(pat) 73 | if err != nil { 74 | t.Errorf("valid pattern should return no error", pat) 75 | } 76 | if s.keepFilesPattern == nil { 77 | t.Errorf(".keepFilesPattern attribute is still nil with a valid pattern") 78 | } 79 | } 80 | 81 | func TestUseDatabase(t *testing.T) { 82 | s := new(Service) 83 | if err := s.UseDatabase("baduri::"); err == nil { 84 | t.Error("should fail on bad uri") 85 | } 86 | // Bad scheme 87 | if err := s.UseDatabase("http://localhost"); err == nil { 88 | t.Error("should fail if URL scheme different from mongodb://") 89 | } 90 | if err := s.UseDatabase("mongodb://localhost/cloudinary"); err != nil { 91 | t.Error("please ensure you have a running MongoDB server on localhost") 92 | } 93 | if s.dbSession == nil || s.col == nil { 94 | t.Error("service's dbSession and col should not be nil") 95 | } 96 | } 97 | 98 | func TestCleanAssetName(t *testing.T) { 99 | assets := [][4]string{ 100 | // order: path, basepath, prepend, expected result 101 | {"/tmp/css/default.css", "/tmp/", "new", "new/css/default"}, 102 | {"/a/b/c.png", "/a", "", "b/c"}, 103 | {"/a/b/c.png", "/a ", " ", "b/c"}, // With spaces 104 | {"/a/b/c.png", "", "/x", "x/a/b/c"}, 105 | } 106 | for _, p := range assets { 107 | c := cleanAssetName(p[0], p[1], p[2]) 108 | if c != p[3] { 109 | t.Errorf("wrong cleaned name. Expect '%s', got '%s'", p[3], c) 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Mathias Monnerville and Anthony Baillard. 2 | // All rights reserved. 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file. 5 | 6 | package cloudinary 7 | 8 | import ( 9 | "crypto/sha1" 10 | "fmt" 11 | "io" 12 | "io/ioutil" 13 | ) 14 | 15 | // Returns SHA1 file checksum 16 | func fileChecksum(path string) (string, error) { 17 | data, err := ioutil.ReadFile(path) 18 | if err != nil { 19 | return "", err 20 | } 21 | hash := sha1.New() 22 | io.WriteString(hash, string(data)) 23 | return fmt.Sprintf("%x", hash.Sum(nil)), nil 24 | } 25 | --------------------------------------------------------------------------------