diff --git a/.env b/.env index e69de29..57b422a 100644 --- a/.env +++ b/.env @@ -0,0 +1,15 @@ +#spring config +APPLICATION_NAME=minio-file +SERVER_PORT=7007 + +#database configs +DATABASE_HOST=localhost +DATABASE_PORT=5432 +DATABASE_NAME=minio_db +DATABASE_USER=root +DATABASE_PASS=1234root + +#minio configs +MINIO_URL=http://localhost:9000 +ACCESS_KEY=root +SECRET_KEY=1234root \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 861b553..4716761 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,48 +1,5 @@ name: erp-retail-module services: - consul: - image: docker.io/bitnami/consul:1.17.0 - ports: - - 127.0.0.1:8300:8300 - - 127.0.0.1:8500:8500 - - 127.0.0.1:8600:8600 - command: consul agent -dev -ui -client 0.0.0.0 -log-level=INFO - - consul-config-loader: - image: jhipster/consul-config-loader:v0.4.1 - volumes: - - ./central-server-config:/config - environment: - - INIT_SLEEP_SECONDS=5 - - CONSUL_URL=consul - - CONSUL_PORT=8500 - -### ------------------------------------------------------- KEYCLOAK ------------------------------------------------------- ### - keycloak: - image: quay.io/keycloak/keycloak:23.0.1 - command: 'start-dev --import-realm' - volumes: - - ./realm-config:/opt/keycloak/data/import - - ./realm-config/keycloak-health-check.sh:/opt/keycloak/health-check.sh - environment: - - KC_DB=dev-file - - KEYCLOAK_ADMIN=admin - - KEYCLOAK_ADMIN_PASSWORD=admin - - KC_FEATURES=scripts - - KC_HTTP_PORT=9080 - - KC_HTTPS_PORT=9443 - - KC_HEALTH_ENABLED=true - ports: - - 127.0.0.1:9080:9080 - - 127.0.0.1:9443:9443 - healthcheck: - test: 'bash /opt/keycloak/health-check.sh' - interval: 5s - timeout: 5s - retries: 20 - start_period: 10s - - #--------------------minio----------------------# minio: image: quay.io/minio/minio container_name: minio @@ -59,23 +16,3 @@ services: command: minio server /data -### ------------------------------------------------------- POSTGRESQL ------------------------------------------------------- ### - postgresql: - image: postgres:16.1 - volumes: - - postgres_data:/var/lib/postgresql/data - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=123 - - POSTGRES_HOST_AUTH_METHOD=trust - healthcheck: - test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER}'] - interval: 5s - timeout: 5s - retries: 10 - ports: - - 127.0.0.1:5432:5432 - -volumes: - postgres_data: - diff --git a/pom.xml b/pom.xml index 72c2572..7537a51 100644 --- a/pom.xml +++ b/pom.xml @@ -1,88 +1,126 @@ - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.4.0 - - - uz.javadev - minio-file - 0.0.1-SNAPSHOT - minio-file - minio-file - - - - - - - - - - - - - - - 17 - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-web - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.1.0 + + uz.javadev + minio-file + 0.0.1 + minio-file + minio-file - - org.postgresql - postgresql - runtime - - - org.projectlombok - lombok - true - - - org.springframework.boot - spring-boot-starter-test - test - - + + 17 + 1.5.5.Final + 8.5.2 + 2.0.2 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - org.projectlombok - lombok - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - - + + org.postgresql + postgresql + runtime + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${swagger.version} + + + io.minio + minio + ${minio.version} + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + provided + + + jakarta.validation + jakarta.validation-api + 3.0.2 + + + org.springframework.boot + spring-boot-starter-jdbc + + + org.springframework.boot + spring-boot-starter-logging + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${java.version} + ${java.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + + + + diff --git a/src/main/java/uz/javadev/MinioFileApplication.java b/src/main/java/uz/javadev/MinioFileApplication.java index 762dd30..47a568f 100644 --- a/src/main/java/uz/javadev/MinioFileApplication.java +++ b/src/main/java/uz/javadev/MinioFileApplication.java @@ -1,13 +1,84 @@ package uz.javadev; +import io.micrometer.common.util.StringUtils; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.core.env.Environment; +import uz.javadev.config.CRLFLogConverter; -@SpringBootApplication +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Optional; + +/** + * Minio File management + * + * @author Javohir Yallayev + */ +@Slf4j +@SpringBootApplication(scanBasePackages = "uz.javadev") public class MinioFileApplication { + public static void main(String[] args) { + SpringApplication app = new SpringApplication(MinioFileApplication.class); + Environment env = app.run(args).getEnvironment(); + logApplicationStartup(env); + } - public static void main(String[] args) { - SpringApplication.run(MinioFileApplication.class, args); - } + private static void logApplicationStartup(Environment env) { + String protocol = Optional.ofNullable(env.getProperty("server.ssl.key-store")).map(key -> "https").orElse("http"); + String applicationName = env.getProperty("spring.application.name"); + String serverPort = env.getProperty("server.port"); + String contextPath = Optional + .ofNullable(env.getProperty("server.servlet.context-path")) + .filter(StringUtils::isNotBlank) + .orElse("/"); + String hostAddress = "localhost"; + String swaggerUiPath = env.getProperty("springdoc.swagger-ui.path"); + try { + hostAddress = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.warn("The host name could not be determined, using `localhost` as fallback"); + } + log.info( + CRLFLogConverter.CRLF_SAFE_MARKER, + """ + + ---------------------------------------------------------- + \tApplication '{}' is running! Access URLs: + \tLocal: \t\t{}://localhost:{}{} + \tExternal: \t{}://{}:{}{} + \tSwagger: \t{}://{}:{}{} + \tProfile(s): \t{} + ----------------------------------------------------------""", + applicationName, + protocol, + serverPort, + contextPath, + protocol, + hostAddress, + serverPort, + contextPath, + protocol, + hostAddress, + serverPort, + swaggerUiPath, + env.getActiveProfiles().length == 0 ? env.getDefaultProfiles() : env.getActiveProfiles() + ); + String configServerStatus = env.getProperty("configserver.status"); + if (configServerStatus == null) { + configServerStatus = "Not found or not setup for this application"; + } + log.info( + CRLFLogConverter.CRLF_SAFE_MARKER, + """ + + ---------------------------------------------------------- + \t\ + Config Server: \t{} + ----------------------------------------------------------""", + configServerStatus + ); + } } diff --git a/src/main/java/uz/javadev/config/CRLFLogConverter.java b/src/main/java/uz/javadev/config/CRLFLogConverter.java index 890634d..573c6c8 100644 --- a/src/main/java/uz/javadev/config/CRLFLogConverter.java +++ b/src/main/java/uz/javadev/config/CRLFLogConverter.java @@ -1,4 +1,4 @@ -package uz.retail.core.config; +package uz.javadev.config; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.pattern.CompositeConverter; @@ -14,12 +14,6 @@ import java.util.List; import java.util.Map; -/** - * Log filter to prevent attackers from forging log entries by submitting input containing CRLF characters. - * CRLF characters are replaced with a red colored _ character. - * - * @see Log Forging Description - */ public class CRLFLogConverter extends CompositeConverter { public static final Marker CRLF_SAFE_MARKER = MarkerFactory.getMarker("CRLF_SAFE"); diff --git a/src/main/java/uz/javadev/config/DatabaseConfiguration.java b/src/main/java/uz/javadev/config/DatabaseConfiguration.java index 93b5e53..ea44835 100644 --- a/src/main/java/uz/javadev/config/DatabaseConfiguration.java +++ b/src/main/java/uz/javadev/config/DatabaseConfiguration.java @@ -4,11 +4,10 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.transaction.annotation.EnableTransactionManagement; -import uz.javadev.repository.slice.SliceBaseRepositoryFactoryBean; @Configuration -@EnableJpaRepositories(value = {"uz.javadev.repository"}, repositoryFactoryBeanClass = SliceBaseRepositoryFactoryBean.class) +@EnableJpaRepositories(value = {"uz.javadev.repo"}) @EnableJpaAuditing @EnableTransactionManagement public class DatabaseConfiguration { diff --git a/src/main/java/uz/javadev/config/MinioConfig.java b/src/main/java/uz/javadev/config/MinioConfig.java index 0c86bcd..2896930 100644 --- a/src/main/java/uz/javadev/config/MinioConfig.java +++ b/src/main/java/uz/javadev/config/MinioConfig.java @@ -1,24 +1,22 @@ -package uz.retail.core.config; +package uz.javadev.config; import io.minio.MinioClient; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import uz.retail.core.config.properties.ApplicationProperties; +import uz.javadev.config.props.MinioProps; @Configuration +@RequiredArgsConstructor public class MinioConfig { - private final ApplicationProperties.MinioProps properties; - - public MinioConfig(ApplicationProperties properties) { - this.properties = properties.getMinioProps(); - } + private final MinioProps props; @Bean public MinioClient getMinioClient() { return MinioClient.builder() - .endpoint(properties.getUrl()) - .credentials(properties.getAccessKey(), properties.getSecretKey()) + .endpoint(props.getUrl()) + .credentials(props.getAccessKey(), props.getSecretKey()) .build(); } diff --git a/src/main/java/uz/javadev/config/SwaggerConfig.java b/src/main/java/uz/javadev/config/SwaggerConfig.java index 2aa6764..ada0f85 100644 --- a/src/main/java/uz/javadev/config/SwaggerConfig.java +++ b/src/main/java/uz/javadev/config/SwaggerConfig.java @@ -1,4 +1,4 @@ -package uz.retail.core.config; +package uz.javadev.config; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; @@ -9,16 +9,14 @@ import org.springframework.context.annotation.Configuration; import static io.swagger.v3.oas.models.security.SecurityScheme.Type.HTTP; -import static uz.retail.core.config.Constants.BEARER; -import static uz.retail.core.config.Constants.JWT; @Configuration public class SwaggerConfig { private SecurityScheme createAPIKeyScheme() { return new SecurityScheme() .type(HTTP) - .bearerFormat(JWT) - .scheme(BEARER); + .bearerFormat("JWT") + .scheme("BEARER"); } @Bean @@ -27,8 +25,8 @@ public OpenAPI openAPI() { addList("Bearer Authentication")) .components(new Components().addSecuritySchemes ("Bearer Authentication", createAPIKeyScheme())) - .info(new Info().title("Calendar control SERVICE") - .description("REST APIS FOR MANAGE Retail control SERVICE") + .info(new Info().title("Minio File") + .description("REST APIS FOR MANAGE Minio file upload and download SERVICE") .version("0.0.1")); } } diff --git a/src/main/java/uz/javadev/config/props/MinioProps.java b/src/main/java/uz/javadev/config/props/MinioProps.java index 487a2e6..ca505f6 100644 --- a/src/main/java/uz/javadev/config/props/MinioProps.java +++ b/src/main/java/uz/javadev/config/props/MinioProps.java @@ -1,4 +1,14 @@ package uz.javadev.config.props; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "minio") public class MinioProps { + private String url; + private String accessKey; + private String secretKey; } diff --git a/src/main/java/uz/javadev/controller/FileController.java b/src/main/java/uz/javadev/controller/FileController.java index df0384a..1deba7a 100644 --- a/src/main/java/uz/javadev/controller/FileController.java +++ b/src/main/java/uz/javadev/controller/FileController.java @@ -1,4 +1,56 @@ package uz.javadev.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import uz.javadev.service.FileService; +import uz.javadev.service.dto.CommonResultData; +import uz.javadev.service.dto.FileDto; + +import java.util.UUID; + +/** + * @author Javohir Yallayev + */ +@Slf4j +@RestController +@RequestMapping("/api/v1/file") +@RequiredArgsConstructor +@Tag(name = "File Controller", description = "This APIs for manage files") public class FileController { + + private final FileService service; + + @Operation(summary = "Upload file", description = "This API for upload multipartFile with bucketName") + @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public CommonResultData uploadFile(@RequestParam(name = "file") + MultipartFile file, + @RequestParam String bucketName) { + log.info("REQUEST upload with bucketName"); + return service.uploadFile(file, bucketName); + } + + @GetMapping("/download/{id}") + public CommonResultData downloadFile(@PathVariable UUID id, + HttpServletResponse response) { + log.info("REQUEST download file with id {}", id); + return service.downloadFile(id, response); + } + + @GetMapping("/preview/{fileId}") + public void previewPhoto(@PathVariable UUID fileId, HttpServletResponse response) { + log.info("REQUEST preview file with id {}", fileId); + service.preview(fileId, response); + } + + @DeleteMapping("/delete/{id}") + public CommonResultData deleteFile(@PathVariable UUID id) { + log.info("REQUEST delete file with id {}", id); + return service.deleteFileById(id); + } } diff --git a/src/main/java/uz/javadev/domain/FileEntity.java b/src/main/java/uz/javadev/domain/FileEntity.java index 01375a2..f50cfa8 100644 --- a/src/main/java/uz/javadev/domain/FileEntity.java +++ b/src/main/java/uz/javadev/domain/FileEntity.java @@ -1,10 +1,18 @@ package uz.javadev.domain; import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; import java.time.Instant; import java.util.UUID; +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor @Entity @Table(name = "file") public class FileEntity { @@ -27,6 +35,6 @@ public class FileEntity { @Column(name = "extension") private String extension; - @Column(name = "createdDate") + @Column(name = "created_date", updatable = false) private Instant createdDate; } diff --git a/src/main/java/uz/javadev/domain/enums/Errors.java b/src/main/java/uz/javadev/domain/enums/Errors.java index 0076d22..692d034 100644 --- a/src/main/java/uz/javadev/domain/enums/Errors.java +++ b/src/main/java/uz/javadev/domain/enums/Errors.java @@ -1,8 +1,9 @@ -package uz.retail.core.domain.enumeration; +package uz.javadev.domain.enums; -import uz.isd.commons.result.HasResult; +import lombok.Getter; -public enum Errors implements HasResult { +@Getter +public enum Errors { FILE_NOT_FOUND(-400, "FILE NOT FOUND"), TEMPLATE_ALREADY_EXIST(-1000, "TEMPLATE WITH THIS NAME ALREADY EXISTS"), FILE_UPLOAD_FAIL(-1001, "FILE COULD NOT BE UPLOADED"), @@ -11,8 +12,7 @@ public enum Errors implements HasResult { SERVICE_NOT_IMPLEMENTED(10021, "SERVICE NOT IMPLEMENTED"), UNSUPPORTED_TEMPLATE_FILE(-1004, "UNSUPPORTED CONTENT TYPE"), INVALID_REQUEST(-1005, "INVALID REQUEST"), - FILE_DELETE_FAIL(-1005,"FILE_DELETE_FAIL") - ; + FILE_DELETE_FAIL(-1005,"FILE DELETE FAIL"); private final String message; private final Integer code; @@ -22,20 +22,4 @@ public enum Errors implements HasResult { this.code = code; } - - @Override - public Integer getCode() { - return this.code; - } - - @Override - public Object getDetails() { - return null; } - - @Override - public String getMessage() { - return this.message; - } - -} diff --git a/src/main/java/uz/javadev/exp/FileException.java b/src/main/java/uz/javadev/exp/FileException.java index 90a0494..229d198 100644 --- a/src/main/java/uz/javadev/exp/FileException.java +++ b/src/main/java/uz/javadev/exp/FileException.java @@ -1,4 +1,15 @@ package uz.javadev.exp; -public class FileException { +import lombok.Getter; +import uz.javadev.domain.enums.Errors; + +@Getter +public class FileException extends RuntimeException { + + private final Errors error; + + public FileException(Errors errors) { + super(errors.getMessage()); + this.error = errors; + } } diff --git a/src/main/java/uz/javadev/exp/InvalidRequestException.java b/src/main/java/uz/javadev/exp/InvalidRequestException.java index 2114908..88f4103 100644 --- a/src/main/java/uz/javadev/exp/InvalidRequestException.java +++ b/src/main/java/uz/javadev/exp/InvalidRequestException.java @@ -1,4 +1,14 @@ package uz.javadev.exp; -public class InvalidRequestException { +import lombok.Getter; +import uz.javadev.domain.enums.Errors; + +@Getter +public class InvalidRequestException extends RuntimeException { + private final Errors error; + + public InvalidRequestException(Errors errors) { + super(errors.getMessage()); + this.error = errors; + } } diff --git a/src/main/java/uz/javadev/repo/FileRepository.java b/src/main/java/uz/javadev/repo/FileRepository.java index e2f311e..666324b 100644 --- a/src/main/java/uz/javadev/repo/FileRepository.java +++ b/src/main/java/uz/javadev/repo/FileRepository.java @@ -1,4 +1,12 @@ -package uz.javadev; +package uz.javadev.repo; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import uz.javadev.domain.FileEntity; + +import java.util.UUID; + +@Repository +public interface FileRepository extends JpaRepository { -public class FileRepository { } diff --git a/src/main/java/uz/javadev/service/FileService.java b/src/main/java/uz/javadev/service/FileService.java index fcd0095..2f4b8b1 100644 --- a/src/main/java/uz/javadev/service/FileService.java +++ b/src/main/java/uz/javadev/service/FileService.java @@ -1,4 +1,73 @@ package uz.javadev.service; -public class FileService { +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import uz.javadev.exp.InvalidRequestException; +import uz.javadev.service.dto.CommonResultData; +import uz.javadev.service.dto.FileDto; + +import java.util.UUID; + +/** + * FileService + * + * @author Javohir Yallayev + */ + +public interface FileService { + + /** + * Uploads a file to MinIO server, saves metadata information into the database, and returns the result. + *

+ * This method takes a file and a bucket name, processes the file name to make it safe, + * uploads it to the specified bucket, and stores the metadata about the file (such as path, + * extension, size) in the database. + * + * @param file The file to be uploaded. + * @param bucketName The name of the bucket where the file should be uploaded. + * @return CommonResultData containing information about the uploaded file. + * @throws InvalidRequestException if file upload fails. + */ + CommonResultData uploadFile(MultipartFile file, String bucketName); + + + /** + * Downloads a file from MinIO and writes it to the HTTP response stream for the client. + *

+ * This method retrieves file metadata based on the file ID, fetches the file from MinIO, + * and streams it to the client as a downloadable response. In case of connection abort + * by the client, a specific error message is logged and returned. + * + * @param fileId The unique identifier of the file to be downloaded. + * @param servletResponse The HTTP response object used to write the file. + * @return CommonResultData indicating success or failure of the download process. + */ + CommonResultData downloadFile(UUID fileId, HttpServletResponse servletResponse); + + + /** + * Deletes a file from MinIO server and removes its metadata from the database. + *

+ * This method retrieves the file metadata using the provided file ID, deletes the physical file + * from the MinIO storage, and then deletes the corresponding metadata from the database. + * If an error occurs during the deletion process, an {@link InvalidRequestException} is thrown. + * + * @param fileId The unique identifier of the file to be deleted. + * @return CommonResultData indicating success or failure of the deletion process. + * @throws InvalidRequestException if the file deletion fails either in MinIO or database. + */ + CommonResultData 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() { + } }