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, too:
Spring Boot with AWS S3, S3Client and Kotlin
What is S3Template?
Before we get our hands dirty with the code, let’s 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.
Let me quote the S3Template documentation here:
Higher level abstraction over S3Client providing methods for the most common use cases.
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.
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.
AWS S3Template Operations
With all of that said, let’s get to work.
Let’s 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.
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 serialize and deserialize objects with AWS S3 and S3Template.
List All Buckets
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.
Additionally, this is a great example of S3Client and S3Template co-existence:
@RestController
@RequestMapping(«/buckets»)
class BucketController(
private val s3Template: S3Template,
private val s3Client: S3Client,
) {
@GetMapping
fun listBuckets(): List<String> {
val response = s3Client.listBuckets()
return response.buckets()
.mapIndexed { index, bucket ->
«Bucket #${index + 1}: ${bucket.name()}»
}
}
}
As we can see, not too much S3Template could improve here, so I bet this is the reason why it was not introduced.
New S3 Bucket
Nextly, let’s take a look at how we can create a brand new bucket:
@PostMapping
fun createBucket(@RequestBody request: BucketRequest) {
s3Template.createBucket(request.bucketName)
}
data class BucketRequest(val bucketName: String)
As we can see, no additional request classes- the only thing we need is the bucket name.
Of course, let’s rerun our application and verify if everything is working:
curl —location —request POST ‘http://localhost:8080/buckets’
—header ‘Content-Type: application/json’
—data-raw ‘{
«bucketName»: «codersee-awesome-bucket»
}’
As a result, we should get 200 OK without a response body.
Upload File to S3 Bucket
Nextly, let’s take a look at how to upload a new file to S3.
We have a few options, among which the store function is the easiest:
@PostMapping(«/{bucketName}/objects»)
fun createObject(@PathVariable bucketName: String, @RequestBody request: ObjectRequest) {
s3Template.store(bucketName, request.objectName, request.content)
}
data class ObjectRequest(val objectName: String, val content: String)
As we can see, this function takes three arguments:
the bucket name,
filename,
and the content to upload (to be specific Object object)
Alternatively, we could use the upload function, which allows us to send InputStream instance and metadata:
@Override
public S3Resource upload(
String bucketName,
String key,
InputStream inputStream,
@Nullable ObjectMetadata objectMetadata
)
Lastly, let’s verify that everything is fine with the following curl:
curl —location —request POST ‘http://localhost:8080/buckets/codersee-awesome-bucket/objects’
—header ‘Content-Type: application/json’
—data-raw ‘{
«objectName»: «file-example.txt»,
«content»: «My file content»
}’
If everything worked, then a new file should be present in our bucket
List Files From The Bucket
Nextly, let’s see how we can list the bucket content with S3Template:
@GetMapping(«/{bucketName}/objects»)
fun listObjects(@PathVariable bucketName: String): List<String> =
s3Template.listObjects(bucketName, «»)
.map { s3Resource -> s3Resource.filename }
We can clearly see that this is not rocket science
Nevertheless, it is worth mentioning that this time we get the S3Resource instance and instead of the key() we use it getFilename method.
And just like previously, let’s see the endpoint in action:
curl —location —request POST ‘http://localhost:8080/buckets/codersee-awesome-bucket/objects’
—header ‘Content-Type: application/json’
—data-raw ‘{
«objectName»: «file-example.txt»,
«content»: «My file content»
}’
# Response status: 200 OK
# Response body:
[
«file-example.txt»
]
Download a File
So what about fetching files from the S3 bucket?
With S3Template, it’s a piece of cake:
@GetMapping(«/{bucketName}/objects/{objectName}»)
fun getObject(@PathVariable bucketName: String, @PathVariable objectName: String): String =
s3Template.download(bucketName, objectName).getContentAsString(UTF_8)
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
Similarly, let’s hit the endpoint:
curl —location —request GET ‘http://localhost:8080/buckets/codersee-awesome-bucket/objects/file-example.txt’
# Response status: 200 OK
# Response body:
«My file content»
Delete the Bucket
Last before least, let’s take a look at how we can get rid of the bucket:
@DeleteMapping(«/{bucketName}»)
fun deleteBucket(@PathVariable bucketName: String) {
s3Template.listObjects(bucketName, «»)
.forEach { s3Template.deleteObject(bucketName, it.filename) }
s3Template.deleteBucket(bucketName)
}
And just like in the previous article- we must ensure the bucket does not contain any objects.
To do so, we list out objects by specifying the bucket name and objects prefix (as we don’t 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.
Of course, let’s verify this logic, too:
curl —location —request DELETE ‘http://localhost:8080/buckets/codersee-awesome-bucket’
If we run this command and the S3 bucket exists, then we should see 200 OK and our bucket will disappear.
Serialize/Deserialize objects
As the last thing, let’s take a look at how easily we can persist objects using the combination of store and read functions:
@PostMapping(«/{bucketName}/objects»)
fun createExampleObject(@PathVariable bucketName: String): Example {
val example = Example(id = UUID.randomUUID(), name = «Some name»)
s3Template.store(bucketName, «example.json», example)
return s3Template.read(bucketName, «example.json», Example::class.java)
}
data class Example(val id: UUID, val name: String)
As we can see, we made a small update to our POST /{bucketName}/objects endpoint logic.
Basically, the first part is exactly the same, we use the store again to push the file to the bucket.
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.
And for the last time, let’s hit our API:
curl —location —request POST ‘http://localhost:8080/buckets/codersee-awesome-bucket/objects’
—data-raw »
# Response status: 200 OK
# Response body:
{
«id»: «3eacd8a3-48b2-4756-86db-e4c9f4e291da»,
«name»: «Some name»
}
And as we can see, the output confirms that everything is working fine.
Summary
And that’s all for this article on how to make our lives easier in Spring Boot with S3Template.
I hope you enjoyed it, and again wanted to invite you to take a look at other content of this series:
Spring Boot with AWS S3, S3Client and Kotlin
Lastly, just wanted to show that you can find the source code in this GitHub repository and that you can join my newsletter to stay up-to-date with Kotlin on the backend.
The post Spring Boot with Kotlin, AWS S3, and S3Template appeared first on Codersee | Kotlin, Ktor, Spring.