Hi! 我是小小,今天是本周的第五篇,小小又懒惰了,没有半夜更新,没办法,一大早起来开始更新。
前言
文件上传是网络开发中常见的环节,对于文件上传,有以下几种的上传方式,分别是秒传,断点续传,分片上传
秒传
什么是秒传
秒传就是服务器会先做MD5校验,根据校验的结果,如果服务器上有相同的文件,则会直接给出这个文件在服务器上的地址,如果没有则会按照正常的方式进行文件的上传。
实现核心逻辑
利用redis的set方法保存文件的上传状态,其中key为文件上传的md5,value为上传完成的标志位。当标志位为true的时候,上传完成,如果有相同的文件上传,进入秒传逻辑,如果标志位为false,那么文件还没有上传完成,此时需要再次调用set方法, 保存文件记录的路径,其中key为文件上传的md5加上一个前缀,value为块号文件的记录的路径
分片上传
什么是分片上传
分片上传,就是把文件进行分割开来进行上传
上传场景
大文件上传 网络环境不好的时候
断点续传
什么是断点续传
断点续传就是文件在下载或者上传的时候,如果临时中断相关操作,那么过一段时间重新开始相关操作,那么相关的操作仍旧可以继续延续。
应用场景
可以使用分片上传的场景,都可以使用断点续传
核心逻辑
需要记录文件上传的进度,在之后继续上传的时候,从原先的进度重新开始。为了避免客户端数据删除,导致重新上传的问题,服务器端也需要进行相关的记录。
实现步骤
方案一,常规步骤
把需要上传的文件,按照一定的分割规则,分割为相同大小的数据块 初始化一个分片上传任务,返回本次分片上传的唯一标识 按照一定的方式,发送相关的数据块。发送完成以后,服务端根据数据是否完整,重新进行相关的整合。
方案二:本文实现的步骤
前端,需要根据固定大小对文件进行分片,请求后端要带上分片序号和大小 服务器创建一个conf文件进行记录分块的位置,conf文件长度为总分片的数量,每上传一个分片,向conf文件写入一个127,那么美上传的就是0,已经上传的就是127. 服务器搜索相关的分片大小,算出初始位置,开始写入文件。
代码实现
前端采用百度提供的webuploader的插件,进行分片。具体链接如下所示:http://fex.baidu.com/webuploader/getting-started.html
后端用两种方式实现文件写入,一种是用RandomAccessFile,如果对RandomAccessFile不熟悉的朋友,可以查看如下链接: https://blog.csdn.net/dimudan2015/article/details/81910690
后端写入的核心代码
RandomAccessFile实现方式
-
@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实现方式
-
@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;
-
}
-
-
}
文件操作核心模板类代码
-
@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);
-
FileUploadDTO fileUploadDTO = this.saveAndFileUploadDTO(param.getFile().getOriginalFilename(), tmpFile);
-
return fileUploadDTO;
-
}
-
String md5 = FileMD5Util.getFileMD5(param.getFile());
-
-
Map<Integer, String>
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");
-
//把该分段标记为 true 表示完成
-
System.out.
println(
"set part " + param.getChunk() +
" complete");
-
//创建conf文件文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认0,已上传的就是Byte.MAX_VALUE 127
-
accessConfFile.setLength(param.getChunks());
-
accessConfFile.seek(param.getChunk());
-
accessConfFile.write(Byte.MAX_VALUE);
-
-
//completeList 检查是否全部完成,如果数组里是否全部都是127(全部分片都成功上传)
-
byte[] completeList = FileUtils.readFileToByteArray(confFile);
-
isComplete = Byte.MAX_VALUE;
-
for (
int i =
0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) {
-
//与运算, 如果有部分没有完成则 isComplete 不是 Byte.MAX_VALUE
-
isComplete = (
byte) (isComplete & completeList[i]);
-
System.out.
println(
"check part " + i +
" complete?:" + completeList[i]);
-
}
-
-
} catch (IOException e) {
-
log.error(e.getMessage(), e);
-
} finally {
-
FileUtil.
close(accessConfFile);
-
}
-
boolean isOk = setUploadProgress2Redis(param, uploadDirPath, fileName, confFile, isComplete);
-
return isOk;
-
}
-
-
/**
-
* 把上传进度信息存进redis
-
*/
-
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()) {
-
System.out
-
.
println(
"upload complete !!" + fileUploadDTO.isUploadComplete() +
" name=" + fileName);
-
//TODO 保存文件信息到数据库
-
-
}
-
-
} catch (Exception e) {
-
log.error(e.getMessage(), e);
-
} finally {
-
-
}
-
return fileUploadDTO;
-
}
-
/**
-
* 文件重命名
-
*
-
* @param toBeRenamed 将要修改名字的文件
-
* @param toFileNewName 新的名字
-
*/
-
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 p = toBeRenamed.getParent();
-
String filePath = p + 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;
-
}
-
}
关于作者
我是小小,双鱼座的程序猿,我们下期再见~bye
END
「 往期文章 」
扫描二维码
获取更多精彩
小明菜市场
来源:网络(侵删)
图片来源:网络(侵删)
点个在看你最好看
转载:https://blog.csdn.net/melovemingming/article/details/112691012