[File] 이미지 다운로드 뷰 방식 변경

This commit is contained in:
2026-03-10 17:31:51 +09:00
parent bb8f6d27f1
commit 940d9002ea
4 changed files with 52 additions and 99 deletions
@@ -14,14 +14,16 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ContentDisposition; import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders; import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus; import org.springframework.http.*;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@Slf4j @Slf4j
@Tag( @Tag(
@@ -183,10 +185,10 @@ public class FileController {
summary = "파일 보기" summary = "파일 보기"
, description = "권한 확인 후 Nginx X-Accel-Redirect로 파일을 inline 조회합니다.") , description = "권한 확인 후 Nginx X-Accel-Redirect로 파일을 inline 조회합니다.")
@GetMapping("/view/{fileUuid}") @GetMapping("/view/{fileUuid}")
public ResponseEntity<Void> fileView( public ResponseEntity<Resource> fileView(
@PathVariable String fileUuid @PathVariable String fileUuid
, HttpServletRequest request , HttpServletRequest request
) { ) throws IOException {
FileDownloadDto fileDownloadDto = new FileDownloadDto(); FileDownloadDto fileDownloadDto = new FileDownloadDto();
fileDownloadDto.setFileUuid(fileUuid); fileDownloadDto.setFileUuid(fileUuid);
fileDownloadDto.setEventType("VIEW"); fileDownloadDto.setEventType("VIEW");
@@ -200,33 +202,36 @@ public class FileController {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} }
if (fileService.isRedirectMode() || fileService.isLocalTestRequest(request)) { Path filePath = fileService.resolveStoredFilePath(target.getSavePath());
return ResponseEntity.status(HttpStatus.FOUND)
.location(URI.create(fileService.buildPublicViewUrl(target.getSavePath()))) if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
.build(); return ResponseEntity.notFound().build();
} }
HttpHeaders headers = new HttpHeaders(); Resource resource = new InputStreamResource(Files.newInputStream(filePath));
headers.add("X-Accel-Redirect", fileService.buildAccelRedirectPath(target.getSavePath())); String contentType = target.getContentType() == null
headers.add(HttpHeaders.CONTENT_TYPE, ? "application/octet-stream"
target.getContentType() == null ? "application/octet-stream" : target.getContentType()); : target.getContentType();
headers.add(HttpHeaders.CONTENT_DISPOSITION,
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.contentLength(Files.size(filePath))
.header(HttpHeaders.CONTENT_DISPOSITION,
ContentDisposition.inline() ContentDisposition.inline()
.filename(target.getOriginName(), StandardCharsets.UTF_8) .filename(target.getOriginName(), StandardCharsets.UTF_8)
.build() .build()
.toString()); .toString())
.body(resource);
return new ResponseEntity<>(headers, HttpStatus.OK);
} }
@Operation( @Operation(
summary = "파일 다운로드" summary = "파일 다운로드"
, description = "권한 확인 후 파일 다운로드 로그를 남기고 redirect 또는 Nginx X-Accel-Redirect로 파일 다운로드 합니다.") , description = "권한 확인 후 파일 다운로드 로그를 남기고 redirect 또는 Nginx X-Accel-Redirect로 파일 다운로드 합니다.")
@GetMapping("/download/{fileUuid}") @GetMapping("/download/{fileUuid}")
public ResponseEntity<Void> fileDownload( public ResponseEntity<Resource> fileDownload(
@PathVariable String fileUuid @PathVariable String fileUuid
, HttpServletRequest request , HttpServletRequest request
) { ) throws IOException {
FileDownloadDto fileDownloadDto = new FileDownloadDto(); FileDownloadDto fileDownloadDto = new FileDownloadDto();
fileDownloadDto.setFileUuid(fileUuid); fileDownloadDto.setFileUuid(fileUuid);
fileDownloadDto.setEventType("DOWNLOAD"); fileDownloadDto.setEventType("DOWNLOAD");
@@ -240,22 +245,25 @@ public class FileController {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} }
if (fileService.isRedirectMode() || fileService.isLocalTestRequest(request)) { Path filePath = fileService.resolveStoredFilePath(target.getSavePath());
return ResponseEntity.status(HttpStatus.FOUND)
.location(URI.create(fileService.buildPublicDownloadUrl(target.getSavePath()))) if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
.build(); return ResponseEntity.notFound().build();
} }
HttpHeaders headers = new HttpHeaders(); Resource resource = new InputStreamResource(Files.newInputStream(filePath));
headers.add("X-Accel-Redirect", fileService.buildAccelRedirectPath(target.getSavePath())); String contentType = target.getContentType() == null
headers.add(HttpHeaders.CONTENT_TYPE, ? "application/octet-stream"
target.getContentType() == null ? "application/octet-stream" : target.getContentType()); : target.getContentType();
headers.add(HttpHeaders.CONTENT_DISPOSITION,
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.contentLength(Files.size(filePath))
.header(HttpHeaders.CONTENT_DISPOSITION,
ContentDisposition.attachment() ContentDisposition.attachment()
.filename(target.getOriginName(), StandardCharsets.UTF_8) .filename(target.getOriginName(), StandardCharsets.UTF_8)
.build() .build()
.toString()); .toString())
.body(resource);
return new ResponseEntity<>(headers, HttpStatus.OK);
} }
} }
@@ -47,12 +47,6 @@ public class FileService {
@Value("${file.upload.final-root}") @Value("${file.upload.final-root}")
private String uploadFinalRoot; private String uploadFinalRoot;
@Value("${file.download.redirect-prefix:/protected-files}")
private String fileDownloadRedirectPrefix;
@Value("${file.download.mode:accel}")
private String fileDownloadMode;
private final FileMapper fileMapper; private final FileMapper fileMapper;
private final JwtTokenProvider jwtTokenProvider; private final JwtTokenProvider jwtTokenProvider;
private final StringRedisTemplate stringRedisTemplate; private final StringRedisTemplate stringRedisTemplate;
@@ -656,63 +650,20 @@ public class FileService {
return fileDetailInfo; return fileDetailInfo;
} }
public String buildAccelRedirectPath(String savePath) { public Path resolveStoredFilePath(String savePath) {
if (savePath == null || savePath.isBlank()) { if (savePath == null || savePath.isBlank()) {
throw new IllegalArgumentException("savePath is empty"); throw new IllegalArgumentException("savePath is empty");
} }
String prefix = (fileDownloadRedirectPrefix == null || fileDownloadRedirectPrefix.isBlank()) Path root = Paths.get(uploadFinalRoot).normalize().toAbsolutePath();
? "/protected-files" String relativePath = savePath.startsWith("/") ? savePath.substring(1) : savePath;
: fileDownloadRedirectPrefix.trim();
if (!prefix.startsWith("/")) { Path resolved = root.resolve(relativePath).normalize();
prefix = "/" + prefix;
if (!resolved.startsWith(root)) {
throw new IllegalArgumentException("invalid savePath");
} }
return prefix + savePath; return resolved;
}
public boolean isRedirectMode() {
return "redirect".equalsIgnoreCase(fileDownloadMode);
}
public String buildPublicViewUrl(String savePath) {
return buildPublicTestUrl("/public-test-view", savePath);
}
public String buildPublicDownloadUrl(String savePath) {
return buildPublicTestUrl("/public-test-download", savePath);
}
private String buildPublicTestUrl(String prefix, String savePath) {
if (savePath == null || savePath.isBlank()) {
throw new IllegalArgumentException("savePath is empty");
}
String baseUrl = publicBaseUrl == null ? "" : publicBaseUrl.trim();
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
}
return baseUrl + prefix + savePath;
}
public boolean isLocalTestRequest(HttpServletRequest request) {
return containsLocalhost(request.getHeader("Referer"))
|| containsLocalhost(request.getHeader("Origin"))
|| containsLocalhost(request.getHeader("Host"))
|| containsLocalhost(request.getHeader("X-Forwarded-Host"));
}
public boolean containsLocalhost(String value) {
if (value == null || value.isBlank()) {
return false;
}
String lower = value.toLowerCase();
return lower.contains("localhost")
|| lower.contains("127.0.0.1")
|| lower.contains("[::1]")
|| lower.contains("::1");
} }
} }
@@ -52,9 +52,6 @@ file:
interrupt-seconds: 30 interrupt-seconds: 30
auth-cache: auth-cache:
ttl-seconds: 20 ttl-seconds: 20
download:
mode: redirect
redirect-prefix: /public-test-files
springdoc: springdoc:
api-docs: api-docs:
-3
View File
@@ -54,9 +54,6 @@ file:
interrupt-seconds: 30 interrupt-seconds: 30
auth-cache: auth-cache:
ttl-seconds: 20 ttl-seconds: 20
download:
mode: accel
redirect-prefix: /protected-files
springdoc: springdoc:
api-docs: api-docs: