[파일 업로드] tusHook 작업
- 멀티업로드 상태 업로드
This commit is contained in:
@@ -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());
|
||||
fileService.saveUploadStatusRedisTusHook(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;
|
||||
}
|
||||
|
||||
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<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
|
||||
|
||||
|
||||
@@ -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">
|
||||
@@ -114,9 +132,17 @@
|
||||
FROM ALISTLMS.FILE_DETAIL LFD
|
||||
INNER JOIN ALISTLMS.test_user_token LTUT
|
||||
ON LTUT.user_idx = LFD.CREATE_MEMBER
|
||||
WHERE LFD.DEL_YN = 'N'
|
||||
AND LFD.FILE_UUID = #{fileUuid}
|
||||
AND LFD.STATUS IN (0,1,2,3) <!-- 정책에 맞게 조정 -->
|
||||
AND LTUT.user_token_idx = #{userTokenIdx}
|
||||
WHERE LFD.DEL_YN = 'N'
|
||||
AND LFD.FILE_UUID = #{fileUuid}
|
||||
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>
|
||||
Reference in New Issue
Block a user