Nginx 컨테이너 띄우기
docker-compose.yml
services:
nginx:
image: nginx
container_name: nginx-file-server
restart: always
ports:
- "40405:40405"
volumes:
- /nginx/file:/storage
default.conf 설정
/etc/nginx/conf.d/default.conf 파일
server {
listen 40405;
listen [::]:40405;
server_name localhost;
charset utf-8;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log debug;
client_max_body_size 500M;
dav_methods PUT DELETE;
dav_access user:rw group:rw all:r;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
location /storage/ {
root /;
client_max_body_size 500M;
charset utf-8;
# Enable DAV methods for file uploads
dav_methods PUT DELETE;
create_full_put_path on;
dav_access user:rw group:rw all:r;
access_log /var/log/nginx/data-access.log;
error_log /var/log/nginx/data-error.log debug;
}
location /file {
alias /storage;
autoindex on;
charset utf-8;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
설정하고 reload
nginx -s reload
Controller (TEST API)
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("${api_prefix}/v1")
public class FileController {
private final FileService fileService;
@Operation(summary = "컨텐츠 업로드")
@PostMapping(value = "/file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponse<?> uploadFile(@AuthenticationPrincipal AuthMember authMember,
@RequestParam MultipartFile file) throws IOException {
fileService.uploadFile(authMember, file);
return ApiResponse.createSuccessWithNoContent(ResponseCode.RESOURCE_CREATED);
}
@Operation(summary = "저장된 모든 컨텐츠 삭제")
@DeleteMapping("/file/all")
public ApiResponse<?> deleteAll(@AuthenticationPrincipal AuthMember authMember) {
fileService.deleteAllFiles();
return ApiResponse.createSuccessWithNoContent(ResponseCode.SUCCESS);
}
@Operation(summary = "특정 컨테츠 업데이트")
@PutMapping(value = "/file/all", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ApiResponse<?> updateAll(@AuthenticationPrincipal AuthMember authMember,
@RequestParam MultipartFile file) throws IOException {
fileService.updateAllFiles(authMember, file);
return ApiResponse.createSuccessWithNoContent(ResponseCode.SUCCESS);
}
@Operation(summary = "컨텐츠 다운로드")
@GetMapping(value = "/file", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<byte[]> downloadFile(@AuthenticationPrincipal AuthMember authMember,
@ModelAttribute FileDownloadRequest downloadRequest) {
FileDownloadResponse fileData = fileService.downloadFile(authMember, downloadRequest);
byte[] file = fileData.getFileContent();
log.info("파일 다운로드 성공 : " + fileData.getFileName());
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileData.getFileName() + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(file.length)
.body(file);
}
}
OpenFeign 설정
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
@FeignClient(name = "nginxFileClient", url = "${nginx.url}")
public interface NginxFileClient {
@PutMapping(value = "/storage/{filePath}", consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE)
void uploadFile(@PathVariable("filePath") String filePath, @RequestBody byte[] file);
@DeleteMapping("/storage/{filePath}")
void deleteFile(@PathVariable("filePath") String filePath);
@GetMapping(value = "/file/{filePath}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
byte[] downloadFile(@PathVariable("filePath") String filePath);
}
Service
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class FileService {
private final FileRepository fileRepository;
private final NginxFileClient nginxFileClient;
private final String PREFIX = "/storage";
@Value("${nginx.path}")
private String basePath;
@Value("${nginx.url}")
private String baseUrl;
public void uploadFile(AuthMember authMember, MultipartFile file) throws IOException {
if (file.isEmpty()) throw new CustomBadRequestException("파일이 첨부되지 않았습니다.");
String fileName = generateFileName(file.getOriginalFilename());
String filePath = basePath + fileName;
String fileUrl = baseUrl + PREFIX + filePath;
/**
* 추후 들어오는 이미지 용도에 따라 filePath 관리
*/
log.info("fileName : " + filePath);
nginxFileClient.uploadFile(filePath, file.getBytes());
log.info("nginx file 업로드 성공");
File uploadFile = File.builder()
.fileName(fileName)
.filePath(filePath)
.fileUrl(fileUrl)
.fileSize(file.getSize())
.build();
/**
* 추후 user랑 file 연결
*/
fileRepository.save(uploadFile);
log.info("파일 생성 완료");
}
public void deleteFile(String filePath) {
log.info(filePath);
nginxFileClient.deleteFile(filePath);
log.info("nginx file 삭제 성공: {}", filePath);
}
public void updateFile(String oldFilePath, MultipartFile newFile) throws IOException {
if (newFile.isEmpty()) throw new CustomBadRequestException("새로운 파일이 첨부되지 않았습니다.");
deleteFile(oldFilePath);
log.info("기존 파일 삭제 성공: {}", oldFilePath);
String newFileName = generateFileName(newFile.getOriginalFilename());
String newFilePath = basePath + newFileName;
String newFileUrl = baseUrl + PREFIX + newFilePath;
/**
* 추후 들어오는 이미지 용도에 따라 filePath 관리
*/
nginxFileClient.uploadFile(newFilePath, newFile.getBytes());
log.info("nginx에 새로운 파일 업로드 성공: {}", newFilePath);
File updatedFile = File.builder()
.fileName(newFileName)
.filePath(newFilePath)
.fileUrl(newFileUrl)
.fileSize(newFile.getSize())
.build();
fileRepository.deleteByFilePath(oldFilePath);
fileRepository.save(updatedFile);
log.info("파일 정보 업데이트 완료: {}", newFilePath);
}
public FileDownloadResponse downloadFile(AuthMember authMember, FileDownloadRequest fileDownloadRequest) {
String fileUrl = fileDownloadRequest.fileUrl().replace("/file/", "/storage/");
File file = fileRepository.findByFileUrl(fileUrl)
.orElseThrow(() -> new CustomBadRequestException("해당 파일이 존재하지 않습니다."));
return FileDownloadResponse.builder()
.fileContent(nginxFileClient.downloadFile(file.getFilePath()))
.fileName(file.getFileName())
.build();
}
public void updateAllFiles(AuthMember authMember, MultipartFile newFile) throws IOException {
List<File> files = fileRepository.findAll();
files.stream().forEach(file -> {
try {
updateFile(file.getFilePath(), newFile);
} catch (IOException e) {
e.printStackTrace();
}
});
}
public void deleteAllFiles() {
List<File> files = fileRepository.findAll();
files.stream().forEach(file -> {
deleteFile(file.getFilePath());
fileRepository.delete(file);
});
}
private String generateFileName(String fileName) {
String uniqueId = UUID.randomUUID().toString();
String extension = fileName.substring(fileName.lastIndexOf("."));
String originalName = fileName.substring(0, fileName.lastIndexOf("."));
return originalName + "_" + uniqueId + extension;
}
}