feat: add upload file

This commit is contained in:
2026-09-15 23:33:33 +07:00
parent 6eee3c09f7
commit cd81d93f5d
6 changed files with 86 additions and 14 deletions
@@ -3,6 +3,7 @@ package aplp.backend.lms.common.storage;
import aplp.backend.core.common.response.ApiResponse; import aplp.backend.core.common.response.ApiResponse;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; 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.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
@@ -26,4 +27,10 @@ public class FileController {
return ResponseEntity.status(HttpStatus.CREATED) return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.ok(fileStorageService.upload(file, folder))); .body(ApiResponse.ok(fileStorageService.upload(file, folder)));
} }
@DeleteMapping
public ResponseEntity<ApiResponse<Void>> delete(@RequestParam("objectKey") String objectKey) {
fileStorageService.delete(objectKey);
return ResponseEntity.ok(ApiResponse.ok("Deleted successfully", null));
}
} }
@@ -4,6 +4,7 @@ import aplp.backend.core.common.exception.DomainException;
import aplp.backend.core.common.exception.ErrorCode; import aplp.backend.core.common.exception.ErrorCode;
import io.minio.MinioClient; import io.minio.MinioClient;
import io.minio.PutObjectArgs; import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@@ -44,13 +45,53 @@ public class FileStorageService {
} }
return new FileResDto( return new FileResDto(
properties.publicUrl() + "/" + properties.bucket() + "/" + objectKey, publicUrl(objectKey),
objectKey, objectKey,
file.getSize(), file.getSize(),
contentType 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) { static String buildObjectKey(String folder, String contentType, String originalFilename) {
return sanitizeFolder(folder) + "/" + UUID.randomUUID() + "." + extensionFor(contentType, originalFilename); return sanitizeFolder(folder) + "/" + UUID.randomUUID() + "." + extensionFor(contentType, originalFilename);
} }
@@ -1,5 +1,6 @@
package aplp.backend.lms.course.application.dtos; package aplp.backend.lms.course.application.dtos;
import aplp.backend.lms.common.storage.FileStorageService;
import aplp.backend.lms.course.domain.entities.Course; import aplp.backend.lms.course.domain.entities.Course;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -11,11 +12,12 @@ public record CourseResDto(
String slug, String slug,
String description, String description,
String image, String image,
String imageUrl,
boolean isPublished, boolean isPublished,
LocalDateTime createdAt, LocalDateTime createdAt,
LocalDateTime updatedAt LocalDateTime updatedAt
) { ) {
public static CourseResDto from(Course course) { public static CourseResDto from(Course course, FileStorageService fileStorageService) {
return new CourseResDto( return new CourseResDto(
course.getId(), course.getId(),
course.getCourseCode(), course.getCourseCode(),
@@ -23,6 +25,7 @@ public record CourseResDto(
course.getSlug(), course.getSlug(),
course.getDescription(), course.getDescription(),
course.getImage(), course.getImage(),
fileStorageService.publicUrl(course.getImage()),
course.isPublished(), course.isPublished(),
course.getCreatedAt(), course.getCreatedAt(),
course.getUpdatedAt() course.getUpdatedAt()
@@ -1,7 +1,6 @@
package aplp.backend.lms.course.application.mappers; package aplp.backend.lms.course.application.mappers;
import aplp.backend.lms.course.application.dtos.CourseReqDto; 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 aplp.backend.lms.course.domain.entities.Course;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
@@ -10,9 +9,6 @@ import org.mapstruct.MappingTarget;
@Mapper(componentModel = "spring") @Mapper(componentModel = "spring")
public interface CourseMapper { public interface CourseMapper {
@Mapping(target = "isPublished", source = "published")
CourseResDto toResponse(Course course);
@Mapping(target = "published", source = "isPublished") @Mapping(target = "published", source = "isPublished")
Course toEntity(CourseReqDto request); Course toEntity(CourseReqDto request);
@@ -1,11 +1,14 @@
package aplp.backend.lms.course.application.services; package aplp.backend.lms.course.application.services;
import aplp.backend.core.common.exception.ResourceNotFoundException; 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.CourseReqDto;
import aplp.backend.lms.course.application.dtos.CourseResDto; import aplp.backend.lms.course.application.dtos.CourseResDto;
import aplp.backend.lms.course.application.mappers.CourseMapper; import aplp.backend.lms.course.application.mappers.CourseMapper;
import aplp.backend.lms.course.domain.entities.Course; import aplp.backend.lms.course.domain.entities.Course;
import aplp.backend.lms.course.domain.repositories.CourseRepository; import aplp.backend.lms.course.domain.repositories.CourseRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.List; import java.util.List;
@@ -13,18 +16,25 @@ import java.util.List;
@Service @Service
public class CourseService { public class CourseService {
private static final Logger log = LoggerFactory.getLogger(CourseService.class);
private final CourseRepository courseRepository; private final CourseRepository courseRepository;
private final CourseMapper courseMapper; 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.courseRepository = courseRepository;
this.courseMapper = courseMapper; this.courseMapper = courseMapper;
this.fileStorageService = fileStorageService;
} }
public List<CourseResDto> getAll() { public List<CourseResDto> getAll() {
return courseRepository.findAll() return courseRepository.findAll()
.stream() .stream()
.map(CourseResDto::from) .map(course -> CourseResDto.from(course, fileStorageService))
.toList(); .toList();
} }
@@ -32,26 +42,41 @@ public class CourseService {
Course course = courseRepository.findById(id) Course course = courseRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Course not found")); .orElseThrow(() -> new ResourceNotFoundException("Course not found"));
return CourseResDto.from(course); return CourseResDto.from(course, fileStorageService);
} }
public CourseResDto create(CourseReqDto request) { public CourseResDto create(CourseReqDto request) {
Course course = courseMapper.toEntity(request); Course course = courseRepository.save(courseMapper.toEntity(request));
return courseMapper.toResponse(courseRepository.save(course)); return CourseResDto.from(course, fileStorageService);
} }
public CourseResDto update(Long id, CourseReqDto request) { public CourseResDto update(Long id, CourseReqDto request) {
Course course = courseRepository.findById(id) Course course = courseRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Course not found")); .orElseThrow(() -> new ResourceNotFoundException("Course not found"));
String previousImage = course.getImage();
courseMapper.updateEntity(request, course); 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) { public void delete(Long id) {
courseRepository.findById(id) Course course = courseRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Course not found")); .orElseThrow(() -> new ResourceNotFoundException("Course not found"));
courseRepository.deleteById(id); 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());
}
} }
} }
+1 -1
View File
@@ -31,7 +31,7 @@ minio:
endpoint: https://minio.koda.id.vn endpoint: https://minio.koda.id.vn
access-key: CHANGE_ME access-key: CHANGE_ME
secret-key: CHANGE_ME secret-key: CHANGE_ME
bucket: lms bucket: aplp
public-url: https://minio.koda.id.vn public-url: https://minio.koda.id.vn
springdoc: springdoc: