From 673635ffebd2023d2c69b9979354f233666c3f98 Mon Sep 17 00:00:00 2001 From: sdw086 Date: Mon, 9 Mar 2026 10:45:00 +0900 Subject: [PATCH] =?UTF-8?q?[=ED=8C=8C=EC=9D=BC=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C]=20tusHook=20=EC=9E=91=EC=97=85=20-=20=EB=A9=80?= =?UTF-8?q?=ED=8B=B0=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/modules/file/FileController.java | 8 +- .../modules/file/mapper/FileMapper.java | 6 + .../modules/file/service/FileService.java | 120 ++++++++++++++++-- src/main/resources/application-local.yaml | 1 + src/main/resources/application-pjt.yaml | 1 + .../mapper/common/file/FileMapper.xml | 34 ++++- 6 files changed, 153 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/alist/api/common/modules/file/FileController.java b/src/main/java/com/alist/api/common/modules/file/FileController.java index 8c9f3fa..6975200 100644 --- a/src/main/java/com/alist/api/common/modules/file/FileController.java +++ b/src/main/java/com/alist/api/common/modules/file/FileController.java @@ -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> tusHook(@RequestBody TusHookForm tusHookForm) { + boolean accepted = fileService.updateFileUploadStatus(tusHookForm.tusHookDto()); - fileService.updateFileUploadStatus(tusHookForm.tusHookDto()); - fileService.saveUploadStatusRedisTusHook(tusHookForm.tusHookDto()); + if (accepted) { + fileService.saveUploadStatusRedisTusHook(tusHookForm.tusHookDto()); + } return ApiResponse.entity("", ApiResponseCode.CODE_200); } diff --git a/src/main/java/com/alist/api/common/modules/file/mapper/FileMapper.java b/src/main/java/com/alist/api/common/modules/file/mapper/FileMapper.java index 7ffffd1..3cd678d 100644 --- a/src/main/java/com/alist/api/common/modules/file/mapper/FileMapper.java +++ b/src/main/java/com/alist/api/common/modules/file/mapper/FileMapper.java @@ -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); } diff --git a/src/main/java/com/alist/api/common/modules/file/service/FileService.java b/src/main/java/com/alist/api/common/modules/file/service/FileService.java index bb3055d..a8cde3e 100644 --- a/src/main/java/com/alist/api/common/modules/file/service/FileService.java +++ b/src/main/java/com/alist/api/common/modules/file/service/FileService.java @@ -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; } + 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 void updateFileUploadStatus(TusHookDto tusHookDto) { + public boolean updateFileUploadStatus(TusHookDto tusHookDto) { if (tusHookDto == null || tusHookDto.getFileUuid() == null || tusHookDto.getFileUuid().isBlank()) { - return; + 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 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; + } } diff --git a/src/main/resources/application-local.yaml b/src/main/resources/application-local.yaml index 14806fd..39cb6b0 100644 --- a/src/main/resources/application-local.yaml +++ b/src/main/resources/application-local.yaml @@ -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 diff --git a/src/main/resources/application-pjt.yaml b/src/main/resources/application-pjt.yaml index b91e499..945fe3c 100644 --- a/src/main/resources/application-pjt.yaml +++ b/src/main/resources/application-pjt.yaml @@ -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 diff --git a/src/main/resources/mapper/common/file/FileMapper.xml b/src/main/resources/mapper/common/file/FileMapper.xml index 38001b1..0dd293f 100644 --- a/src/main/resources/mapper/common/file/FileMapper.xml +++ b/src/main/resources/mapper/common/file/FileMapper.xml @@ -96,6 +96,24 @@ END, LFM.UPDATE_DATE = NOW() + + /*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 + + + /*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) + + \ No newline at end of file