Implementing Chunked, Instant, and Resumable File Uploads in a Java Backend
This article explains how to design and implement instant (秒传), chunked (分片上传), and resumable (断点续传) file upload mechanisms in Java, covering the underlying MD5 deduplication logic, Redis state tracking, server‑side file handling with RandomAccessFile and MappedByteBuffer, and practical deployment considerations.
File upload is a common requirement, but handling large files efficiently requires more than a simple byte‑stream upload; otherwise users suffer from interruptions and must restart the upload.
Instant Upload (秒传)
The server first checks the MD5 of the incoming file; if a file with the same MD5 already exists, the server returns a reference to the existing file, avoiding a full upload. Changing the file content (not just the name) changes the MD5 and disables instant upload.
Core Logic
Redis stores the upload status using the file MD5 as the key. When the status flag is true, subsequent uploads of the same file trigger the instant‑upload path; otherwise the upload proceeds normally.
Chunked Upload (分片上传)
The file is split into equal‑size parts (chunks) on the client side and each part is uploaded separately. After all parts are received, the server merges them into the original file. This approach is suitable for large files and unreliable networks.
Workflow
Divide the file into fixed‑size chunks.
Initialize a chunked upload task and obtain a unique identifier.
Upload each chunk sequentially or in parallel.
After all chunks are uploaded, the server assembles the final file.
Resumable Upload (断点续传)
Resumable upload extends chunked upload by persisting the upload progress so that interrupted uploads can continue from the last successful chunk without restarting.
Implementation Steps
Client records the current chunk index and size.
Server creates a .conf file whose length equals the total number of chunks; each uploaded chunk writes a marker byte (127) at its position.
On resume, the server reads the .conf file to determine which chunks are already complete.
Backend Write Operations
The backend provides two main strategies for writing chunk data to disk.
RandomAccessFile Strategy
@UploadMode(mode = UploadModeEnum.RANDOM_ACCESS)
@Slf4j
public class RandomAccessUploadStrategy extends SliceUploadTemplate {
@Autowired
private FilePathUtil filePathUtil;
@Value("${upload.chunkSize}")
private long defaultChunkSize;
@Override
public boolean upload(FileUploadRequestDTO param) {
RandomAccessFile accessTmpFile = null;
try {
String uploadDirPath = filePathUtil.getPath(param);
File tmpFile = super.createTmpFile(param);
accessTmpFile = new RandomAccessFile(tmpFile, "rw");
long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024 : param.getChunkSize();
long offset = chunkSize * param.getChunk();
accessTmpFile.seek(offset);
accessTmpFile.write(param.getFile().getBytes());
boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath);
return isOk;
} catch (IOException e) {
log.error(e.getMessage(), e);
} finally {
FileUtil.close(accessTmpFile);
}
return false;
}
}MappedByteBuffer Strategy
@UploadMode(mode = UploadModeEnum.MAPPED_BYTEBUFFER)
@Slf4j
public class MappedByteBufferUploadStrategy extends SliceUploadTemplate {
@Autowired
private FilePathUtil filePathUtil;
@Value("${upload.chunkSize}")
private long defaultChunkSize;
@Override
public boolean upload(FileUploadRequestDTO param) {
RandomAccessFile tempRaf = null;
FileChannel fileChannel = null;
MappedByteBuffer mappedByteBuffer = null;
try {
String uploadDirPath = filePathUtil.getPath(param);
File tmpFile = super.createTmpFile(param);
tempRaf = new RandomAccessFile(tmpFile, "rw");
fileChannel = tempRaf.getChannel();
long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024 : param.getChunkSize();
long offset = chunkSize * param.getChunk();
byte[] fileData = param.getFile().getBytes();
mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);
mappedByteBuffer.put(fileData);
boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath);
return isOk;
} catch (IOException e) {
log.error(e.getMessage(), e);
} finally {
FileUtil.freedMappedByteBuffer(mappedByteBuffer);
FileUtil.close(fileChannel);
FileUtil.close(tempRaf);
}
return false;
}
}Template Class
@Slf4j
public abstract class SliceUploadTemplate implements SliceUploadStrategy {
public abstract boolean upload(FileUploadRequestDTO param);
protected File createTmpFile(FileUploadRequestDTO param) {
FilePathUtil filePathUtil = SpringContextHolder.getBean(FilePathUtil.class);
param.setPath(FileUtil.withoutHeadAndTailDiagonal(param.getPath()));
String fileName = param.getFile().getOriginalFilename();
String uploadDirPath = filePathUtil.getPath(param);
String tempFileName = fileName + "_tmp";
File tmpDir = new File(uploadDirPath);
File tmpFile = new File(uploadDirPath, tempFileName);
if (!tmpDir.exists()) {
tmpDir.mkdirs();
}
return tmpFile;
}
@Override
public FileUploadDTO sliceUpload(FileUploadRequestDTO param) {
boolean isOk = this.upload(param);
if (isOk) {
File tmpFile = this.createTmpFile(param);
return this.saveAndFileUploadDTO(param.getFile().getOriginalFilename(), tmpFile);
}
String md5 = FileMD5Util.getFileMD5(param.getFile());
Map
map = new HashMap<>();
map.put(param.getChunk(), md5);
return FileUploadDTO.builder().chunkMd5Info(map).build();
}
public boolean checkAndSetUploadProgress(FileUploadRequestDTO param, String uploadDirPath) {
String fileName = param.getFile().getOriginalFilename();
File confFile = new File(uploadDirPath, fileName + ".conf");
byte isComplete = 0;
RandomAccessFile accessConfFile = null;
try {
accessConfFile = new RandomAccessFile(confFile, "rw");
accessConfFile.setLength(param.getChunks());
accessConfFile.seek(param.getChunk());
accessConfFile.write(Byte.MAX_VALUE);
byte[] completeList = FileUtils.readFileToByteArray(confFile);
isComplete = Byte.MAX_VALUE;
for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) {
isComplete = (byte) (isComplete & completeList[i]);
}
} catch (IOException e) {
log.error(e.getMessage(), e);
} finally {
FileUtil.close(accessConfFile);
}
return setUploadProgress2Redis(param, uploadDirPath, fileName, confFile, isComplete);
}
private boolean setUploadProgress2Redis(FileUploadRequestDTO param, String uploadDirPath, String fileName, File confFile, byte isComplete) {
RedisUtil redisUtil = SpringContextHolder.getBean(RedisUtil.class);
if (isComplete == Byte.MAX_VALUE) {
redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "true");
redisUtil.del(FileConstant.FILE_MD5_KEY + param.getMd5());
confFile.delete();
return true;
} else {
if (!redisUtil.hHasKey(FileConstant.FILE_UPLOAD_STATUS, param.getMd5())) {
redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "false");
redisUtil.set(FileConstant.FILE_MD5_KEY + param.getMd5(), uploadDirPath + FileConstant.FILE_SEPARATORCHAR + fileName + ".conf");
}
return false;
}
}
public FileUploadDTO saveAndFileUploadDTO(String fileName, File tmpFile) {
FileUploadDTO fileUploadDTO = null;
try {
fileUploadDTO = renameFile(tmpFile, fileName);
if (fileUploadDTO.isUploadComplete()) {
// TODO: persist file metadata to database
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return fileUploadDTO;
}
private FileUploadDTO renameFile(File toBeRenamed, String toFileNewName) {
FileUploadDTO fileUploadDTO = new FileUploadDTO();
if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) {
log.info("File does not exist: {}", toBeRenamed.getName());
fileUploadDTO.setUploadComplete(false);
return fileUploadDTO;
}
String ext = FileUtil.getExtension(toFileNewName);
String parent = toBeRenamed.getParent();
String filePath = parent + FileConstant.FILE_SEPARATORCHAR + toFileNewName;
File newFile = new File(filePath);
boolean uploadFlag = toBeRenamed.renameTo(newFile);
fileUploadDTO.setMtime(DateUtil.getCurrentTimeStamp());
fileUploadDTO.setUploadComplete(uploadFlag);
fileUploadDTO.setPath(filePath);
fileUploadDTO.setSize(newFile.length());
fileUploadDTO.setFileExt(ext);
fileUploadDTO.setFileId(toFileNewName);
return fileUploadDTO;
}
}Summary
Successful chunked upload requires strict coordination of chunk size and index between client and server; otherwise uploads fail. A dedicated file server (e.g., FastDFS, HDFS) is typically needed, though object storage services like Alibaba OSS can be used for simple upload/download scenarios.
In a test environment (4 CPU, 8 GB RAM) uploading a 24 GB file took about 30 minutes, with most time spent on client‑side MD5 calculation; server‑side write speed was acceptable. For projects that do not need a custom file server, OSS provides a convenient alternative, but it is not ideal for heavy delete/modify workloads.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn 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.