diff --git a/Screenshot.png b/Screenshot.png
new file mode 100644
index 0000000..e7ba10e
Binary files /dev/null and b/Screenshot.png differ
diff --git a/backblaze.png b/backblaze.png
new file mode 100644
index 0000000..61131b9
Binary files /dev/null and b/backblaze.png differ
diff --git a/pom.xml b/pom.xml
index bbdda38..08cf05d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -54,6 +54,15 @@
spring-boot-starter-webclient-test
test
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ software.amazon.awssdk
+ s3
+ 2.25.0
+
org.springframework.boot
spring-boot-starter-webmvc-test
diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/B2Config.java b/src/main/java/net/hackyourfuture/hyfshop/product/B2Config.java
new file mode 100644
index 0000000..d679cc7
--- /dev/null
+++ b/src/main/java/net/hackyourfuture/hyfshop/product/B2Config.java
@@ -0,0 +1,44 @@
+package net.hackyourfuture.hyfshop.product;
+
+import java.net.URI;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.presigner.S3Presigner;
+
+
+@Configuration
+public class B2Config {
+
+ @Value("${b2.endpoint}") private String endpoint;
+ @Value("${b2.region}") private String region;
+ @Value("${b2.access-key}") private String accessKey;
+ @Value("${b2.secret-key}") private String secretKey;
+
+ @Bean
+ public S3Client s3Client() {
+ return S3Client.builder()
+ .credentialsProvider(StaticCredentialsProvider.create(
+ AwsBasicCredentials.create(accessKey, secretKey)))
+ .endpointOverride(URI.create(endpoint))
+ .region(Region.of(region))
+ .build();
+ }
+
+ @Bean
+ public S3Presigner s3Presigner() {
+ return S3Presigner.builder()
+ .credentialsProvider(StaticCredentialsProvider.create(
+ AwsBasicCredentials.create(accessKey, secretKey)))
+ .endpointOverride(URI.create(endpoint))
+ .region(Region.of(region))
+ .build();
+ }
+}
+
diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/FileService.java b/src/main/java/net/hackyourfuture/hyfshop/product/FileService.java
new file mode 100644
index 0000000..ef7a72c
--- /dev/null
+++ b/src/main/java/net/hackyourfuture/hyfshop/product/FileService.java
@@ -0,0 +1,68 @@
+package net.hackyourfuture.hyfshop.product;
+
+import java.time.Duration;
+import java.util.UUID;
+
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import org.springframework.beans.factory.annotation.Value;
+
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+import software.amazon.awssdk.services.s3.presigner.S3Presigner;
+import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
+
+@Service
+public class FileService {
+
+ private final S3Client s3Client;
+ private final S3Presigner s3Presigner;
+
+ @Value("${b2.bucket}")
+ private String bucket;
+
+ @Value("${b2.endpoint}")
+ private String endpoint;
+
+ public FileService(S3Client s3Client, S3Presigner s3Presigner) {
+ this.s3Client = s3Client;
+ this.s3Presigner = s3Presigner;
+ }
+
+ // Upload — returns the public URL to save in your database
+ public String upload(MultipartFile file) throws Exception {
+ String key = "uploads/" + UUID.randomUUID() + "-" + file.getOriginalFilename();
+ s3Client.putObject(
+ PutObjectRequest.builder()
+ .bucket(bucket).key(key)
+ .contentType(file.getContentType()).build(),
+ RequestBody.fromInputStream(file.getInputStream(), file.getSize())
+ );
+ // https://s3.us-west-004.backblazeb2.com/file/restapitest/uploads/620fab18-34fc-46c6-9292-ee57f7bebbfc-example.svg
+ return endpoint + "/file/" + bucket + "/" + key;
+ }
+
+ // Presigned URL — expires after given minutes
+ public String getLink(String key, int minutes) {
+ return s3Presigner.presignGetObject(
+ GetObjectPresignRequest.builder()
+ .signatureDuration(Duration.ofMinutes(minutes))
+ .getObjectRequest(r -> r.bucket(bucket).key(key))
+ .build()
+ ).url().toString();
+ }
+
+ // Delete — accepts the stored public URL (or a raw key)
+ public void delete(String urlOrKey) {
+ String prefix = endpoint + "/file/" + bucket + "/";
+ String key = urlOrKey.startsWith(prefix) ? urlOrKey.substring(prefix.length()) : urlOrKey;
+ s3Client.deleteObject(
+ DeleteObjectRequest.builder()
+ .bucket(bucket).key(key).build()
+ );
+ }
+}
+
diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/Product.java b/src/main/java/net/hackyourfuture/hyfshop/product/Product.java
index ccc6204..a3108b9 100644
--- a/src/main/java/net/hackyourfuture/hyfshop/product/Product.java
+++ b/src/main/java/net/hackyourfuture/hyfshop/product/Product.java
@@ -6,6 +6,7 @@
import lombok.Setter;
import java.math.BigDecimal;
+import java.util.Map;
@AllArgsConstructor
@NoArgsConstructor
@@ -17,4 +18,5 @@ public class Product {
private BigDecimal price;
private String category;
private String imageUrl;
+ private Map details;
}
diff --git a/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java b/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java
index 2766734..b3525d0 100644
--- a/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java
+++ b/src/main/java/net/hackyourfuture/hyfshop/product/ProductRepository.java
@@ -5,25 +5,39 @@
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.stereotype.Repository;
import java.util.List;
+import java.util.Map;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.core.type.TypeReference;
@Repository
@AllArgsConstructor
public class ProductRepository {
private final JdbcClient jdbcClient;
+ private static final ObjectMapper objectMapper = new ObjectMapper();
public static final RowMapper PRODUCT_ROW_MAPPER = (rs, _) -> {
+
var product = new Product();
product.setId(rs.getInt("id"));
product.setTitle(rs.getString("title"));
product.setPrice(rs.getBigDecimal("price"));
product.setCategory(rs.getString("category"));
product.setImageUrl(rs.getString("image_url"));
+ try {
+ String json = rs.getString("details");
+ if (json != null) {
+ product.setDetails(objectMapper.readValue(json,
+ new TypeReference