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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up JDK 8 with SBT cache
- name: Set up JDK 11 with SBT cache
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '8'
java-version: '11'
cache: 'sbt'

- name: Install SBT and jq
Expand Down
3 changes: 1 addition & 2 deletions cloudinary-core/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pomExtra := {
}

libraryDependencies ++= Seq(
"com.ning" % "async-http-client" % "1.9.40",
"org.asynchttpclient" % "async-http-client" % "3.0.3",
"org.json4s" %% "json4s-native" % "3.6.10",
"org.json4s" %% "json4s-ext" % "3.6.10",
"org.scalatest" %% "scalatest" % "3.2.2" % "test",
Expand All @@ -48,4 +48,3 @@ libraryDependencies += "org.scalamock" %% "scalamock-scalatest-support" % "3.4.1
resolvers ++= Seq("sonatype snapshots" at "https://oss.sonatype.org/content/repositories/snapshots", "sonatype releases" at "https://oss.sonatype.org/content/repositories/releases")

scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")

8 changes: 3 additions & 5 deletions cloudinary-core/src/main/scala/com/cloudinary/Api.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package com.cloudinary
import java.util.Date
import java.util.TimeZone
import scala.concurrent.Future
import com.ning.http.client.Realm
import com.ning.http.client.RequestBuilder
import org.asynchttpclient.Realm
import org.asynchttpclient.RequestBuilder
import response._
import parameters.UpdateParameters
import java.text.SimpleDateFormat
Expand Down Expand Up @@ -55,9 +55,7 @@ class Api(implicit cloudinary: Cloudinary) {
}
}

val realm = new Realm.RealmBuilder()
.setPrincipal(cloudinary.apiKey())
.setPassword(cloudinary.apiSecret())
val realm = new Realm.Builder(cloudinary.apiKey(), cloudinary.apiSecret())
.setUsePreemptiveAuth(true)
.setScheme(Realm.AuthScheme.BASIC)
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.cloudinary

import scala.concurrent.Promise

import _root_.com.ning.http.client.{
import _root_.org.asynchttpclient.{
AsyncHandler,
AsyncCompletionHandler,
HttpResponseStatus,
Expand Down Expand Up @@ -68,4 +68,4 @@ class AsyncCloudinaryHandler[JValue](result: Promise[JsonAST.JValue]) extends As
case Left(e) => result.failure(e)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import java.security.NoSuchAlgorithmException
import java.net.URI
import java.net.URLDecoder
import java.io.UnsupportedEncodingException
import _root_.com.ning.http.client.RequestBuilder
import _root_.org.asynchttpclient.RequestBuilder

object Cloudinary {
final val CF_SHARED_CDN = "d3jpl91pxevbkh.cloudfront.net";
Expand Down Expand Up @@ -60,7 +60,7 @@ object Cloudinary {
val digest = sign(params.mkString("&"), apiSecret)
bytes2Hex(digest).toLowerCase()
}

def sign(toSign:String, apiSecret: String) = {
var md: MessageDigest = null
try {
Expand Down
15 changes: 8 additions & 7 deletions cloudinary-core/src/main/scala/com/cloudinary/HttpClient.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package com.cloudinary

import java.util.concurrent.atomic.AtomicReference
import java.time.Duration

import com.ning.http.client.AsyncHttpClientConfig
import com.ning.http.client.AsyncHttpClient
import org.asynchttpclient.{AsyncHttpClient, DefaultAsyncHttpClient, DefaultAsyncHttpClientConfig}
import org.json4s._
import org.json4s.native.JsonMethods._

import scala.concurrent.{Future, Promise}
import com.ning.http.client.Request
import org.asynchttpclient.Request

import concurrent.ExecutionContext.Implicits.global
import com.cloudinary.response.RawResponse
Expand Down Expand Up @@ -53,10 +53,11 @@ object HttpClient {

private[cloudinary] def newClient(): AsyncHttpClient = {

val asyncHttpConfig = new AsyncHttpClientConfig.Builder()
asyncHttpConfig.setUserAgent(Cloudinary.USER_AGENT)
asyncHttpConfig.setReadTimeout(-1)
new AsyncHttpClient(asyncHttpConfig.build())
val asyncHttpConfig = new DefaultAsyncHttpClientConfig.Builder()
.setUserAgent(Cloudinary.USER_AGENT)
.setReadTimeout(Duration.ofMillis(-1))
.build()
new DefaultAsyncHttpClient(asyncHttpConfig)
}

/**
Expand Down
13 changes: 7 additions & 6 deletions cloudinary-core/src/main/scala/com/cloudinary/Uploader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package com.cloudinary

import java.io._
import java.nio.charset.StandardCharsets
import java.time.Duration

import com.cloudinary.parameters._
import com.cloudinary.response._
import com.ning.http.client.RequestBuilder
import com.ning.http.client.multipart.{ByteArrayPart, FilePart, StringPart}
import org.asynchttpclient.RequestBuilder
import org.asynchttpclient.request.body.multipart.{ByteArrayPart, FilePart, StringPart}
import org.json4s.JsonDSL._
import org.json4s.native.JsonMethods._

Expand All @@ -31,7 +32,7 @@ class Uploader(implicit val cloudinary: Cloudinary) {

val apiUrlBuilder = new RequestBuilder("POST")
apiUrlBuilder.setUrl(apiUrl)
requestTimeout.map(timeout => apiUrlBuilder.setRequestTimeout(timeout)) //in milliseconds
requestTimeout.map(timeout => apiUrlBuilder.setRequestTimeout(Duration.ofMillis(timeout))) //in milliseconds

processedParams foreach {
param =>
Expand All @@ -40,10 +41,10 @@ class Uploader(implicit val cloudinary: Cloudinary) {
case list: Iterable[_] => list.foreach {
v =>
apiUrlBuilder.addBodyPart(
new StringPart(k + "[]", v.toString, StringPart.DEFAULT_CONTENT_TYPE, StandardCharsets.UTF_8))
new StringPart(k + "[]", v.toString))
}
case null =>
case _ => apiUrlBuilder.addBodyPart(new StringPart(k, v.toString, StringPart.DEFAULT_CONTENT_TYPE, StandardCharsets.UTF_8))
case _ => apiUrlBuilder.addBodyPart(new StringPart(k, v.toString))
}
}

Expand All @@ -52,7 +53,7 @@ class Uploader(implicit val cloudinary: Cloudinary) {
case fp: FilePart => apiUrlBuilder.addBodyPart(fp)
case f: File => apiUrlBuilder.addBodyPart(new FilePart("file", f))
case fn: String if !fn.matches(illegalFileName) => apiUrlBuilder.addBodyPart(new FilePart("file", new File(fn)))
case body: String => apiUrlBuilder.addBodyPart(new StringPart("file", body, StringPart.DEFAULT_CONTENT_TYPE, StandardCharsets.UTF_8))
case body: String => apiUrlBuilder.addBodyPart(new StringPart("file", body))
case body: Array[Byte] => apiUrlBuilder.addBodyPart(new ByteArrayPart("file", body))
case null =>
case _ => throw new IOException("Unrecognized file parameter " + file);
Expand Down
4 changes: 2 additions & 2 deletions cloudinary-core/src/main/scala/com/cloudinary/Url.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.cloudinary

import java.net.URLDecoder
import com.ning.http.util.Base64
import java.util.Base64

case class Url(
cloudName: String,
Expand Down Expand Up @@ -154,7 +154,7 @@ case class Url(
val signature = if (signUrl) {
val toSign = List(transformationStr, Some(signableSource)).flatten.mkString("/")
Some("s--" +
Base64.encode(Cloudinary.sign(toSign, apiSecret.getOrElse(throw new Exception("Must supply api secret to sign URLs")))).
Base64.getEncoder.encodeToString(Cloudinary.sign(toSign, apiSecret.getOrElse(throw new Exception("Must supply api secret to sign URLs")))).
take(8).
replace('+', '-').replace('/', '_') + "--")
} else None
Expand Down
33 changes: 13 additions & 20 deletions cloudinary-core/src/test/scala/com/cloudinary/ApiSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import java.util.{Date, TimeZone}
import com.cloudinary.Implicits._
import com.cloudinary.parameters._
import com.cloudinary.response._
import com.ning.http.client._
import org.asynchttpclient._
import org.scalamock.scalatest.MockFactory
import org.scalatest._
import matchers.should._
Expand Down Expand Up @@ -85,11 +85,10 @@ class ApiSpec extends MockableFlatSpec with Matchers with OptionValues with Insi
it should "allow listing resources with cursor" in {
val cursor = "OJNASGONQG0230JGV0JV3Q0IDVO"
val (provider, api) = mockApi()
(provider.execute _) expects where { (request: Request, _) => {
(provider.executeRequest _) expects where { (request: Request, _: AsyncHandler[_]) =>
val params = getQuery(request)
params.contains(("next_cursor", cursor))
}
}
api.resources(maxResults = 1, nextCursor = cursor)
}

Expand Down Expand Up @@ -139,12 +138,11 @@ class ApiSpec extends MockableFlatSpec with Matchers with OptionValues with Insi
df.setTimeZone(TimeZone.getTimeZone("UTC"))
val (provider, api) = mockApi()
val startAt = df.parse("22 Aug 2016 07:57:34 UTC")
(provider.execute _) expects where { (request: Request, *) => {
(provider.executeRequest _) expects where { (request: Request, _: AsyncHandler[_]) =>
val params = getQuery(request)
params.contains(("start_at", "22 Aug 2016 07:57:34 UTC GMT")) &&
params.contains(("direction", "asc"))
}
}
api.resources(`type` = "upload", startAt = Some(startAt), direction = Some(Api.ASCENDING))
}

Expand Down Expand Up @@ -244,9 +242,8 @@ class ApiSpec extends MockableFlatSpec with Matchers with OptionValues with Insi

it should "allow listing transformations with next_cursor" in {
val (provider, api) = mockApi()
(provider.execute _) expects where { (request: Request, *) => {
request.getQueryParams.contains(new Param("next_cursor", "1234567"))
}
(provider.executeRequest _) expects where { (request: Request, _: AsyncHandler[_]) =>
request.getQueryParams.contains(new Param("next_cursor", "1234567"))
}
api.transformations(nextCursor = "1234567")
}
Expand Down Expand Up @@ -282,24 +279,21 @@ class ApiSpec extends MockableFlatSpec with Matchers with OptionValues with Insi
val t = Transformation().c_("scale").w_(100)
val (provider, api) = mockApi()
inSequence {
(provider.execute _) expects where {
(request: Request, *) => {
(provider.executeRequest _) expects where {
(request: Request, _: AsyncHandler[_]) =>
request.getQueryParams.contains(new Param("next_cursor", "1234567")) &&
request.getUrl.matches(".+/" + apiTestTransformation + "?.+")
}
}
(provider.execute _) expects where {
(request: Request, *) => {
(provider.executeRequest _) expects where {
(request: Request, _: AsyncHandler[_]) =>
request.getQueryParams.contains(new Param("next_cursor", "1234567")) &&
request.getUrl.matches(".+/" + apiTestTransformation + "?.+")
}
}
(provider.execute _) expects where {
(request: Request, *) => {
(provider.executeRequest _) expects where {
(request: Request, _: AsyncHandler[_]) =>
request.getQueryParams.contains(new Param("max_results", "111")) &&
!request.getQueryParams.asScala.exists(p => p.getName == "next_cursor") &&
request.getUrl.matches(".+/" + apiTestTransformation + "?.+")
}
}
}
api.transformationByName(apiTestTransformation, "1234567")
Expand All @@ -309,10 +303,9 @@ class ApiSpec extends MockableFlatSpec with Matchers with OptionValues with Insi

it should "allow listing transformation by name with next_cursor" in {
val (provider, api) = mockApi()
provider.execute _ expects where {
(request: Request, handler: AsyncHandler[Nothing]) => {
(provider.executeRequest _) expects where {
(request: Request, _: AsyncHandler[_]) =>
request.getQueryParams.contains(new Param("next_cursor", "1234567"))
}
}
api.transformationByName(apiTestTransformation, nextCursor = "1234567")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ package com.cloudinary

import java.net.URLDecoder

import com.ning.http.client.multipart.StringPart
import com.ning.http.client.{AsyncHttpClient, AsyncHttpClientConfig, AsyncHttpProvider, Request}
import org.asynchttpclient.request.body.multipart.StringPart
import org.asynchttpclient.{AsyncHttpClient, DefaultAsyncHttpClientConfig, Request, AsyncHandler, ListenableFuture}
import org.scalamock.clazz.Mock
import org.scalamock.scalatest.MockFactory
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.BeforeAndAfterEach

import scala.collection.JavaConverters._

trait MockableAsyncHttpClient extends AsyncHttpClient {
override def executeRequest[T](request: Request, handler: AsyncHandler[T]): ListenableFuture[T]
}

class MockableFlatSpec extends AnyFlatSpec with MockFactory with BeforeAndAfterEach{
protected val prefix = "cloudinary_scala"
protected val suffix = sys.env.getOrElse("TRAVIS_JOB_ID", (10000 + scala.util.Random.nextInt(89999)).toString)
Expand All @@ -26,36 +30,34 @@ class MockableFlatSpec extends AnyFlatSpec with MockFactory with BeforeAndAfterE
}

/**
* Mock the AsyncHttpProvider so that calls do not invoke the server side.
* Expectations can be set on the execute method of AsyncHttpProvider.
* Mock the AsyncHttpClient so that calls do not invoke the server side.
* Expectations can be set on the executeRequest method of AsyncHttpClient.
* @return the mocked instance
*/
def mockHttp() = {
val mockProvider: AsyncHttpProvider = mock[AsyncHttpProvider]
val asyncHttpConfig = new AsyncHttpClientConfig.Builder()
asyncHttpConfig.setUserAgent(Cloudinary.USER_AGENT)
(mockProvider, new AsyncHttpClient(mockProvider, asyncHttpConfig.build()))
val mockClient: MockableAsyncHttpClient = mock[MockableAsyncHttpClient]
(mockClient, mockClient)
}

/**
* Returns an instance of [[com.cloudinary.Api Api]] with a mocked [[com.ning.http.client.AsyncHttpProvider AsyncHttpProvider]]
* Returns an instance of [[com.cloudinary.Api Api]] with a mocked [[org.asynchttpclient.AsyncHttpClient AsyncHttpClient]]
*/
def mockApi() = {
val api = cloudinary.api()
val (mockProvider, client) = mockHttp()
val (mockClient, client) = mockHttp()
api.httpclient.client = client
(mockProvider, api)
(mockClient, api)
}


/**
* Returns an instance of [[com.cloudinary.Uploader Uploader]] with a mocked [[com.ning.http.client.AsyncHttpProvider AsyncHttpProvider]]
* Returns an instance of [[com.cloudinary.Uploader Uploader]] with a mocked [[org.asynchttpclient.AsyncHttpClient AsyncHttpClient]]
*/
def mockUploader() = {
val uploader = cloudinary.uploader()
val (mockProvider, client) = mockHttp()
val (mockClient, client) = mockHttp()
uploader.httpclient.client = client
(mockProvider, uploader)
(mockClient, uploader)
}


Expand All @@ -65,7 +67,7 @@ class MockableFlatSpec extends AnyFlatSpec with MockFactory with BeforeAndAfterE
* @return an array of tuples in the form of (name, value)
*/
def getParts(request: Request): scala.collection.mutable.Buffer[(String, String)] = {
request.getParts.asScala.map(p => {
request.getBodyParts.asScala.map(p => {
val sp = p.asInstanceOf[StringPart]
(sp.getName, sp.getValue)
})
Expand Down
19 changes: 7 additions & 12 deletions cloudinary-core/src/test/scala/com/cloudinary/UploaderSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import java.util.concurrent.TimeoutException
import com.cloudinary.Implicits._
import com.cloudinary.parameters._
import com.cloudinary.response._
import com.ning.http.client.Request
import com.ning.http.client.multipart.StringPart
import org.asynchttpclient.{Request, AsyncHandler, ListenableFuture}
import org.asynchttpclient.request.body.multipart.StringPart
import org.scalatest.{BeforeAndAfterAll, Inside, OptionValues, Tag, matchers}
import matchers.should._

Expand Down Expand Up @@ -204,25 +204,20 @@ class UploaderSpec extends MockableFlatSpec with Matchers with OptionValues with

it should "support generating sprites" in {
val sprite_test_tag: String = "sprite_test_tag" + suffix
val (provider, uploader )= mockUploader()
val tagPart: StringPart = new StringPart("tag", sprite_test_tag, "UTF-8")
(provider.execute _) expects where { (request: Request, *) => {
val map = getParts(request)
map.contains(("tag", sprite_test_tag))
}
val (provider, uploader) = mockUploader()
(provider.executeRequest _) expects where { (request: Request, _: AsyncHandler[_]) =>
getParts(request).contains(("tag", sprite_test_tag))
}
(provider.execute _) expects where { (request: Request, *) => {
(provider.executeRequest _) expects where { (request: Request, _: AsyncHandler[_]) =>
val map = getParts(request)
map.contains(("tag", sprite_test_tag)) &&
map.contains(("transformation", "w_100"))
}
}
(provider.execute _) expects where { (request: Request, *) => {
(provider.executeRequest _) expects where { (request: Request, _: AsyncHandler[_]) =>
val map = getParts(request)
map.contains(("tag", sprite_test_tag)) &&
map.contains(("transformation", "f_jpg,w_100"))
}
}
uploader.generateSprite(sprite_test_tag)
uploader.generateSprite(sprite_test_tag, transformation = new Transformation().w_(100))
uploader.generateSprite(sprite_test_tag, transformation = new Transformation().w_(100), format = "jpg")
Expand Down