[파일 업로드] tusHook 작업

- 멀티업로드 상태 업로드
This commit is contained in:
2026-03-09 10:45:00 +09:00
parent 47a5e85789
commit 673635ffeb
6 changed files with 153 additions and 17 deletions
@@ -8,8 +8,6 @@ import com.alist.api.common.modules.file.form.TusHookForm;
import com.alist.api.common.modules.file.form.UploadStatusForm;
import com.alist.api.common.response.ApiResponse;
import com.alist.api.common.response.ApiResponseCode;
import com.alist.api.common.modules.file.dto.UploadStartDto;
import com.alist.api.common.modules.file.form.UploadStartForm;
import com.alist.api.common.modules.file.service.FileService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -102,9 +100,11 @@ public class FileController {
@PostMapping("/tusHook")
public ResponseEntity<ApiResponse<String>> tusHook(@RequestBody TusHookForm tusHookForm) {
boolean accepted = fileService.updateFileUploadStatus(tusHookForm.tusHookDto());
fileService.updateFileUploadStatus(tusHookForm.tusHookDto());
if (accepted) {
fileService.saveUploadStatusRedisTusHook(tusHookForm.tusHookDto());
}
return ApiResponse.entity("", ApiResponseCode.CODE_200);
}
@@ -20,4 +20,10 @@ public interface FileMapper {
void updateFileDetailStatus(TusHookDto tusHookDto);
void updateFileMasterAggregateByFileUuid(String fileUuid);
FileUploadMetaDto selectFileUploadMetaByFileUuid(String fileUuid);
void updateTusUploadIdIfNull(FileUploadMetaDto fileUploadMetaDto);
void updateFileDetailCanceledByFileUuid(String fileUuid);
}
@@ -31,6 +31,9 @@ public class FileService {
@Value("${file.upload.auth-cache.ttl-seconds:20}")
private long uploadAuthCacheTtlSeconds;
@Value("${file.upload.interrupt-seconds:30}")
private long uploadInterruptSeconds;
private final FileMapper fileMapper;
private final JwtTokenProvider jwtTokenProvider;
private final StringRedisTemplate stringRedisTemplate;
@@ -127,29 +130,96 @@ public class FileService {
return null;
}
uploadStatusDto.setFileUuid((String) map.getOrDefault("fileUuid", uploadStatusDto.getFileUuid()));
uploadStatusDto.setStatus((String) map.getOrDefault("status", "PENDING"));
uploadStatusDto.setUploadedBytes(parseLong(map.get("uploadedBytes")));
uploadStatusDto.setTotalBytes(parseLong(map.get("totalBytes")));
uploadStatusDto.setPercent((int) parseLong(map.get("percent")));
uploadStatusDto.setUpdatedAt((String) map.getOrDefault("updatedAt", ""));
String fileUuid = (String) map.getOrDefault("fileUuid", uploadStatusDto.getFileUuid());
String status = (String) map.getOrDefault("status", "PENDING");
long uploadedBytes = parseLong(map.get("uploadedBytes"));
long totalBytes = parseLong(map.get("totalBytes"));
int percent = (int) parseLong(map.get("percent"));
String updatedAt = (String) map.getOrDefault("updatedAt", "");
if (isStaleUploading(status, updatedAt)) {
status = "CANCELED";
updatedAt = Instant.now().toString();
saveCanceledStatusRedis(fileUuid, uploadedBytes, totalBytes);
fileMapper.updateFileDetailCanceledByFileUuid(fileUuid);
fileMapper.updateFileMasterAggregateByFileUuid(fileUuid);
}
uploadStatusDto.setFileUuid(fileUuid);
uploadStatusDto.setStatus(status);
uploadStatusDto.setUploadedBytes(uploadedBytes);
uploadStatusDto.setTotalBytes(totalBytes);
uploadStatusDto.setPercent(percent);
uploadStatusDto.setUpdatedAt(updatedAt);
return uploadStatusDto;
}
/*db에 상태값 저장*/
@Transactional
public void updateFileUploadStatus(TusHookDto tusHookDto) {
if (tusHookDto == null || tusHookDto.getFileUuid() == null || tusHookDto.getFileUuid().isBlank()) {
return;
private boolean isStaleUploading(String status, String updatedAt) {
if (!"UPLOADING".equalsIgnoreCase(status)) {
return false;
}
if (updatedAt == null || updatedAt.isBlank()) {
return false;
}
try {
Instant lastUpdatedAt = Instant.parse(updatedAt);
long elapsedSeconds = Duration.between(lastUpdatedAt, Instant.now()).getSeconds();
return elapsedSeconds >= safeInterruptSeconds();
} catch (Exception e) {
log.warn("upload status updatedAt parse fail. updatedAt={}", updatedAt, e);
return false;
}
}
private long safeInterruptSeconds() {
return (uploadInterruptSeconds > 0) ? uploadInterruptSeconds : 30L;
}
/*db에 상태값 저장*/
@Transactional
public boolean updateFileUploadStatus(TusHookDto tusHookDto) {
if (tusHookDto == null || tusHookDto.getFileUuid() == null || tusHookDto.getFileUuid().isBlank()) {
return true;
}
String fileUuid = tusHookDto.getFileUuid().trim();
String type = tusHookDto.getType() == null ? "" : tusHookDto.getType().trim().toLowerCase();
String uploadId = tusHookDto.getUploadId() == null ? "" : tusHookDto.getUploadId().trim();
long offset = tusHookDto.getOffset() == null ? 0L : tusHookDto.getOffset();
long size = tusHookDto.getSize() == null ? 0L : tusHookDto.getSize();
fileMapper.insertFileUploadEventLog(tusHookDto);
FileUploadMetaDto fileUploadMetaInfo = fileMapper.selectFileUploadMetaByFileUuid(fileUuid);
if (fileUploadMetaInfo == null) return true;
if (isMetadataMismatch(fileUploadMetaInfo, tusHookDto)) {
fileMapper.updateFileDetailCanceledByFileUuid(fileUuid);
fileMapper.updateFileMasterAggregateByFileUuid(fileUuid);
saveCanceledStatusRedis(fileUuid, offset, size);
return false;
}
if (!uploadId.isBlank()) {
if (fileUploadMetaInfo.getTusUploadId() == null || fileUploadMetaInfo.getTusUploadId().isBlank()) {
FileUploadMetaDto fileUploadMetaDto = new FileUploadMetaDto();
fileUploadMetaDto.setFileUuid(fileUuid);
fileUploadMetaDto.setTusUploadId(uploadId);
fileMapper.updateTusUploadIdIfNull(fileUploadMetaDto);
fileUploadMetaInfo = fileMapper.selectFileUploadMetaByFileUuid(fileUuid);
}
if (fileUploadMetaInfo.getTusUploadId() == null || fileUploadMetaInfo.getTusUploadId().isBlank()
|| !fileUploadMetaInfo.getTusUploadId().equals(uploadId)) {
fileMapper.updateFileDetailCanceledByFileUuid(fileUuid);
fileMapper.updateFileMasterAggregateByFileUuid(fileUuid);
saveCanceledStatusRedis(fileUuid, offset, size);
return false;
}
}
tusHookDto.setUploadedBytes(offset);
switch (type) {
@@ -164,7 +234,7 @@ public class FileService {
tusHookDto.setStatus(5); // CANCELED (정책에 따라 FAILED=4)
break;
default:
return;
return false;
}
fileMapper.updateFileDetailStatus(tusHookDto);
@@ -172,6 +242,8 @@ public class FileService {
if ("post-finish".equals(type) || "post-terminate".equals(type)) {
fileMapper.updateFileMasterAggregateByFileUuid(tusHookDto.getFileUuid());
}
return true;
}
/*레이디스에 상태값 저장*/
@@ -209,6 +281,20 @@ public class FileService {
stringRedisTemplate.expire(key, Duration.ofHours(24));
}
private void saveCanceledStatusRedis(String fileUuid, long uploadedBytes, long totalBytes) {
String key = buildUploadStatusKey(fileUuid);
int percent = (totalBytes > 0) ? (int) Math.min(100, (uploadedBytes * 100) / totalBytes) : 0;
HashOperations<String, Object, Object> ops = stringRedisTemplate.opsForHash();
ops.put(key, "fileUuid", fileUuid);
ops.put(key, "status", "CANCELED");
ops.put(key, "uploadedBytes", String.valueOf(Math.max(0, uploadedBytes)));
ops.put(key, "totalBytes", String.valueOf(Math.max(0, totalBytes)));
ops.put(key, "percent", String.valueOf(percent));
ops.put(key, "updatedAt", Instant.now().toString());
stringRedisTemplate.expire(key, Duration.ofHours(24));
}
private String buildUploadStatusKey(String fileUuid) {
return "upload:alist:status:" + fileUuid;
}
@@ -252,4 +338,20 @@ public class FileService {
// 설정 실수 방어: 0/음수면 기본값 20초 사용
return (uploadAuthCacheTtlSeconds > 0) ? uploadAuthCacheTtlSeconds : 20L;
}
private boolean isMetadataMismatch(FileUploadMetaDto fileUploadMetaDto, TusHookDto tusHookDto) {
if (tusHookDto.getSize() != null && fileUploadMetaDto.getSizeBytes() != null && !tusHookDto.getSize().equals(fileUploadMetaDto.getSizeBytes())) {
return true;
}
if (tusHookDto.getFilename() != null && !tusHookDto.getFilename().isBlank()
&& fileUploadMetaDto.getOriginName() != null && !fileUploadMetaDto.getOriginName().equals(tusHookDto.getFilename())) {
return true;
}
if (tusHookDto.getFiletype() != null && !tusHookDto.getFiletype().isBlank()
&& fileUploadMetaDto.getContentType() != null && !fileUploadMetaDto.getContentType().isBlank()
&& !fileUploadMetaDto.getContentType().equalsIgnoreCase(tusHookDto.getFiletype())) {
return true;
}
return false;
}
}
@@ -46,6 +46,7 @@ file:
upload:
tus-endpoint: https://file-alist.pjt.kr/tus/files/
public-base-url: https://file-alist.pjt.kr
interrupt-seconds: 30
auth-cache:
ttl-seconds: 20
+1
View File
@@ -48,6 +48,7 @@ file:
upload:
tus-endpoint: https://file-alist.pjt.kr/tus/files/
public-base-url: https://file-alist.pjt.kr
interrupt-seconds: 30
auth-cache:
ttl-seconds: 20
@@ -96,6 +96,24 @@
END,
LFM.UPDATE_DATE = NOW()
</update>
<update id="updateTusUploadIdIfNull" parameterType="com.alist.api.common.modules.file.dto.FileUploadMetaDto">
/*FileMapper.updateTusUploadIdIfNull*/
UPDATE ALISTLMS.FILE_DETAIL
SET TUS_UPLOAD_ID = #{tusUploadId}
, UPDATE_DATE = NOW()
WHERE FILE_UUID = #{fileUuid}
AND DEL_YN = 'N'
AND TUS_UPLOAD_ID IS NULL
</update>
<update id="updateFileDetailCanceledByFileUuid">
/*FileMapper.updateFileDetailCanceledByFileUuid*/
UPDATE ALISTLMS.FILE_DETAIL
SET STATUS = 5,
UPDATE_DATE = NOW()
WHERE FILE_UUID = #{fileUuid}
AND DEL_YN = 'N'
AND STATUS IN (0,1,2)
</update>
<select id="selectFileMasterCountByFileMasterKeyAndDelYnAndStatus" resultType="int">
@@ -119,4 +137,12 @@
AND LFD.STATUS IN (0,1,2,3) <!-- 정책에 맞게 조정 -->
AND LTUT.user_token_idx = #{userTokenIdx}
</select>
<select id="selectFileUploadMetaByFileUuid" resultType="com.alist.api.common.modules.file.dto.FileUploadMetaDto">
/*FileMapper.selectFileUploadMetaByFileUuid*/
SELECT FILE_UUID, ORIGIN_NAME, SIZE_BYTES, CONTENT_TYPE, TUS_UPLOAD_ID, STATUS
FROM ALISTLMS.FILE_DETAIL
WHERE FILE_UUID = #{fileUuid}
AND DEL_YN = 'N'
LIMIT 1
</select>
</mapper>