Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added Screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added backblaze.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@
<artifactId>spring-boot-starter-webclient-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>2.25.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
Expand Down
44 changes: 44 additions & 0 deletions src/main/java/net/hackyourfuture/hyfshop/product/B2Config.java
Original file line number Diff line number Diff line change
@@ -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();
}
}

68 changes: 68 additions & 0 deletions src/main/java/net/hackyourfuture/hyfshop/product/FileService.java
Original file line number Diff line number Diff line change
@@ -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()
);
}
}

2 changes: 2 additions & 0 deletions src/main/java/net/hackyourfuture/hyfshop/product/Product.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import lombok.Setter;

import java.math.BigDecimal;
import java.util.Map;

@AllArgsConstructor
@NoArgsConstructor
Expand All @@ -17,4 +18,5 @@ public class Product {
private BigDecimal price;
private String category;
private String imageUrl;
private Map<String, Object> details;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,47 @@
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> 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<Map<String, Object>>() {}));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return product;
};

public List<Product> getAllProducts() {
return jdbcClient
.sql("SELECT id, title, price, category, image_url FROM products")
.sql("SELECT id, title, price, category, image_url, details FROM products")
.query(PRODUCT_ROW_MAPPER)
.list();

}

public Product findById(int id) {
return jdbcClient
.sql("SELECT id, title, price, category, image_url FROM products WHERE id = :id")
.sql("SELECT id, title, price, category, image_url, details FROM products WHERE id = :id")
.param("id", id)
.query(PRODUCT_ROW_MAPPER)
.single();
Expand All @@ -49,12 +63,25 @@ public void setImageUrl(int id, String imageUrl) {
}

public List<Product> findByColor(String color) {
// TODO: Implement
throw new UnsupportedOperationException("Not implemented yet");
return jdbcClient.sql("""
SELECT id, title, price, category, image_url, details
FROM products
WHERE details ->> 'color' = :color
LIMIT 10
""")
.param("color", color)
.query(PRODUCT_ROW_MAPPER)
.list();
}

public Product setSize(int id, String size) {
// TODO: Implement
throw new UnsupportedOperationException("Not implemented yet");
public void setSize(int id, String size) {
jdbcClient.sql("""
UPDATE products
SET details = jsonb_set(details, '{size}', to_jsonb(:size::text))
WHERE id = :id
""")
.param("id", id)
.param("size", size)
.update();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
private final FileService fileService;

public List<ProductResponse> getAllProducts() {
return productRepository.getAllProducts().stream().map(ProductResponse::from).toList();
Expand All @@ -26,14 +27,23 @@ public ProductResponse setProductSize(int id, String size) {
}

public ProductResponse setProductImage(int id, MultipartFile file) {
// TODO: Implement
// call ProductRepository.setImageUrl() afterwards with the new URL
throw new UnsupportedOperationException("Not implemented yet");
try {
String key = fileService.upload(file); // uploads to B2, returns the object key
productRepository.setImageUrl(id, key); // save the key in the DB
return ProductResponse.from(productRepository.findById(id));
} catch (Exception e) {
throw new RuntimeException("Failed to upload product image", e);
}
}

public ProductResponse deleteProductImage(int id) {
// TODO: Implement
// call ProductRepository.setImageUrl() to set the image url to null
throw new UnsupportedOperationException("Not implemented yet");
Product product = productRepository.findById(id);
String key = product.getImageUrl();
if (key != null) {
fileService.delete(key);
}
productRepository.setImageUrl(id, null); // clear the column
return ProductResponse.from(productRepository.findById(id));

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,25 @@
import net.hackyourfuture.hyfshop.product.Product;

import java.math.BigDecimal;
import java.util.Map;

public record ProductResponse (
int id,
String title,
BigDecimal price,
String category,
String imageUrl
String imageUrl,
Map<String, Object> details

){
public static ProductResponse from(Product product) {
return new ProductResponse(
product.getId(),
product.getTitle(),
product.getPrice(),
product.getCategory(),
product.getImageUrl()
product.getImageUrl(),
product.getDetails()
);
}
}
18 changes: 15 additions & 3 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,20 @@ spring:
application:
name: hyf_shop
datasource:
url: '${DB_URL}'
username: '${DB_USERNAME}'
password: '${DB_PASSWORD}'
url: jdbc:postgresql://localhost:5432/hyfshop
username: YOUR_USER_NAME
password: YOUR_PASSWORD

sql:
init:
# runs schema.sql automatically every time the app starts
mode: always
server:
port: '8080'

b2:
endpoint: https://s3.us-west-004.backblazeb2.com
region: us-west-004
access-key: YOUR_KEY_ID
secret-key: YOUR_APPLICATION_KEY
bucket: YOUR_BUCKET_NAME