Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions asset/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ func (a Asset) String() (result string, err error) {
}
}()

a.Transformation = transformation.RawTransformation(collapseSlashes(string(a.Transformation)))
a.PublicID = collapseSlashes(a.PublicID)

assetURL := a.assetURL()
query := a.query()

Expand Down Expand Up @@ -268,10 +271,9 @@ func (a Asset) source() string {
source := fileNameWithoutExt(a.PublicID)

if !isURL(source) {
var err error
source, err = url.QueryUnescape(strings.Replace(source, "%20", "+", -1))
if err != nil {
panic(err)
// Decode a pre-escaped public ID. Keep it as is if it is not valid escaping (for example, a literal "%").
if unescaped, err := url.PathUnescape(source); err == nil {
source = unescaped
}
}

Expand Down Expand Up @@ -339,8 +341,16 @@ func isURL(candidate string) bool {
return urlRegexp.MatchString(candidate)
}

var multipleSlashesRegexp = regexp.MustCompile(`([^:])/+`)

// collapseSlashes replaces "//" with "/", but keeps "://".
func collapseSlashes(str string) string {
return multipleSlashesRegexp.ReplaceAllString(str, "$1/")
}

func smartEscape(str string) string {
revert := strings.NewReplacer("%3A", ":", "%2F", "/")
// QueryEscape encodes a space as "+" (a literal "+" becomes "%2B"). In a URL path, a space must be "%20".
revert := strings.NewReplacer("%3A", ":", "%2F", "/", "+", "%20")

return revert.Replace(url.QueryEscape(str))
}
35 changes: 35 additions & 0 deletions asset/asset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"github.com/cloudinary/cloudinary-go/v2/api"
"github.com/cloudinary/cloudinary-go/v2/asset"
"github.com/cloudinary/cloudinary-go/v2/config"
"github.com/cloudinary/cloudinary-go/v2/internal/cldtest"
"github.com/stretchr/testify/assert"
"testing"
Expand Down Expand Up @@ -108,3 +109,37 @@ func TestAsset_Media(t *testing.T) {
}
assert.Contains(t, getAssetUrl(t, m), fmt.Sprintf("image/upload/%s", cldtest.PublicID))
}

// Expected URLs match the Python SDK (cloudinary_url with raw_transformation).
func TestAsset_PublicIDEncoding(t *testing.T) {
conf, _ := config.NewFromParams(cldtest.CloudName, cldtest.APIKey, cldtest.APISecret)

cases := []struct{ publicID, expected string }{
{"my image", "image/upload/my%20image"},
{"a+b", "image/upload/a%2Bb"},
{"100%", "image/upload/100%25"},
{"a%20b", "image/upload/a%20b"},
{"a%2Bb", "image/upload/a%2Bb"},
}

for _, c := range cases {
i, _ := asset.Image(c.publicID, conf)
assert.Contains(t, getAssetUrl(t, i), c.expected, c.publicID)
}
}

func TestAsset_CollapseSlashes(t *testing.T) {
conf, _ := config.NewFromParams(cldtest.CloudName, cldtest.APIKey, cldtest.APISecret)

i, _ := asset.Image("folder//img", conf)
i.Transformation = "w_100//e_sepia"

assert.Contains(t, getAssetUrl(t, i), "image/upload/w_100/e_sepia/v1/folder/img")

s, _ := asset.Image("sample", conf)
s.DeliveryType = api.Authenticated
s.Transformation = "w_100//e_sepia"
s.Config.URL.SignURL = true

assert.Contains(t, getAssetUrl(t, s), "image/authenticated/s--15JMHXkE--/w_100/e_sepia/sample")
}
Loading