[파일 업로드] tusHook 작업
- 여러번 db 호출 redis로 캐싱화
This commit is contained in:
@@ -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}")
|
||||||
@@ -31,6 +31,10 @@ public class RedisSessionConfig {
|
|||||||
serializer.setUseSecureCookie(cookieSecure); // 환경별 설정 (local: false, pjt: true)
|
serializer.setUseSecureCookie(cookieSecure); // 환경별 설정 (local: false, pjt: true)
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user