Learn how to upload folders in a spring boot application with this article.

HTMLInputElement.webkitdirectory

In the browser, we usually select the file to be uploaded by the <input type="file"/> tag. By default, it can only select one file or multiple files, not the whole folder directly.

If the <input/> tag has an attribute called webkitdirectory, the user can select the entire folder and the browser will upload all the files under the folder to the server at once.

The HTMLInputElement.webkitdirectory is a property that reflects the webkitdirectory HTML attribute and indicates that the <input> element should let the user select directories instead of files. When a directory is selected, the directory and its entire hierarchy of contents are included in the set of selected items. The selected file system entries can be obtained using the webkitEntries property.

https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory

HTML

Create an index.html with the following content.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!DOCTYPE html>
<html>
    <head>
    <meta charset="UTF-8">
    <title>Uploading a folder to the server</title>
    </head>
    <body>
        <form action="/upload" method="POST" enctype="multipart/form-data">
            <input type="file" name="file" multiple webkitdirectory/>
            <input type="submit"/>
        </form>
    </body>
</html>

The browser will submit the relative path of the file in the folder as the file name to the server, and the server can create the file locally based on this relative path, thus ensuring consistency with the directory structure of the client.

Controller

The server needs to write each file to a local folder according to the client’s file layout.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


/**
 * 
 * @author KevinBlandy
 *
 */
@RestController
@RequestMapping("/upload")
public class UploadController {

    private static final Logger LOGGER = LoggerFactory.getLogger(UploadController.class);

    /**
    * By default, the public folder in the working directory is a static resource directory, which can be accessed directly by the client.
    */
    private static final Path PUBLIC_DIR = Paths.get(System.getProperty("user.dir"), "public");

    @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public String upload (HttpServletRequest request) throws IOException, ServletException{
        
        for (var part : request.getParts()) {
            
            // The name of the file is the relative path of the file in the client folder
            // Convert the client path separator to the server file path separator.
            String fileName = FilenameUtils.separatorsToSystem(part.getSubmittedFileName());
            
            // Resolve the absolute path to the file based on the public folder
            Path file = PUBLIC_DIR.resolve(fileName);
            
            // Try to create the folder where the file is located
            if (Files.notExists(file.getParent())) {
                Files.createDirectories(file.getParent());
            }
            
            // Write data to file
            try (var inputStream = part.getInputStream()){
                Files.copy(inputStream, file, StandardCopyOption.REPLACE_EXISTING);
            }
            
            LOGGER.info("write file: [{}] {}", part.getSize(), file);
        }
        return "ok";
    }
}

FilenameUtils is a tool class from commons-io to unify file separators between different operating systems.

1
2
3
4
5
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>

Also you may need to configure some configuration in application.yml about file uploads.

1
2
3
4
5
6
7
8
spring:
 servlet:
   multipart:
     enabled: true
     max-file-size: -1 # No file size limit
     max-request-size: -1  # No limit on request size
     location: ${java.io.tmpdir}
     file-size-threshold: 10KB

Testing

Put index.html in the public folder of the spring boot application and start the application.

Open your browser and visit: http://localhost:8080

Click the Choose Files button to select a folder. A warning dialog box will pop up in your browser telling you that this uploads all the files in the folder.

Select the folder to upload

After clicking the Upload button, you can see the number of files in the folder next to the Choose files button.

Select the folder to upload

The folder foo that is uploaded in this demo has the following structure.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
C:\Users\KevinBlandy\Desktop\foo>tree /F
...
C:.
│  1.txt
│  2.txt
└─bar
    │  3.txt
    └─temp
        │  4.txt
        └─spring
                main.java

Click the Submit button to submit to the server. And observe the log output from the server’s console.

1
2
3
4
5
2022-11-29 13:16:58.249  INFO 6312 --- [nio-8080-exec-2] i.s.web.controller.UploadController      : write file: [1] C:\eclipse\eclipse-jee-2022-09-R-win32-x86_64\project\springcloud-cache\public\foo\1.txt
2022-11-29 13:16:58.262  INFO 6312 --- [nio-8080-exec-2] i.s.web.controller.UploadController      : write file: [6] C:\eclipse\eclipse-jee-2022-09-R-win32-x86_64\project\springcloud-cache\public\foo\2.txt
2022-11-29 13:16:58.276  INFO 6312 --- [nio-8080-exec-2] i.s.web.controller.UploadController      : write file: [0] C:\eclipse\eclipse-jee-2022-09-R-win32-x86_64\project\springcloud-cache\public\foo\bar\3.txt
2022-11-29 13:16:58.288  INFO 6312 --- [nio-8080-exec-2] i.s.web.controller.UploadController      : write file: [4] C:\eclipse\eclipse-jee-2022-09-R-win32-x86_64\project\springcloud-cache\public\foo\bar\temp\4.txt
2022-11-29 13:16:58.290  INFO 6312 --- [nio-8080-exec-2] i.s.web.controller.UploadController      : write file: [2081] C:\eclipse\eclipse-jee-2022-09-R-win32-x86_64\project\springcloud-cache\public\foo\bar\temp\spring\main.java

As you can see from the logs, the server has successfully written all the files to the expected directory according to the layout of the client files.

Check the folder uploaded by the client to the server, everything is OK.

Folders uploaded by the client to the server

Asynchronous Upload

You can also use Javascript to upload a folder asynchronously, and you can do things like filter the uploaded files.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
document.querySelector('input[name="file"]').addEventListener('change', e => {

    let formData = new FormData();

    // Iterate through every file in the folder.
    for (let file of e.target.files){
        // The file.webkitRelativePath property is the relative path of the file in the directory.
        // The browser will submit file.webkitRelativePath as the name of the file to the server.
        formData.append("file", file);
    }

    fetch('/upload', {
        method: 'POST',
        body: formData
    }).then(response => {
        if(response.ok){
            response.text().then(payload => {
                console.log(payload);
            });
        } 
    }).catch(error => {
        console.error(error);
    });
});