Implementing Large File Upload and Chunked Download with Spring Boot and WebUploader
This article demonstrates how to build a robust large‑file upload and download solution in Spring Boot by using Baidu's WebUploader for client‑side chunking, handling resumable uploads on the server, and implementing HTTP range requests for efficient chunked downloads.
WebUploader, developed by Baidu's WebFE team, is a modern file‑upload component that primarily uses HTML5 with optional Flash fallback.
Large File Upload
Implementation ideas:
Chunking: split a large file into smaller fragments based on a custom buffer size.
Resumable upload: each fragment receives a unique name; if a fragment already exists, it is skipped, and the last fragment triggers a merge.
Merge: after all fragments are uploaded, a thread‑pool task merges them in order, handling cases where the last fragment arrives before earlier ones.
Instant upload: if a file with the same MD5 already exists on the server, the upload is skipped.
Create a Spring Boot project and add the following 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>Test the environment with a simple controller:
@Controller
public class UploadController {
@RequestMapping("/up")
@ResponseBody
public String upload(HttpServletRequest request, HttpServletResponse response) {
return "搭建成功";
}
}Client‑side HTML and JavaScript (WebUploader configuration):
<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 handlers for logging, UI updates, md5 calculation, etc.
</script>Server‑side resumable upload handling (Spring MVC controller):
@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 chunk = null, chunks = null;
String name = null;
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())) {
chunk = Integer.parseInt(item.getString(utf8));
}
if ("chunks".equals(item.getFieldName())) {
chunks = Integer.parseInt(item.getString(utf8));
}
if ("name".equals(item.getFieldName())) {
name = item.getString(utf8);
}
}
}
for (FileItem item : items) {
if (!item.isFormField()) {
String tempFileName = name;
if (name != null && chunk != null) {
tempFileName = chunk + "_" + name;
}
File tempFile = new File(path, tempFileName);
if (!tempFile.exists()) {
item.write(tempFile);
}
}
}
if (chunk != null && chunk.intValue() == chunks.intValue() - 1) {
File finalFile = new File(path, name);
os = new BufferedOutputStream(new FileOutputStream(finalFile));
for (int i = 0; i < chunks; i++) {
File part = new File(path, i + "_" + name);
while (!part.exists()) {
Thread.sleep(100);
}
byte[] bytes = FileUtils.readFileToByteArray(part);
os.write(bytes);
os.flush();
part.delete();
}
os.flush();
response.getWriter().write("上传成功");
}
} finally {
if (os != null) {
os.close();
}
}
}
}Server‑side chunked download controller (supports HTTP Range):
@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));
BufferedOutputStream os = new BufferedOutputStream(response.getOutputStream());
BufferedInputStream is = new BufferedInputStream(new FileInputStream(file));
is.skip(pos);
byte[] buffer = new byte[1024];
int length;
while (sum < rangeLength) {
length = is.read(buffer, 0, (rangeLength - sum) <= buffer.length ? (int) (rangeLength - sum) : buffer.length);
sum += length;
os.write(buffer, 0, length);
}
os.flush();
}
}
}Client‑side chunked download implementation (uses multiple threads to request ranges and merges parts locally):
@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;
public 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");
InputStream is = response.getEntity().getContent();
FileOutputStream fos = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int ch;
while ((ch = is.read(buffer)) != -1) {
fos.write(buffer, 0, ch);
}
is.close();
fos.flush();
fos.close();
if (end - Long.valueOf(fSize) > 0) {
try { mergeFile(fName, page); } catch (Exception e) { e.printStackTrace(); }
}
return new FileInfo(Long.valueOf(fSize), fName);
}
private void mergeFile(String fName, long page) throws Exception {
File file = new File(down_path, fName);
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();
}
os.flush();
os.close();
}
class FileInfo { long fSize; String fName; public FileInfo(long fSize, String fName) { this.fSize = fSize; this.fName = fName; } }
}The article concludes by encouraging readers to share the tutorial, join the architecture community, and follow the author’s platform for more resources.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.