diff --git a/deploy-chunked-simple.sh b/deploy-chunked-simple.sh index 1dccaeb..4f9fe4c 100644 --- a/deploy-chunked-simple.sh +++ b/deploy-chunked-simple.sh @@ -208,14 +208,14 @@ upload_file_in_chunks() { if [ "$DEBUG_MODE" = "true" ]; then echo "" echo "DEBUG: SESSION_ID='$SESSION_ID'" - echo "DEBUG: Uploading to: $API_BASE/upload/chunk?uploadSessionId=$SESSION_ID&fileType=$file_type&chunkIndex=$chunk_index&totalChunks=$total_chunks&fileName=$file_name" + echo "DEBUG: Uploading chunk to: $API_BASE/upload/chunk?uploadSessionId=$SESSION_ID&fileType=$file_type&chunkIndex=$chunk_index&totalChunks=$total_chunks&fileName=$file_name" fi - # Send metadata as query params (for Java proxy to capture) and only file as multipart - # Add explicit headers that nginx might require + # Send metadata as query params and chunk file as raw binary body + # This works with Java proxy that forwards body as-is RESPONSE=$(curl -s -X POST "$API_BASE/upload/chunk?uploadSessionId=$SESSION_ID&fileType=$file_type&chunkIndex=$chunk_index&totalChunks=$total_chunks&fileName=$file_name" \ - -H "X-Forwarded-For: 127.0.0.1" \ - -F "chunk=@$chunk_file") + -H "Content-Type: application/octet-stream" \ + --data-binary "@$chunk_file") if [ "$DEBUG_MODE" = "true" ]; then echo "DEBUG: Chunk response: $RESPONSE" diff --git a/src/main/java/com/cfdeployer/controller/CfDeployController.java b/src/main/java/com/cfdeployer/controller/CfDeployController.java index d7b02c9..9a0a9ea 100644 --- a/src/main/java/com/cfdeployer/controller/CfDeployController.java +++ b/src/main/java/com/cfdeployer/controller/CfDeployController.java @@ -182,6 +182,39 @@ public class CfDeployController { } } + @PostMapping(value = "/upload/chunk", consumes = "application/octet-stream") + public ResponseEntity uploadChunkRaw( + @RequestParam("uploadSessionId") String uploadSessionId, + @RequestParam("fileType") String fileType, + @RequestParam("chunkIndex") Integer chunkIndex, + @RequestParam("totalChunks") Integer totalChunks, + @RequestParam(value = "fileName", required = false) String fileName, + @RequestBody byte[] chunkData) { + try { + log.debug("Receiving raw chunk {}/{} for session: {}, fileType: {} ({} bytes)", + chunkIndex + 1, totalChunks, uploadSessionId, fileType, chunkData.length); + + // Validate file type + if (!fileType.equals("jarFile") && !fileType.equals("manifest")) { + throw new IllegalArgumentException("Invalid file type. Must be 'jarFile' or 'manifest'"); + } + + chunkedUploadService.uploadChunkRaw(uploadSessionId, fileType, fileName, + chunkIndex, totalChunks, chunkData); + + var session = chunkedUploadService.getSession(uploadSessionId); + var fileState = session.getFileStates().get(fileType); + + return ResponseEntity.ok(ChunkUploadResponse.success( + uploadSessionId, fileType, chunkIndex, totalChunks, + fileState.getReceivedChunkCount())); + } catch (Exception e) { + log.error("Error uploading raw chunk", e); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(ChunkUploadResponse.failure("Failed to upload chunk: " + e.getMessage())); + } + } + @PostMapping("/upload/finalize") public ResponseEntity finalizeUpload( @RequestParam("uploadSessionId") String uploadSessionId, diff --git a/src/main/java/com/cfdeployer/service/ChunkedUploadService.java b/src/main/java/com/cfdeployer/service/ChunkedUploadService.java index bee762e..dbe96a7 100644 --- a/src/main/java/com/cfdeployer/service/ChunkedUploadService.java +++ b/src/main/java/com/cfdeployer/service/ChunkedUploadService.java @@ -101,6 +101,62 @@ public class ChunkedUploadService { } } + public synchronized void uploadChunkRaw(String sessionId, String fileType, String fileName, + int chunkIndex, int totalChunks, byte[] chunkData) throws IOException { + UploadSession session = activeSessions.get(sessionId); + if (session == null) { + throw new IllegalArgumentException("Upload session not found or expired: " + sessionId); + } + + session.updateLastAccessed(); + + // Get or create file upload state + UploadSession.FileUploadState fileState = session.getFileStates() + .computeIfAbsent(fileType, k -> { + String targetFileName = fileType.equals("manifest") ? "manifest.yml" : fileName; + Path targetPath = session.getWorkingDirectory().resolve(targetFileName); + return new UploadSession.FileUploadState(fileName, totalChunks, targetPath); + }); + + // Validate total chunks consistency + if (fileState.getTotalChunks() != totalChunks) { + throw new IllegalArgumentException( + String.format("Total chunks mismatch for %s: expected %d, got %d", + fileType, fileState.getTotalChunks(), totalChunks)); + } + + // Write chunk to file using sequential append mode + // This supports variable chunk sizes - chunks MUST be uploaded in order (0, 1, 2, ...) + Path targetPath = fileState.getTargetPath(); + + // Verify chunks are uploaded in order + if (chunkIndex != fileState.getReceivedChunkCount()) { + throw new IllegalArgumentException( + String.format("Chunks must be uploaded in order. Expected chunk %d but received %d", + fileState.getReceivedChunkCount(), chunkIndex)); + } + + try (var outputStream = Files.newOutputStream(targetPath, + java.nio.file.StandardOpenOption.CREATE, + java.nio.file.StandardOpenOption.APPEND)) { + + // Write raw byte array directly + outputStream.write(chunkData); + + log.debug("Appended raw chunk {} ({} bytes) to {}", + chunkIndex, chunkData.length, targetPath.getFileName()); + } + + fileState.markChunkReceived(chunkIndex); + log.info("Session {}: Received raw chunk {}/{} for {} ({} bytes)", + sessionId, chunkIndex + 1, totalChunks, fileType, chunkData.length); + + if (fileState.isComplete()) { + log.info("Session {}: File {} upload completed ({} chunks)", + sessionId, fileType, totalChunks); + } + } + public UploadSession getSession(String sessionId) { UploadSession session = activeSessions.get(sessionId); if (session != null) {