{"id":129,"date":"2024-09-17T05:00:00","date_gmt":"2024-09-17T05:00:00","guid":{"rendered":"https:\/\/imcodinggenius.com\/?p=129"},"modified":"2024-09-17T05:00:00","modified_gmt":"2024-09-17T05:00:00","slug":"spring-boot-with-kotlin-aws-s3-and-s3template","status":"publish","type":"post","link":"https:\/\/imcodinggenius.com\/?p=129","title":{"rendered":"Spring Boot with Kotlin, AWS S3, and S3Template"},"content":{"rendered":"<p>Welcome to the <strong>second article<\/strong> in a series dedicated to integrating a Spring Boot Kotlin app with <strong>AWS S3 <\/strong>Object Storage, in which we will learn how to make our lives easier with <strong>S3Template<\/strong>. <\/p>\n<p>Of course, I highly encourage you to take a look at other articles in this series, too: <\/p>\n<p><a href=\"https:\/\/codersee.com\/spring-boot-aws-s3-s3client-kotlin\/\">Spring Boot with AWS S3, S3Client and Kotlin<\/a><\/p>\n<h2 class=\"wp-block-heading\">What is S3Template? <\/h2>\n<p>Before we get our hands dirty with the code, let\u2019s take a second to understand better with S3Template is and how it can make our lives easier when integrating a Spring Boot app with S3. <\/p>\n<p>Let me quote the S3Template documentation here: <\/p>\n<p>Higher level abstraction over S3Client providing methods for the most common use cases.<\/p>\n<p>So, if you have already worked with, or you have seen my previous article with S3Client, then you saw that simple operations require some boilerplate. And that is exactly what S3Template solves. <\/p>\n<p>And as a note from my end, I just wanted to note S3Template handles only some subset of S3Client operations, so in our projects those two will rather coexist, instead of being each others alternatives. <\/p>\n<h2 class=\"wp-block-heading\">AWS S3Template Operations<\/h2>\n<p>With all of that said, let\u2019s get to work. <\/p>\n<p>Let\u2019s add the controller package and BucketController class to it. We will use it to expose a bunch of endpoints triggering various operations on S3 buckets and files. <\/p>\n<p>When it comes to the operations- we will use the same ones as in the previous article, and as the last one, I will show you how to <strong>serialize and deserialize objects with AWS S3 and S3Template.<\/strong><\/p>\n<h3 class=\"wp-block-heading\">List All Buckets<\/h3>\n<p>Although the S3Template does not expose any method that would help us with this task, I wanted to mention it as we discussed it in the previous tutorial. <\/p>\n<p>Additionally, this is a great example of S3Client and S3Template co-existence: <\/p>\n<p>@RestController<br \/>\n@RequestMapping(&#171;\/buckets&#187;)<br \/>\nclass BucketController(<br \/>\n  private val s3Template: S3Template,<br \/>\n  private val s3Client: S3Client,<br \/>\n) {<\/p>\n<p>  @GetMapping<br \/>\n  fun listBuckets(): List&lt;String&gt; {<br \/>\n    val response = s3Client.listBuckets()<\/p>\n<p>    return response.buckets()<br \/>\n      .mapIndexed { index, bucket -&gt;<br \/>\n        &#171;Bucket #${index + 1}: ${bucket.name()}&#187;<br \/>\n      }<br \/>\n  }<br \/>\n}<\/p>\n<p>As we can see, not too much S3Template could improve here, so I bet this is the reason why it was not introduced. <\/p>\n<h3 class=\"wp-block-heading\">New S3 Bucket<\/h3>\n<p>Nextly, let\u2019s take a look at how we can create a brand new bucket: <\/p>\n<p>@PostMapping<br \/>\nfun createBucket(@RequestBody request: BucketRequest) {<br \/>\n  s3Template.createBucket(request.bucketName)<br \/>\n}<\/p>\n<p>data class BucketRequest(val bucketName: String)<\/p>\n<p>As we can see, no additional request classes- the only thing we need is the bucket name. <\/p>\n<p>Of course, let\u2019s rerun our application and verify if everything is working: <\/p>\n<p>curl &#8212;location &#8212;request POST &#8216;http:\/\/localhost:8080\/buckets&#8217; <br \/>\n&#8212;header &#8216;Content-Type: application\/json&#8217; <br \/>\n&#8212;data-raw &#8216;{<br \/>\n    &#171;bucketName&#187;: &#171;codersee-awesome-bucket&#187;<br \/>\n}&#8217;<\/p>\n<p>As a result, we should get 200 OK without a response body. <\/p>\n<h3 class=\"wp-block-heading\">Upload File to S3 Bucket<\/h3>\n<p>Nextly, let\u2019s take a look at how to upload a new file to S3. <\/p>\n<p>We have a few options, among which the store function is the easiest: <\/p>\n<p>@PostMapping(&#171;\/{bucketName}\/objects&#187;)<br \/>\nfun createObject(@PathVariable bucketName: String, @RequestBody request: ObjectRequest) {<br \/>\n  s3Template.store(bucketName, request.objectName, request.content)<br \/>\n}<\/p>\n<p>data class ObjectRequest(val objectName: String, val content: String)<\/p>\n<p>As we can see, this function takes three arguments: <\/p>\n<p>the bucket name, <\/p>\n<p>filename,<\/p>\n<p>and the content to upload (to be specific Object object) <\/p>\n<p>Alternatively, we could use the upload function, which allows us to send InputStream instance and metadata: <\/p>\n<p>@Override<br \/>\npublic S3Resource upload(<br \/>\n  String bucketName,<br \/>\n  String key,<br \/>\n  InputStream inputStream,<br \/>\n  @Nullable ObjectMetadata objectMetadata<br \/>\n) <\/p>\n<p>Lastly, let\u2019s verify that everything is fine with the following curl: <\/p>\n<p>curl &#8212;location &#8212;request POST &#8216;http:\/\/localhost:8080\/buckets\/codersee-awesome-bucket\/objects&#8217; <br \/>\n&#8212;header &#8216;Content-Type: application\/json&#8217; <br \/>\n&#8212;data-raw &#8216;{<br \/>\n    &#171;objectName&#187;: &#171;file-example.txt&#187;,<br \/>\n    &#171;content&#187;: &#171;My file content&#187;<br \/>\n}&#8217;<\/p>\n<p>If everything worked, then a new file should be present in our bucket  <\/p>\n<h3 class=\"wp-block-heading\">List Files From The Bucket<\/h3>\n<p>Nextly, let\u2019s see how we can list the bucket content with <em>S3Template<\/em>: <\/p>\n<p>@GetMapping(&#171;\/{bucketName}\/objects&#187;)<br \/>\nfun listObjects(@PathVariable bucketName: String): List&lt;String&gt; =<br \/>\n  s3Template.listObjects(bucketName, &#171;&#187;)<br \/>\n    .map { s3Resource -&gt; s3Resource.filename }<\/p>\n<p>We can clearly see that this is not rocket science  <\/p>\n<p>Nevertheless, it is worth mentioning that this time we get the S3Resource instance and instead of the key() we use it getFilename method. <\/p>\n<p>And just like previously, let\u2019s see the endpoint in action: <\/p>\n<p>curl &#8212;location &#8212;request POST &#8216;http:\/\/localhost:8080\/buckets\/codersee-awesome-bucket\/objects&#8217; <br \/>\n&#8212;header &#8216;Content-Type: application\/json&#8217; <br \/>\n&#8212;data-raw &#8216;{<br \/>\n    &#171;objectName&#187;: &#171;file-example.txt&#187;,<br \/>\n    &#171;content&#187;: &#171;My file content&#187;<br \/>\n}&#8217;<\/p>\n<p># Response status: 200 OK<br \/>\n# Response body:<br \/>\n[<br \/>\n    &#171;file-example.txt&#187;<br \/>\n]<\/p>\n<h3 class=\"wp-block-heading\">Download a File<\/h3>\n<p>So what about fetching files from the S3 bucket? <\/p>\n<p>With <em>S3Template<\/em>, it\u2019s a piece of cake: <\/p>\n<p>@GetMapping(&#171;\/{bucketName}\/objects\/{objectName}&#187;)<br \/>\nfun getObject(@PathVariable bucketName: String, @PathVariable objectName: String): String =<br \/>\n  s3Template.download(bucketName, objectName).getContentAsString(UTF_8)<\/p>\n<p>We specify the bucket name and the object name, and as a result, we get the S3Resource that exposes a bunch of methods. Among others, the getContentAsString that is pretty descriptive  <\/p>\n<p>Similarly, let\u2019s hit the endpoint: <\/p>\n<p>curl &#8212;location &#8212;request GET &#8216;http:\/\/localhost:8080\/buckets\/codersee-awesome-bucket\/objects\/file-example.txt&#8217;<\/p>\n<p># Response status: 200 OK<br \/>\n# Response body:<br \/>\n&#171;My file content&#187;<\/p>\n<h3 class=\"wp-block-heading\">Delete the Bucket<\/h3>\n<p>Last before least, let\u2019s take a look at how we can get rid of the bucket: <\/p>\n<p>@DeleteMapping(&#171;\/{bucketName}&#187;)<br \/>\nfun deleteBucket(@PathVariable bucketName: String) {<br \/>\n  s3Template.listObjects(bucketName, &#171;&#187;)<br \/>\n    .forEach { s3Template.deleteObject(bucketName, it.filename) }<\/p>\n<p>  s3Template.deleteBucket(bucketName)<br \/>\n}<\/p>\n<p>And just like in the previous article- we must ensure the bucket does not contain any objects. <\/p>\n<p>To do so, we list out objects by specifying the bucket name and objects prefix (as we don\u2019t have any, we pass an empty String). Then, for each object, we use its key to delete it. And lastly, we simply invoke the deleteBucket by passing the name of a bucket to delete. <\/p>\n<p>Of course, let\u2019s verify this logic, too: <\/p>\n<p>curl &#8212;location &#8212;request DELETE &#8216;http:\/\/localhost:8080\/buckets\/codersee-awesome-bucket&#8217;<\/p>\n<p>If we run this command and the S3 bucket exists, then we should see 200 OK and our bucket will disappear. <\/p>\n<h3 class=\"wp-block-heading\">Serialize\/Deserialize objects <\/h3>\n<p>As the last thing, let\u2019s take a look at how easily we can persist objects using the combination of store and read functions: <\/p>\n<p>@PostMapping(&#171;\/{bucketName}\/objects&#187;)<br \/>\nfun createExampleObject(@PathVariable bucketName: String): Example {<br \/>\n  val example = Example(id = UUID.randomUUID(), name = &#171;Some name&#187;)<\/p>\n<p>  s3Template.store(bucketName, &#171;example.json&#187;, example)<\/p>\n<p>  return s3Template.read(bucketName, &#171;example.json&#187;, Example::class.java)<br \/>\n}<\/p>\n<p>data class Example(val id: UUID, val name: String)<\/p>\n<p>As we can see, we made a small update to our POST \/{bucketName}\/objects endpoint logic. <\/p>\n<p>Basically, the first part is exactly the same, we use the store again to push the file to the bucket. <\/p>\n<p>Nevertheless, instead of the download we saw previously, we use the read function that uses the S3ObjectConverter that will automatically deserialize the JSON into the Example class instance. <\/p>\n<p>And for the last time, let\u2019s hit our API: <\/p>\n<p>curl &#8212;location &#8212;request POST &#8216;http:\/\/localhost:8080\/buckets\/codersee-awesome-bucket\/objects&#8217; <br \/>\n&#8212;data-raw &#187;<\/p>\n<p># Response status: 200 OK<br \/>\n# Response body:<br \/>\n{<br \/>\n    &#171;id&#187;: &#171;3eacd8a3-48b2-4756-86db-e4c9f4e291da&#187;,<br \/>\n    &#171;name&#187;: &#171;Some name&#187;<br \/>\n}<\/p>\n<p>And as we can see, the output confirms that everything is working fine. <\/p>\n<p>Summary<\/p>\n<p>And that\u2019s all for this article on how to make our lives easier in Spring Boot with S3Template. <\/p>\n<p>I hope you enjoyed it, and again wanted to invite you to take a look at other content of this series: <\/p>\n<p><a href=\"https:\/\/codersee.com\/spring-boot-aws-s3-s3client-kotlin\/\">Spring Boot with AWS S3, S3Client and Kotlin<\/a><\/p>\n<p>Lastly, just wanted to show that you can find the source code in <a href=\"https:\/\/github.com\/codersee-blog\/spring-boot-3-kotlin-s3template\" target=\"_blank\" rel=\"noopener\">this GitHub repository<\/a> and that you can join my <a href=\"https:\/\/codersee.com\/newsletter\/\">newsletter<\/a> to stay up-to-date with Kotlin on the backend. <\/p>\n<p>The post <a href=\"https:\/\/codersee.com\/spring-boot-with-kotlin-aws-s3-and-s3template\/\">Spring Boot with Kotlin, AWS S3, and S3Template<\/a> appeared first on <a href=\"https:\/\/codersee.com\/\">Codersee | Kotlin, Ktor, Spring<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>Welcome to the second article in a series dedicated to integrating a Spring Boot Kotlin app with AWS S3 Object Storage, in which we will learn how to make our lives easier with S3Template. Of course, I highly encourage you to take a look at other articles in this series, &#8230; <\/p>\n<div><a class=\"more-link bs-book_btn\" href=\"https:\/\/imcodinggenius.com\/?p=129\">Read More<\/a><\/div>\n","protected":false},"author":0,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-129","post","type-post","status-publish","format-standard","hentry","category-news"],"_links":{"self":[{"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/posts\/129"}],"collection":[{"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=129"}],"version-history":[{"count":0,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/posts\/129\/revisions"}],"wp:attachment":[{"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=129"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=129"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=129"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}