Backend Development 16 min read

Implementing Large File Upload and Chunked Download with WebUploader and Spring Boot

This tutorial explains how to use Baidu's WebUploader component together with a Spring Boot backend to achieve large file uploads via chunking, breakpoint resume, merging, and instant MD5‑based upload, and also demonstrates a multithreaded chunked download solution with Java client code.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
Implementing Large File Upload and Chunked Download with WebUploader and Spring Boot

WebUploader is a simple modern file upload component developed by Baidu WebFE (FEX) that primarily uses HTML5 with Flash as a fallback.

The article outlines a strategy for handling large files by splitting them into chunks, supporting breakpoint resume, merging chunks on the server, and enabling instant upload when a file with the same MD5 already exists.

Implementation steps:

1. Create a Spring Boot project and add the required Maven dependencies.

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-autoconfigure</artifactId>
    </dependency>
    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.3.1</version>
    </dependency>
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.4</version>
    </dependency>
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpcore</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

Verify the environment with a simple test controller:

@Controller
public class UploadController {
    @RequestMapping("/up")
    @ResponseBody
    public String upload(HttpServletRequest request, HttpServletResponse response) {
        return "搭建成功";
    }
}

Frontend page markup and JavaScript to initialize WebUploader:

<body>
    <div id="upload-container">上传</div>
    <div id="upload-list"></div>
    <button id="picker">点击上传</button>
</body>
<script>
    $("#upload-container").click(function(){
        $("#picker").find('input').click();
    });
    var uploader = WebUploader.create({
        auto: true,
        swf: 'Uploader.swf',
        server: 'http://localhost:8080/upload',
        dnd: '#upload-container',
        pick: '#picker',
        multiple: true,
        chunked: true,
        threads: 20,
        method: 'POST',
        fileSizeLimit: 1024*1024*1024*10,
        fileSingleSizeLimit: 1024*1024*1024,
        fileVal: 'upload'
    });
    // event listeners for logging and UI updates omitted for brevity
</script>

Server‑side upload handling with chunk storage, breakpoint resume, and final merging:

@Controller
public class UploadController {
    private static final String utf8 = "utf-8";
    @RequestMapping("/up")
    @ResponseBody
    public void upload(HttpServletRequest request, HttpServletResponse response) throws Exception {
        response.setCharacterEncoding(utf8);
        Integer schunk = null; // current chunk index
        Integer schunks = null; // total chunks
        String name = null; // original file name
        String path = "D:\\file";
        BufferedOutputStream os = null;
        try {
            DiskFileItemFactory factory = new DiskFileItemFactory();
            factory.setSizeThreshold(1024);
            factory.setRepository(new File(path));
            ServletFileUpload upload = new ServletFileUpload(factory);
            upload.setFileSizeMax(5L*1024*1024*1024);
            upload.setSizeMax(10L*1024*1024*1024);
            List
items = upload.parseRequest(request);
            for (FileItem item : items) {
                if (item.isFormField()) {
                    if ("chunk".equals(item.getFieldName())) {
                        schunk = Integer.parseInt(item.getString(utf8));
                    }
                    if ("chunks".equals(item.getFieldName())) {
                        schunks = Integer.parseInt(item.getString(utf8));
                    }
                    if ("name".equals(item.getFieldName())) {
                        name = item.getString(utf8);
                    }
                }
            }
            for (FileItem item : items) {
                if (!item.isFormField()) {
                    String temFileName = name;
                    if (name != null && schunk != null) {
                        temFileName = schunk + "_" + name;
                    }
                    File temfile = new File(path, temFileName);
                    if (!temfile.exists()) {
                        item.write(temfile);
                    }
                }
            }
            // Merge when last chunk arrives
            if (schunk != null && schunk.intValue() == schunks.intValue() - 1) {
                File tempFile = new File(path, name);
                os = new BufferedOutputStream(new FileOutputStream(tempFile));
                for (int i = 0; i < schunks; i++) {
                    File file = new File(path, i + "_" + name);
                    while (!file.exists()) {
                        Thread.sleep(100);
                    }
                    byte[] bytes = FileUtils.readFileToByteArray(file);
                    os.write(bytes);
                    os.flush();
                    file.delete();
                }
                os.flush();
                response.getWriter().write("上传成功");
            }
        } finally {
            if (os != null) os.close();
        }
    }
}

Server‑side chunked download endpoint that respects the HTTP Range header:

@Controller
public class DownLoadController {
    private static final String utf8 = "utf-8";
    @RequestMapping("/down")
    public void downLoadFile(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setCharacterEncoding(utf8);
        File file = new File("D:\\File\\a.mp4");
        long fSize = file.length();
        response.setContentType("application/x-download");
        String fileName = URLEncoder.encode(file.getName(), utf8);
        response.addHeader("Content-Disposition", "attachment;filename=" + fileName);
        response.setHeader("Accept-Range", "bytes");
        response.setHeader("fSize", String.valueOf(fSize));
        response.setHeader("fName", fileName);
        long pos = 0, last = fSize - 1, sum = 0;
        if (request.getHeader("Range") != null) {
            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
            String numRange = request.getHeader("Range").replaceAll("bytes=", "");
            String[] strRange = numRange.split("-");
            if (strRange.length == 2) {
                pos = Long.parseLong(strRange[0].trim());
                last = Long.parseLong(strRange[1].trim());
                if (last > fSize - 1) last = fSize - 1;
            } else {
                pos = Long.parseLong(numRange.replaceAll("-", "").trim());
            }
        }
        long rangeLength = last - pos + 1;
        String contentRange = new StringBuffer("bytes").append(pos).append("-").append(last).append("/").append(fSize).toString();
        response.setHeader("Content-Range", contentRange);
        response.setHeader("Content-Lenght", String.valueOf(rangeLength));
        try (BufferedOutputStream os = new BufferedOutputStream(response.getOutputStream());
             BufferedInputStream is = new BufferedInputStream(new FileInputStream(file))) {
            is.skip(pos);
            byte[] buffer = new byte[1024];
            int length = 0;
            long read = 0;
            while (read < rangeLength) {
                int toRead = (int) Math.min(buffer.length, rangeLength - read);
                length = is.read(buffer, 0, toRead);
                if (length == -1) break;
                os.write(buffer, 0, length);
                read += length;
            }
        }
    }
}

Java client that performs multithreaded chunked download, stores each chunk, and merges them after the final chunk:

@RestController
public class DownloadClient {
    private static final long per_page = 1024L*1024L*50L;
    private static final String down_path = "D:\\File";
    ExecutorService pool = Executors.newFixedThreadPool(10);

    @RequestMapping("/downloadFile")
    public String downloadFile() throws IOException {
        FileInfo fileInfo = download(0, 10, -1, null);
        if (fileInfo != null) {
            long pages = fileInfo.fSize / per_page;
            for (int i = 0; i <= pages; i++) {
                pool.submit(new Download(i*per_page, (i+1)*per_page-1, i, fileInfo.fName));
            }
        }
        return "成功";
    }

    class Download implements Runnable {
        long start, end, page; String fName;
        Download(long start, long end, long page, String fName) { this.start=start; this.end=end; this.page=page; this.fName=fName; }
        @Override
        public void run() {
            try { download(start, end, page, fName); } catch (IOException e) { e.printStackTrace(); }
        }
    }

    private FileInfo download(long start, long end, long page, String fName) throws IOException {
        File file = new File(down_path, page + "-" + fName);
        if (file.exists() && page != -1 && file.length() == per_page) return null;
        HttpClient client = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet("http://127.0.0.1:8080/down");
        httpGet.setHeader("Range", "bytes=" + start + "-" + end);
        HttpResponse response = client.execute(httpGet);
        String fSize = response.getFirstHeader("fSize").getValue();
        fName = URLDecoder.decode(response.getFirstHeader("fName").getValue(), "utf-8");
        HttpEntity entity = response.getEntity();
        try (InputStream is = entity.getContent();
             FileOutputStream fos = new FileOutputStream(file)) {
            byte[] buffer = new byte[1024];
            int ch;
            while ((ch = is.read(buffer)) != -1) {
                fos.write(buffer, 0, ch);
            }
        }
        if (end - Long.valueOf(fSize) > 0) {
            mergeFile(fName, page);
        }
        return new FileInfo(Long.valueOf(fSize), fName);
    }

    private void mergeFile(String fName, long page) throws Exception {
        File file = new File(down_path, fName);
        try (BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
            for (int i = 0; i <= page; i++) {
                File tempFile = new File(down_path, i + "-" + fName);
                while (!tempFile.exists() || (i != page && tempFile.length() < per_page)) {
                    Thread.sleep(100);
                }
                byte[] bytes = FileUtils.readFileToByteArray(tempFile);
                os.write(bytes);
                os.flush();
                tempFile.delete();
            }
        }
    }

    class FileInfo { long fSize; String fName; FileInfo(long fSize, String fName){ this.fSize=fSize; this.fName=fName; } }
}

The article concludes by encouraging readers to share the content, join the community group, and explore additional resources linked at the end.

JavaBackend DevelopmentSpring Bootfile downloadChunked UploadWebUploader
Java Architect Essentials
Written by

Java Architect Essentials

Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.