[파일 업로드] tusHook 작업

- 실시간 상태 조회
This commit is contained in:
2026-03-06 11:44:36 +09:00
parent a40e2b5161
commit c57b80b950
8 changed files with 140 additions and 16 deletions
@@ -1,7 +1,9 @@
package com.alist.api.common.modules.file; 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.dto.UploadTokenDto;
import com.alist.api.common.modules.file.form.TusHookForm; 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.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.dto.UploadStartDto;
@@ -82,6 +84,17 @@ public class FileController {
return ApiResponse.entity("", ApiResponseCode.CODE_200); return ApiResponse.entity("", ApiResponseCode.CODE_200);
} }
@PostMapping("/uploadStatus")
public ResponseEntity<ApiResponse<UploadStatusDto>> 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") @PostMapping("/tusHook")
public ResponseEntity<ApiResponse<String>> tusHook(@RequestBody TusHookForm tusHookForm) { public ResponseEntity<ApiResponse<String>> tusHook(@RequestBody TusHookForm tusHookForm) {
// log.info("tusHookForm: {}", tusHookForm); // log.info("tusHookForm: {}", tusHookForm);
@@ -98,6 +111,8 @@ public class FileController {
log.info("[tusHookDto] filename={}", tusHookForm.tusHookDto().getFilename()); log.info("[tusHookDto] filename={}", tusHookForm.tusHookDto().getFilename());
log.info("[tusHookDto] filetype={}", tusHookForm.tusHookDto().getFiletype()); log.info("[tusHookDto] filetype={}", tusHookForm.tusHookDto().getFiletype());
fileService.saveUploadStatusFromTusHook(tusHookForm.tusHookDto());
return ApiResponse.entity("", ApiResponseCode.CODE_200); return ApiResponse.entity("", ApiResponseCode.CODE_200);
} }
} }
@@ -9,6 +9,7 @@ public class TusHookDto {
private String type; private String type;
private String uploadId; private String uploadId;
private Long size; private Long size;
private Long offset;
private String fileMasterKey; private String fileMasterKey;
private String filename; private String filename;
private String filetype; private String filetype;
@@ -12,17 +12,17 @@ public class UploadStartDto {
private String uploadToken; private String uploadToken;
@JsonIgnore @JsonIgnore
String originName; private String originName;
@JsonIgnore @JsonIgnore
String contentType; private String contentType;
@JsonIgnore @JsonIgnore
Long sizeBytes; private Long sizeBytes;
@JsonIgnore @JsonIgnore
String fileCategory; private String fileCategory;
@JsonIgnore @JsonIgnore
String folderPath; private String folderPath;
@JsonIgnore @JsonIgnore
Integer userIdx; private Integer userIdx;
@JsonIgnore @JsonIgnore
int status; private int status;
} }
@@ -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;
}
@@ -17,24 +17,25 @@ public class TusHookForm {
private TusHookEventForm tusHookEventForm; private TusHookEventForm tusHookEventForm;
public TusHookDto tusHookDto() { 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) { if (this.tusHookEventForm == null || this.tusHookEventForm.getTusHookUploadForm() == null) {
return dto; return tusHookDto;
} }
TusHookUploadForm upload = this.tusHookEventForm.getTusHookUploadForm(); TusHookUploadForm upload = this.tusHookEventForm.getTusHookUploadForm();
dto.setUploadId(upload.getId()); tusHookDto.setUploadId(upload.getId());
dto.setSize(upload.getSize()); tusHookDto.setSize(upload.getSize());
tusHookDto.setOffset(upload.getOffset());
if (upload.getMetaData() != null) { if (upload.getMetaData() != null) {
dto.setFileMasterKey(upload.getMetaData().get("fileMasterKey")); tusHookDto.setFileMasterKey(upload.getMetaData().get("fileMasterKey"));
dto.setFilename(upload.getMetaData().get("filename")); tusHookDto.setFilename(upload.getMetaData().get("filename"));
dto.setFiletype(upload.getMetaData().get("filetype")); tusHookDto.setFiletype(upload.getMetaData().get("filetype"));
} }
return dto; return tusHookDto;
} }
} }
@@ -17,6 +17,9 @@ public class TusHookUploadForm {
@JsonProperty("Size") @JsonProperty("Size")
private Long size; private Long size;
@JsonProperty("Offset")
private Long offset;
@JsonProperty("MetaData") @JsonProperty("MetaData")
private Map<String, String> metaData; private Map<String, String> metaData;
} }
@@ -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;
}
}
@@ -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.TusHookDto;
import com.alist.api.common.modules.file.dto.UploadStartDto; 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.dto.UploadTokenDto;
import com.alist.api.common.modules.file.mapper.FileMapper; import com.alist.api.common.modules.file.mapper.FileMapper;
import com.alist.api.config.jwt.JwtTokenProvider; import com.alist.api.config.jwt.JwtTokenProvider;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -14,7 +16,9 @@ import java.nio.charset.StandardCharsets;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.time.Duration; import java.time.Duration;
import java.time.Instant;
import java.util.HexFormat; import java.util.HexFormat;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
@Slf4j @Slf4j
@@ -105,10 +109,76 @@ public class FileService {
return uploadTokenDto; return uploadTokenDto;
} }
public UploadStatusDto getUploadStatus(UploadStatusDto uploadStatusDto) {
String key = buildUploadStatusKey(uploadStatusDto.getFileMasterKey());
Map<Object, Object> 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<String, Object, Object> 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) { 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) { private String buildAuthCacheKey(String fileMasterKey, String uploadToken) {
return "upload:alist:auth:" + fileMasterKey + ":" + sha256(uploadToken); return "upload:alist:auth:" + fileMasterKey + ":" + sha256(uploadToken);
} }