Azure deployment setup
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>}
|
||||
|
||||
Reference in New Issue
Block a user