文件上传 小文件(图片、文档、视频)上传可以直接使用很多ui框架封装的上传组件,或者自己写一个input 上传,利用FormData 对象提交文件数据,后端使用spring提供的MultipartFile进行文件的接收,然后写入即可。但是对于比较大的文件,比如上传2G左右的文件(http上传),就需要将文件分片上传(file.slice()),否则中间http长时间连接可能会断掉。
分片上传 分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(我们称之为Part)来进行分别上传,上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件。
秒传 通俗的说,你把要上传的东西上传,服务器会先做MD5校验,如果服务器上有一样的东西,它就直接给你个新地址,其实你下载的都是服务器上的同一个文件,想要不秒传,其实只要让MD5改变,就是对文件本身做一下修改(改名字不行),例如一个文本文件,你多加几个字,MD5就变了,就不会秒传了.
断点续传 断点续传是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传或者下载未完成的部分,而没有必要从头开始上传或者下载。本文的断点续传主要是针对断点上传场景。
simple-uploader文档案例:https://github.com/simple-uploader/vue-uploader vue-simple-uploader文档案例:https://github.com/simple-uploader/Uploader/blob/develop/README_zh-CN.md
使用前必须要了解的概念和方法 相关概念 chunkNumber: 当前块的次序,第一个块是 1,注意不是从 0 开始的。 totalChunks: 文件被分成块的总数。 chunkSize: 分块大小,根据 totalSize 和这个值你就可以计算出总共的块数。注意最后一块的大小可能会比这个要大。 currentChunkSize: 当前块的大小,实际大小。 totalSize: 文件总大小。 identifier: 这个就是MD5值,每个文件的唯一标示。 filename: 文件名
相关方法 .upload() 开始或者继续上传。 .pause() 暂停上传。 .resume() 继续上传。 .cancel() 取消所有上传文件,文件会被移除掉。 .progress() 返回一个0-1的浮点数,当前上传进度。 .isUploading() 返回一个布尔值标示是否还有文件正在上传中。 .addFile(file) 添加一个原生的文件对象到上传列表中。 .removeFile(file) 从上传列表中移除一个指定的 Uploader.File 实例对象。
快速安装:
npm install --save spark-md5
在组件中使用spark-md5时先引入:
import SparkMD5 from 'spark-md5';
spark-md5提供了两个计算md5的方法。一种是用SparkMD5.hashBinary() 直接将整个文件的二进制码传入,直接返回文件的md5。这种方法对于小文件会比较有优势——简单而且速度超快。
另一种方法是利用js中File对象的slice()方法(File.prototype.slice)将文件分片后逐个传入spark.appendBinary()方法来计算、最后通过spark.end()方法输出md5。很显然,此方法就是我们前面讲到的分片计算md5。这种方法对于大文件和超大文件会非常有利,不容易出错,不占用大内存,并且能够提供计算的进度信息。
源码链接: https://gitee.com/KT1205529635/simple-uploader/tree/master/vue-uploader-master 本次参考了官方文档已经给位大佬的案例,根据自己的想法,实现了大文件的分片上传、断点续传及秒传 其中前端写了三个案例
VueUploader.vue https://gitee.com/KT1205529635/simple-uploader/blob/master/vue-uploader-master/src/views/VueUploader.vue#
根据插槽和钩子函数,实现自定义插件样式,也实现简单的下载。
DiyUpload1.vue https://gitee.com/KT1205529635/simple-uploader/blob/master/vue-uploader-master/src/views/DiyUpload1.vue#
在自定义uploader1上实现可上传文件夹
源码链接: https://gitee.com/KT1205529635/simple-uploader/tree/master/springboot-upload-master 后端实现简单粗暴:springboot + jpa + hutool + mysql 主要实现:
目录结构如下:
关键代码如下:
DROP TABLE IF EXISTS `file_chunk`;
CREATE TABLE `file_chunk` (
`id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`file_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件名',
`chunk_number` int(11) NULL DEFAULT NULL COMMENT '当前分片,从1开始',
`chunk_size` float NULL DEFAULT NULL COMMENT '分片大小',
`current_chunk_size` float NULL DEFAULT NULL COMMENT '当前分片大小',
`total_size` double(20, 0) NULL DEFAULT NULL COMMENT '文件总大小',
`total_chunk` int(11) NULL DEFAULT NULL COMMENT '总分片数',
`identifier` varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件标识',
`relative_path` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'md5校验码',
`createtime` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updatetime` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0),
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1529 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for tool_local_storage
-- ----------------------------
DROP TABLE IF EXISTS `tool_local_storage`;
CREATE TABLE `tool_local_storage` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`real_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件真实的名称',
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件名',
`suffix` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '后缀',
`path` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '路径',
`type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '类型',
`size` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '大小',
`identifier` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'md5校验码\r\n',
`create_by` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建者',
`update_by` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '更新者',
`createtime` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updatetime` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0),
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3360 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '文件存储' ROW_FORMAT = Compact;
package cn.kt.springbootuploadmaster.controller;
import cn.kt.springbootuploadmaster.domin.FileChunkParam;
import cn.kt.springbootuploadmaster.domin.ResultVO;
import cn.kt.springbootuploadmaster.service.FileChunkService;
import cn.kt.springbootuploadmaster.service.FileService;
import cn.kt.springbootuploadmaster.service.LocalStorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Created by tao.
* Date: 2022/6/29 11:56
* 描述:
*/
@RestController
@Slf4j
@RequestMapping("/api")
public class FileUploadController {
@Autowired
private FileService fileService;
@Autowired
private FileChunkService fileChunkService;
@Autowired
private LocalStorageService localStorageService;
@GetMapping("/upload")
public ResultVO<Map<String, Object>> checkUpload(FileChunkParam param) {
log.info("文件MD5:" + param.getIdentifier());
List<FileChunkParam> list = fileChunkService.findByMd5(param.getIdentifier());
Map<String, Object> data = new HashMap<>(1);
// 判断文件存不存在
if (list.size() == 0) {
data.put("uploaded", false);
return new ResultVO<>(200, "上传成功", data);
}
// 处理单文件
if (list.get(0).getTotalChunks() == 1) {
data.put("uploaded", true);
data.put("url", "");
return new ResultVO<Map<String, Object>>(200, "上传成功", data);
}
// 处理分片
int[] uploadedFiles = new int[list.size()];
int index = 0;
for (FileChunkParam fileChunkItem : list) {
uploadedFiles[index] = fileChunkItem.getChunkNumber();
index++;
}
data.put("uploadedChunks", uploadedFiles);
return new ResultVO<Map<String, Object>>(200, "上传成功", data);
}
@PostMapping("/upload")
public ResultVO chunkUpload(FileChunkParam param) {
log.info("上传文件:{}", param);
boolean flag = fileService.uploadFile(param);
if (!flag) {
return new ResultVO(211, "上传失败");
}
return new ResultVO(200, "上传成功");
}
@GetMapping(value = "/download/{md5}/{name}")
public void downloadbyname(HttpServletRequest request, HttpServletResponse response, @PathVariable String name, @PathVariable String md5) throws IOException {
localStorageService.downloadByName(name, md5, request, response);
}
}
package cn.kt.springbootuploadmaster.service;
import cn.kt.springbootuploadmaster.domin.FileChunkParam;
/**
* Created by tao.
* Date: 2022/6/29 11:22
* 描述:
*/
public interface FileService {
/**
* 上传文件
* @param param 参数
* @return
*/
boolean uploadFile(FileChunkParam param);
}
FileServiceImpl.java
package cn.kt.springbootuploadmaster.service.impl;
import cn.kt.springbootuploadmaster.domin.FileChunkParam;
import cn.kt.springbootuploadmaster.enums.MessageEnum;
import cn.kt.springbootuploadmaster.exception.BusinessException;
import cn.kt.springbootuploadmaster.repository.LocalStorageRepository;
import cn.kt.springbootuploadmaster.service.FileChunkService;
import cn.kt.springbootuploadmaster.service.FileService;
import cn.kt.springbootuploadmaster.service.LocalStorageService;
import cn.kt.springbootuploadmaster.utils.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import sun.misc.Cleaner;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.Method;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.security.AccessController;
import java.security.PrivilegedAction;
/**
* Created by tao.
* Date: 2022/6/29 11:22
* 描述:
*/
@Service("fileService")
@Slf4j
public class FileServiceImpl implements FileService {
/**
* 默认的分片大小:20MB
*/
public static final long DEFAULT_CHUNK_SIZE = 20 * 1024 * 1024;
@Value("${file.BASE_FILE_SAVE_PATH}")
private String BASE_FILE_SAVE_PATH;
@Autowired
private FileChunkService fileChunkService;
@Autowired
private LocalStorageService localStorageService;
@Override
public boolean uploadFile(FileChunkParam param) {
if (null == param.getFile()) {
throw new BusinessException(MessageEnum.UPLOAD_FILE_NOT_NULL);
}
// 判断目录是否存在,不存在则创建目录
File savePath = new File(BASE_FILE_SAVE_PATH);
if (!savePath.exists()) {
boolean flag = savePath.mkdirs();
if (!flag) {
log.error("保存目录创建失败");
return false;
}
}
// todo 处理文件夹上传(上传目录下新建上传的文件夹)
/*String relativePath = param.getRelativePath();
if (relativePath.contains("/") || relativePath.contains(File.separator)) {
String div = relativePath.contains(File.separator) ? File.separator : "/";
String tempPath = relativePath.substring(0, relativePath.lastIndexOf(div));
savePath = new File(BASE_FILE_SAVE_PATH + File.separator + tempPath);
if (!savePath.exists()) {
boolean flag = savePath.mkdirs();
if (!flag) {
log.error("保存目录创建失败");
return false;
}
}
}*/
// 这里可以使用 uuid 来指定文件名,上传完成后再重命名,File.separator指文件目录分割符,win上的"\",Linux上的"/"。
String fullFileName = savePath + File.separator + param.getFilename();
// 单文件上传
if (param.getTotalChunks() == 1) {
return uploadSingleFile(fullFileName, param);
}
// 分片上传,这里使用 uploadFileByRandomAccessFile 方法,也可以使用 uploadFileByMappedByteBuffer 方法上传
boolean flag = uploadFileByRandomAccessFile(fullFileName, param);
if (!flag) {
return false;
}
// 保存分片上传信息
fileChunkService.saveFileChunk(param);
return true;
}
private boolean uploadFileByRandomAccessFile(String resultFileName, FileChunkParam param) {
try (RandomAccessFile randomAccessFile = new RandomAccessFile(resultFileName, "rw")) {
// 分片大小必须和前端匹配,否则上传会导致文件损坏
long chunkSize = param.getChunkSize() == 0L ? DEFAULT_CHUNK_SIZE : param.getChunkSize().longValue();
// 偏移量
long offset = chunkSize * (param.getChunkNumber() - 1);
// 定位到该分片的偏移量
randomAccessFile.seek(offset);
// 写入
randomAccessFile.write(param.getFile().getBytes());
} catch (IOException e) {
log.error("文件上传失败:" + e);
return false;
}
return true;
}
private boolean uploadFileByMappedByteBuffer(String resultFileName, FileChunkParam param) {
// 分片上传
try (RandomAccessFile randomAccessFile = new RandomAccessFile(resultFileName, "rw");
FileChannel fileChannel = randomAccessFile.getChannel()) {
// 分片大小必须和前端匹配,否则上传会导致文件损坏
long chunkSize = param.getChunkSize() == 0L ? DEFAULT_CHUNK_SIZE : param.getChunkSize().longValue();
// 写入文件
long offset = chunkSize * (param.getChunkNumber() - 1);
byte[] fileBytes = param.getFile().getBytes();
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileBytes.length);
mappedByteBuffer.put(fileBytes);
// 释放
unmap(mappedByteBuffer);
} catch (IOException e) {
log.error("文件上传失败:" + e);
return false;
}
return true;
}
private boolean uploadSingleFile(String resultFileName, FileChunkParam param) {
File saveFile = new File(resultFileName);
try {
// 写入
param.getFile().transferTo(saveFile);
localStorageService.saveLocalStorage(param);
} catch (IOException e) {
log.error("文件上传失败:" + e);
return false;
}
return true;
}
/**
* 释放 MappedByteBuffer
* 在 MappedByteBuffer 释放后再对它进行读操作的话就会引发 jvm crash,在并发情况下很容易发生
* 正在释放时另一个线程正开始读取,于是 crash 就发生了。所以为了系统稳定性释放前一般需要检
* 查是否还有线程在读或写
* 来源:https://my.oschina.net/feichexia/blog/212318
*
* @param mappedByteBuffer mappedByteBuffer
*/
public static void unmap(final MappedByteBuffer mappedByteBuffer) {
try {
if (mappedByteBuffer == null) {
return;
}
mappedByteBuffer.force();
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
try {
Method getCleanerMethod = mappedByteBuffer.getClass()
.getMethod("cleaner");
getCleanerMethod.setAccessible(true);
Cleaner cleaner =
(Cleaner) getCleanerMethod
.invoke(mappedByteBuffer, new Object[0]);
cleaner.clean();
} catch (Exception e) {
log.error("MappedByteBuffer 释放失败:" + e);
}
System.out.println("clean MappedByteBuffer completed");
return null;
});
} catch (Exception e) {
log.error("unmap error:" + e);
}
}
}
其他实现的细节可自己查看源码,也可以根据自己的想法在这个demo中进行拓展。理清楚其中的大文件传输、秒传、断点续传后,自己开发一个小网盘也不是什么难事了 ^_^
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。