[파일 업로드] 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.modules.file.form.UploadStatusForm;
|
||||||
import com.alist.api.common.response.ApiResponse;
|
import com.alist.api.common.response.ApiResponse;
|
||||||
import com.alist.api.common.response.ApiResponseCode;
|
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 com.alist.api.common.modules.file.service.FileService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@@ -102,9 +100,11 @@ public class FileController {
|
|||||||
|
|
||||||
@PostMapping("/tusHook")
|
@PostMapping("/tusHook")
|
||||||
public ResponseEntity<ApiResponse<String>> tusHook(@RequestBody TusHookForm tusHookForm) {
|
public ResponseEntity<ApiResponse<String>> tusHook(@RequestBody TusHookForm tusHookForm) {
|
||||||
|
boolean accepted = fileService.updateFileUploadStatus(tusHookForm.tusHookDto());
|
||||||
|
|
||||||
fileService.updateFileUploadStatus(tusHookForm.tusHookDto());
|
if (accepted) {
|
||||||
fileService.saveUploadStatusRedisTusHook(tusHookForm.tusHookDto());
|
fileService.saveUploadStatusRedisTusHook(tusHookForm.tusHookDto());
|
||||||
|
}
|
||||||
|
|
||||||
return ApiResponse.entity("", ApiResponseCode.CODE_200);
|
return ApiResponse.entity("", ApiResponseCode.CODE_200);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,4 +20,10 @@ public interface FileMapper {
|
|||||||
void updateFileDetailStatus(TusHookDto tusHookDto);
|
void updateFileDetailStatus(TusHookDto tusHookDto);
|
||||||
|
|
||||||
void updateFileMasterAggregateByFileUuid(String fileUuid);
|
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}")
|
@Value("${file.upload.auth-cache.ttl-seconds:20}")
|
||||||
private long uploadAuthCacheTtlSeconds;
|
private long uploadAuthCacheTtlSeconds;
|
||||||
|
|
||||||
|
@Value("${file.upload.interrupt-seconds:30}")
|
||||||
|
private long uploadInterruptSeconds;
|
||||||
|
|
||||||
private final FileMapper fileMapper;
|
private final FileMapper fileMapper;
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
private final StringRedisTemplate stringRedisTemplate;
|
private final StringRedisTemplate stringRedisTemplate;
|
||||||
@@ -127,29 +130,96 @@ public class FileService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadStatusDto.setFileUuid((String) map.getOrDefault("fileUuid", uploadStatusDto.getFileUuid()));
|
String fileUuid = (String) map.getOrDefault("fileUuid", uploadStatusDto.getFileUuid());
|
||||||
uploadStatusDto.setStatus((String) map.getOrDefault("status", "PENDING"));
|
String status = (String) map.getOrDefault("status", "PENDING");
|
||||||
uploadStatusDto.setUploadedBytes(parseLong(map.get("uploadedBytes")));
|
long uploadedBytes = parseLong(map.get("uploadedBytes"));
|
||||||
uploadStatusDto.setTotalBytes(parseLong(map.get("totalBytes")));
|
long totalBytes = parseLong(map.get("totalBytes"));
|
||||||
uploadStatusDto.setPercent((int) parseLong(map.get("percent")));
|
int percent = (int) parseLong(map.get("percent"));
|
||||||
uploadStatusDto.setUpdatedAt((String) map.getOrDefault("updatedAt", ""));
|
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;
|
return uploadStatusDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*db에 상태값 저장*/
|
private boolean isStaleUploading(String status, String updatedAt) {
|
||||||
@Transactional
|
if (!"UPLOADING".equalsIgnoreCase(status)) {
|
||||||
public void updateFileUploadStatus(TusHookDto tusHookDto) {
|
return false;
|
||||||
if (tusHookDto == null || tusHookDto.getFileUuid() == null || tusHookDto.getFileUuid().isBlank()) {
|
}
|
||||||
return;
|
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 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 offset = tusHookDto.getOffset() == null ? 0L : tusHookDto.getOffset();
|
||||||
long size = tusHookDto.getSize() == null ? 0L : tusHookDto.getSize();
|
long size = tusHookDto.getSize() == null ? 0L : tusHookDto.getSize();
|
||||||
|
|
||||||
fileMapper.insertFileUploadEventLog(tusHookDto);
|
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);
|
tusHookDto.setUploadedBytes(offset);
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -164,7 +234,7 @@ public class FileService {
|
|||||||
tusHookDto.setStatus(5); // CANCELED (정책에 따라 FAILED=4)
|
tusHookDto.setStatus(5); // CANCELED (정책에 따라 FAILED=4)
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fileMapper.updateFileDetailStatus(tusHookDto);
|
fileMapper.updateFileDetailStatus(tusHookDto);
|
||||||
@@ -172,6 +242,8 @@ public class FileService {
|
|||||||
if ("post-finish".equals(type) || "post-terminate".equals(type)) {
|
if ("post-finish".equals(type) || "post-terminate".equals(type)) {
|
||||||
fileMapper.updateFileMasterAggregateByFileUuid(tusHookDto.getFileUuid());
|
fileMapper.updateFileMasterAggregateByFileUuid(tusHookDto.getFileUuid());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*레이디스에 상태값 저장*/
|
/*레이디스에 상태값 저장*/
|
||||||
@@ -209,6 +281,20 @@ public class FileService {
|
|||||||
stringRedisTemplate.expire(key, Duration.ofHours(24));
|
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) {
|
private String buildUploadStatusKey(String fileUuid) {
|
||||||
return "upload:alist:status:" + fileUuid;
|
return "upload:alist:status:" + fileUuid;
|
||||||
}
|
}
|
||||||
@@ -252,4 +338,20 @@ public class FileService {
|
|||||||
// 설정 실수 방어: 0/음수면 기본값 20초 사용
|
// 설정 실수 방어: 0/음수면 기본값 20초 사용
|
||||||
return (uploadAuthCacheTtlSeconds > 0) ? uploadAuthCacheTtlSeconds : 20L;
|
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:
|
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
|
||||||
|
interrupt-seconds: 30
|
||||||
auth-cache:
|
auth-cache:
|
||||||
ttl-seconds: 20
|
ttl-seconds: 20
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ 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
|
||||||
|
interrupt-seconds: 30
|
||||||
auth-cache:
|
auth-cache:
|
||||||
ttl-seconds: 20
|
ttl-seconds: 20
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,24 @@
|
|||||||
END,
|
END,
|
||||||
LFM.UPDATE_DATE = NOW()
|
LFM.UPDATE_DATE = NOW()
|
||||||
</update>
|
</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">
|
<select id="selectFileMasterCountByFileMasterKeyAndDelYnAndStatus" resultType="int">
|
||||||
@@ -119,4 +137,12 @@
|
|||||||
AND LFD.STATUS IN (0,1,2,3) <!-- 정책에 맞게 조정 -->
|
AND LFD.STATUS IN (0,1,2,3) <!-- 정책에 맞게 조정 -->
|
||||||
AND LTUT.user_token_idx = #{userTokenIdx}
|
AND LTUT.user_token_idx = #{userTokenIdx}
|
||||||
</select>
|
</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>
|
</mapper>
|
||||||
Reference in New Issue
Block a user