Azure deployment setup

This commit is contained in:
2026-04-14 21:29:00 -06:00
parent aa9f3ad444
commit 316f6f45ed
10 changed files with 263 additions and 65 deletions

View File

@@ -1,7 +1,9 @@
package com.petshop.backend.service;
import com.petshop.backend.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.PathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
@@ -22,10 +24,14 @@ import java.util.UUID;
public class AvatarStorageService {
private static final String STORED_PREFIX = "/uploads/avatars/";
private static final String BLOB_CONTAINER = "avatars";
@Value("${app.upload.base-dir:uploads}")
private String uploadBaseDir;
@Autowired
private AzureBlobService blobService;
private Path avatarDirectory;
@PostConstruct
@@ -34,18 +40,22 @@ public class AvatarStorageService {
}
public String storeAvatar(MultipartFile file) throws IOException {
Files.createDirectories(avatarDirectory);
String originalFilename = file.getOriginalFilename();
String extension = resolveExtension(originalFilename);
String extension = resolveExtension(file.getOriginalFilename());
String filename = UUID.randomUUID() + extension;
Path filePath = avatarDirectory.resolve(filename).normalize();
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
if (blobService.isEnabled()) {
blobService.upload(BLOB_CONTAINER, filename, file.getBytes());
} else {
Files.createDirectories(avatarDirectory);
Files.copy(file.getInputStream(), avatarDirectory.resolve(filename).normalize(), StandardCopyOption.REPLACE_EXISTING);
}
return STORED_PREFIX + filename;
}
public Resource loadAvatarResource(User user) {
String filename = extractFilename(user.getAvatarUrl());
if (blobService.isEnabled()) {
return new ByteArrayResource(blobService.download(BLOB_CONTAINER, filename));
}
Path filePath = resolveStoredAvatarPath(user.getAvatarUrl());
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
throw new IllegalArgumentException("Avatar file was not found");
@@ -54,15 +64,21 @@ public class AvatarStorageService {
}
public void deleteAvatar(User user) throws IOException {
if (user.getAvatarUrl() == null || user.getAvatarUrl().isBlank()) {
return;
}
try {
Files.deleteIfExists(resolveStoredAvatarPath(user.getAvatarUrl()));
} catch (IllegalArgumentException ignored) {
if (user.getAvatarUrl() == null || user.getAvatarUrl().isBlank()) return;
if (blobService.isEnabled()) {
blobService.delete(BLOB_CONTAINER, extractFilename(user.getAvatarUrl()));
} else {
try {
Files.deleteIfExists(resolveStoredAvatarPath(user.getAvatarUrl()));
} catch (IllegalArgumentException ignored) {}
}
}
private String extractFilename(String avatarUrl) {
if (avatarUrl == null || !avatarUrl.startsWith(STORED_PREFIX)) throw new IllegalArgumentException("Avatar file was not found");
return avatarUrl.substring(STORED_PREFIX.length());
}
public String toOwnerAvatarUrl(User user) {
return hasAvatar(user) ? "/api/v1/users/" + user.getId() + "/avatar/file" : null;
}

View File

@@ -0,0 +1,44 @@
package com.petshop.backend.service;
import com.azure.storage.blob.BlobContainerClient;
import com.azure.storage.blob.BlobServiceClient;
import com.azure.storage.blob.BlobServiceClientBuilder;
import com.azure.core.util.BinaryData;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
public class AzureBlobService {
private final BlobServiceClient client;
private final String containerPrefix;
public AzureBlobService(
@Value("${azure.storage.connection-string:}") String connectionString,
@Value("${azure.storage.container-prefix:petshop}") String containerPrefix) {
this.containerPrefix = containerPrefix;
this.client = (connectionString != null && !connectionString.isBlank())
? new BlobServiceClientBuilder().connectionString(connectionString).buildClient()
: null;
}
public boolean isEnabled() {
return client != null;
}
public void upload(String container, String blobName, byte[] bytes) {
getContainerClient(container).getBlobClient(blobName).upload(BinaryData.fromBytes(bytes), true);
}
public byte[] download(String container, String blobName) {
return getContainerClient(container).getBlobClient(blobName).downloadContent().toBytes();
}
public void delete(String container, String blobName) {
getContainerClient(container).getBlobClient(blobName).deleteIfExists();
}
private BlobContainerClient getContainerClient(String container) {
return client.createBlobContainerIfNotExists(containerPrefix + "-" + container);
}
}

View File

@@ -1,6 +1,8 @@
package com.petshop.backend.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.PathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
@@ -21,31 +23,58 @@ public class CatalogImageStorageService {
private static final String PET_PREFIX = "/uploads/pets/";
private static final String PRODUCT_PREFIX = "/uploads/products/";
private static final String BLOB_PETS = "pets";
private static final String BLOB_PRODUCTS = "products";
@Value("${app.upload.base-dir:uploads}")
private String uploadBaseDir;
@Autowired
private AzureBlobService blobService;
public String storePetImage(MultipartFile file) throws IOException {
return storeImage(file, Paths.get(uploadBaseDir, "pets").toAbsolutePath().normalize(), PET_PREFIX);
String extension = resolveExtension(file.getOriginalFilename());
String filename = UUID.randomUUID() + extension;
if (blobService.isEnabled()) {
blobService.upload(BLOB_PETS, filename, file.getBytes());
} else {
Path directory = Paths.get(uploadBaseDir, "pets").toAbsolutePath().normalize();
Files.createDirectories(directory);
Files.copy(file.getInputStream(), directory.resolve(filename).normalize(), StandardCopyOption.REPLACE_EXISTING);
}
return PET_PREFIX + filename;
}
public String storeProductImage(MultipartFile file) throws IOException {
return storeImage(file, Paths.get(uploadBaseDir, "products").toAbsolutePath().normalize(), PRODUCT_PREFIX);
String extension = resolveExtension(file.getOriginalFilename());
String filename = UUID.randomUUID() + extension;
if (blobService.isEnabled()) {
blobService.upload(BLOB_PRODUCTS, filename, file.getBytes());
} else {
Path directory = Paths.get(uploadBaseDir, "products").toAbsolutePath().normalize();
Files.createDirectories(directory);
Files.copy(file.getInputStream(), directory.resolve(filename).normalize(), StandardCopyOption.REPLACE_EXISTING);
}
return PRODUCT_PREFIX + filename;
}
public Resource loadPetImage(String storedPath) {
Resource resource = new PathResource(resolveStoredPath(storedPath, Paths.get(uploadBaseDir, "pets").toAbsolutePath().normalize(), PET_PREFIX));
if (!resource.exists()) {
throw new IllegalArgumentException("Image file was not found");
String filename = extractFilename(storedPath, PET_PREFIX);
if (blobService.isEnabled()) {
return new ByteArrayResource(blobService.download(BLOB_PETS, filename));
}
Resource resource = new PathResource(resolveStoredPath(storedPath, Paths.get(uploadBaseDir, "pets").toAbsolutePath().normalize(), PET_PREFIX));
if (!resource.exists()) throw new IllegalArgumentException("Image file was not found");
return resource;
}
public Resource loadProductImage(String storedPath) {
Resource resource = new PathResource(resolveStoredPath(storedPath, Paths.get(uploadBaseDir, "products").toAbsolutePath().normalize(), PRODUCT_PREFIX));
if (!resource.exists()) {
throw new IllegalArgumentException("Image file was not found");
String filename = extractFilename(storedPath, PRODUCT_PREFIX);
if (blobService.isEnabled()) {
return new ByteArrayResource(blobService.download(BLOB_PRODUCTS, filename));
}
Resource resource = new PathResource(resolveStoredPath(storedPath, Paths.get(uploadBaseDir, "products").toAbsolutePath().normalize(), PRODUCT_PREFIX));
if (!resource.exists()) throw new IllegalArgumentException("Image file was not found");
return resource;
}
@@ -53,28 +82,35 @@ public class CatalogImageStorageService {
return MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM);
}
public MediaType resolveMediaTypeByName(String filename) {
return MediaTypeFactory.getMediaType(filename).orElse(MediaType.APPLICATION_OCTET_STREAM);
}
public void deletePetImage(String storedPath) throws IOException {
deleteImage(storedPath, Paths.get(uploadBaseDir, "pets").toAbsolutePath().normalize(), PET_PREFIX);
if (storedPath == null || storedPath.isBlank()) return;
String filename = extractFilename(storedPath, PET_PREFIX);
if (blobService.isEnabled()) {
blobService.delete(BLOB_PETS, filename);
} else {
Files.deleteIfExists(resolveStoredPath(storedPath, Paths.get(uploadBaseDir, "pets").toAbsolutePath().normalize(), PET_PREFIX));
}
}
public void deleteProductImage(String storedPath) throws IOException {
deleteImage(storedPath, Paths.get(uploadBaseDir, "products").toAbsolutePath().normalize(), PRODUCT_PREFIX);
}
private String storeImage(MultipartFile file, Path directory, String prefix) throws IOException {
Files.createDirectories(directory);
String extension = resolveExtension(file.getOriginalFilename());
String filename = UUID.randomUUID() + extension;
Path filePath = directory.resolve(filename).normalize();
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
return prefix + filename;
}
private void deleteImage(String storedPath, Path directory, String prefix) throws IOException {
if (storedPath == null || storedPath.isBlank()) {
return;
if (storedPath == null || storedPath.isBlank()) return;
String filename = extractFilename(storedPath, PRODUCT_PREFIX);
if (blobService.isEnabled()) {
blobService.delete(BLOB_PRODUCTS, filename);
} else {
Files.deleteIfExists(resolveStoredPath(storedPath, Paths.get(uploadBaseDir, "products").toAbsolutePath().normalize(), PRODUCT_PREFIX));
}
Files.deleteIfExists(resolveStoredPath(storedPath, directory, prefix));
}
private String extractFilename(String storedPath, String prefix) {
if (storedPath == null || !storedPath.startsWith(prefix)) throw new IllegalArgumentException("Image file was not found");
String filename = storedPath.substring(prefix.length());
if (filename.isBlank() || filename.contains("/") || filename.contains("\\") || filename.contains("..")) throw new IllegalArgumentException("Image file was not found");
return filename;
}
private Path resolveStoredPath(String storedPath, Path directory, String prefix) {

View File

@@ -1,5 +1,8 @@
package com.petshop.backend.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.PathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
@@ -18,26 +21,37 @@ import java.util.UUID;
public class ChatAttachmentStorageService {
private static final String STORED_PREFIX = "/uploads/chat/";
private final Path chatDirectory = Paths.get("uploads", "chat").toAbsolutePath().normalize();
private static final String BLOB_CONTAINER = "chat";
@Value("${app.upload.base-dir:uploads}")
private String uploadBaseDir;
@Autowired
private AzureBlobService blobService;
public String storeAttachment(MultipartFile file) throws IOException {
Files.createDirectories(chatDirectory);
String originalFilename = file.getOriginalFilename();
String extension = "";
if (originalFilename != null && originalFilename.contains(".")) {
extension = originalFilename.substring(originalFilename.lastIndexOf("."));
}
String extension = (originalFilename != null && originalFilename.contains("."))
? originalFilename.substring(originalFilename.lastIndexOf(".")) : "";
String filename = UUID.randomUUID() + extension;
Path filePath = chatDirectory.resolve(filename).normalize();
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
if (blobService.isEnabled()) {
blobService.upload(BLOB_CONTAINER, filename, file.getBytes());
} else {
Path chatDirectory = Paths.get(uploadBaseDir, "chat").toAbsolutePath().normalize();
Files.createDirectories(chatDirectory);
Files.copy(file.getInputStream(), chatDirectory.resolve(filename).normalize(), StandardCopyOption.REPLACE_EXISTING);
}
return STORED_PREFIX + filename;
}
public Resource loadAttachmentResource(String attachmentUrl) {
Path filePath = resolveStoredPath(attachmentUrl);
String filename = extractFilename(attachmentUrl);
if (blobService.isEnabled()) {
return new ByteArrayResource(blobService.download(BLOB_CONTAINER, filename));
}
Path chatDirectory = Paths.get(uploadBaseDir, "chat").toAbsolutePath().normalize();
Path filePath = chatDirectory.resolve(filename).normalize();
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
throw new IllegalArgumentException("Attachment file was not found");
}
@@ -45,27 +59,18 @@ public class ChatAttachmentStorageService {
}
public MediaType resolveMediaType(String attachmentUrl) {
try {
return MediaTypeFactory.getMediaType(loadAttachmentResource(attachmentUrl)).orElse(MediaType.APPLICATION_OCTET_STREAM);
} catch (IllegalArgumentException ex) {
return MediaType.APPLICATION_OCTET_STREAM;
}
String filename = extractFilename(attachmentUrl);
return MediaTypeFactory.getMediaType(filename).orElse(MediaType.APPLICATION_OCTET_STREAM);
}
private Path resolveStoredPath(String attachmentUrl) {
private String extractFilename(String attachmentUrl) {
if (attachmentUrl == null || attachmentUrl.isBlank() || !attachmentUrl.startsWith(STORED_PREFIX)) {
throw new IllegalArgumentException("Invalid attachment URL");
}
String filename = attachmentUrl.substring(STORED_PREFIX.length());
if (filename.isBlank() || filename.contains("/") || filename.contains("\\") || filename.contains("..")) {
throw new IllegalArgumentException("Invalid attachment filename");
}
Path resolved = chatDirectory.resolve(filename).normalize();
if (!resolved.startsWith(chatDirectory)) {
throw new IllegalArgumentException("Invalid attachment path");
}
return resolved;
return filename;
}
}

View File

@@ -33,7 +33,7 @@ spring:
open-in-view: false
flyway:
enabled: false
enabled: ${FLYWAY_ENABLED:false}
server:
port: ${SERVER_PORT:8080}
@@ -52,6 +52,11 @@ app:
base-dir: ${UPLOAD_BASE_DIR:uploads}
frontend-url: ${FRONTEND_URL:http://localhost:3000}
azure:
storage:
connection-string: ${AZURE_STORAGE_CONNECTION_STRING:}
container-prefix: ${AZURE_STORAGE_CONTAINER_PREFIX:petshop}
resend:
api-key: ${RESEND_API_KEY:}
from: ${RESEND_FROM:PetShop <onboarding@resend.dev>}