From f5f50c1f64470d508f4594c78fbee5e6417616b5 Mon Sep 17 00:00:00 2001 From: tinp Date: Wed, 26 Jul 2023 15:47:09 -0700 Subject: [PATCH 01/17] Upload input validation --- java/compress_image/Index.java | 176 ++++++++++++++++++++++++++++++++ java/compress_image/README.md | 45 ++++++++ java/compress_image/deps.gradle | 3 + 3 files changed, 224 insertions(+) create mode 100644 java/compress_image/Index.java create mode 100644 java/compress_image/README.md create mode 100644 java/compress_image/deps.gradle diff --git a/java/compress_image/Index.java b/java/compress_image/Index.java new file mode 100644 index 00000000..1f6864f2 --- /dev/null +++ b/java/compress_image/Index.java @@ -0,0 +1,176 @@ +import java.util.Map; +import java.util.HashMap; +import java.net.HttpURLConnection; +import java.net.URL; +import java.io.BufferedReader; +import java.util.stream.Collectors; +import java.util.stream.*; + +import com.google.gson.Gson; + +/** + * Enum for provider names + * @param name is provider name + * TINY_PNG is TinyPNG provider + * KRAKENIO is Kraken.io provider + * getName() is getter for provider name + */ + +private enum Provider { + TINY_PNG("tinypng"), KRAKENIO("krakenio"); + + private final String name; + + Provider(String name) { + this.name = name; + } + + String getName() { + return name; + } + + // check if provider is valid + static boolean validateProvider(String name) { + for (Provider providerName : Provider.values()) { + if (providerName.name.equals(name)) { + return true; + } + } + return false; + } +} + + final Gson gson = new Gson(); + + public RuntimeResponse main(RuntimeRequest req, RuntimeResponse res) throws Exception { + + // payload string + String payloadString = req.getPayload() == null || req.getPayload().isEmpty() ? "{}" : req.getPayload(); + + // convert payload to map + Map payload = gson.fromJson(payloadString, Map.class); + + // check if requested payload and variables are present + RuntimeResponse errorResponse = checkPayloadAndVariables(req, res); + if (errorResponse != null) { + return errorResponse; + } + + // check if payload contains provider and image + errorResponse = validatePayload(payload, res); + if (errorResponse != null) { + return errorResponse; + } + + // get provider from payload and image from payload + String provider = payload.get("provider").toString(); + String image = payload.get("image").toString(); + + // check API key is present in variables + String apiKeyVariable = Provider.TINY_PNG.getName().equals(provider) ? "TINYPNG_API_KEY" : "KRAKENIO_API_KEY"; + + errorResponse = checkEmptyApiKey(req, res, apiKeyVariable); + if (errorResponse != null) { + return errorResponse; + } + String apiKey = req.getVariables().get(apiKeyVariable); + + // compressed image in Base64 string + String compressedImage = "compressed image is under implementation"; + + // response data to return + Map responseData = new HashMap<>(); + + // TODO: compress image using provider API and store the result in compressedImage variable + + // TODO: check if compressedImage is valid + + // If input valid then return success true and compressed image + responseData.put("success", true); + responseData.put("image", compressedImage); + + return res.json(responseData); + } + + /** + * Check if requested payload and variables are present + * @param req is request object from function call + * @param res is response object from function call + * @return null if payload and variables are present, otherwise return error response + */ + private RuntimeResponse checkPayloadAndVariables(RuntimeRequest req, RuntimeResponse res) { + Map responseData = new HashMap<>(); + + // check if requested payload and variables are present + if (req.getPayload() == null || req.getPayload().trim().isEmpty() || req.getPayload().trim().equals("{}")) { + responseData.put("success", false); + responseData.put("message", "Payload is empty, please provide provider and image"); + return res.json(responseData); + } + + if (req.getVariables() == null) { + responseData.put("success", false); + responseData.put("message", "Variables are empty, please provide an API key for provider"); + return res.json(responseData); + } + return null; + } + + /** + * Validate payload whether it contains provider and image + * @param payload is an object from request payload which contains provider and image + * @param res is response object from function call + * @return null if payload is valid, otherwise return error response + */ + + private RuntimeResponse validatePayload(Map payload, RuntimeResponse res) { + Map responseData = new HashMap<>(); + + // check if payload contains provider and image + if (!payload.containsKey("provider") || !payload.containsKey("image")) { + responseData.put("success", false); + responseData.put("message", "Payload is invalid, please provide provider and image"); + return res.json(responseData); + } + + // get provider from payload and image from payload + String provider = payload.get("provider").toString(); + String image = payload.get("image").toString(); + + // check if provider is valid + if (!Provider.validateProvider(provider)) { + responseData.put("success", false); + String providerNames = Stream.of(Provider.values()).map(Provider::getName).collect(Collectors.joining(", ")); // get all provider names + responseData.put("message", "Provider " + provider + "is invalid, please provide one of providers: " + providerNames); + return res.json(responseData); + } + + // check if image is valid in Base64 with regex pattern matching + if (!image.matches("^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$") || image.trim().isEmpty()) { + responseData.put("success", false); + responseData.put("message", "Image is invalid, please provide a valid Base64 image"); + return res.json(responseData); + } + return null; + } + + /** + * Check if API key is present in variables for provider + * @param req is request object from function call + * @param res is response object from function call + * @param apiKeyVariable is API key variable name for provider + * @return null if API key is present, otherwise return error response + */ + + // check API key is present in variables + private RuntimeResponse checkEmptyApiKey(RuntimeRequest req, RuntimeResponse res, String apiKeyVariable) { + Map variables = req.getVariables(); + + if (!variables.containsKey(apiKeyVariable) || variables.get(apiKeyVariable) == null || variables.get(apiKeyVariable).trim().isEmpty()) { + Map responseData = new HashMap<>(); + responseData.put("success", false); + responseData.put("message", "API key is not present in variables, please provide " + apiKeyVariable + " for the provider"); + return res.json(responseData); + } + return null; + } \ No newline at end of file diff --git a/java/compress_image/README.md b/java/compress_image/README.md new file mode 100644 index 00000000..5a2458d3 --- /dev/null +++ b/java/compress_image/README.md @@ -0,0 +1,45 @@ +# compress_image + +Welcome to the documentation of this function 👋 We strongly recommend keeping this file in sync with your function's logic to make sure anyone can easily understand your function in the future. If you don't need documentation, you can remove this file. + +## 🤖 Documentation + +Simple function similar to typical "hello world" example, but instead, we return a simple JSON that tells everyone how awesome developers are. + + + +_Example input:_ + +This function expects no input + + + +_Example output:_ + + + +```json +{ + "areDevelopersAwesome": true +} +``` + +## 📝 Environment Variables + +List of environment variables used by this cloud function: + +- No special environment variables are required for the cloud function. + +## 🚀 Deployment + +There are two ways of deploying the Appwrite function, both having the same results, but each using a different process. We highly recommend using CLI deployment to achieve the best experience. + +### Using CLI + +Make sure you have [Appwrite CLI](https://appwrite.io/docs/command-line#installation) installed, and you have successfully logged into your Appwrite server. To make sure Appwrite CLI is ready, you can use the command `appwrite client --debug` and it should respond with green text `✓ Success`. + +Make sure you are in the same folder as your `appwrite.json` file and run `appwrite deploy function` to deploy your function. You will be prompted to select which functions you want to deploy. + +### Manual using tar.gz + +Manual deployment has no requirements and uses Appwrite Console to deploy the tag. First, enter the folder of your function. Then, create a tarball of the whole folder and gzip it. After creating `.tar.gz` file, visit Appwrite Console, click on the `Deploy Tag` button and switch to the `Manual` tab. There, set the `entrypoint` to `src/Index.java`, and upload the file we just generated. diff --git a/java/compress_image/deps.gradle b/java/compress_image/deps.gradle new file mode 100644 index 00000000..a2e7f58b --- /dev/null +++ b/java/compress_image/deps.gradle @@ -0,0 +1,3 @@ +dependencies { + implementation 'com.google.code.gson:gson:2.9.0' +} \ No newline at end of file From 1082a98a05e5e8ab155881b8dfff14d2afeefb8c Mon Sep 17 00:00:00 2001 From: brinkbrink Date: Tue, 1 Aug 2023 15:41:31 -0700 Subject: [PATCH 02/17] conversion method created --- java/compress_image/Index.java | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/java/compress_image/Index.java b/java/compress_image/Index.java index 1f6864f2..12f91884 100644 --- a/java/compress_image/Index.java +++ b/java/compress_image/Index.java @@ -5,7 +5,7 @@ import java.io.BufferedReader; import java.util.stream.Collectors; import java.util.stream.*; - +import java.util.Base64; import com.google.gson.Gson; /** @@ -173,4 +173,25 @@ private RuntimeResponse checkEmptyApiKey(RuntimeRequest req, RuntimeResponse res return res.json(responseData); } return null; - } \ No newline at end of file + } + + + /** + * Converts Base64 input of payload to byte array + * @param String baseInput is base64 string variable from payload + * @return byte [] baseInput decoded as byte array + */ + + private byte [] convertToByte(String baseInput) { + return Base64.getDecoder().decode(baseInput); + } + + /** + * Converts byte array input returned by compression method to Base64 + * @param byte [] byteInput is byte array variable returned from compression method + * @return String byteInput encoded as Base64 String + */ + + private String convertToByte(byte [] byteInput) { + return Base64.getEncoder().encodeToString(byteInput); + } From 7afa74381e975ff1a06af8143c9e3becf4e71ca2 Mon Sep 17 00:00:00 2001 From: brinkbrink Date: Tue, 1 Aug 2023 16:00:24 -0700 Subject: [PATCH 03/17] fixed method header --- java/compress_image/Index.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/compress_image/Index.java b/java/compress_image/Index.java index 12f91884..9c152dfc 100644 --- a/java/compress_image/Index.java +++ b/java/compress_image/Index.java @@ -192,6 +192,6 @@ private RuntimeResponse checkEmptyApiKey(RuntimeRequest req, RuntimeResponse res * @return String byteInput encoded as Base64 String */ - private String convertToByte(byte [] byteInput) { + private String convertToBase64(byte [] byteInput) { return Base64.getEncoder().encodeToString(byteInput); } From ea43842a4f4c85a3cbe70fd9278f0b688228e800 Mon Sep 17 00:00:00 2001 From: brinkbrink Date: Wed, 2 Aug 2023 09:05:20 -0700 Subject: [PATCH 04/17] tinify compression method --- java/compress_image/Index.java | 12 ++++++++++++ java/compress_image/deps.gradle | 1 + 2 files changed, 13 insertions(+) diff --git a/java/compress_image/Index.java b/java/compress_image/Index.java index 9c152dfc..6bef1dc7 100644 --- a/java/compress_image/Index.java +++ b/java/compress_image/Index.java @@ -195,3 +195,15 @@ private RuntimeResponse checkEmptyApiKey(RuntimeRequest req, RuntimeResponse res private String convertToBase64(byte [] byteInput) { return Base64.getEncoder().encodeToString(byteInput); } + + + /** + * Compresses image in byte array format using TinyPNG provider + * @param byte [] image is image to compress in byte array format + * @return byte [] compressed image + */ + + private String tinifyCompress(byte [] image) { + Source source = Tinify.fromBuffer(image); + return source.toBuffer(); + } diff --git a/java/compress_image/deps.gradle b/java/compress_image/deps.gradle index a2e7f58b..1698b5e5 100644 --- a/java/compress_image/deps.gradle +++ b/java/compress_image/deps.gradle @@ -1,3 +1,4 @@ dependencies { implementation 'com.google.code.gson:gson:2.9.0' + compile 'com.tinify:tinify:latest.release' } \ No newline at end of file From dae50f86331e9fa7e80b03ca693442f4d8c10ae5 Mon Sep 17 00:00:00 2001 From: brinkbrink Date: Wed, 2 Aug 2023 10:11:36 -0700 Subject: [PATCH 05/17] API key and import statement added --- java/compress_image/Index.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/java/compress_image/Index.java b/java/compress_image/Index.java index 6bef1dc7..7da19cdd 100644 --- a/java/compress_image/Index.java +++ b/java/compress_image/Index.java @@ -7,6 +7,7 @@ import java.util.stream.*; import java.util.Base64; import com.google.gson.Gson; +import com.tinify.*; /** * Enum for provider names @@ -183,6 +184,7 @@ private RuntimeResponse checkEmptyApiKey(RuntimeRequest req, RuntimeResponse res */ private byte [] convertToByte(String baseInput) { + // TODO: return Base64.getDecoder().decode(baseInput); } @@ -204,6 +206,7 @@ private String convertToBase64(byte [] byteInput) { */ private String tinifyCompress(byte [] image) { + Tinify.setKey("YOUR_API_KEY"); Source source = Tinify.fromBuffer(image); return source.toBuffer(); } From 6472a0bd367171ca1b3b5b52f69ee37f8ed190ed Mon Sep 17 00:00:00 2001 From: brinkbrink Date: Wed, 2 Aug 2023 10:13:49 -0700 Subject: [PATCH 06/17] apikey parameter --- java/compress_image/Index.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java/compress_image/Index.java b/java/compress_image/Index.java index 7da19cdd..c4d39c2a 100644 --- a/java/compress_image/Index.java +++ b/java/compress_image/Index.java @@ -205,8 +205,8 @@ private String convertToBase64(byte [] byteInput) { * @return byte [] compressed image */ - private String tinifyCompress(byte [] image) { - Tinify.setKey("YOUR_API_KEY"); + private String tinifyCompress(byte [] image, String apiKey) { + Tinify.setKey(apiKey); Source source = Tinify.fromBuffer(image); return source.toBuffer(); } From 0c9a0bda85c60804afc2adc417d393b6fe855e12 Mon Sep 17 00:00:00 2001 From: tinp Date: Wed, 9 Aug 2023 11:17:26 -0700 Subject: [PATCH 07/17] update current code --- java/compress_image/Index.java | 57 +++++++++++++++++++++++++++--- java/compress_image/README.md | 61 ++++++++++++++++++++++----------- java/compress_image/deps.gradle | 7 ++-- 3 files changed, 99 insertions(+), 26 deletions(-) diff --git a/java/compress_image/Index.java b/java/compress_image/Index.java index c4d39c2a..353ba0f0 100644 --- a/java/compress_image/Index.java +++ b/java/compress_image/Index.java @@ -7,8 +7,14 @@ import java.util.stream.*; import java.util.Base64; import com.google.gson.Gson; -import com.tinify.*; - +import com.tinify.Source; +import com.tinify.Tinify; +import java.io.*; +import io.kraken.client.model.response.SuccessfulUploadResponse; +import io.kraken.client.model.request.DirectUploadRequest; +import io.kraken.client.impl.DefaultKrakenIoClient; +import io.kraken.client.KrakenIoClient; +import io.kraken.client.exception.KrakenIoRequestException; /** * Enum for provider names * @param name is provider name @@ -75,14 +81,36 @@ public RuntimeResponse main(RuntimeRequest req, RuntimeResponse res) throws Exce return errorResponse; } String apiKey = req.getVariables().get(apiKeyVariable); + String secretKey = req.getVariables().get("KRAKENIO_API_SECRET"); // compressed image in Base64 string - String compressedImage = "compressed image is under implementation"; + String compressedImage = ""; // response data to return Map responseData = new HashMap<>(); // TODO: compress image using provider API and store the result in compressedImage variable + if (Provider.TINY_PNG.getName().equals(provider)) { + // Decode image from Base64 string + byte[] imageByte = convertToByte(image); + + // Compress image + byte[] compressedImageByte = tinifyCompress(imageByte, apiKey); + + // Encode image to Base64 string + compressedImage = convertToBase64(compressedImageByte); + } else { + // Decode input string + byte[] imageBytes = convertToByte(image); + + String urlResponse = krakenioCompress(imageBytes, apiKey, secretKey); + + URL url = new URL(urlResponse); + InputStream inputStream = url.openStream(); + byte[] compressedImageBytes = inputStream.readAllBytes(); + compressedImage = convertToBase64(compressedImageBytes); + inputStream.close(); + } // TODO: check if compressedImage is valid @@ -205,8 +233,29 @@ private String convertToBase64(byte [] byteInput) { * @return byte [] compressed image */ - private String tinifyCompress(byte [] image, String apiKey) { + private byte [] tinifyCompress(byte [] image, String apiKey) throws Exception { Tinify.setKey(apiKey); Source source = Tinify.fromBuffer(image); return source.toBuffer(); } + + private String krakenioCompress(byte [] image, String apiKey, String secretKey) throws Exception { + if (apiKey == null || apiKey.trim().isEmpty()) { + throw new IllegalArgumentException("API key is empty"); + } + final KrakenIoClient client = new DefaultKrakenIoClient(apiKey, secretKey); + + final DirectUploadRequest request = DirectUploadRequest.builder(new ByteArrayInputStream(image)).withLossy(true).build(); + final SuccessfulUploadResponse response = client.directUpload(request); + try { + + if (response.getSuccess()) { + return response.getKrakedUrl(); + } else { + throw new IOException("Kraken.io failed to compress image"); + } + } catch (Exception e) { + System.out.println("Error: " + e.getMessage()); + } + return null; + } diff --git a/java/compress_image/README.md b/java/compress_image/README.md index 5a2458d3..7f77ab40 100644 --- a/java/compress_image/README.md +++ b/java/compress_image/README.md @@ -1,45 +1,66 @@ -# compress_image +# Compress .png Images -Welcome to the documentation of this function 👋 We strongly recommend keeping this file in sync with your function's logic to make sure anyone can easily understand your function in the future. If you don't need documentation, you can remove this file. +A Ruby Cloud Function that compresses png images. -## 🤖 Documentation -Simple function similar to typical "hello world" example, but instead, we return a simple JSON that tells everyone how awesome developers are. - - +`image` and `provider` are recieved from the payload, where `image` is a base64 encoded string and `provider` is either [`tinypng`](https://tinypng.com) or [`krakenio`](https://kraken.io) _Example input:_ -This function expects no input +```json +{ + "provider": "tinypng", + "image": "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAf0lEQVR4nO2Wuw2AMAxEbw1gpMwDDMBcGQpooDKydGVAoXCK6J7k6qyc83MCCFGP/Yz+CkDF4KHmjgowbQF0CKFrCDUiwztqxabHCL0/xwcNhoI2UdsjC8g0mQvaSs1zwkg0uQAsAEaGm9/UPCeU7eMj6loTEpf6ZOQWMxd98gAhZnS6XEZcNQAAAABJRU5ErkJggg==", + +} +``` - +> `krakenio` is also a supported provider _Example output:_ - - ```json { - "areDevelopersAwesome": true + "success": true, + "image": "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgBAMAAACBVGfHAAAAG1BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACUUeIgAAAACHRSTlMA8712Sxr5g97cFtUAAAA9SURBVCjPY6Aa6AADfAIcDSA8KoBTgLGVgSFCAEmAqZmBwUIBSYClzTQ4wwE52Cs6OtpR4oFFUciBerEKAP58HnyLtZsYAAAAAElFTkSuQmCC" } ``` -## 📝 Environment Variables +## 📝 Variables -List of environment variables used by this cloud function: +> only selected provider's api keys are neccessary, ie. kraken's api keys are not neccessary when choosing tinypng as the provider. -- No special environment variables are required for the cloud function. +- **TINYPNG_API** - API key for tinypng service +- **KRAKENIO_KEY** - API key for kraken-io service +- **KRAKENIO_SECRET** - API Secret for kraken-io service ## 🚀 Deployment -There are two ways of deploying the Appwrite function, both having the same results, but each using a different process. We highly recommend using CLI deployment to achieve the best experience. +1. Clone this repository, and enter this function folder: + +``` +$ git clone https://github.com/open-runtimes/examples.git && cd examples +$ cd ruby/compress_image +``` -### Using CLI +2. Enter this function folder and build the code: +``` +docker run --rm --interactive --tty --volume $PWD:/usr/code openruntimes/java:v2-11.0 sh /usr/local/src/build.sh +``` +As a result, a `code.tar.gz` file will be generated. +3. Start the Open Runtime: +``` +docker run -p 3000:3000 -e INTERNAL_RUNTIME_KEY=secret-key -e INTERNAL_RUNTIME_ENTRYPOINT=Index.java --rm --interactive --tty --volume $PWD/code.tar.gz:/tmp/code.tar.gz:ro openruntimes/java:v2-11.0 sh /usr/local/src/start.sh +``` -Make sure you have [Appwrite CLI](https://appwrite.io/docs/command-line#installation) installed, and you have successfully logged into your Appwrite server. To make sure Appwrite CLI is ready, you can use the command `appwrite client --debug` and it should respond with green text `✓ Success`. +4. Execute function: -Make sure you are in the same folder as your `appwrite.json` file and run `appwrite deploy function` to deploy your function. You will be prompted to select which functions you want to deploy. +```shell +curl http://localhost:3000/ -d '{"variables":{"TINYPNG_API":"[YOUR_API_KEY]"},"payload":"{\"provider\":\"tinypng\",\"image\":\"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAf0lEQVR4nO2Wuw2AMAxEbw1gpMwDDMBcGQpooDKydGVAoXCK6J7k6qyc83MCCFGP/Yz+CkDF4KHmjgowbQF0CKFrCDUiwztqxabHCL0/xwcNhoI2UdsjC8g0mQvaSs1zwkg0uQAsAEaGm9/UPCeU7eMj6loTEpf6ZOQWMxd98gAhZnS6XEZcNQAAAABJRU5ErkJggg==\"}"}' -H "X-Internal-Challenge: secret-key" -H "Content-Type: application/json" +``` -### Manual using tar.gz +Your function is now listening on port `3000`, and you can execute it by sending `POST` request with appropriate authorization headers. To learn more about runtime, you can visit Python runtime [README](https://github.com/open-runtimes/open-runtimes/tree/main/runtimes/java-11.0). -Manual deployment has no requirements and uses Appwrite Console to deploy the tag. First, enter the folder of your function. Then, create a tarball of the whole folder and gzip it. After creating `.tar.gz` file, visit Appwrite Console, click on the `Deploy Tag` button and switch to the `Manual` tab. There, set the `entrypoint` to `src/Index.java`, and upload the file we just generated. +## 📝 Notes +- This function is designed for use with Appwrite Cloud Functions. You can learn more about it in [Appwrite docs](https://appwrite.io/docs/functions). +- This example is compatible with Java 11.0. Other versions may work but are not guaranteed to work as they haven't been tested. \ No newline at end of file diff --git a/java/compress_image/deps.gradle b/java/compress_image/deps.gradle index 1698b5e5..a2039a58 100644 --- a/java/compress_image/deps.gradle +++ b/java/compress_image/deps.gradle @@ -1,4 +1,7 @@ dependencies { - implementation 'com.google.code.gson:gson:2.9.0' - compile 'com.tinify:tinify:latest.release' + implementation 'com.google.code.gson:gson:2.10.1' + implementation 'com.tinify:tinify:1.8.3' + implementation('io.kraken.client:client:1.1.2'){ + exclude module: 'javax.inject' + } } \ No newline at end of file From 6b93cb19402fb4c0ba06da99b29b32a9ff097655 Mon Sep 17 00:00:00 2001 From: tinp Date: Wed, 9 Aug 2023 11:23:50 -0700 Subject: [PATCH 08/17] update current code --- java/compress_image/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/compress_image/README.md b/java/compress_image/README.md index 7f77ab40..820fbd56 100644 --- a/java/compress_image/README.md +++ b/java/compress_image/README.md @@ -40,7 +40,7 @@ _Example output:_ ``` $ git clone https://github.com/open-runtimes/examples.git && cd examples -$ cd ruby/compress_image +$ cd java/compress_image ``` 2. Enter this function folder and build the code: From 6343cd3621c5eb8fb3d73d928ce2bc03252bdc3d Mon Sep 17 00:00:00 2001 From: tinp Date: Wed, 9 Aug 2023 11:29:10 -0700 Subject: [PATCH 09/17] update current code --- java/compress_image/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/compress_image/README.md b/java/compress_image/README.md index 820fbd56..10ff6a2a 100644 --- a/java/compress_image/README.md +++ b/java/compress_image/README.md @@ -1,6 +1,6 @@ # Compress .png Images -A Ruby Cloud Function that compresses png images. +A Java Cloud Function that compresses png images. `image` and `provider` are recieved from the payload, where `image` is a base64 encoded string and `provider` is either [`tinypng`](https://tinypng.com) or [`krakenio`](https://kraken.io) From 02b628aa4fabafc13dde593049b372a5106eb83d Mon Sep 17 00:00:00 2001 From: tinp Date: Wed, 9 Aug 2023 11:43:24 -0700 Subject: [PATCH 10/17] update current code --- java/compress_image/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/java/compress_image/README.md b/java/compress_image/README.md index 10ff6a2a..1d0eec10 100644 --- a/java/compress_image/README.md +++ b/java/compress_image/README.md @@ -30,9 +30,9 @@ _Example output:_ > only selected provider's api keys are neccessary, ie. kraken's api keys are not neccessary when choosing tinypng as the provider. -- **TINYPNG_API** - API key for tinypng service -- **KRAKENIO_KEY** - API key for kraken-io service -- **KRAKENIO_SECRET** - API Secret for kraken-io service +- **TINYPNG_API_KEY** - API key for tinypng service +- **KRAKENIO_API_KEY** - API key for kraken-io service +- **KRAKENIO_API_SECRET** - API Secret for kraken-io service ## 🚀 Deployment @@ -56,7 +56,7 @@ docker run -p 3000:3000 -e INTERNAL_RUNTIME_KEY=secret-key -e INTERNAL_RUNTIME_E 4. Execute function: ```shell -curl http://localhost:3000/ -d '{"variables":{"TINYPNG_API":"[YOUR_API_KEY]"},"payload":"{\"provider\":\"tinypng\",\"image\":\"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAf0lEQVR4nO2Wuw2AMAxEbw1gpMwDDMBcGQpooDKydGVAoXCK6J7k6qyc83MCCFGP/Yz+CkDF4KHmjgowbQF0CKFrCDUiwztqxabHCL0/xwcNhoI2UdsjC8g0mQvaSs1zwkg0uQAsAEaGm9/UPCeU7eMj6loTEpf6ZOQWMxd98gAhZnS6XEZcNQAAAABJRU5ErkJggg==\"}"}' -H "X-Internal-Challenge: secret-key" -H "Content-Type: application/json" +curl http://localhost:3000/ -d '{"variables":{"TINYPNG_API_KEY":"[YOUR_API_KEY]"},"payload":"{\"provider\":\"tinypng\",\"image\":\"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAf0lEQVR4nO2Wuw2AMAxEbw1gpMwDDMBcGQpooDKydGVAoXCK6J7k6qyc83MCCFGP/Yz+CkDF4KHmjgowbQF0CKFrCDUiwztqxabHCL0/xwcNhoI2UdsjC8g0mQvaSs1zwkg0uQAsAEaGm9/UPCeU7eMj6loTEpf6ZOQWMxd98gAhZnS6XEZcNQAAAABJRU5ErkJggg==\"}"}' -H "X-Internal-Challenge: secret-key" -H "Content-Type: application/json" ``` Your function is now listening on port `3000`, and you can execute it by sending `POST` request with appropriate authorization headers. To learn more about runtime, you can visit Python runtime [README](https://github.com/open-runtimes/open-runtimes/tree/main/runtimes/java-11.0). From 149adb6527584576df44d3a5305be6bcffa3e7ec Mon Sep 17 00:00:00 2001 From: tinp Date: Wed, 9 Aug 2023 12:22:18 -0700 Subject: [PATCH 11/17] update current code --- java/compress_image/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/java/compress_image/README.md b/java/compress_image/README.md index 1d0eec10..d30e88d8 100644 --- a/java/compress_image/README.md +++ b/java/compress_image/README.md @@ -45,9 +45,10 @@ $ cd java/compress_image 2. Enter this function folder and build the code: ``` -docker run --rm --interactive --tty --volume $PWD:/usr/code openruntimes/java:v2-11.0 sh /usr/local/src/build.sh +docker run -e INTERNAL_RUNTIME_ENTRYPOINT=Index.java --rm --interactive --tty --volume $PWD:/usr/code openruntimes/java:v2-11.0 sh /usr/local/src/build.sh ``` As a result, a `code.tar.gz` file will be generated. + 3. Start the Open Runtime: ``` docker run -p 3000:3000 -e INTERNAL_RUNTIME_KEY=secret-key -e INTERNAL_RUNTIME_ENTRYPOINT=Index.java --rm --interactive --tty --volume $PWD/code.tar.gz:/tmp/code.tar.gz:ro openruntimes/java:v2-11.0 sh /usr/local/src/start.sh From 91690fc3838d5bb9ee81c9aead334a9479ea1762 Mon Sep 17 00:00:00 2001 From: tinp Date: Wed, 9 Aug 2023 12:38:56 -0700 Subject: [PATCH 12/17] update current code --- java/compress_image/Index.java | 38 +++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/java/compress_image/Index.java b/java/compress_image/Index.java index 353ba0f0..ab517ec0 100644 --- a/java/compress_image/Index.java +++ b/java/compress_image/Index.java @@ -89,7 +89,7 @@ public RuntimeResponse main(RuntimeRequest req, RuntimeResponse res) throws Exce // response data to return Map responseData = new HashMap<>(); - // TODO: compress image using provider API and store the result in compressedImage variable + // compress image using provider API and store the result in compressedImage variable if (Provider.TINY_PNG.getName().equals(provider)) { // Decode image from Base64 string byte[] imageByte = convertToByte(image); @@ -103,8 +103,10 @@ public RuntimeResponse main(RuntimeRequest req, RuntimeResponse res) throws Exce // Decode input string byte[] imageBytes = convertToByte(image); + // Compress image String urlResponse = krakenioCompress(imageBytes, apiKey, secretKey); + // Decode compressed image from URL URL url = new URL(urlResponse); InputStream inputStream = url.openStream(); byte[] compressedImageBytes = inputStream.readAllBytes(); @@ -112,7 +114,11 @@ public RuntimeResponse main(RuntimeRequest req, RuntimeResponse res) throws Exce inputStream.close(); } - // TODO: check if compressedImage is valid + // Check if compressedImage is valid + errorResponse = checkCompressedImage(compressedImage, res); + if (errorResponse != null) { + return errorResponse; + } // If input valid then return success true and compressed image responseData.put("success", true); @@ -191,7 +197,6 @@ private RuntimeResponse validatePayload(Map payload, RuntimeResp * @return null if API key is present, otherwise return error response */ - // check API key is present in variables private RuntimeResponse checkEmptyApiKey(RuntimeRequest req, RuntimeResponse res, String apiKeyVariable) { Map variables = req.getVariables(); @@ -204,6 +209,25 @@ private RuntimeResponse checkEmptyApiKey(RuntimeRequest req, RuntimeResponse res return null; } + /** + * Check if compressed image is valid + * @param compressedImage is compressed image in Base64 string + * @param res is response object from function call + * @return null if compressed image is valid, otherwise return error response + */ + + private RuntimeResponse checkCompressedImage(String compressedImage, RuntimeResponse res) { + Map responseData = new HashMap<>(); + + // check if compressed image is valid + if (!compressedImage.matches("^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$") || compressedImage.trim().isEmpty()) { + responseData.put("success", false); + responseData.put("message", "Compressed image is invalid, please provide a valid Base64 image"); + return res.json(responseData); + } + return null; + } + /** * Converts Base64 input of payload to byte array @@ -212,7 +236,6 @@ private RuntimeResponse checkEmptyApiKey(RuntimeRequest req, RuntimeResponse res */ private byte [] convertToByte(String baseInput) { - // TODO: return Base64.getDecoder().decode(baseInput); } @@ -239,6 +262,12 @@ private String convertToBase64(byte [] byteInput) { return source.toBuffer(); } + /** + * Compresses image in byte array format using Kraken.io provider + * @param byte [] image is image to compress in byte array format + * @return byte [] url of compressed image in String format + */ + private String krakenioCompress(byte [] image, String apiKey, String secretKey) throws Exception { if (apiKey == null || apiKey.trim().isEmpty()) { throw new IllegalArgumentException("API key is empty"); @@ -248,7 +277,6 @@ private String krakenioCompress(byte [] image, String apiKey, String secretKey) final DirectUploadRequest request = DirectUploadRequest.builder(new ByteArrayInputStream(image)).withLossy(true).build(); final SuccessfulUploadResponse response = client.directUpload(request); try { - if (response.getSuccess()) { return response.getKrakedUrl(); } else { From ada6f53d91694b64319cf8461fe3ef20e10e92cc Mon Sep 17 00:00:00 2001 From: Ali Sharif Date: Thu, 10 Aug 2023 21:40:53 -0700 Subject: [PATCH 13/17] add krakenCompress method --- java/compress_image/Index.java | 61 +++++++++++++++++++-------------- java/compress_image/deps.gradle | 4 +-- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/java/compress_image/Index.java b/java/compress_image/Index.java index ab517ec0..5fd65d16 100644 --- a/java/compress_image/Index.java +++ b/java/compress_image/Index.java @@ -1,8 +1,6 @@ import java.util.Map; import java.util.HashMap; -import java.net.HttpURLConnection; import java.net.URL; -import java.io.BufferedReader; import java.util.stream.Collectors; import java.util.stream.*; import java.util.Base64; @@ -10,11 +8,15 @@ import com.tinify.Source; import com.tinify.Tinify; import java.io.*; -import io.kraken.client.model.response.SuccessfulUploadResponse; -import io.kraken.client.model.request.DirectUploadRequest; -import io.kraken.client.impl.DefaultKrakenIoClient; -import io.kraken.client.KrakenIoClient; -import io.kraken.client.exception.KrakenIoRequestException; +import java.io.InputStream; +import java.io.ByteArrayInputStream; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.MultipartBody; + /** * Enum for provider names * @param name is provider name @@ -104,7 +106,7 @@ public RuntimeResponse main(RuntimeRequest req, RuntimeResponse res) throws Exce byte[] imageBytes = convertToByte(image); // Compress image - String urlResponse = krakenioCompress(imageBytes, apiKey, secretKey); + String urlResponse = krakenCompress(imageBytes, apiKey, secretKey); // Decode compressed image from URL URL url = new URL(urlResponse); @@ -265,25 +267,32 @@ private String convertToBase64(byte [] byteInput) { /** * Compresses image in byte array format using Kraken.io provider * @param byte [] image is image to compress in byte array format - * @return byte [] url of compressed image in String format + * @return String url of compressed image in String format */ - private String krakenioCompress(byte [] image, String apiKey, String secretKey) throws Exception { - if (apiKey == null || apiKey.trim().isEmpty()) { - throw new IllegalArgumentException("API key is empty"); - } - final KrakenIoClient client = new DefaultKrakenIoClient(apiKey, secretKey); - - final DirectUploadRequest request = DirectUploadRequest.builder(new ByteArrayInputStream(image)).withLossy(true).build(); - final SuccessfulUploadResponse response = client.directUpload(request); - try { - if (response.getSuccess()) { - return response.getKrakedUrl(); - } else { - throw new IOException("Kraken.io failed to compress image"); - } + public String krakenCompress(byte[] image, String apiKey, String apiSecret) throws IOException { + OkHttpClient client = new OkHttpClient(); + String krakedUrl = ""; + + String data = "{\"auth\":{\"api_key\":\"" + apiKey + "\",\"api_secret\":\"" + apiSecret + "\"}, \"wait\": true}"; + RequestBody requestBody = new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("data", data) + .addFormDataPart("upload", "image.jpg", RequestBody.create(MediaType.parse("image/jpg"), image)) + .build(); + + Request request = new Request.Builder() + .url("https://api.kraken.io/v1/upload") + .post(requestBody) + .addHeader("Content-Type", "application/json") + .build(); + + try (Response response = client.newCall(request).execute()) { + Gson responseGson = new Gson(); + Map responseData = responseGson.fromJson(response.body().string(), Map.class); + krakedUrl = responseData.get("kraked_url").toString(); } catch (Exception e) { - System.out.println("Error: " + e.getMessage()); + e.printStackTrace(); } - return null; - } + return krakedUrl; + }; diff --git a/java/compress_image/deps.gradle b/java/compress_image/deps.gradle index a2039a58..c60effe1 100644 --- a/java/compress_image/deps.gradle +++ b/java/compress_image/deps.gradle @@ -1,7 +1,5 @@ dependencies { implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.tinify:tinify:1.8.3' - implementation('io.kraken.client:client:1.1.2'){ - exclude module: 'javax.inject' - } + implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11' } \ No newline at end of file From c16f32bef8d1c81b85a03476aa1755f084cea1e3 Mon Sep 17 00:00:00 2001 From: Ali Sharif Date: Thu, 10 Aug 2023 22:33:05 -0700 Subject: [PATCH 14/17] cleaned up import statements --- java/compress_image/Index.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/java/compress_image/Index.java b/java/compress_image/Index.java index 5fd65d16..c7d87ae0 100644 --- a/java/compress_image/Index.java +++ b/java/compress_image/Index.java @@ -2,7 +2,8 @@ import java.util.HashMap; import java.net.URL; import java.util.stream.Collectors; -import java.util.stream.*; +import java.util.stream.Stream; +import java.io.IOException; import java.util.Base64; import com.google.gson.Gson; import com.tinify.Source; From 46c104405a4c2072eb9a245764f1b81e45cf7c1a Mon Sep 17 00:00:00 2001 From: Ali Sharif Date: Thu, 10 Aug 2023 22:41:02 -0700 Subject: [PATCH 15/17] clean up import statements --- java/compress_image/Index.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/java/compress_image/Index.java b/java/compress_image/Index.java index c7d87ae0..6ea8426f 100644 --- a/java/compress_image/Index.java +++ b/java/compress_image/Index.java @@ -8,9 +8,7 @@ import com.google.gson.Gson; import com.tinify.Source; import com.tinify.Tinify; -import java.io.*; import java.io.InputStream; -import java.io.ByteArrayInputStream; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -126,7 +124,6 @@ public RuntimeResponse main(RuntimeRequest req, RuntimeResponse res) throws Exce // If input valid then return success true and compressed image responseData.put("success", true); responseData.put("image", compressedImage); - return res.json(responseData); } From b9152f23bb2bdb5381e4d9514f061f0140196173 Mon Sep 17 00:00:00 2001 From: Ali Sharif Date: Thu, 10 Aug 2023 22:50:25 -0700 Subject: [PATCH 16/17] change image type in requestBody to png --- java/compress_image/Index.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/compress_image/Index.java b/java/compress_image/Index.java index 6ea8426f..2fbf77f3 100644 --- a/java/compress_image/Index.java +++ b/java/compress_image/Index.java @@ -276,7 +276,7 @@ public String krakenCompress(byte[] image, String apiKey, String apiSecret) thro RequestBody requestBody = new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("data", data) - .addFormDataPart("upload", "image.jpg", RequestBody.create(MediaType.parse("image/jpg"), image)) + .addFormDataPart("upload", "image.png", RequestBody.create(MediaType.parse("image/png"), image)) .build(); Request request = new Request.Builder() From f63601a8ab7d2f4e1f941fca8ff361f2a672bb3f Mon Sep 17 00:00:00 2001 From: tinp Date: Wed, 16 Aug 2023 14:19:33 -0700 Subject: [PATCH 17/17] add secret key validation --- java/compress_image/Index.java | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/java/compress_image/Index.java b/java/compress_image/Index.java index 2fbf77f3..a82e5447 100644 --- a/java/compress_image/Index.java +++ b/java/compress_image/Index.java @@ -75,14 +75,23 @@ public RuntimeResponse main(RuntimeRequest req, RuntimeResponse res) throws Exce String image = payload.get("image").toString(); // check API key is present in variables - String apiKeyVariable = Provider.TINY_PNG.getName().equals(provider) ? "TINYPNG_API_KEY" : "KRAKENIO_API_KEY"; + String apiKeyVariable; + String apiSecretVariable; - errorResponse = checkEmptyApiKey(req, res, apiKeyVariable); + if (Provider.TINY_PNG.getName().equals(provider)) { + apiKeyVariable = "TINYPNG_API_KEY"; + apiSecretVariable = null; + } else { + apiKeyVariable = "KRAKENIO_API_KEY"; + apiSecretVariable = "KRAKENIO_API_SECRET"; + } + + errorResponse = checkEmptyApiKeyAndSecret(req, res, apiKeyVariable, apiSecretVariable); if (errorResponse != null) { return errorResponse; } String apiKey = req.getVariables().get(apiKeyVariable); - String secretKey = req.getVariables().get("KRAKENIO_API_SECRET"); + String secretKey = req.getVariables().get(apiSecretVariable); // compressed image in Base64 string String compressedImage = ""; @@ -197,7 +206,7 @@ private RuntimeResponse validatePayload(Map payload, RuntimeResp * @return null if API key is present, otherwise return error response */ - private RuntimeResponse checkEmptyApiKey(RuntimeRequest req, RuntimeResponse res, String apiKeyVariable) { + private RuntimeResponse checkEmptyApiKeyAndSecret(RuntimeRequest req, RuntimeResponse res, String apiKeyVariable, String apiSecretVariable) { Map variables = req.getVariables(); if (!variables.containsKey(apiKeyVariable) || variables.get(apiKeyVariable) == null || variables.get(apiKeyVariable).trim().isEmpty()) { @@ -206,6 +215,13 @@ private RuntimeResponse checkEmptyApiKey(RuntimeRequest req, RuntimeResponse res responseData.put("message", "API key is not present in variables, please provide " + apiKeyVariable + " for the provider"); return res.json(responseData); } + + if (apiSecretVariable != null && (!variables.containsKey(apiSecretVariable) || variables.get(apiSecretVariable) == null || variables.get(apiSecretVariable).trim().isEmpty())) { + Map responseData = new HashMap<>(); + responseData.put("success", false); + responseData.put("message", "API secret is not present in variables, please provide " + apiSecretVariable + " for the provider"); + return res.json(responseData); + } return null; } @@ -293,4 +309,4 @@ public String krakenCompress(byte[] image, String apiKey, String apiSecret) thro e.printStackTrace(); } return krakedUrl; - }; + }; \ No newline at end of file