[tus] 파일업로드 추가

This commit is contained in:
2026-03-05 11:55:16 +09:00
parent f2fe4f10c1
commit 93446e5b41
17 changed files with 348 additions and 7 deletions
@@ -0,0 +1,67 @@
package com.alist.api.common.modules.file;
import com.alist.api.common.modules.file.dto.UploadTokenDto;
import com.alist.api.common.modules.file.form.UploadTokenForm;
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;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@Slf4j
@Tag(name = "99. 파일 관리", description = "파일 관리")
@RestController
@RequestMapping("/files")
public class FileController {
private final FileService fileService;
public FileController(FileService fileService) {
this.fileService = fileService;
}
@Operation(
summary = "파일 업로드 시작",
description = "업로드 토큰 획득 및 업로드 정보 db 기록"
)
@PostMapping("/uploadStart")
public ResponseEntity<ApiResponse<UploadStartDto>> uploadStart(@Valid @RequestBody UploadStartForm uploadStartForm, HttpServletRequest request) {
HttpSession session = request.getSession(false);
Integer userTokenIdx = (session == null) ? null : (Integer) session.getAttribute("userTokenIdx");
Integer userIdx = (session == null) ? null : (Integer) session.getAttribute("userIdx");
if (userTokenIdx == null || userIdx == null) {
return ApiResponse.entity(new UploadStartDto(), ApiResponseCode.CODE_401);
}
UploadStartDto uploadInitDto = fileService.insertFileMasterUpload(userIdx, userTokenIdx, uploadStartForm.uploadStartDto());
return ApiResponse.entity(uploadInitDto, ApiResponseCode.CODE_200);
}
@Operation(
summary = "업로드 권한 체크",
description = "업로드 토큰 발급 후 실제 업로드 권한이 있는지 확인 과정입니다."
)
@PostMapping("/uploadAuthCheck")
public ResponseEntity<ApiResponse<String>> uploadAuthCheck(@RequestHeader(value = "Authorization", required = false) String uploadToken, UploadTokenForm uploadTokenForm) {
uploadTokenForm.setUploadToken(uploadToken.substring(7).trim());
UploadTokenDto uploadTokenInfo = fileService.isUploadTokenCheck(uploadTokenForm.uploadTokenDto());
if (uploadTokenInfo.getResultCode() == 401) {
return ApiResponse.entity("", ApiResponseCode.CODE_401);
} else if (uploadTokenInfo.getResultCode() == 403) {
return ApiResponse.entity("", ApiResponseCode.CODE_403);
}
return ApiResponse.entity("", ApiResponseCode.CODE_200);
}
}
@@ -0,0 +1,28 @@
package com.alist.api.common.modules.file.dto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UploadStartDto {
private String fileMasterKey;
private String tusEndpoint;
private String uploadToken;
@JsonIgnore
String originName;
@JsonIgnore
String contentType;
@JsonIgnore
Long sizeBytes;
@JsonIgnore
String fileCategory;
@JsonIgnore
String folderPath;
@JsonIgnore
Integer userIdx;
@JsonIgnore
int status;
}
@@ -0,0 +1,16 @@
package com.alist.api.common.modules.file.dto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UploadTokenDto {
private Integer userTokenIdx;
private String fileMasterKey;
private String uploadToken;
@JsonIgnore
private int resultCode;
}
@@ -0,0 +1,33 @@
package com.alist.api.common.modules.file.form;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UploadCompleteForm {
@NotBlank
String fileMastryKey;
@NotBlank
String tusUploadUrl;
@NotBlank
String originName;
@NotBlank
String saveName;
@NotBlank
String savePath;
@NotBlank
String contentType;
@NotNull
@Positive
Long sizeBytes;
}
@@ -0,0 +1,39 @@
package com.alist.api.common.modules.file.form;
import com.alist.api.common.modules.file.dto.UploadStartDto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UploadStartForm {
@NotBlank
private String originName;
@NotBlank
private String contentType;
@NotNull
@Positive
private Long sizeBytes;
@NotBlank
private String fileCategory;
private String folderPath;
public UploadStartDto uploadStartDto() {
UploadStartDto uploadStartDto = new UploadStartDto();
uploadStartDto.setOriginName(this.originName);
uploadStartDto.setContentType(this.contentType);
uploadStartDto.setSizeBytes(this.sizeBytes);
uploadStartDto.setFileCategory(this.fileCategory);
uploadStartDto.setFolderPath(this.folderPath);
return uploadStartDto;
}
}
@@ -0,0 +1,20 @@
package com.alist.api.common.modules.file.form;
import com.alist.api.common.modules.file.dto.UploadTokenDto;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class UploadTokenForm {
private String fileMasterKey;
private String uploadToken;
public UploadTokenDto uploadTokenDto() {
UploadTokenDto uploadTokenDto = new UploadTokenDto();
uploadTokenDto.setFileMasterKey(this.fileMasterKey);
uploadTokenDto.setUploadToken(this.uploadToken);
return uploadTokenDto;
}
}
@@ -0,0 +1,12 @@
package com.alist.api.common.modules.file.mapper;
import com.alist.api.common.modules.file.dto.UploadStartDto;
import com.alist.api.common.modules.file.dto.UploadTokenDto;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface FileMapper {
void insertFileMasterUpload(UploadStartDto uploadStartDto);
int selectFileMasterCountByFileMasterKeyAndDelYnAndStatus(UploadTokenDto uploadTokenDto);
}
@@ -0,0 +1,70 @@
package com.alist.api.common.modules.file.service;
import com.alist.api.common.modules.file.dto.UploadStartDto;
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 org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service
public class FileService {
@Value("${file.upload.tus-endpoint}")
private String tusEndpoint;
@Value("${file.upload.public-base-url}")
private String publicBaseUrl;
private final FileMapper fileMapper;
private final JwtTokenProvider jwtTokenProvider;
public FileService(FileMapper fileMapper, JwtTokenProvider jwtTokenProvider) {
this.fileMapper = fileMapper;
this.jwtTokenProvider = jwtTokenProvider;
}
public UploadStartDto insertFileMasterUpload(Integer userIdx, Integer userTokenIdx, UploadStartDto uploadStartDto) {
uploadStartDto.setFileMasterKey(UUID.randomUUID().toString());
uploadStartDto.setUserIdx(userIdx);
uploadStartDto.setStatus(1);
uploadStartDto.setFolderPath(uploadStartDto.getFolderPath() == null ? "/" : uploadStartDto.getFolderPath());
fileMapper.insertFileMasterUpload(uploadStartDto);
uploadStartDto.setUploadToken(jwtTokenProvider.createUploadToken(userTokenIdx));
uploadStartDto.setTusEndpoint(tusEndpoint);
return uploadStartDto;
}
public UploadTokenDto isUploadTokenCheck(UploadTokenDto uploadTokenDto) {
if (!jwtTokenProvider.validateToken(uploadTokenDto.getUploadToken())) {
uploadTokenDto.setResultCode(401);
return uploadTokenDto;
}
if (!jwtTokenProvider.hasUploadScope(uploadTokenDto.getUploadToken())) {
uploadTokenDto.setResultCode(403);
return uploadTokenDto;
}
try {
uploadTokenDto.setUserTokenIdx(Integer.parseInt(jwtTokenProvider.getUserId(uploadTokenDto.getUploadToken())));
} catch (NumberFormatException e) {
uploadTokenDto.setResultCode(401);
return uploadTokenDto;
}
int isOk = fileMapper.selectFileMasterCountByFileMasterKeyAndDelYnAndStatus(uploadTokenDto);
if (isOk > 0) {
uploadTokenDto.setResultCode(200);
} else {
uploadTokenDto.setResultCode(403);
}
return uploadTokenDto;
}
}
@@ -12,7 +12,6 @@ import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
@@ -90,4 +90,28 @@ public class JwtTokenProvider {
.getBody();
}
/* 업로드 전용 토큰*/
public String createUploadToken(long userTokenIdx) {
Instant now = Instant.now();
Instant expiry = now.plusSeconds(300); // 5분
return Jwts.builder()
.setSubject(String.valueOf(userTokenIdx))
.claim("scope", "UPLOAD")
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(expiry))
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
}
public boolean hasUploadScope(String token) {
try {
Claims claims = parseClaims(token);
return "UPLOAD".equals(claims.get("scope", String.class));
} catch (Exception e) {
return false;
}
}
}
@@ -22,11 +22,11 @@ public class TestLoginDto {
@JsonIgnore
private String userApiKey;
@JsonIgnore
private int userTokenIdx;
private Integer userTokenIdx;
@JsonIgnore
private Instant expiresAt;
@JsonIgnore
private int userIdx;
private Integer userIdx;
@JsonIgnore
private int resultCode;
}
@@ -4,7 +4,7 @@ import lombok.Getter;
@Getter
public class TestLoginTokenVo {
private int userTokenIdx;
private Integer userTokenIdx;
private String userApiKey;
private String userRole;
private String refreshToken;
@@ -4,7 +4,7 @@ import lombok.Getter;
@Getter
public class TestLoginVo {
private int userIdx;
private Integer userIdx;
private String id;
private String password;
}
@@ -6,8 +6,8 @@ import lombok.Setter;
@Setter
@Getter
public class TestUserTokenDto {
private int userTokenIdx;
private int userIdx;
private Integer userTokenIdx;
private Integer userIdx;
private String userApiKey;
private String userRole;
private int resultCode;
@@ -42,6 +42,11 @@ swagger:
id: alist
password: "1qaz2wsx!@"
file:
upload:
tus-endpoint: https://file-alist.pjt.kr/tus/files/
public-base-url: https://file-alist.pjt.kr
springdoc:
api-docs:
enabled: true
+5
View File
@@ -44,6 +44,11 @@ swagger:
id: ${SWAGGER_ID}
password: ${SWAGGER_PASSWORD}
file:
upload:
tus-endpoint: https://file-alist.pjt.kr/tus/files/
public-base-url: https://file-alist.pjt.kr
springdoc:
api-docs:
enabled: true
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.alist.api.common.modules.file.mapper.FileMapper">
<insert id="insertFileMasterUpload">
/*FileMapper.insertFileMasterUpload*/
INSERT INTO ALISTLMS.FILE_MASTER (
FILE_MASTER_KEY, USER_IDX, FILE_CATEGORY, FOLDER_PATH, STATUS, CREATE_MEMBER, UPDATE_MEMBER
) values (
#{fileMasterKey}, #{userIdx}, #{fileCategory}, #{folderPath}, #{status}, #{userIdx}, #{userIdx}
)
</insert>
<select id="selectFileMasterCountByFileMasterKeyAndDelYnAndStatus" resultType="int">
/*FileMapper.selectFileMasterCountByFileMasterKeyAndDelYnAndStatus*/
SELECT COUNT(*)
FROM ALISTLMS.FILE_MASTER LFM
INNER JOIN ALISTLMS.TEST_USER_TOKEN LTUT ON LTUT.USER_IDX = LFM.USER_IDX
WHERE LFM.DEL_YN = 'N'
AND LFM.FILE_MASTER_KEY = #{fileMasterKey}
AND LFM.STATUS = 1
</select>
</mapper>