From cd81d93f5d85ac2e1e2f25617a3d4d03409bcf39 Mon Sep 17 00:00:00 2001 From: namdh861 Date: Tue, 15 Sep 2026 23:33:33 +0700 Subject: [PATCH] feat: add upload file --- .../lms/common/storage/FileController.java | 7 +++ .../common/storage/FileStorageService.java | 43 ++++++++++++++++++- .../course/application/dtos/CourseResDto.java | 5 ++- .../application/mappers/CourseMapper.java | 4 -- .../application/services/CourseService.java | 39 ++++++++++++++--- src/main/resources/application.yaml | 2 +- 6 files changed, 86 insertions(+), 14 deletions(-) diff --git a/src/main/java/aplp/backend/lms/common/storage/FileController.java b/src/main/java/aplp/backend/lms/common/storage/FileController.java index 0cbbddf..3c20279 100644 --- a/src/main/java/aplp/backend/lms/common/storage/FileController.java +++ b/src/main/java/aplp/backend/lms/common/storage/FileController.java @@ -3,6 +3,7 @@ package aplp.backend.lms.common.storage; import aplp.backend.core.common.response.ApiResponse; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -26,4 +27,10 @@ public class FileController { return ResponseEntity.status(HttpStatus.CREATED) .body(ApiResponse.ok(fileStorageService.upload(file, folder))); } + + @DeleteMapping + public ResponseEntity> delete(@RequestParam("objectKey") String objectKey) { + fileStorageService.delete(objectKey); + return ResponseEntity.ok(ApiResponse.ok("Deleted successfully", null)); + } } diff --git a/src/main/java/aplp/backend/lms/common/storage/FileStorageService.java b/src/main/java/aplp/backend/lms/common/storage/FileStorageService.java index 0f1f2d8..3857363 100644 --- a/src/main/java/aplp/backend/lms/common/storage/FileStorageService.java +++ b/src/main/java/aplp/backend/lms/common/storage/FileStorageService.java @@ -4,6 +4,7 @@ import aplp.backend.core.common.exception.DomainException; import aplp.backend.core.common.exception.ErrorCode; import io.minio.MinioClient; import io.minio.PutObjectArgs; +import io.minio.RemoveObjectArgs; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -44,13 +45,53 @@ public class FileStorageService { } return new FileResDto( - properties.publicUrl() + "/" + properties.bucket() + "/" + objectKey, + publicUrl(objectKey), objectKey, file.getSize(), contentType ); } + public String publicUrl(String objectKey) { + if (objectKey == null || objectKey.isBlank()) { + return null; + } + if (isAbsoluteUrl(objectKey)) { + return objectKey; + } + return properties.publicUrl() + "/" + properties.bucket() + "/" + objectKey; + } + + public void delete(String objectKey) { + String key = toObjectKey(objectKey); + if (key == null) { + throw new DomainException(ErrorCode.INVALID_ARGUMENT, "objectKey is required"); + } + try { + minioClient.removeObject(RemoveObjectArgs.builder() + .bucket(properties.bucket()) + .object(key) + .build()); + } catch (Exception e) { + throw new DomainException(ErrorCode.INTERNAL_ERROR, "Failed to delete file: " + e.getMessage()); + } + } + + private String toObjectKey(String value) { + if (value == null || value.isBlank()) { + return null; + } + String prefix = properties.publicUrl() + "/" + properties.bucket() + "/"; + if (value.startsWith(prefix)) { + return value.substring(prefix.length()); + } + return isAbsoluteUrl(value) ? null : value; + } + + private static boolean isAbsoluteUrl(String value) { + return value.startsWith("http://") || value.startsWith("https://"); + } + static String buildObjectKey(String folder, String contentType, String originalFilename) { return sanitizeFolder(folder) + "/" + UUID.randomUUID() + "." + extensionFor(contentType, originalFilename); } diff --git a/src/main/java/aplp/backend/lms/course/application/dtos/CourseResDto.java b/src/main/java/aplp/backend/lms/course/application/dtos/CourseResDto.java index 67da8f3..6b9aee9 100644 --- a/src/main/java/aplp/backend/lms/course/application/dtos/CourseResDto.java +++ b/src/main/java/aplp/backend/lms/course/application/dtos/CourseResDto.java @@ -1,5 +1,6 @@ package aplp.backend.lms.course.application.dtos; +import aplp.backend.lms.common.storage.FileStorageService; import aplp.backend.lms.course.domain.entities.Course; import java.time.LocalDateTime; @@ -11,11 +12,12 @@ public record CourseResDto( String slug, String description, String image, + String imageUrl, boolean isPublished, LocalDateTime createdAt, LocalDateTime updatedAt ) { - public static CourseResDto from(Course course) { + public static CourseResDto from(Course course, FileStorageService fileStorageService) { return new CourseResDto( course.getId(), course.getCourseCode(), @@ -23,6 +25,7 @@ public record CourseResDto( course.getSlug(), course.getDescription(), course.getImage(), + fileStorageService.publicUrl(course.getImage()), course.isPublished(), course.getCreatedAt(), course.getUpdatedAt() diff --git a/src/main/java/aplp/backend/lms/course/application/mappers/CourseMapper.java b/src/main/java/aplp/backend/lms/course/application/mappers/CourseMapper.java index 45742ab..725b8b5 100644 --- a/src/main/java/aplp/backend/lms/course/application/mappers/CourseMapper.java +++ b/src/main/java/aplp/backend/lms/course/application/mappers/CourseMapper.java @@ -1,7 +1,6 @@ package aplp.backend.lms.course.application.mappers; import aplp.backend.lms.course.application.dtos.CourseReqDto; -import aplp.backend.lms.course.application.dtos.CourseResDto; import aplp.backend.lms.course.domain.entities.Course; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @@ -10,9 +9,6 @@ import org.mapstruct.MappingTarget; @Mapper(componentModel = "spring") public interface CourseMapper { - @Mapping(target = "isPublished", source = "published") - CourseResDto toResponse(Course course); - @Mapping(target = "published", source = "isPublished") Course toEntity(CourseReqDto request); diff --git a/src/main/java/aplp/backend/lms/course/application/services/CourseService.java b/src/main/java/aplp/backend/lms/course/application/services/CourseService.java index 6d98ea0..d6191a8 100644 --- a/src/main/java/aplp/backend/lms/course/application/services/CourseService.java +++ b/src/main/java/aplp/backend/lms/course/application/services/CourseService.java @@ -1,11 +1,14 @@ package aplp.backend.lms.course.application.services; import aplp.backend.core.common.exception.ResourceNotFoundException; +import aplp.backend.lms.common.storage.FileStorageService; import aplp.backend.lms.course.application.dtos.CourseReqDto; import aplp.backend.lms.course.application.dtos.CourseResDto; import aplp.backend.lms.course.application.mappers.CourseMapper; import aplp.backend.lms.course.domain.entities.Course; import aplp.backend.lms.course.domain.repositories.CourseRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.util.List; @@ -13,18 +16,25 @@ import java.util.List; @Service public class CourseService { + private static final Logger log = LoggerFactory.getLogger(CourseService.class); + private final CourseRepository courseRepository; private final CourseMapper courseMapper; + private final FileStorageService fileStorageService; - public CourseService(CourseRepository courseRepository, CourseMapper courseMapper) { + public CourseService( + CourseRepository courseRepository, + CourseMapper courseMapper, + FileStorageService fileStorageService) { this.courseRepository = courseRepository; this.courseMapper = courseMapper; + this.fileStorageService = fileStorageService; } public List getAll() { return courseRepository.findAll() .stream() - .map(CourseResDto::from) + .map(course -> CourseResDto.from(course, fileStorageService)) .toList(); } @@ -32,26 +42,41 @@ public class CourseService { Course course = courseRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Course not found")); - return CourseResDto.from(course); + return CourseResDto.from(course, fileStorageService); } public CourseResDto create(CourseReqDto request) { - Course course = courseMapper.toEntity(request); - return courseMapper.toResponse(courseRepository.save(course)); + Course course = courseRepository.save(courseMapper.toEntity(request)); + return CourseResDto.from(course, fileStorageService); } public CourseResDto update(Long id, CourseReqDto request) { Course course = courseRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Course not found")); + String previousImage = course.getImage(); courseMapper.updateEntity(request, course); + Course saved = courseRepository.save(course); - return courseMapper.toResponse(courseRepository.save(course)); + if (previousImage != null && !previousImage.isBlank() && !previousImage.equals(saved.getImage())) { + deleteFileQuietly(previousImage); + } + + return CourseResDto.from(saved, fileStorageService); } public void delete(Long id) { - courseRepository.findById(id) + Course course = courseRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Course not found")); courseRepository.deleteById(id); + deleteFileQuietly(course.getImage()); + } + + private void deleteFileQuietly(String objectKey) { + try { + fileStorageService.delete(objectKey); + } catch (Exception e) { + log.warn("Failed to delete image '{}': {}", objectKey, e.getMessage()); + } } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 84f2341..1df6544 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -31,7 +31,7 @@ minio: endpoint: https://minio.koda.id.vn access-key: CHANGE_ME secret-key: CHANGE_ME - bucket: lms + bucket: aplp public-url: https://minio.koda.id.vn springdoc: