[파일 업로드] tusHook 작업

- 여러번 db 호출 redis로 캐싱화
This commit is contained in:
2026-03-06 10:45:00 +09:00
parent 82cd4049af
commit a40e2b5161
6 changed files with 82 additions and 40 deletions
@@ -1,13 +0,0 @@
package com.alist.api.common.modules.file.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@JsonIgnoreProperties(ignoreUnknown = true)
public class TusHookEventDto {
private String Type;
}
@@ -1,16 +0,0 @@
package com.alist.api.common.modules.file.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Getter;
import lombok.Setter;
import java.util.Map;
@Getter
@Setter
@JsonIgnoreProperties(ignoreUnknown = true)
public class TusHookUploadDto {
private String id;
private Long size;
private Map<String, String> metaData;
}
@@ -1,16 +1,20 @@
package com.alist.api.common.modules.file.service; package com.alist.api.common.modules.file.service;
import com.alist.api.common.modules.file.dto.TusHookDto; import com.alist.api.common.modules.file.dto.TusHookDto;
import com.alist.api.common.modules.file.dto.TusHookUploadDto;
import com.alist.api.common.modules.file.dto.UploadStartDto; import com.alist.api.common.modules.file.dto.UploadStartDto;
import com.alist.api.common.modules.file.dto.UploadTokenDto; import com.alist.api.common.modules.file.dto.UploadTokenDto;
import com.alist.api.common.modules.file.mapper.FileMapper; import com.alist.api.common.modules.file.mapper.FileMapper;
import com.alist.api.config.jwt.JwtTokenProvider; import com.alist.api.config.jwt.JwtTokenProvider;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Map; import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.HexFormat;
import java.util.UUID; import java.util.UUID;
@Slf4j @Slf4j
@@ -22,12 +26,17 @@ public class FileService {
@Value("${file.upload.public-base-url}") @Value("${file.upload.public-base-url}")
private String publicBaseUrl; private String publicBaseUrl;
@Value("${file.upload.auth-cache.ttl-seconds:20}")
private long uploadAuthCacheTtlSeconds;
private final FileMapper fileMapper; private final FileMapper fileMapper;
private final JwtTokenProvider jwtTokenProvider; private final JwtTokenProvider jwtTokenProvider;
private final StringRedisTemplate stringRedisTemplate;
public FileService(FileMapper fileMapper, JwtTokenProvider jwtTokenProvider) { public FileService(FileMapper fileMapper, JwtTokenProvider jwtTokenProvider, StringRedisTemplate stringRedisTemplate) {
this.fileMapper = fileMapper; this.fileMapper = fileMapper;
this.jwtTokenProvider = jwtTokenProvider; this.jwtTokenProvider = jwtTokenProvider;
this.stringRedisTemplate = stringRedisTemplate;
} }
public UploadStartDto insertFileMasterUpload(Integer userIdx, Integer userTokenIdx, UploadStartDto uploadStartDto) { public UploadStartDto insertFileMasterUpload(Integer userIdx, Integer userTokenIdx, UploadStartDto uploadStartDto) {
@@ -62,12 +71,35 @@ public class FileService {
return uploadTokenDto; return uploadTokenDto;
} }
int isOk = fileMapper.selectFileMasterCountByFileMasterKeyAndDelYnAndStatus(uploadTokenDto); String cacheKey = buildAuthCacheKey(uploadTokenDto.getFileMasterKey(), uploadTokenDto.getUploadToken());
if (isOk > 0) { try {
uploadTokenDto.setResultCode(200); String cached = stringRedisTemplate.opsForValue().get(cacheKey);
} else { if (cached != null) {
uploadTokenDto.setResultCode(403); try {
uploadTokenDto.setResultCode(Integer.parseInt(cached));
return uploadTokenDto;
} catch (NumberFormatException parseEx) {
log.warn("uploadAuth cache parse fail. key={}, value={}", cacheKey, cached);
stringRedisTemplate.delete(cacheKey); // 오염 캐시 정리
}
}
} catch (Exception redisGetEx) {
log.warn("uploadAuth cache get fail. fallback DB. key={}", cacheKey, redisGetEx);
}
int isOk = fileMapper.selectFileMasterCountByFileMasterKeyAndDelYnAndStatus(uploadTokenDto);
int resultCode = (isOk > 0) ? 200 : 403;
uploadTokenDto.setResultCode(resultCode);
try {
stringRedisTemplate.opsForValue().set(
cacheKey,
String.valueOf(resultCode),
Duration.ofSeconds(safeTtlSeconds())
);
} catch (Exception redisSetEx) {
log.warn("uploadAuth cache set fail. key={}, resultCode={}", cacheKey, resultCode, redisSetEx);
} }
return uploadTokenDto; return uploadTokenDto;
@@ -76,4 +108,35 @@ public class FileService {
public void insertFileAndSave(TusHookDto tusHookDto) { public void insertFileAndSave(TusHookDto tusHookDto) {
} }
private String buildAuthCacheKey(String fileMasterKey, String uploadToken) {
return "upload:alist:auth:" + fileMasterKey + ":" + sha256(uploadToken);
}
private String sha256(String value) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashed = digest.digest(value.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(hashed);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("SHA-256 algorithm not available", e);
}
}
private void evictUploadAuthCacheByFileMasterKey(String fileMasterKey) {
try {
String pattern = "upload:alist:auth:" + fileMasterKey + ":*";
var keys = stringRedisTemplate.keys(pattern);
if (!keys.isEmpty()) {
stringRedisTemplate.delete(keys);
}
} catch (Exception e) {
log.warn("uploadAuth cache evict fail. fileMasterKey={}", fileMasterKey, e);
}
}
private long safeTtlSeconds() {
// 설정 실수 방어: 0/음수면 기본값 20초 사용
return (uploadAuthCacheTtlSeconds > 0) ? uploadAuthCacheTtlSeconds : 20L;
}
} }
@@ -14,7 +14,7 @@ public class RedisSessionConfig {
@Value("${cookie.secure}") @Value("${cookie.secure}")
private boolean cookieSecure; private boolean cookieSecure;
@Value("${cookie.domain}") @Value("${cookie.domain:}")
private String cookieDomain; private String cookieDomain;
@Value("${cookie.name}") @Value("${cookie.name}")
@@ -32,6 +32,10 @@ public class RedisSessionConfig {
serializer.setUseHttpOnlyCookie(true); // XSS 방지 serializer.setUseHttpOnlyCookie(true); // XSS 방지
serializer.setSameSite("Lax"); // CSRF 방지 serializer.setSameSite("Lax"); // CSRF 방지
if (cookieDomain != null && !cookieDomain.isBlank()) {
serializer.setDomainName(cookieDomain.trim());
}
return serializer; return serializer;
} }
} }
+4 -2
View File
@@ -33,8 +33,8 @@ jwt:
refresh-token-validity-seconds: 2592000 refresh-token-validity-seconds: 2592000
cookie: cookie:
secure: true # 로컬 개발 환경 (HTTP) secure: false # 로컬 개발 환경 (HTTP)
domain: pjt.kr domain:
name: ALIST_SESSION name: ALIST_SESSION
swagger: swagger:
@@ -46,6 +46,8 @@ file:
upload: upload:
tus-endpoint: https://file-alist.pjt.kr/tus/files/ tus-endpoint: https://file-alist.pjt.kr/tus/files/
public-base-url: https://file-alist.pjt.kr public-base-url: https://file-alist.pjt.kr
auth-cache:
ttl-seconds: 20
springdoc: springdoc:
api-docs: api-docs:
+2
View File
@@ -48,6 +48,8 @@ file:
upload: upload:
tus-endpoint: https://file-alist.pjt.kr/tus/files/ tus-endpoint: https://file-alist.pjt.kr/tus/files/
public-base-url: https://file-alist.pjt.kr public-base-url: https://file-alist.pjt.kr
auth-cache:
ttl-seconds: 20
springdoc: springdoc:
api-docs: api-docs: