Skip to main content

REST API for uploading file in chunks and merging them on the server-side.

Design Summary for Spring Boot REST API with Chunked File Upload:


This design outlines a Spring Boot REST API for receiving file chunks and merging them on the server-side. The API allows clients to upload large files in smaller chunks, improving upload reliability and efficiency.

Introduction:

  • The system provides a RESTful API for uploading and merging file chunks.
  • It addresses the challenge of uploading large files over HTTP efficiently.
System Architecture:

  • Utilizes a single Spring Boot application.
  • RESTful architecture for client-server interaction.
  • Relies on Spring's built-in components for handling HTTP requests.
Data Design:

  • Files are stored temporarily in memory.
  • Utilizes an in-memory map to store uploaded chunks temporarily.
  • Chunks are merged upon receiving all required parts.
  • User Interface Design:

This below design outlines a Spring Boot REST API that uses Apache Commons FileUpload to efficiently handle file uploads. It demonstrates a more advanced approach for handling file uploads and multipart data.

Introduction:

  • The system provides a Spring Boot REST API for handling file uploads using Apache Commons FileUpload.
  • It focuses on efficient handling of large file uploads and multipart requests.
System Architecture:

  • Similar to the previous design, but incorporates Apache Commons FileUpload for parsing multipart requests.
Data Design:

  • Utilizes Apache Commons FileUpload's FileItem to handle file data and form fields.
  • Handles multiple file uploads, even with the same filename, by generating unique identifiers.
  • User Interface Design:


Business Logic Design:

  1. Clients upload file chunks and metadata as form fields in the multipart request.
  2. Apache Commons FileUpload parses the request, extracting form fields and file data.
  3. Chunks are stored temporarily in memory using a Map with a generated UUID as the key.
  4. Upon receiving all chunks, they are merged into a complete file and stored in a configured directory.
  5. These design summaries provide an overview of the key aspects of each program's design, highlighting the architectural, data, user interface, and business logic considerations.

--------------------------

Apache Commons FileUpload can be used to efficiently handle file uploads in a Spring Boot application. Here's how you can integrate Apache Commons FileUpload into the example I provided earlier:

  1. Add the Apache Commons FileUpload dependency to your pom.xml or build.gradle:

    For Maven:

    xml
    <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.4</version> </dependency>

    For Gradle:

    groovy
    implementation 'commons-fileupload:commons-fileupload:1.4'
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory; 
import org.apache.commons.fileupload.FileUploadException; 
import org.apache.commons.fileupload.disk.DiskFileItemFactory; 
import org.apache.commons.fileupload.servlet.ServletFileUpload;
@RestController @RequestMapping("/api/files") 
public class FileUploadController { 
 
@Value("${upload.dir}") private String uploadDir;
private Map<String, ChunkData> fileChunksMap = new HashMap<>();

try { if (ServletFileUpload.isMultipartContent(request)) { FileItemFactory factory = new DiskFileItemFactory(); ServletFileUpload upload = new ServletFileUpload(factory); List<FileItem> items = upload.parseRequest(request); String fileId = null; int chunkNumber = 0; int totalChunks = 0; String filename = null
byte[] chunkData = null;
for (FileItem item : items) { if (item.isFormField()) { if ("chunkNumber".equals(item.getFieldName())) { chunkNumber = Integer.parseInt(item.getString()); } else if ("totalChunks".equals(item.getFieldName())) { totalChunks = Integer.parseInt(item.getString()); } else if ("filename".equals(item.getFieldName())) { filename = item.getString(); } else if ("fileId".equals(item.getFieldName())) { fileId = item.getString(); } } else { chunkData = item.get(); } } if (fileId == null) { fileId = filename + "-" + UUID.randomUUID(); 
}
ChunkData chunkDataObj = fileChunksMap.computeIfAbsent(fileId, k -> new ChunkData(filename, totalChunks)); chunkDataObj.addChunkData(chunkNumber, chunkData); if (chunkDataObj.isAllChunksReceived()) { mergeChunks(fileId, chunkDataObj); fileChunksMap.remove(fileId); } return ResponseEntity.ok("Chunk uploaded successfully"); } else { return ResponseEntity.badRequest().body("Not a multipart request"); } } catch (FileUploadException | IOException e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body("Failed to upload chunk: " + e.getMessage()); } } // ... 
}

And here's the ChunkData class:

java
public class ChunkData
private final String originalFilename; 
private final int totalChunks; 
private final List<byte[]> chunkList; 
public ChunkData(String originalFilename, int totalChunks) {
this.originalFilename = originalFilename; this.totalChunks = totalChunks; this.chunkList = new ArrayList<>(totalChunks); 
 }
public void addChunkData(int chunkNumber, byte[] chunkData) { chunkList.add(chunkNumber - 1, chunkData); } public boolean isAllChunksReceived() { return chunkList.size() == totalChunks; } public byte[] getMergedData() { return chunkList.stream().reduce((a, b) -> { byte[] combined = new byte[a.length + b.length]; System.arraycopy(a, 0, combined, 0, a.length); System.arraycopy(b, 0, combined, a.length, b.length); return combined; }).orElseThrow(); } public String getOriginalFilename() { return originalFilename; } }
--------------------------------------------------------


If you were to introduce a user interface to the scenario of uploading files in chunks, 
here's how the UI could divide the file and upload it in chunks:

File Selection:

The user interface would provide a way for users to select a file they want to upload.
After selecting the file, the UI can display the selected file's name and size.
Chunk Division:

The UI would automatically divide the selected file into smaller chunks. The chunk size can be predefined or configurable.
Each chunk would be of a manageable size, such as 1MB or 2MB, depending on the expected file sizes and network conditions.
Upload Progress:

The UI would display an upload progress bar or indicator to show the progress of uploading each chunk.
As each chunk is successfully uploaded, the progress bar would update to reflect the overall progress.
Chunk Upload:

When the user initiates the upload, the UI would begin sending the chunks one by one to the server using HTTP POST requests.
Each chunk would be sent as part of a multipart request, containing the chunk data and relevant metadata (chunk number, total chunks, original filename).
Pause and Resume:

The UI could offer features to pause and resume the upload process. This would be useful in case of interrupted network connections or if the user wants to pause the upload temporarily.
Retry Mechanism:

If a chunk upload fails due to a network error or any other issue, the UI could provide an option to retry uploading that specific chunk.
Completion and Notification:

Once all chunks are successfully uploaded and merged on the server, the UI would receive a completion notification.
The UI can display a success message to the user, indicating that the file upload was successful.
Remember that the division of the file into chunks and the upload process would be managed by the client-side code (in this case, the UI). The server-side code, as demonstrated earlier, would receive and process the individual chunks and merge them into a complete file. This approach helps improve reliability and upload speed, especially for larger files and in scenarios where network connectivity might be unstable.





How UI will assign chunk number

Assigning chunk numbers is a crucial part of dividing a file into chunks for upload. The UI needs to ensure that each chunk has a unique identifier, and the order of the chunks is preserved. Here's how the UI can assign chunk numbers:

Chunk Size Calculation:

Determine the size of each chunk. This could be a fixed size or a configurable value based on factors like network conditions and server capabilities.
Divide File into Chunks:

When the user selects a file for upload, the UI calculates the total number of chunks needed to divide the file.
For example, if the file size is 10MB and the chunk size is 2MB, the UI calculates that the file needs to be divided into 5 chunks.
Assign Chunk Numbers:

As the UI prepares each chunk for upload, it assigns a unique chunk number to it.
The chunk numbers could start from 1 and increment sequentially for each subsequent chunk.
Here's a simplified example in JavaScript to demonstrate how chunk numbers could be assigned:

javascript
Copy code
const chunkSize = 2 * 1024 * 1024; // 2MB chunk size
const selectedFile = ... // User-selected file object

// Function to upload a chunk
function uploadChunk(chunkData, chunkNumber, totalChunks) {
    // Use an HTTP library (e.g., fetch or Axios) to send the chunk to the server
    // Attach metadata like chunkNumber and totalChunks to the request
}

// Function to handle file upload
function uploadFile(file) {
    const fileSize = file.size;
    const totalChunks = Math.ceil(fileSize / chunkSize);

    for (let i = 0; i < totalChunks; i++) {
        const start = i * chunkSize;
        const end = Math.min(fileSize, start + chunkSize);
        const chunk = file.slice(start, end);

        // Assign the chunk number and upload the chunk
        uploadChunk(chunk, i + 1, totalChunks);
    }
}

// Call the uploadFile function with the selected file
uploadFile(selectedFile);
In this example, the uploadFile function divides the selected file into chunks of a specified size. The uploadChunk function is called for each chunk, and it includes the chunk number and total chunks in the HTTP request.

By assigning chunk numbers and sending them to the server, the UI ensures that the server can correctly reassemble the file from the received chunks in the correct order.

Comments

Popular posts from this blog

The relationship between Spring Data repositories methods and `Optional`

 In Spring Data repositories, the relationship between repository methods and `Optional` can vary depending on the specific use case and method signature. 1 . `Optional` as a return type:    When a repository method is defined to return a single entity or an optional result, you can use the `Optional` type as the return type. This indicates that the method may or may not find a matching entity in the database.    Example:    ```java    Optional<YourEntity> findById(Long id);    ```    In this case, the `findById` method is defined to return an `Optional<YourEntity>`. If the entity exists in the database, the `Optional` will contain the entity. Otherwise, it will be empty. 2. `Optional` as a parameter type:    You can also use `Optional` as a parameter type to indicate an optional input value for a repository method. This allows you to handle scenarios where a parameter may or may not be present.    Example:    ```java    List<YourEntity> findByCategory(Optional<Str

What is Sealed Classes in Java 17

Java 17, released in September 2021, introduced several new features and enhancements. While some of the most prominent changes in Java 17 have received widespread attention, here's a lesser-known unique feature: 1. Sealed Classes: Sealed classes are a feature that allows developers to control the extent to which other classes can inherit from them. By declaring a class as sealed, you can specify which other classes are allowed to subclass it. This feature enhances encapsulation and provides more control over class hierarchies and extensions. Sealed classes are declared using the `sealed` keyword, and the permitted subclasses are specified using the `permits` keyword. By default, a sealed class allows subclasses only from within the same module. However, you can also explicitly specify other modules that are permitted to subclass the sealed class. This feature enables developers to create more secure and maintainable code by restricting inheritance to a defined set of classes, prev