feat(files): complete v2 upload sessions
This commit is contained in:
@@ -53,6 +53,13 @@ public class UploadSessionV2Controller {
|
||||
return ApiV2Response.success(toResponse(uploadSessionService.cancelOwnedSession(user, sessionId)));
|
||||
}
|
||||
|
||||
@PostMapping("/{sessionId}/complete")
|
||||
public ApiV2Response<UploadSessionV2Response> completeSession(@AuthenticationPrincipal UserDetails userDetails,
|
||||
@PathVariable String sessionId) {
|
||||
User user = userDetailsService.loadDomainUser(userDetails.getUsername());
|
||||
return ApiV2Response.success(toResponse(uploadSessionService.completeOwnedSession(user, sessionId)));
|
||||
}
|
||||
|
||||
private UploadSessionV2Response toResponse(UploadSession session) {
|
||||
return new UploadSessionV2Response(
|
||||
session.getSessionId(),
|
||||
|
||||
@@ -21,22 +21,26 @@ public class UploadSessionService {
|
||||
|
||||
private final UploadSessionRepository uploadSessionRepository;
|
||||
private final StoredFileRepository storedFileRepository;
|
||||
private final FileService fileService;
|
||||
private final long maxFileSize;
|
||||
private final Clock clock;
|
||||
|
||||
@Autowired
|
||||
public UploadSessionService(UploadSessionRepository uploadSessionRepository,
|
||||
StoredFileRepository storedFileRepository,
|
||||
FileService fileService,
|
||||
FileStorageProperties properties) {
|
||||
this(uploadSessionRepository, storedFileRepository, properties, Clock.systemUTC());
|
||||
this(uploadSessionRepository, storedFileRepository, fileService, properties, Clock.systemUTC());
|
||||
}
|
||||
|
||||
UploadSessionService(UploadSessionRepository uploadSessionRepository,
|
||||
StoredFileRepository storedFileRepository,
|
||||
FileService fileService,
|
||||
FileStorageProperties properties,
|
||||
Clock clock) {
|
||||
this.uploadSessionRepository = uploadSessionRepository;
|
||||
this.storedFileRepository = storedFileRepository;
|
||||
this.fileService = fileService;
|
||||
this.maxFileSize = properties.getMaxFileSize();
|
||||
this.clock = clock;
|
||||
}
|
||||
@@ -83,6 +87,46 @@ public class UploadSessionService {
|
||||
return uploadSessionRepository.save(session);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public UploadSession completeOwnedSession(User user, String sessionId) {
|
||||
UploadSession session = getOwnedSession(user, sessionId);
|
||||
if (session.getStatus() == UploadSessionStatus.COMPLETED) {
|
||||
return session;
|
||||
}
|
||||
if (session.getStatus() == UploadSessionStatus.CANCELLED || session.getStatus() == UploadSessionStatus.FAILED) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "上传会话不能完成");
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.ofInstant(clock.instant(), clock.getZone());
|
||||
if (session.getExpiresAt().isBefore(now)) {
|
||||
session.setStatus(UploadSessionStatus.EXPIRED);
|
||||
session.setUpdatedAt(now);
|
||||
uploadSessionRepository.save(session);
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "上传会话已过期");
|
||||
}
|
||||
|
||||
session.setStatus(UploadSessionStatus.COMPLETING);
|
||||
session.setUpdatedAt(now);
|
||||
uploadSessionRepository.save(session);
|
||||
|
||||
try {
|
||||
fileService.completeUpload(user, new CompleteUploadRequest(
|
||||
session.getTargetPath(),
|
||||
session.getFilename(),
|
||||
session.getObjectKey(),
|
||||
session.getContentType(),
|
||||
session.getSize()
|
||||
));
|
||||
session.setStatus(UploadSessionStatus.COMPLETED);
|
||||
session.setUpdatedAt(now);
|
||||
return uploadSessionRepository.save(session);
|
||||
} catch (RuntimeException ex) {
|
||||
session.setStatus(UploadSessionStatus.FAILED);
|
||||
session.setUpdatedAt(now);
|
||||
uploadSessionRepository.save(session);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private void validateTarget(User user, String normalizedPath, String filename, long size) {
|
||||
long effectiveMaxUploadSize = Math.min(maxFileSize, user.getMaxUploadSizeBytes());
|
||||
if (size > effectiveMaxUploadSize) {
|
||||
|
||||
@@ -87,6 +87,22 @@ class UploadSessionV2ControllerTest {
|
||||
.andExpect(jsonPath("$.data.status").value("CREATED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCompleteUploadSessionWithV2Envelope() throws Exception {
|
||||
User user = createUser(7L);
|
||||
UploadSession session = createSession(user);
|
||||
session.setStatus(UploadSessionStatus.COMPLETED);
|
||||
when(userDetailsService.loadDomainUser("alice")).thenReturn(user);
|
||||
when(uploadSessionService.completeOwnedSession(user, "session-1")).thenReturn(session);
|
||||
|
||||
mockMvc.perform(post("/api/v2/files/upload-sessions/session-1/complete")
|
||||
.with(user(userDetails())))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.sessionId").value("session-1"))
|
||||
.andExpect(jsonPath("$.data.status").value("COMPLETED"));
|
||||
}
|
||||
|
||||
private UserDetails userDetails() {
|
||||
return org.springframework.security.core.userdetails.User
|
||||
.withUsername("alice")
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.yoyuzh.config.FileStorageProperties;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@@ -18,7 +19,9 @@ import java.util.Optional;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UploadSessionServiceTest {
|
||||
@@ -27,6 +30,8 @@ class UploadSessionServiceTest {
|
||||
private UploadSessionRepository uploadSessionRepository;
|
||||
@Mock
|
||||
private StoredFileRepository storedFileRepository;
|
||||
@Mock
|
||||
private FileService fileService;
|
||||
|
||||
private UploadSessionService uploadSessionService;
|
||||
|
||||
@@ -37,6 +42,7 @@ class UploadSessionServiceTest {
|
||||
uploadSessionService = new UploadSessionService(
|
||||
uploadSessionRepository,
|
||||
storedFileRepository,
|
||||
fileService,
|
||||
properties,
|
||||
Clock.fixed(Instant.parse("2026-04-08T06:00:00Z"), ZoneOffset.UTC)
|
||||
);
|
||||
@@ -91,6 +97,39 @@ class UploadSessionServiceTest {
|
||||
)).isInstanceOf(BusinessException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCompleteOwnedSessionThroughLegacyFileCommitPath() {
|
||||
User user = createUser(7L);
|
||||
UploadSession session = createSession(user);
|
||||
when(uploadSessionRepository.findBySessionIdAndUserId("session-1", 7L))
|
||||
.thenReturn(Optional.of(session));
|
||||
when(uploadSessionRepository.save(any(UploadSession.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
UploadSession result = uploadSessionService.completeOwnedSession(user, "session-1");
|
||||
|
||||
assertThat(result.getStatus()).isEqualTo(UploadSessionStatus.COMPLETED);
|
||||
assertThat(result.getUpdatedAt()).isEqualTo(LocalDateTime.of(2026, 4, 8, 6, 0));
|
||||
ArgumentCaptor<CompleteUploadRequest> requestCaptor = ArgumentCaptor.forClass(CompleteUploadRequest.class);
|
||||
verify(fileService).completeUpload(eq(user), requestCaptor.capture());
|
||||
assertThat(requestCaptor.getValue().path()).isEqualTo("/docs");
|
||||
assertThat(requestCaptor.getValue().filename()).isEqualTo("movie.mp4");
|
||||
assertThat(requestCaptor.getValue().storageName()).isEqualTo("blobs/session-1");
|
||||
assertThat(requestCaptor.getValue().contentType()).isEqualTo("video/mp4");
|
||||
assertThat(requestCaptor.getValue().size()).isEqualTo(20L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectCompletingCancelledSession() {
|
||||
User user = createUser(7L);
|
||||
UploadSession session = createSession(user);
|
||||
session.setStatus(UploadSessionStatus.CANCELLED);
|
||||
when(uploadSessionRepository.findBySessionIdAndUserId("session-1", 7L))
|
||||
.thenReturn(Optional.of(session));
|
||||
|
||||
assertThatThrownBy(() -> uploadSessionService.completeOwnedSession(user, "session-1"))
|
||||
.isInstanceOf(BusinessException.class);
|
||||
}
|
||||
|
||||
private User createUser(Long id) {
|
||||
User user = new User();
|
||||
user.setId(id);
|
||||
@@ -100,4 +139,23 @@ class UploadSessionServiceTest {
|
||||
user.setCreatedAt(LocalDateTime.now());
|
||||
return user;
|
||||
}
|
||||
|
||||
private UploadSession createSession(User user) {
|
||||
UploadSession session = new UploadSession();
|
||||
session.setSessionId("session-1");
|
||||
session.setUser(user);
|
||||
session.setTargetPath("/docs");
|
||||
session.setFilename("movie.mp4");
|
||||
session.setContentType("video/mp4");
|
||||
session.setSize(20L);
|
||||
session.setObjectKey("blobs/session-1");
|
||||
session.setChunkSize(8L * 1024 * 1024);
|
||||
session.setChunkCount(1);
|
||||
session.setUploadedPartsJson("[]");
|
||||
session.setStatus(UploadSessionStatus.CREATED);
|
||||
session.setCreatedAt(LocalDateTime.of(2026, 4, 8, 6, 0));
|
||||
session.setUpdatedAt(LocalDateTime.of(2026, 4, 8, 6, 0));
|
||||
session.setExpiresAt(LocalDateTime.of(2026, 4, 9, 6, 0));
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user