44import com .climbup .climbup .attempt .dto .request .CreateAttemptRequest ;
55import com .climbup .climbup .attempt .dto .response .CreateAttemptResponse ;
66import com .climbup .climbup .attempt .entity .UserMissionAttempt ;
7+ import com .climbup .climbup .attempt .exception .*;
78import com .climbup .climbup .attempt .repository .UserMissionAttemptRepository ;
9+ import com .climbup .climbup .attempt .upload .dto .request .RouteMissionUploadChunkRequest ;
10+ import com .climbup .climbup .attempt .upload .dto .request .RouteMissionUploadSessionInitializeRequest ;
11+ import com .climbup .climbup .attempt .upload .dto .response .RouteMissionUploadChunkResponse ;
12+ import com .climbup .climbup .attempt .upload .dto .response .RouteMissionUploadSessionFinalizeResponse ;
13+ import com .climbup .climbup .attempt .upload .dto .response .RouteMissionUploadSessionInitializeResponse ;
14+ import com .climbup .climbup .attempt .upload .dto .response .RouteMissionUploadStatusResponse ;
15+ import com .climbup .climbup .attempt .upload .entity .UploadSession ;
16+ import com .climbup .climbup .attempt .upload .entity .Chunk ;
17+ import com .climbup .climbup .attempt .upload .enums .UploadStatus ;
18+ import com .climbup .climbup .attempt .upload .repository .UploadSessionRepository ;
19+ import com .climbup .climbup .attempt .upload .repository .ChunkRepository ;
20+ import com .climbup .climbup .common .exception .CommonBusinessException ;
821import com .climbup .climbup .common .exception .ErrorCode ;
922import com .climbup .climbup .common .exception .ValidationException ;
1023import com .climbup .climbup .route .entity .RouteMission ;
2336import org .springframework .stereotype .Service ;
2437import org .springframework .transaction .annotation .Transactional ;
2538
39+ import java .util .Comparator ;
40+ import java .util .List ;
41+ import java .util .UUID ;
42+ import java .io .*;
43+ import java .nio .file .*;
44+
2645@ Service
2746@ RequiredArgsConstructor
2847@ Slf4j
@@ -34,6 +53,61 @@ public class AttemptServiceImpl implements AttemptService {
3453 private final UserRepository userRepository ;
3554 private final UserSessionRepository sessionRepository ;
3655 private final SRHistoryRepository srHistoryRepository ;
56+ private final UploadSessionRepository uploadSessionRepository ;
57+ private final ChunkRepository chunkRepository ;
58+
59+
60+ private Path getUploadSessionDirectory (UUID uploadId ) {
61+ return Paths .get ("uploads" , uploadId .toString (), "chunks" );
62+ }
63+
64+ private void ensureDirectoryExists (Path uploadDirectory ) {
65+ try {
66+ Files .createDirectories (uploadDirectory );
67+ } catch (IOException e ) {
68+ throw new CommonBusinessException (ErrorCode .INTERNAL_SERVER_ERROR );
69+ }
70+ }
71+
72+ private Path getChunkFilePath (UUID uploadId , int chunkIndex ) {
73+ return Paths .get ("uploads" , uploadId .toString (), "chunks" , String .valueOf (chunkIndex ));
74+ }
75+
76+ private String combineChunks (UploadSession uploadSession )
77+ {
78+ String fileName = uploadSession .getFileName () + "." + uploadSession .getFileType ();
79+ Path finalFilePath = Paths .get ("uploads" , uploadSession .getId ().toString (), fileName );
80+ ensureDirectoryExists (finalFilePath .getParent ());
81+
82+ List <Chunk > sortedChunks = uploadSession .getChunks ().stream ()
83+ .sorted (Comparator .comparing (Chunk ::getChunkIndex ))
84+ .toList ();
85+
86+ try (FileOutputStream fos = new FileOutputStream (finalFilePath .toFile ());
87+ BufferedOutputStream bos = new BufferedOutputStream (fos )) {
88+
89+ for (Chunk chunk : sortedChunks ) {
90+ Path chunkPath = Paths .get (chunk .getFilePath ());
91+ byte [] chunkData = Files .readAllBytes (chunkPath );
92+ bos .write (chunkData );
93+ }
94+ bos .flush ();
95+ } catch (IOException e ) {
96+ log .error (e .toString ());
97+ throw new CommonBusinessException (ErrorCode .INTERNAL_SERVER_ERROR );
98+ }
99+
100+ for (Chunk chunk : sortedChunks ) {
101+ try {
102+ Files .deleteIfExists (Paths .get (chunk .getFilePath ()));
103+ } catch (IOException e ) {
104+ log .warn ("Failed to delete chunk file: {}" , chunk .getFilePath (), e );
105+ }
106+ }
107+
108+ return finalFilePath .toString ();
109+ }
110+
37111
38112 @ Transactional
39113 public CreateAttemptResponse createAttempt (Long userId , CreateAttemptRequest request ) {
@@ -98,4 +172,119 @@ public CreateAttemptResponse createAttempt(Long userId, CreateAttemptRequest req
98172 .currentSr (currentSr )
99173 .build ();
100174 }
175+
176+ @ Override
177+ @ Transactional (readOnly = true )
178+ public RouteMissionUploadStatusResponse getAttemptUploadStatus (Long attemptId ) {
179+ UserMissionAttempt attempt = attemptRepository .findById (attemptId ).orElseThrow (AttemptNotFoundException ::new );
180+
181+ var uploadSession = attempt .getUpload ();
182+
183+ if (uploadSession == null ) {
184+ throw new UploadSessionNotFoundException ();
185+ }
186+
187+ return RouteMissionUploadStatusResponse .toDto (uploadSession );
188+ }
189+
190+ @ Override
191+ @ Transactional
192+ public RouteMissionUploadSessionInitializeResponse initializeAttemptUploadSession (Long attemptId , RouteMissionUploadSessionInitializeRequest request ) {
193+ UserMissionAttempt attempt = attemptRepository .findById (attemptId ).orElseThrow (AttemptNotFoundException ::new );
194+
195+ if (attempt .getUpload () != null ) {
196+ throw new UploadSessionAlreadyExistsException ();
197+ }
198+
199+ UploadSession uploadSession = UploadSession .builder ().status (UploadStatus .NOT_STARTED ).chunkLength (request .getChunkLength ()).chunkSize (request .getChunkSize ()).fileSize (request .getFileSize ()).fileName (request .getFileName ()).fileType (request .getFileType ()).build ();
200+
201+ uploadSession = uploadSessionRepository .save (uploadSession );
202+
203+ attempt .setUpload (uploadSession );
204+
205+ attemptRepository .save (attempt );
206+
207+ return RouteMissionUploadSessionInitializeResponse .builder ().uploadId (uploadSession .getId ()).build ();
208+ }
209+
210+ @ Override
211+ @ Transactional
212+ public RouteMissionUploadChunkResponse uploadChunk (UUID uploadId , RouteMissionUploadChunkRequest request ) {
213+
214+ UploadSession uploadSession = uploadSessionRepository .findById (uploadId ).orElseThrow (UploadSessionNotFoundException ::new );
215+
216+ if (uploadSession .hasChunk (request .getIndex ())) {
217+ throw new UploadSessionChunkAlreadyExistsException ();
218+ }
219+
220+ try {
221+ Path sessionDir = getUploadSessionDirectory (uploadId );
222+ ensureDirectoryExists (sessionDir );
223+
224+ Path chunkFilePath = getChunkFilePath (uploadId , request .getIndex ());
225+ Files .write (chunkFilePath , request .getChunk ());
226+
227+ Chunk chunk = new Chunk ();
228+ chunk .setChunkIndex (request .getIndex ());
229+ chunk .setChunkSize ((long ) request .getChunk ().length );
230+ chunk .setCompleted (true );
231+ chunk .setFilePath (chunkFilePath .toString ());
232+
233+ uploadSession .addChunk (chunk );
234+
235+ String chunkName = String .format ("chunk_%d_%s" , request .getIndex (), uploadSession .getFileName ());
236+ chunk .setName (chunkName );
237+
238+ chunkRepository .save (chunk );
239+
240+
241+ } catch (IOException e ) {
242+ log .error ("Failed to store chunk {} for upload session {}" , request .getIndex (), uploadId , e );
243+ throw new UploadSessionChunkIncompleteException ();
244+ }
245+
246+ if (uploadSession .getStatus () == UploadStatus .NOT_STARTED ) {
247+ uploadSession .setStatus (UploadStatus .IN_PROGRESS );
248+ uploadSessionRepository .save (uploadSession );
249+ }
250+
251+
252+ return RouteMissionUploadChunkResponse .builder ()
253+ .index (request .getIndex ())
254+ .totalChunkReceived (uploadSession .getReceivedChunkCount ().intValue ())
255+ .totalChunkExpected (uploadSession .getChunkLength ())
256+ .build ();
257+ }
258+
259+ @ Override
260+ @ Transactional
261+ public RouteMissionUploadSessionFinalizeResponse finalizeUploadSession (UUID uploadId ) {
262+ UploadSession uploadSession = uploadSessionRepository .findById (uploadId ).orElseThrow (UploadSessionNotFoundException ::new );
263+
264+ long receivedChunks = uploadSession .getReceivedChunkCount ();
265+ int expectedChunks = uploadSession .getChunkLength ();
266+
267+ if (receivedChunks != expectedChunks ) {
268+ throw new UploadSessionChunkIncompleteException ();
269+ }
270+
271+ String finalVideoPath = combineChunks (uploadSession );
272+
273+ uploadSession .setStatus (UploadStatus .FINISHED );
274+ uploadSessionRepository .save (uploadSession );
275+
276+ log .info ("video saved in {}" , finalVideoPath );
277+
278+ // upload the video to NCP storage
279+
280+ UserMissionAttempt attempt = attemptRepository .findByUploadId (uploadId )
281+ .orElseThrow (AttemptNotFoundException ::new );
282+
283+ attemptRepository .save (attempt );
284+
285+ return RouteMissionUploadSessionFinalizeResponse .builder ()
286+ .fileName (uploadSession .getFileName ())
287+ .build ();
288+ }
289+
101290}
0 commit comments