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 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<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 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);
}
@@ -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()
@@ -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);
@@ -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<CourseResDto> 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());
}
}
}
+1 -1
View File
@@ -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: