Azure deployment setup
This commit is contained in:
69
.github/workflows/deploy.yml
vendored
Normal file
69
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
name: Build and Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
BACKEND_IMAGE: ghcr.io/${{ github.repository_owner }}/petshop-backend
|
||||||
|
FRONTEND_IMAGE: ghcr.io/${{ github.repository_owner }}/petshop-web
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push backend image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: ./backend
|
||||||
|
push: true
|
||||||
|
tags: ${{ env.BACKEND_IMAGE }}:latest
|
||||||
|
|
||||||
|
- name: Build and push frontend image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: ./web
|
||||||
|
push: true
|
||||||
|
tags: ${{ env.FRONTEND_IMAGE }}:latest
|
||||||
|
build-args: |
|
||||||
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${{ secrets.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY }}
|
||||||
|
|
||||||
|
- name: Log in to Azure
|
||||||
|
uses: azure/login@v1
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_CREDENTIALS }}
|
||||||
|
|
||||||
|
- name: Deploy backend
|
||||||
|
run: |
|
||||||
|
az containerapp update \
|
||||||
|
--name ${{ secrets.AZURE_BACKEND_APP_NAME }} \
|
||||||
|
--resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \
|
||||||
|
--image ${{ env.BACKEND_IMAGE }}:latest \
|
||||||
|
--registry-server ${{ env.REGISTRY }} \
|
||||||
|
--registry-username ${{ github.actor }} \
|
||||||
|
--registry-password ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Deploy frontend
|
||||||
|
run: |
|
||||||
|
az containerapp update \
|
||||||
|
--name ${{ secrets.AZURE_FRONTEND_APP_NAME }} \
|
||||||
|
--resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \
|
||||||
|
--image ${{ env.FRONTEND_IMAGE }}:latest \
|
||||||
|
--registry-server ${{ env.REGISTRY }} \
|
||||||
|
--registry-username ${{ github.actor }} \
|
||||||
|
--registry-password ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -9,6 +9,5 @@ RUN mvn -q -DskipTests package
|
|||||||
FROM eclipse-temurin:25-jre
|
FROM eclipse-temurin:25-jre
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=build /app/target/*.jar app.jar
|
COPY --from=build /app/target/*.jar app.jar
|
||||||
COPY uploads ./uploads
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
ENTRYPOINT ["java","-jar","app.jar"]
|
ENTRYPOINT ["java","-jar","app.jar"]
|
||||||
|
|||||||
@@ -102,6 +102,12 @@
|
|||||||
<version>3.1.0</version>
|
<version>3.1.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.azure</groupId>
|
||||||
|
<artifactId>azure-storage-blob</artifactId>
|
||||||
|
<version>12.29.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.petshop.backend.service;
|
package com.petshop.backend.service;
|
||||||
|
|
||||||
import com.petshop.backend.entity.User;
|
import com.petshop.backend.entity.User;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.core.io.ByteArrayResource;
|
||||||
import org.springframework.core.io.PathResource;
|
import org.springframework.core.io.PathResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@@ -22,10 +24,14 @@ import java.util.UUID;
|
|||||||
public class AvatarStorageService {
|
public class AvatarStorageService {
|
||||||
|
|
||||||
private static final String STORED_PREFIX = "/uploads/avatars/";
|
private static final String STORED_PREFIX = "/uploads/avatars/";
|
||||||
|
private static final String BLOB_CONTAINER = "avatars";
|
||||||
|
|
||||||
@Value("${app.upload.base-dir:uploads}")
|
@Value("${app.upload.base-dir:uploads}")
|
||||||
private String uploadBaseDir;
|
private String uploadBaseDir;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AzureBlobService blobService;
|
||||||
|
|
||||||
private Path avatarDirectory;
|
private Path avatarDirectory;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
@@ -34,18 +40,22 @@ public class AvatarStorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String storeAvatar(MultipartFile file) throws IOException {
|
public String storeAvatar(MultipartFile file) throws IOException {
|
||||||
Files.createDirectories(avatarDirectory);
|
String extension = resolveExtension(file.getOriginalFilename());
|
||||||
|
|
||||||
String originalFilename = file.getOriginalFilename();
|
|
||||||
String extension = resolveExtension(originalFilename);
|
|
||||||
String filename = UUID.randomUUID() + extension;
|
String filename = UUID.randomUUID() + extension;
|
||||||
Path filePath = avatarDirectory.resolve(filename).normalize();
|
if (blobService.isEnabled()) {
|
||||||
|
blobService.upload(BLOB_CONTAINER, filename, file.getBytes());
|
||||||
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
|
} else {
|
||||||
|
Files.createDirectories(avatarDirectory);
|
||||||
|
Files.copy(file.getInputStream(), avatarDirectory.resolve(filename).normalize(), StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
return STORED_PREFIX + filename;
|
return STORED_PREFIX + filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Resource loadAvatarResource(User user) {
|
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());
|
Path filePath = resolveStoredAvatarPath(user.getAvatarUrl());
|
||||||
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
|
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
|
||||||
throw new IllegalArgumentException("Avatar file was not found");
|
throw new IllegalArgumentException("Avatar file was not found");
|
||||||
@@ -54,15 +64,21 @@ public class AvatarStorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void deleteAvatar(User user) throws IOException {
|
public void deleteAvatar(User user) throws IOException {
|
||||||
if (user.getAvatarUrl() == null || user.getAvatarUrl().isBlank()) {
|
if (user.getAvatarUrl() == null || user.getAvatarUrl().isBlank()) return;
|
||||||
return;
|
if (blobService.isEnabled()) {
|
||||||
}
|
blobService.delete(BLOB_CONTAINER, extractFilename(user.getAvatarUrl()));
|
||||||
try {
|
} else {
|
||||||
Files.deleteIfExists(resolveStoredAvatarPath(user.getAvatarUrl()));
|
try {
|
||||||
} catch (IllegalArgumentException ignored) {
|
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) {
|
public String toOwnerAvatarUrl(User user) {
|
||||||
return hasAvatar(user) ? "/api/v1/users/" + user.getId() + "/avatar/file" : null;
|
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;
|
package com.petshop.backend.service;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.core.io.ByteArrayResource;
|
||||||
import org.springframework.core.io.PathResource;
|
import org.springframework.core.io.PathResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@@ -21,31 +23,58 @@ public class CatalogImageStorageService {
|
|||||||
|
|
||||||
private static final String PET_PREFIX = "/uploads/pets/";
|
private static final String PET_PREFIX = "/uploads/pets/";
|
||||||
private static final String PRODUCT_PREFIX = "/uploads/products/";
|
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}")
|
@Value("${app.upload.base-dir:uploads}")
|
||||||
private String uploadBaseDir;
|
private String uploadBaseDir;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AzureBlobService blobService;
|
||||||
|
|
||||||
public String storePetImage(MultipartFile file) throws IOException {
|
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 {
|
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) {
|
public Resource loadPetImage(String storedPath) {
|
||||||
Resource resource = new PathResource(resolveStoredPath(storedPath, Paths.get(uploadBaseDir, "pets").toAbsolutePath().normalize(), PET_PREFIX));
|
String filename = extractFilename(storedPath, PET_PREFIX);
|
||||||
if (!resource.exists()) {
|
if (blobService.isEnabled()) {
|
||||||
throw new IllegalArgumentException("Image file was not found");
|
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;
|
return resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Resource loadProductImage(String storedPath) {
|
public Resource loadProductImage(String storedPath) {
|
||||||
Resource resource = new PathResource(resolveStoredPath(storedPath, Paths.get(uploadBaseDir, "products").toAbsolutePath().normalize(), PRODUCT_PREFIX));
|
String filename = extractFilename(storedPath, PRODUCT_PREFIX);
|
||||||
if (!resource.exists()) {
|
if (blobService.isEnabled()) {
|
||||||
throw new IllegalArgumentException("Image file was not found");
|
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;
|
return resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,28 +82,35 @@ public class CatalogImageStorageService {
|
|||||||
return MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM);
|
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 {
|
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 {
|
public void deleteProductImage(String storedPath) throws IOException {
|
||||||
deleteImage(storedPath, Paths.get(uploadBaseDir, "products").toAbsolutePath().normalize(), PRODUCT_PREFIX);
|
if (storedPath == null || storedPath.isBlank()) return;
|
||||||
}
|
String filename = extractFilename(storedPath, PRODUCT_PREFIX);
|
||||||
|
if (blobService.isEnabled()) {
|
||||||
private String storeImage(MultipartFile file, Path directory, String prefix) throws IOException {
|
blobService.delete(BLOB_PRODUCTS, filename);
|
||||||
Files.createDirectories(directory);
|
} else {
|
||||||
String extension = resolveExtension(file.getOriginalFilename());
|
Files.deleteIfExists(resolveStoredPath(storedPath, Paths.get(uploadBaseDir, "products").toAbsolutePath().normalize(), PRODUCT_PREFIX));
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
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) {
|
private Path resolveStoredPath(String storedPath, Path directory, String prefix) {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package com.petshop.backend.service;
|
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.PathResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@@ -18,26 +21,37 @@ import java.util.UUID;
|
|||||||
public class ChatAttachmentStorageService {
|
public class ChatAttachmentStorageService {
|
||||||
|
|
||||||
private static final String STORED_PREFIX = "/uploads/chat/";
|
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 {
|
public String storeAttachment(MultipartFile file) throws IOException {
|
||||||
Files.createDirectories(chatDirectory);
|
|
||||||
|
|
||||||
String originalFilename = file.getOriginalFilename();
|
String originalFilename = file.getOriginalFilename();
|
||||||
String extension = "";
|
String extension = (originalFilename != null && originalFilename.contains("."))
|
||||||
if (originalFilename != null && originalFilename.contains(".")) {
|
? originalFilename.substring(originalFilename.lastIndexOf(".")) : "";
|
||||||
extension = originalFilename.substring(originalFilename.lastIndexOf("."));
|
|
||||||
}
|
|
||||||
|
|
||||||
String filename = UUID.randomUUID() + extension;
|
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;
|
return STORED_PREFIX + filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Resource loadAttachmentResource(String attachmentUrl) {
|
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)) {
|
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
|
||||||
throw new IllegalArgumentException("Attachment file was not found");
|
throw new IllegalArgumentException("Attachment file was not found");
|
||||||
}
|
}
|
||||||
@@ -45,27 +59,18 @@ public class ChatAttachmentStorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public MediaType resolveMediaType(String attachmentUrl) {
|
public MediaType resolveMediaType(String attachmentUrl) {
|
||||||
try {
|
String filename = extractFilename(attachmentUrl);
|
||||||
return MediaTypeFactory.getMediaType(loadAttachmentResource(attachmentUrl)).orElse(MediaType.APPLICATION_OCTET_STREAM);
|
return MediaTypeFactory.getMediaType(filename).orElse(MediaType.APPLICATION_OCTET_STREAM);
|
||||||
} catch (IllegalArgumentException ex) {
|
|
||||||
return MediaType.APPLICATION_OCTET_STREAM;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Path resolveStoredPath(String attachmentUrl) {
|
private String extractFilename(String attachmentUrl) {
|
||||||
if (attachmentUrl == null || attachmentUrl.isBlank() || !attachmentUrl.startsWith(STORED_PREFIX)) {
|
if (attachmentUrl == null || attachmentUrl.isBlank() || !attachmentUrl.startsWith(STORED_PREFIX)) {
|
||||||
throw new IllegalArgumentException("Invalid attachment URL");
|
throw new IllegalArgumentException("Invalid attachment URL");
|
||||||
}
|
}
|
||||||
|
|
||||||
String filename = attachmentUrl.substring(STORED_PREFIX.length());
|
String filename = attachmentUrl.substring(STORED_PREFIX.length());
|
||||||
if (filename.isBlank() || filename.contains("/") || filename.contains("\\") || filename.contains("..")) {
|
if (filename.isBlank() || filename.contains("/") || filename.contains("\\") || filename.contains("..")) {
|
||||||
throw new IllegalArgumentException("Invalid attachment filename");
|
throw new IllegalArgumentException("Invalid attachment filename");
|
||||||
}
|
}
|
||||||
|
return filename;
|
||||||
Path resolved = chatDirectory.resolve(filename).normalize();
|
|
||||||
if (!resolved.startsWith(chatDirectory)) {
|
|
||||||
throw new IllegalArgumentException("Invalid attachment path");
|
|
||||||
}
|
|
||||||
return resolved;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ spring:
|
|||||||
open-in-view: false
|
open-in-view: false
|
||||||
|
|
||||||
flyway:
|
flyway:
|
||||||
enabled: false
|
enabled: ${FLYWAY_ENABLED:false}
|
||||||
|
|
||||||
server:
|
server:
|
||||||
port: ${SERVER_PORT:8080}
|
port: ${SERVER_PORT:8080}
|
||||||
@@ -52,6 +52,11 @@ app:
|
|||||||
base-dir: ${UPLOAD_BASE_DIR:uploads}
|
base-dir: ${UPLOAD_BASE_DIR:uploads}
|
||||||
frontend-url: ${FRONTEND_URL:http://localhost:3000}
|
frontend-url: ${FRONTEND_URL:http://localhost:3000}
|
||||||
|
|
||||||
|
azure:
|
||||||
|
storage:
|
||||||
|
connection-string: ${AZURE_STORAGE_CONNECTION_STRING:}
|
||||||
|
container-prefix: ${AZURE_STORAGE_CONTAINER_PREFIX:petshop}
|
||||||
|
|
||||||
resend:
|
resend:
|
||||||
api-key: ${RESEND_API_KEY:}
|
api-key: ${RESEND_API_KEY:}
|
||||||
from: ${RESEND_FROM:PetShop <onboarding@resend.dev>}
|
from: ${RESEND_FROM:PetShop <onboarding@resend.dev>}
|
||||||
|
|||||||
17
web/Dockerfile
Normal file
17
web/Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
FROM node:22-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY . .
|
||||||
|
ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
||||||
|
ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:22-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production PORT=3000 HOSTNAME=0.0.0.0
|
||||||
|
COPY --from=build /app/.next/standalone ./
|
||||||
|
COPY --from=build /app/.next/static ./.next/static
|
||||||
|
COPY --from=build /app/public ./public
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", "server.js"]
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
|
output: 'standalone',
|
||||||
reactCompiler: true,
|
reactCompiler: true,
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
source: "/api/:path*",
|
source: "/api/:path*",
|
||||||
destination: "http://localhost:8080/api/:path*",
|
destination: `${process.env.BACKEND_URL || "http://localhost:8080"}/api/:path*`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user