Skip to content

Commit fefef6e

Browse files
committed
feat: implement sticker storage using s3 compatible APIs
1 parent e2bb78b commit fefef6e

13 files changed

Lines changed: 503 additions & 12 deletions

File tree

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
{
22
"version" : "v1",
3-
"lastUpdate" : "07/05/2025 12:16:59",
4-
"plugins" : { }
3+
"lastUpdate" : "27/05/2025 14:51:23",
4+
"plugins" : {
5+
"tls" : {
6+
"name" : "tls",
7+
"type" : "extension",
8+
"location" : "io.quarkus:quarkus-tls-registry-cli:3.22.3",
9+
"description" : null,
10+
"inProjectCatalog" : false
11+
}
12+
}
513
}

docker-compose.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,36 @@ services:
127127
timeout: 5s
128128
retries: 5
129129

130+
minio:
131+
image: minio/minio:latest
132+
container_name: stickerlandia-minio
133+
ports:
134+
- "9000:9000"
135+
- "9001:9001"
136+
environment:
137+
MINIO_ROOT_USER: minioadmin
138+
MINIO_ROOT_PASSWORD: minioadmin
139+
command: server /data --console-address ":9001"
140+
volumes:
141+
- minio-data:/data
142+
healthcheck:
143+
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
144+
interval: 30s
145+
timeout: 20s
146+
retries: 3
147+
148+
minio-init:
149+
image: minio/mc:latest
150+
container_name: minio-init
151+
depends_on:
152+
minio:
153+
condition: service_healthy
154+
entrypoint: ["/bin/sh", "-c"]
155+
command: >
156+
"mc alias set myminio http://minio:9000 minioadmin minioadmin &&
157+
mc mb myminio/sticker-images --ignore-existing"
158+
restart: "no"
159+
130160
sticker-award:
131161
build:
132162
context: ./sticker-award
@@ -137,6 +167,8 @@ services:
137167
condition: service_healthy
138168
redpanda:
139169
condition: service_healthy
170+
minio:
171+
condition: service_healthy
140172
environment:
141173
QUARKUS_DATASOURCE_JDBC_URL: jdbc:postgresql://sticker-award-db:5432/sticker_award
142174
QUARKUS_DATASOURCE_USERNAME: sticker_user
@@ -146,6 +178,13 @@ services:
146178
KAFKA_BOOTSTRAP_SERVERS: redpanda:9092
147179
MP_MESSAGING_CONNECTOR_SMALLRYE_KAFKA_BOOTSTRAP_SERVERS: redpanda:9092
148180
QUARKUS_KAFKA_STREAMS_BOOTSTRAP_SERVERS: redpanda:9092
181+
QUARKUS_S3_ENDPOINT_OVERRIDE: http://minio:9000
182+
QUARKUS_S3_AWS_REGION: us-east-1
183+
QUARKUS_S3_AWS_CREDENTIALS_TYPE: static
184+
QUARKUS_S3_AWS_CREDENTIALS_STATIC_PROVIDER_ACCESS_KEY_ID: minioadmin
185+
QUARKUS_S3_AWS_CREDENTIALS_STATIC_PROVIDER_SECRET_ACCESS_KEY: minioadmin
186+
QUARKUS_S3_PATH_STYLE_ACCESS: "true"
187+
STICKER_IMAGES_BUCKET: sticker-images
149188
labels:
150189
- "traefik.enable=true"
151190
- "traefik.http.routers.sticker-award.rule=PathPrefix(`/api/award`)"
@@ -179,3 +218,4 @@ volumes:
179218
sticker-award-data:
180219
user-management-data:
181220
redpanda-data:
221+
minio-data:

sticker-award/pom.xml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@
2626
<type>pom</type>
2727
<scope>import</scope>
2828
</dependency>
29+
<dependency>
30+
<groupId>${quarkus.platform.group-id}</groupId>
31+
<artifactId>quarkus-amazon-services-bom</artifactId>
32+
<version>${quarkus.platform.version}</version>
33+
<type>pom</type>
34+
<scope>import</scope>
35+
</dependency>
2936
</dependencies>
3037
</dependencyManagement>
3138

@@ -72,19 +79,28 @@
7279
<groupId>io.quarkus</groupId>
7380
<artifactId>quarkus-messaging-kafka</artifactId>
7481
</dependency>
82+
<dependency>
83+
<groupId>io.quarkiverse.amazonservices</groupId>
84+
<artifactId>quarkus-amazon-s3</artifactId>
85+
</dependency>
86+
<dependency>
87+
<groupId>software.amazon.awssdk</groupId>
88+
<artifactId>url-connection-client</artifactId>
89+
</dependency>
90+
7591
<dependency>
7692
<groupId>io.quarkus</groupId>
7793
<artifactId>quarkus-junit5</artifactId>
7894
<scope>test</scope>
7995
</dependency>
8096

97+
<!-- Kafka Testing Dependencies -->
8198
<dependency>
8299
<groupId>io.rest-assured</groupId>
83100
<artifactId>rest-assured</artifactId>
84101
<scope>test</scope>
85102
</dependency>
86103

87-
<!-- Kafka Testing Dependencies -->
88104
<dependency>
89105
<groupId>io.quarkus</groupId>
90106
<artifactId>quarkus-junit5-mockito</artifactId>
@@ -96,7 +112,6 @@
96112
<artifactId>awaitility</artifactId>
97113
<scope>test</scope>
98114
</dependency>
99-
100115
<dependency>
101116
<groupId>org.mockito</groupId>
102117
<artifactId>mockito-core</artifactId>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.datadoghq.stickerlandia.stickeraward.sticker;
2+
3+
import jakarta.enterprise.context.ApplicationScoped;
4+
import jakarta.inject.Inject;
5+
import org.eclipse.microprofile.config.inject.ConfigProperty;
6+
import software.amazon.awssdk.core.sync.RequestBody;
7+
import software.amazon.awssdk.services.s3.S3Client;
8+
import software.amazon.awssdk.services.s3.model.*;
9+
10+
import java.io.InputStream;
11+
import java.time.Duration;
12+
import java.util.UUID;
13+
14+
@ApplicationScoped
15+
public class StickerImageService {
16+
17+
@ConfigProperty(name = "sticker.images.bucket")
18+
String bucketName;
19+
20+
@Inject
21+
S3Client s3Client;
22+
23+
public String uploadImage(InputStream imageStream, String contentType, long contentLength) {
24+
String key = "stickers/" + UUID.randomUUID().toString();
25+
26+
PutObjectRequest putRequest = PutObjectRequest.builder()
27+
.bucket(bucketName)
28+
.key(key)
29+
.contentType(contentType)
30+
.contentLength(contentLength)
31+
.build();
32+
33+
s3Client.putObject(putRequest, RequestBody.fromInputStream(imageStream, contentLength));
34+
35+
return key;
36+
}
37+
38+
public InputStream getImage(String key) {
39+
GetObjectRequest getRequest = GetObjectRequest.builder()
40+
.bucket(bucketName)
41+
.key(key)
42+
.build();
43+
44+
return s3Client.getObject(getRequest);
45+
}
46+
47+
public String getImageUrl(String key) {
48+
GetUrlRequest getUrlRequest = GetUrlRequest.builder()
49+
.bucket(bucketName)
50+
.key(key)
51+
.build();
52+
53+
return s3Client.utilities().getUrl(getUrlRequest).toString();
54+
}
55+
56+
57+
public void deleteImage(String key) {
58+
DeleteObjectRequest deleteRequest = DeleteObjectRequest.builder()
59+
.bucket(bucketName)
60+
.key(key)
61+
.build();
62+
63+
s3Client.deleteObject(deleteRequest);
64+
}
65+
}

sticker-award/src/main/java/com/datadoghq/stickerlandia/stickeraward/sticker/StickerRepository.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,16 @@ public StickerDTO updateStickerMetadata(String stickerId, UpdateStickerRequest r
9797
return toStickerMetadata(sticker);
9898
}
9999

100+
@Transactional
101+
public void updateStickerImageKey(String stickerId, String imageKey) {
102+
Sticker sticker = Sticker.findById(stickerId);
103+
if (sticker != null) {
104+
sticker.setImageKey(imageKey);
105+
sticker.setUpdatedAt(Instant.now());
106+
sticker.persist();
107+
}
108+
}
109+
100110
@Transactional
101111
public boolean deleteSticker(String stickerId) {
102112
Sticker sticker = Sticker.findById(stickerId);
@@ -154,6 +164,7 @@ private StickerDTO toStickerMetadata(Sticker sticker) {
154164
metadata.setStickerDescription(sticker.getDescription());
155165
metadata.setStickerQuantityRemaining(sticker.getStickerQuantityRemaining());
156166
metadata.setImageUrl(buildImageUrl(sticker.getStickerId()));
167+
metadata.setImageKey(sticker.getImageKey());
157168
metadata.setCreatedAt(Date.from(sticker.getCreatedAt()));
158169
metadata.setUpdatedAt(sticker.getUpdatedAt() != null ? Date.from(sticker.getUpdatedAt()) : null);
159170
return metadata;

sticker-award/src/main/java/com/datadoghq/stickerlandia/stickeraward/sticker/StickerResource.java

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import com.datadoghq.stickerlandia.stickeraward.sticker.dto.GetAllStickersResponse;
66
import com.datadoghq.stickerlandia.stickeraward.sticker.dto.CreateStickerResponse;
77
import com.datadoghq.stickerlandia.stickeraward.sticker.dto.StickerDTO;
8+
import com.datadoghq.stickerlandia.stickeraward.sticker.dto.StickerImageUploadResponse;
9+
import com.datadoghq.stickerlandia.stickeraward.sticker.dto.StickerMetadata;
810
import com.datadoghq.stickerlandia.stickeraward.sticker.dto.UpdateStickerRequest;
911
import io.smallrye.common.constraint.NotNull;
1012
import jakarta.ws.rs.Consumes;
@@ -22,13 +24,17 @@
2224
import jakarta.inject.Inject;
2325

2426
import java.io.InputStream;
27+
import java.util.Date;
2528

2629
@Path("/api/award/v1/stickers")
2730
public class StickerResource {
2831

2932
@Inject
3033
StickerRepository stickerRepository;
3134

35+
@Inject
36+
StickerImageService stickerImageService;
37+
3238
@GET
3339
@Produces("application/json")
3440
@Operation(description = "Get all stickers in the catalog")
@@ -98,9 +104,27 @@ public Response deleteSticker(@PathParam("stickerId") String stickerId) {
98104
@Produces("image/png")
99105
@Operation(description = "Get the sticker image")
100106
public Response getStickerImage(@PathParam("stickerId") String stickerId) {
101-
return Response.status(Response.Status.NOT_IMPLEMENTED)
102-
.entity("Image storage not implemented yet")
103-
.build();
107+
StickerDTO metadata = stickerRepository.getStickerMetadata(stickerId);
108+
if (metadata == null) {
109+
return Response.status(Response.Status.NOT_FOUND).build();
110+
}
111+
112+
if (metadata.getImageKey() == null) {
113+
return Response.status(Response.Status.NOT_FOUND)
114+
.entity("No image found for this sticker")
115+
.build();
116+
}
117+
118+
try {
119+
InputStream imageStream = stickerImageService.getImage(metadata.getImageKey());
120+
return Response.ok(imageStream)
121+
.type("image/png")
122+
.build();
123+
} catch (Exception e) {
124+
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
125+
.entity("Failed to retrieve image")
126+
.build();
127+
}
104128
}
105129

106130
@PUT
@@ -111,9 +135,31 @@ public Response getStickerImage(@PathParam("stickerId") String stickerId) {
111135
public Response uploadStickerImage(
112136
@PathParam("stickerId") String stickerId,
113137
@NotNull InputStream data) {
114-
return Response.status(Response.Status.NOT_IMPLEMENTED)
115-
.entity("Image upload not implemented yet")
116-
.build();
138+
StickerDTO metadata = stickerRepository.getStickerMetadata(stickerId);
139+
if (metadata == null) {
140+
return Response.status(Response.Status.NOT_FOUND).build();
141+
}
142+
143+
try {
144+
byte[] imageBytes = data.readAllBytes();
145+
String imageKey = stickerImageService.uploadImage(new java.io.ByteArrayInputStream(imageBytes), "image/png", imageBytes.length);
146+
147+
stickerRepository.updateStickerImageKey(stickerId, imageKey);
148+
149+
String imageUrl = stickerImageService.getImageUrl(imageKey);
150+
151+
StickerImageUploadResponse response = new StickerImageUploadResponse();
152+
response.setStickerId(stickerId);
153+
response.setImageUrl(imageUrl);
154+
response.setUploadedAt(new Date());
155+
156+
return Response.ok(response).build();
157+
} catch (Exception e) {
158+
System.out.println(e);
159+
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
160+
.entity("Failed to upload image")
161+
.build();
162+
}
117163
}
118164

119165
@GET
@@ -130,4 +176,4 @@ public Response getStickerAssignments(
130176
}
131177
return Response.ok(data).build();
132178
}
133-
}
179+
}

sticker-award/src/main/java/com/datadoghq/stickerlandia/stickeraward/sticker/dto/StickerDTO.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.util.Date;
44
import com.fasterxml.jackson.annotation.JsonFormat;
5+
import com.fasterxml.jackson.annotation.JsonIgnore;
56
import com.fasterxml.jackson.annotation.JsonInclude;
67
import com.fasterxml.jackson.annotation.JsonProperty;
78
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
@@ -45,6 +46,9 @@ public class StickerDTO {
4546
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'", timezone = "UTC")
4647
@JsonProperty("updatedAt")
4748
private Date updatedAt;
49+
50+
@JsonIgnore
51+
private String imageKey;
4852

4953
@JsonProperty("stickerId")
5054
public String getStickerId() {
@@ -132,4 +136,12 @@ public void setUpdatedAt(Date updatedAt) {
132136
this.updatedAt = updatedAt;
133137
}
134138

139+
public String getImageKey() {
140+
return imageKey;
141+
}
142+
143+
public void setImageKey(String imageKey) {
144+
this.imageKey = imageKey;
145+
}
146+
135147
}

0 commit comments

Comments
 (0)