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 c6addea..e28965d 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 @@ -1,7 +1,9 @@ package com.alist.api.common.modules.file; +import com.alist.api.common.modules.file.dto.UploadStatusDto; import com.alist.api.common.modules.file.dto.UploadTokenDto; 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; @@ -82,6 +84,17 @@ public class FileController { return ApiResponse.entity("", ApiResponseCode.CODE_200); } + @PostMapping("/uploadStatus") + public ResponseEntity> uploadStatus( + @Valid @RequestBody UploadStatusForm uploadStatusForm + ) { + UploadStatusDto status = fileService.getUploadStatus(uploadStatusForm.uploadStatusDto()); + if (status == null) { + return ApiResponse.entity(new UploadStatusDto(), ApiResponseCode.CODE_2003); + } + return ApiResponse.entity(status, ApiResponseCode.CODE_200); + } + @PostMapping("/tusHook") public ResponseEntity> tusHook(@RequestBody TusHookForm tusHookForm) { // log.info("tusHookForm: {}", tusHookForm); @@ -98,6 +111,8 @@ public class FileController { log.info("[tusHookDto] filename={}", tusHookForm.tusHookDto().getFilename()); log.info("[tusHookDto] filetype={}", tusHookForm.tusHookDto().getFiletype()); + fileService.saveUploadStatusFromTusHook(tusHookForm.tusHookDto()); + return ApiResponse.entity("", ApiResponseCode.CODE_200); } } diff --git a/src/main/java/com/alist/api/common/modules/file/dto/TusHookDto.java b/src/main/java/com/alist/api/common/modules/file/dto/TusHookDto.java index a053db3..5d57f8a 100644 --- a/src/main/java/com/alist/api/common/modules/file/dto/TusHookDto.java +++ b/src/main/java/com/alist/api/common/modules/file/dto/TusHookDto.java @@ -9,6 +9,7 @@ public class TusHookDto { private String type; private String uploadId; private Long size; + private Long offset; private String fileMasterKey; private String filename; private String filetype; diff --git a/src/main/java/com/alist/api/common/modules/file/dto/UploadStartDto.java b/src/main/java/com/alist/api/common/modules/file/dto/UploadStartDto.java index 3eeeb73..c248a09 100644 --- a/src/main/java/com/alist/api/common/modules/file/dto/UploadStartDto.java +++ b/src/main/java/com/alist/api/common/modules/file/dto/UploadStartDto.java @@ -12,17 +12,17 @@ public class UploadStartDto { private String uploadToken; @JsonIgnore - String originName; + private String originName; @JsonIgnore - String contentType; + private String contentType; @JsonIgnore - Long sizeBytes; + private Long sizeBytes; @JsonIgnore - String fileCategory; + private String fileCategory; @JsonIgnore - String folderPath; + private String folderPath; @JsonIgnore - Integer userIdx; + private Integer userIdx; @JsonIgnore - int status; + private int status; } diff --git a/src/main/java/com/alist/api/common/modules/file/dto/UploadStatusDto.java b/src/main/java/com/alist/api/common/modules/file/dto/UploadStatusDto.java new file mode 100644 index 0000000..6b7e95e --- /dev/null +++ b/src/main/java/com/alist/api/common/modules/file/dto/UploadStatusDto.java @@ -0,0 +1,15 @@ +package com.alist.api.common.modules.file.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UploadStatusDto { + private String fileMasterKey; + private String status; // PENDING, UPLOADING, DONE, FAILED + private long uploadedBytes; + private long totalBytes; + private int percent; + private String updatedAt; +} diff --git a/src/main/java/com/alist/api/common/modules/file/form/TusHookForm.java b/src/main/java/com/alist/api/common/modules/file/form/TusHookForm.java index d6bd196..01c199d 100644 --- a/src/main/java/com/alist/api/common/modules/file/form/TusHookForm.java +++ b/src/main/java/com/alist/api/common/modules/file/form/TusHookForm.java @@ -17,24 +17,25 @@ public class TusHookForm { private TusHookEventForm tusHookEventForm; public TusHookDto tusHookDto() { - TusHookDto dto = new TusHookDto(); + TusHookDto tusHookDto = new TusHookDto(); - dto.setType(this.type); + tusHookDto.setType(this.type); if (this.tusHookEventForm == null || this.tusHookEventForm.getTusHookUploadForm() == null) { - return dto; + return tusHookDto; } TusHookUploadForm upload = this.tusHookEventForm.getTusHookUploadForm(); - dto.setUploadId(upload.getId()); - dto.setSize(upload.getSize()); + tusHookDto.setUploadId(upload.getId()); + tusHookDto.setSize(upload.getSize()); + tusHookDto.setOffset(upload.getOffset()); if (upload.getMetaData() != null) { - dto.setFileMasterKey(upload.getMetaData().get("fileMasterKey")); - dto.setFilename(upload.getMetaData().get("filename")); - dto.setFiletype(upload.getMetaData().get("filetype")); + tusHookDto.setFileMasterKey(upload.getMetaData().get("fileMasterKey")); + tusHookDto.setFilename(upload.getMetaData().get("filename")); + tusHookDto.setFiletype(upload.getMetaData().get("filetype")); } - return dto; + return tusHookDto; } } diff --git a/src/main/java/com/alist/api/common/modules/file/form/TusHookUploadForm.java b/src/main/java/com/alist/api/common/modules/file/form/TusHookUploadForm.java index ef1b6fc..eb3ac19 100644 --- a/src/main/java/com/alist/api/common/modules/file/form/TusHookUploadForm.java +++ b/src/main/java/com/alist/api/common/modules/file/form/TusHookUploadForm.java @@ -17,6 +17,9 @@ public class TusHookUploadForm { @JsonProperty("Size") private Long size; + @JsonProperty("Offset") + private Long offset; + @JsonProperty("MetaData") private Map metaData; } diff --git a/src/main/java/com/alist/api/common/modules/file/form/UploadStatusForm.java b/src/main/java/com/alist/api/common/modules/file/form/UploadStatusForm.java new file mode 100644 index 0000000..ed0fcef --- /dev/null +++ b/src/main/java/com/alist/api/common/modules/file/form/UploadStatusForm.java @@ -0,0 +1,19 @@ +package com.alist.api.common.modules.file.form; + +import com.alist.api.common.modules.file.dto.UploadStatusDto; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UploadStatusForm { + @NotBlank + private String fileMasterKey; + + public UploadStatusDto uploadStatusDto() { + UploadStatusDto uploadStatusDto = new UploadStatusDto(); + uploadStatusDto.setFileMasterKey(this.fileMasterKey == null ? null : this.fileMasterKey.trim()); + return uploadStatusDto; + } +} 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 2ecab84..d65886a 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 @@ -2,11 +2,13 @@ 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.UploadStartDto; +import com.alist.api.common.modules.file.dto.UploadStatusDto; import com.alist.api.common.modules.file.dto.UploadTokenDto; import com.alist.api.common.modules.file.mapper.FileMapper; import com.alist.api.config.jwt.JwtTokenProvider; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; @@ -14,7 +16,9 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Duration; +import java.time.Instant; import java.util.HexFormat; +import java.util.Map; import java.util.UUID; @Slf4j @@ -105,10 +109,76 @@ public class FileService { return uploadTokenDto; } + public UploadStatusDto getUploadStatus(UploadStatusDto uploadStatusDto) { + String key = buildUploadStatusKey(uploadStatusDto.getFileMasterKey()); + Map map = stringRedisTemplate.opsForHash().entries(key); + + if (map.isEmpty()) { + return null; + } + + UploadStatusDto dto = new UploadStatusDto(); + dto.setFileMasterKey((String) map.getOrDefault("fileMasterKey", uploadStatusDto.getFileMasterKey())); + dto.setStatus((String) map.getOrDefault("status", "PENDING")); + dto.setUploadedBytes(parseLong(map.get("uploadedBytes"))); + dto.setTotalBytes(parseLong(map.get("totalBytes"))); + dto.setPercent((int) parseLong(map.get("percent"))); + dto.setUpdatedAt((String) map.getOrDefault("updatedAt", "")); + + return dto; + } + + public void saveUploadStatusFromTusHook(TusHookDto tusHookDto) { + if (tusHookDto == null || tusHookDto.getFileMasterKey() == null || tusHookDto.getFileMasterKey().isBlank()) { + return; + } + + String key = buildUploadStatusKey(tusHookDto.getFileMasterKey().trim()); + String type = tusHookDto.getType() == null ? "" : tusHookDto.getType().trim().toLowerCase(); + long total = tusHookDto.getSize() == null ? 0L : tusHookDto.getSize(); + long uploaded = tusHookDto.getOffset() == null ? 0L : tusHookDto.getOffset(); + + String status = "UPLOADING"; + if ("post-finish".equals(type)) { + status = "DONE"; + uploaded = total; + } else if ("post-terminate".equals(type)) { + status = "FAILED"; + } else if ("pre-create".equals(type)) { + status = "PENDING"; + } + + int percent = (total > 0) ? (int) Math.min(100, (uploaded * 100) / total) : 0; + + HashOperations ops = stringRedisTemplate.opsForHash(); + ops.put(key, "fileMasterKey", tusHookDto.getFileMasterKey()); + ops.put(key, "status", status); + ops.put(key, "uploadedBytes", String.valueOf(uploaded)); + ops.put(key, "totalBytes", String.valueOf(total)); + ops.put(key, "percent", String.valueOf(percent)); + ops.put(key, "updatedAt", Instant.now().toString()); + + // 복원용으로 하루 유지 + stringRedisTemplate.expire(key, Duration.ofHours(24)); + } + public void insertFileAndSave(TusHookDto tusHookDto) { } + private String buildUploadStatusKey(String fileMasterKey) { + return "upload:alist:status:" + fileMasterKey; + } + + private long parseLong(Object value) { + if (value == null) return 0L; + try { + return Long.parseLong(String.valueOf(value)); + } catch (Exception e) { + return 0L; + } + } + private String buildAuthCacheKey(String fileMasterKey, String uploadToken) { return "upload:alist:auth:" + fileMasterKey + ":" + sha256(uploadToken); }