deleteFileById(UUID fileId);
+
+ /**
+ * Streams an image file from MinIO for previewing within the client browser.
+ *
+ * This method retrieves file metadata using the provided file ID and then streams the file
+ * to the HTTP response for inline viewing. Only image files are allowed for preview.
+ * If the content type is invalid or any error occurs, an appropriate response status is set.
+ *
+ * @param fileId The unique identifier of the file to be previewed.
+ * @param servletResponse The HTTP response object used to stream the file.
+ */
+ void preview(UUID fileId, HttpServletResponse servletResponse);
}
diff --git a/src/main/java/uz/javadev/service/MinioService.java b/src/main/java/uz/javadev/service/MinioService.java
index b4c9cee..003dcf3 100644
--- a/src/main/java/uz/javadev/service/MinioService.java
+++ b/src/main/java/uz/javadev/service/MinioService.java
@@ -1,4 +1,57 @@
package uz.javadev.service;
+import io.minio.GenericResponse;
+import io.minio.GetObjectResponse;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+import uz.javadev.exp.FileException;
+
+/**
+ * This class abstracts common operations for working with MinIO, allowing
+ * for easy management of files such as uploading, downloading, and deleting files.
+ *
+ * @author Javohir Yallayev
+ */
+
public interface MinioService {
+
+ /**
+ * Uploads a file to the MinIO server.
+ *
+ * This method takes a MultipartFile and uploads it to the specified bucket at the given path.
+ * If the bucket does not exist, it will create the bucket before uploading the file.
+ *
+ * @param file The file to be uploaded.
+ * @param bucketName The name of the bucket where the file should be stored.
+ * @param path The destination path inside the bucket for storing the file.
+ * @return GenericResponse containing information about the uploaded file.
+ * @throws FileException if the file cannot be uploaded to MinIO.
+ */
+ GenericResponse uploadFile(MultipartFile file, String bucketName, String path);
+
+ /**
+ * Downloads a file from the MinIO server.
+ *
+ * This method takes the file name and bucket name, retrieves the corresponding file from the MinIO server,
+ * and returns the file as a GetObjectResponse stream.
+ *
+ * @param fileName The name of the file to be downloaded.
+ * @param bucketName The name of the bucket where the file is located.
+ * @return GetObjectResponse containing the input stream of the downloaded file.
+ * @throws FileException if the file cannot be downloaded from MinIO.
+ */
+ GetObjectResponse downloadFile(String fileName, String bucketName);
+
+ /**
+ * Deletes a file from the MinIO server.
+ *
+ * This method removes the specified file from the given bucket.
+ * If the bucket or file does not exist, a FileException will be thrown.
+ *
+ * @param bucketName The name of the bucket containing the file to be deleted.
+ * @param path The path of the file inside the bucket to be deleted.
+ * @throws FileException if the file cannot be deleted from MinIO.
+ */
+ void deleteFile(String bucketName, String path);
+
}
diff --git a/src/main/java/uz/javadev/service/dto/CommonResultData.java b/src/main/java/uz/javadev/service/dto/CommonResultData.java
index 5d7a3fe..44fc359 100644
--- a/src/main/java/uz/javadev/service/dto/CommonResultData.java
+++ b/src/main/java/uz/javadev/service/dto/CommonResultData.java
@@ -1,4 +1,4 @@
-package uz.retail.core.service.dto;
+package uz.javadev.service.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -17,7 +17,6 @@ public class CommonResultData implements Serializable {
private T data;
public CommonResultData(T data) {
-// super(SUCCESS);
this.data = data;
}
diff --git a/src/main/java/uz/javadev/service/dto/FileDto.java b/src/main/java/uz/javadev/service/dto/FileDto.java
index 77cfeb0..febc0f7 100644
--- a/src/main/java/uz/javadev/service/dto/FileDto.java
+++ b/src/main/java/uz/javadev/service/dto/FileDto.java
@@ -1,4 +1,23 @@
package uz.javadev.service.dto;
+import lombok.Data;
+
+import java.time.Instant;
+import java.util.UUID;
+
+@Data
public class FileDto {
+ private UUID id;
+
+ private String filePath;
+
+ private String bucketName;
+
+ private String fileName;
+
+ private Long size;
+
+ private String extension;
+
+ private Instant createdDate;
}
diff --git a/src/main/java/uz/javadev/service/impl/FileServiceImpl.java b/src/main/java/uz/javadev/service/impl/FileServiceImpl.java
index ab7295b..3713efc 100644
--- a/src/main/java/uz/javadev/service/impl/FileServiceImpl.java
+++ b/src/main/java/uz/javadev/service/impl/FileServiceImpl.java
@@ -1,4 +1,144 @@
package uz.javadev.service.impl;
-public class FileServiceImpl {
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.transaction.Transactional;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.catalina.connector.ClientAbortException;
+import org.springframework.http.HttpHeaders;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+import uz.javadev.domain.FileEntity;
+import uz.javadev.exp.FileException;
+import uz.javadev.exp.InvalidRequestException;
+import uz.javadev.repo.FileRepository;
+import uz.javadev.service.FileService;
+import uz.javadev.service.dto.CommonResultData;
+import uz.javadev.service.dto.FileDto;
+import uz.javadev.service.mapper.FileMapper;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.UUID;
+
+import static uz.javadev.domain.enums.Errors.*;
+import static uz.javadev.utils.FilesUtils.*;
+
+/**
+ * @author Javohir Yallayev javadev0612
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class FileServiceImpl implements FileService {
+
+ private final FileMapper fileMapper;
+ private final FileRepository repo;
+ private final MinioServiceImpl minioServiceImpl;
+
+ @Override
+ public CommonResultData uploadFile(MultipartFile file, String bucketName) {
+
+ String fileName = sanitizeFileName(getFileName(file.getOriginalFilename()));
+ String filePath = createPath(fileName);
+
+ try {
+ var response = minioServiceImpl.uploadFile(file, bucketName, filePath);
+
+ var savedFile = repo.save(FileEntity.builder()
+ .filePath(filePath)
+ .fileName(fileName)
+ .extension(getExtension(fileName))
+ .bucketName(response.bucket())
+ .size(file.getSize())
+ .build());
+
+ var dto = fileMapper.toDto(savedFile);
+
+ log.info("File uploaded successfully: {}", dto);
+ return new CommonResultData<>(dto);
+
+ } catch (FileException e) {
+ log.error("File upload failed: {}", e.getMessage());
+ throw new InvalidRequestException(INVALID_REQUEST);
+ }
+ }
+
+ @Override
+ public CommonResultData downloadFile(UUID fileId, HttpServletResponse servletResponse) {
+ var fileManagement = getFileOrThrow(fileId);
+
+ servletResponse.setContentType("application/octet-stream");
+ servletResponse.setHeader(HttpHeaders.CONTENT_DISPOSITION,
+ "attachment; filename=\"" + fileManagement.getFileName() + "\"");
+ servletResponse.setContentLengthLong(fileManagement.getSize());
+
+
+ long startTime = System.currentTimeMillis();
+ try (InputStream is = minioServiceImpl.downloadFile(fileManagement.getFilePath(), fileManagement.getBucketName());
+ OutputStream os = servletResponse.getOutputStream()) {
+
+ byte[] buffer = new byte[64 * 1024];
+ int bytesRead;
+ while ((bytesRead = is.read(buffer)) != -1) {
+ os.write(buffer, 0, bytesRead);
+ }
+ os.flush();
+
+ long endTime = System.currentTimeMillis();
+ log.info("File with id {} downloaded in {} ms", fileId, (endTime - startTime));
+ } catch (ClientAbortException e) {
+ log.warn("Client aborted connection while downloading file with id {}", fileId);
+ return CommonResultData.failed("CLIENT_ABORTED_CONNECTION");
+ } catch (IOException e) {
+ log.error("Error downloading file with id {}: {}", fileId, e.getMessage());
+ return CommonResultData.failed("CLIENT_FILES_NOT_FOUND");
+ }
+
+ return CommonResultData.success();
+ }
+
+ @Override
+ public void preview(UUID fileId, HttpServletResponse servletResponse) {
+ var fileManagement = getFileOrThrow(fileId);
+
+ var contentType = getContentType(fileManagement.getFileName());
+ if (contentType == null || !contentType.startsWith("image/")) {
+ log.warn("Invalid content type to preview: {}", contentType);
+ servletResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ }
+ servletResponse.setContentType(contentType);
+
+ try (InputStream is = minioServiceImpl.downloadFile(fileManagement.getFilePath(), fileManagement.getBucketName())) {
+ is.transferTo(servletResponse.getOutputStream());
+ } catch (IOException e) {
+ log.error("Error previewing file with id {}: {}", fileId, e.getMessage());
+ servletResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @Override
+ @Transactional
+ public CommonResultData deleteFileById(UUID fileId) {
+ var file = getFileOrThrow(fileId);
+ try {
+ minioServiceImpl.deleteFile(file.getBucketName(), file.getFilePath());
+ repo.deleteById(fileId);
+
+ log.info("Successfully deleted file with id: {}", fileId);
+ return CommonResultData.success();
+ } catch (Exception e) {
+ log.error("Error deleting file with id {}: {}", fileId, e.getMessage());
+ throw new InvalidRequestException(FILE_DELETE_FAIL);
+ }
+ }
+
+ private FileEntity getFileOrThrow(UUID id) {
+ return repo.findById(id)
+ .orElseThrow(() -> {
+ log.error("File not found for id: {}", id);
+ return new InvalidRequestException(FILE_NOT_FOUND);
+ });
+ }
}
diff --git a/src/main/java/uz/javadev/service/impl/MinioServiceImpl.java b/src/main/java/uz/javadev/service/impl/MinioServiceImpl.java
index 62e4dab..9b70fed 100644
--- a/src/main/java/uz/javadev/service/impl/MinioServiceImpl.java
+++ b/src/main/java/uz/javadev/service/impl/MinioServiceImpl.java
@@ -1,4 +1,4 @@
-package uz.javadev.service;
+package uz.javadev.service.impl;
import io.minio.*;
import lombok.RequiredArgsConstructor;
@@ -6,22 +6,21 @@
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import uz.javadev.exp.FileException;
+import uz.javadev.service.MinioService;
import static uz.javadev.domain.enums.Errors.*;
/**
- * This class abstracts common operations for working with MinIO, allowing
- * for easy management of files such as uploading, downloading, and deleting files.
- *
* @author Javohir Yallayev
*/
@Service
@Slf4j
@RequiredArgsConstructor
-public class MinioServiceImpl{
+public class MinioServiceImpl implements MinioService {
private final MinioClient minioClient;
+ @Override
public GenericResponse uploadFile(MultipartFile file, String bucketName, String path) {
try {
return minioClient.putObject(
@@ -37,6 +36,7 @@ public GenericResponse uploadFile(MultipartFile file, String bucketName, String
}
}
+ @Override
public GetObjectResponse downloadFile(String fileName, String bucketName) {
try {
return minioClient.getObject(GetObjectArgs.builder()
@@ -48,20 +48,7 @@ public GetObjectResponse downloadFile(String fileName, String bucketName) {
}
}
- private String checkBucketName(String bucketName) {
- try {
- if (minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
- return bucketName;
- }
- minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
- return bucketName;
- } catch (Exception e) {
- log.error("Check on bucket exception: {}", e.getMessage());
- throw new FileException(INVALID_BUCKET_NAME);
- }
- }
-
-
+ @Override
public void deleteFile(String bucketName, String path) {
try {
minioClient.removeObject(
@@ -76,4 +63,16 @@ public void deleteFile(String bucketName, String path) {
}
}
+ private String checkBucketName(String bucketName) {
+ try {
+ if (minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
+ return bucketName;
+ }
+ minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
+ return bucketName;
+ } catch (Exception e) {
+ log.error("Check on bucket exception: {}", e.getMessage());
+ throw new FileException(INVALID_BUCKET_NAME);
+ }
+ }
}
diff --git a/src/main/java/uz/javadev/service/mapper/EntityMapper.java b/src/main/java/uz/javadev/service/mapper/EntityMapper.java
index 43d8c99..094cd68 100644
--- a/src/main/java/uz/javadev/service/mapper/EntityMapper.java
+++ b/src/main/java/uz/javadev/service/mapper/EntityMapper.java
@@ -1,4 +1,4 @@
-package uz.retail.core.service.mapper;
+package uz.javadev.service.mapper;
import org.mapstruct.*;
diff --git a/src/main/java/uz/javadev/service/mapper/FileMapper.java b/src/main/java/uz/javadev/service/mapper/FileMapper.java
index 02a0156..0ec0780 100644
--- a/src/main/java/uz/javadev/service/mapper/FileMapper.java
+++ b/src/main/java/uz/javadev/service/mapper/FileMapper.java
@@ -1,9 +1,9 @@
-package uz.retail.core.service.mapper;
+package uz.javadev.service.mapper;
import org.mapstruct.Mapper;
-import uz.retail.core.domain.FileManagement;
-import uz.retail.core.service.dto.FileManagementDTO;
+import uz.javadev.domain.FileEntity;
+import uz.javadev.service.dto.FileDto;
@Mapper(componentModel = "spring")
-public interface FileManagementMapper extends EntityMapper {
+public interface FileMapper extends EntityMapper {
}
diff --git a/src/main/java/uz/javadev/utils/FilesUtils.java b/src/main/java/uz/javadev/utils/FilesUtils.java
index e56dd5d..724c4b8 100644
--- a/src/main/java/uz/javadev/utils/FilesUtils.java
+++ b/src/main/java/uz/javadev/utils/FilesUtils.java
@@ -1,11 +1,8 @@
-package uz.retail.core.utils;
+package uz.javadev.utils;
-import com.google.common.io.ByteStreams;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
-import java.io.IOException;
-import java.io.InputStream;
import java.time.LocalDate;
import static java.time.format.DateTimeFormatter.ofPattern;
@@ -14,15 +11,6 @@
@Slf4j
public class FilesUtils {
- public static byte[] toByteArray(InputStream response) {
- try (InputStream is = response) {
- return ByteStreams.toByteArray(is);
- } catch (IOException e) {
- log.error("Cannot convert to byte array -> {}", e.getMessage());
- throw new RuntimeException(e);
- }
- }
-
public static String createPath(String fileName) {
var date = LocalDate.now().format(ofPattern("yyyy/MM/dd"));
return date + "/" + fileName;
@@ -40,12 +28,14 @@ public static String getContentType(String fileName) {
};
}
+ public static String sanitizeFileName(String fileName) {
+ return fileName.replaceAll("[^a-zA-Z0-9._-]", "_");
+ }
public static String getFileName(String originalName) {
return System.currentTimeMillis() + originalName;
}
-
public static String getExtension(String fileName) {
if (fileName == null || fileName.isEmpty())
return null;
diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt
index e69de29..2ad3297 100644
--- a/src/main/resources/banner.txt
+++ b/src/main/resources/banner.txt
@@ -0,0 +1,9 @@
+ ███╗ ███╗██╗███╗ ██╗██╗ ██████╗ ███████╗██╗██╗ ███████╗
+ ████╗ ████║██║████╗ ██║██║██╔═══██╗ ██╔════╝██║██║ ██╔════╝
+ ██╔████╔██║██║██╔██╗ ██║██║██║ ██║ ███ █████╗ ██║██║ █████╗
+ ██║╚██╔╝██║██║██║╚██╗██║██║██║ ██║ ██╔══╝ ██║██║ ██╔══╝
+ ██║ ╚═╝ ██║██║██║ ╚████║██║╚██████╔╝ ██║ ██║███████╗███████╗
+ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝
+
+ :: SPRING BOOT :: (v3.2.4) MinIO File
+ Developer: Javohir Yallayev
diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml
index acb7a0a..56e77cd 100644
--- a/src/main/resources/config/application.yml
+++ b/src/main/resources/config/application.yml
@@ -8,20 +8,27 @@ spring:
username: ${DATABASE_USER}
password: ${DATABASE_PASS}
hikari:
- poolName: CrudExampleHikari
+ poolName: minio
auto-commit: false
jpa:
generate-ddl: true
hibernate:
ddl-auto: update
- properties:
- hibernate:
- dialect: org.hibernate.dialect.PostgreSQLDialect
+
+
+
servlet:
multipart:
max-file-size: 300MB
max-request-size: 300MB
+server:
+ port: ${SERVER_PORT}
+
+logging:
+ level:
+ ROOT: info
+ uz.retail: info
springdoc:
swagger-ui:
@@ -33,9 +40,6 @@ springdoc:
path: "/v2/api-docs"
version: openapi_3_0
-server:
- port: ${SERVER_PORT}
-
minio:
url: ${MINIO_URL}
accessKey: ${ACCESS_KEY}
diff --git a/src/test/java/uz/javadev/MinioFileApplicationTests.java b/src/test/java/uz/javadev/MinioFileApplicationTests.java
index 5ae9466..defea40 100644
--- a/src/test/java/uz/javadev/MinioFileApplicationTests.java
+++ b/src/test/java/uz/javadev/MinioFileApplicationTests.java
@@ -6,8 +6,8 @@
@SpringBootTest
class MinioFileApplicationTests {
- @Test
- void contextLoads() {
- }
+ @Test
+ void contextLoads() {
+ }
}