springboot+vue+elementui大文件分片上传

工具类方法:

 /**
     * 大文件分片上传
     * @param fileName 文件名
     * @param file       文件
     * @param fileKey    文件key
     * @param shardIndex 当前分片下标
     * @param shardTotal 分片总量
     */
    public static void bigUpload(String fileName,MultipartFile file, String fileKey, Long shardIndex, Long shardTotal) throws Exception {
        String fileDir = getDefaultBaseDir() +"/"+ DateUtils.datePath() + "/" + fileKey;
        File dir=new File(fileDir);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        File dest = new File(fileDir+"/" + fileKey + "." + shardIndex);
        // 分片文件保存到文件目录
        file.transferTo(dest);
        if (shardIndex == shardTotal) {
            merge(fileName, shardTotal, fileKey);
        }
    }

    /**
     * 分片大文件上传,文件合并
     *
     * @param fileName   文件名比如123.mp4
     * @param shardTotal 分片总量
     * @param fileKey    文件key
     * @throws Exception
     */
    private static void merge(String fileName, Long shardTotal, String fileKey) throws Exception {
        String mergeFilePath = getDefaultBaseDir()+"/" + DateUtils.datePath() + "/" + fileKey + "/" + fileName;
        File newFile = new File(mergeFilePath);
        if (newFile.exists()) {
            newFile.delete();
        }
        FileOutputStream outputStream = new FileOutputStream(newFile, true);//文件追加写入
        FileInputStream fileInputStream = null;//分片文件
        byte[] byt = new byte[10 * 1024 * 1024];
        int len;
        try {
            for (int i = 0; i < shardTotal; i++) {
                // 读取第i个分片
                String shardFilePath = getDefaultBaseDir() +"/"+ DateUtils.datePath() + "/" + fileKey + "/" + fileKey + "." + (i + 1);
                fileInputStream = new FileInputStream(shardFilePath);
                while ((len = fileInputStream.read(byt)) != -1) {
                    outputStream.write(byt, 0, len);//一直追加到合并的新文件中
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fileInputStream != null) {
                    fileInputStream.close();
                }
                outputStream.close();
                System.gc();
            } catch (Exception e) {
            }
        }
    }

controller需要实现两个接口:上传文件和分片文件状态检查。

  @GetMapping("/check")
    public AjaxResult check(@RequestParam String key) {
        PanoramicFileTb fileTb = panoramicFileTbService.selectLatestIndex(key);
        log.info("检查分片:{}", key);
        return AjaxResult.success(fileTb);

    }
/**
     * 大文件上传
     *
     * @param file
     * @param filePojo
     * @return
     * @throws Exception
     */
    @PreAuthorize("@ss.hasPermi('system:BusinessFile:add')")
    @Log(title = "文件记录", businessType = BusinessType.INSERT)
    @PostMapping("/big-upload")
    public AjaxResult bigUpload(@RequestParam(value = "file") MultipartFile file,
                                FilePojoVo filePojo) throws Exception {
        FileUploadUtils.bigUpload(filePojo.getFileName(),file, filePojo.getKey(), filePojo.getShardIndex(), filePojo.getShardTotal());
        log.info("文件分片 {} 保存完成", filePojo.getShardIndex());
        PanoramicFileTb fileTb = PanoramicFileTb.builder()
                .fKey(filePojo.getKey())
                .fIndex(filePojo.getShardIndex())
                .fTotal(filePojo.getShardTotal())
                .fName(filePojo.getFileName())
                .build();
        if (panoramicFileTbService.isNotExist(filePojo.getKey())) {
            panoramicFileTbService.saveFile(fileTb);
        } else {
            panoramicFileTbService.UpdateFile(fileTb);
        }
        return AjaxResult.success();
    }
public class FilePojoVo {

    private String key;
    private String fileName;
    private Long shardIndex;
    private Long shardSize;
    private Long shardTotal;
    private Long size;
    private String suffix;

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public String getFileName() {
        return fileName;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public Long getShardIndex() {
        return shardIndex;
    }

    public void setShardIndex(Long shardIndex) {
        this.shardIndex = shardIndex;
    }

    public Long getShardSize() {
        return shardSize;
    }

    public void setShardSize(Long shardSize) {
        this.shardSize = shardSize;
    }

    public Long getShardTotal() {
        return shardTotal;
    }

    public void setShardTotal(Long shardTotal) {
        this.shardTotal = shardTotal;
    }

    public Long getSize() {
        return size;
    }

    public void setSize(Long size) {
        this.size = size;
    }

    public String getSuffix() {
        return suffix;
    }

    public void setSuffix(String suffix) {
        this.suffix = suffix;
    }
}
FilePojoVo
@Builder
public class PanoramicFileTb extends BaseEntity
{
    private static final long serialVersionUID = 1L;

    /** $column.columnComment */
    private Integer id;

    /** 文件唯一标识 */
    @Excel(name = "文件唯一标识")
    private String fKey;

    /** 第几个分片 */
    @Excel(name = "第几个分片")
    private Long fIndex;

    /** 共有几个分片 */
    @Excel(name = "共有几个分片")
    private Long fTotal;

    /** 文件名称,后面可以返回出去 */
    @Excel(name = "文件名称,后面可以返回出去")
    private String fName;

    public void setId(Integer id) 
    {
        this.id = id;
    }

    public Integer getId() 
    {
        return id;
    }
    public void setfKey(String fKey) 
    {
        this.fKey = fKey;
    }

    public String getfKey() 
    {
        return fKey;
    }
    public void setfIndex(Long fIndex) 
    {
        this.fIndex = fIndex;
    }

    public Long getfIndex() 
    {
        return fIndex;
    }
    public void setfTotal(Long fTotal) 
    {
        this.fTotal = fTotal;
    }

    public Long getfTotal() 
    {
        return fTotal;
    }
    public void setfName(String fName) 
    {
        this.fName = fName;
    }

    public String getfName() 
    {
        return fName;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
            .append("id", getId())
            .append("fKey", getfKey())
            .append("fIndex", getfIndex())
            .append("fTotal", getfTotal())
            .append("fName", getfName())
            .toString();
    }
}
PanoramicFileTb

上面两个实体类,FilePojoVo是必须的,需要和页面做数据交互,PanoramicFileTb是非必须的,可以选择把FilePojoVo存储到数据库、内存、redis等都可以,只要能验证到对应文件的md5值是否已存在。我这里存到数据库是因为可以做急速上传,已上传的文件md5值可能会一样,加上其他验证方式,这样已上传过的文件再上传其实就不需要再传了。

下面附上对应的service方法,其中mapper方法无非就是用key去查数据或更新数据。就不提供出来了:

@Override
    public void saveFile(PanoramicFileTb fileTb) {
        panoramicFileTbMapper.insertPanoramicFileTb(fileTb);
    }

    @Override
    public void UpdateFile(PanoramicFileTb fileTb) {
        panoramicFileTbMapper.UpdateFile(fileTb);
    }

    @Override
    public boolean isNotExist(String key){
        Integer id = panoramicFileTbMapper.isExist(key);
        if (ObjectUtils.isEmpty(id)) {
            return true;
        }
        return false;
    }

    @Override
    public PanoramicFileTb selectLatestIndex(String key) {
        PanoramicFileTb fileTb = panoramicFileTbMapper.selectLatestIndex(key);
        if (ObjectUtils.isEmpty(fileTb)) {
            fileTb = PanoramicFileTb.builder().fKey(key).fIndex(-1L).fName("").build();
        }
        return fileTb;
    }

以上就是后台相关代码,可以根据自己的需求扩展功能。

下面是前端代码,需要npm install --save js-md5安装,引用import md5 from 'js-md5';

<template>
    <div class="file-upload">
        <h1>大文件分片上传、极速秒传</h1>
        <div class="file-upload-el">

            <el-upload
                    class="upload-demo"
                    drag
                    ref="upload"
                    :limit=1
                    :action="actionUrl"
                    :on-exceed="handleExceed"
                    :http-request="handUpLoad"
                    :auto-upload="false"
            >
                <i class="el-icon-upload"></i>
                <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
            </el-upload>
            <el-button style="margin-left: 10px;" size="small" type="success" @click="submitUpload">上传到服务器</el-button>
        </div>
        <div>
            <!-- autoplay-->
            <el-card class="v-box-card">
                <video :src="videoUrl"
                       controls
                       autoplay
                       class="video"
                       width="100%">

                </video>
            </el-card>
        </div>
    </div>
</template>

<script>
    export default {
        name: "FileUpload",
        data() {

            return {
                actionUrl: 'http://localhost:8098/upload',//上传的后台地址
                shardSize: 10 * 1024 * 1024,
                videoUrl: ''

            };
        },
        methods: {

            handleExceed(files, fileList) {
                this.$message.warning(`当前限制选择 1个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.length} 个文件`);
            },
            submitUpload() {
                this.$refs.upload.submit();
            },
            async check(key) {
                var res = await this.$http.get('/check', {
                    params: {'key': key}
                })
                let resData = res.data;
                return resData.data;
            },
            async recursionUpload(param, file) {
                //FormData私有类对象,访问不到,可以通过get判断值是否传进去
                let _this = this;
                let key = param.key;
                let shardIndex = param.shardIndex;
                let shardTotal = param.shardTotal;
                let shardSize = param.shardSize;
                let size = param.size;
                let fileName = param.fileName;
                let suffix = param.suffix;

                let fileShard = _this.getFileShard(shardIndex, shardSize, file);

                //param.append("file", fileShard);//文件切分后的分片
                //param.file = fileShard;
                let totalParam = new FormData();
                totalParam.append('file', fileShard);
                totalParam.append("key", key);
                totalParam.append("shardIndex", shardIndex);
                totalParam.append("shardSize", shardSize);
                totalParam.append("shardTotal", shardTotal);
                totalParam.append("size", size);
                totalParam.append("fileName", fileName);
                totalParam.append("suffix", suffix);
                let config = {
                    //添加请求头
                    headers: {"Content-Type": "multipart/form-data"}
                };
                console.log(param);
                var res = await this.$http.post('/upload', totalParam, config)

                var resData = res.data;
                if (resData.status) {
                    if (shardIndex < shardTotal) {
                        this.$notify({
                            title: '成功',
                            message: '分片' + shardIndex + '上传完成。。。。。。',
                            type: 'success'
                        });
                    } else {
                        this.videoUrl = resData.data;//把地址赋值给视频标签
                        this.$notify({
                            title: '全部成功',
                            message: '文件上传完成。。。。。。',
                            type: 'success'
                        });
                    }

                    if (shardIndex < shardTotal) {
                        console.log('下一份片开始。。。。。。');
                        // 上传下一个分片
                        param.shardIndex = param.shardIndex + 1;
                        _this.recursionUpload(param, file);
                    }
                }


            },

            async handUpLoad(req) {
                let _this = this;
                var file = req.file;
                /*  console.log('handUpLoad', req)
                  console.log(file);*/
                //let param = new FormData();
                //通过append向form对象添加数据

                //文件名称和格式,方便后台合并的时候知道要合成什么格式
                let fileName = file.name;
                let suffix = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length).toLowerCase();
                //这里判断文件格式,有其他格式的自行判断
                if (suffix != 'mp4') {
                    this.$message.error('文件格式错了哦。。');
                    return;
                }

                // 文件分片
                // let shardSize = 10 * 1024 * 1024;    //以10MB为一个分片
                // let shardSize = 50 * 1024;    //以50KB为一个分片
                let shardSize = _this.shardSize;
                let shardIndex = 1;        //分片索引,1表示第1个分片
                let size = file.size;
                let shardTotal = Math.ceil(size / shardSize); //总片数
                // 生成文件标识,标识多次上传的是不是同一个文件
                let key = this.$md5(file.name + file.size + file.type);
                let param = {
                    key: key,
                    shardIndex: shardIndex,
                    shardSize: shardSize,
                    shardTotal: shardTotal,
                    size: size,
                    fileName: fileName,
                    suffix: suffix
                }
                /*param.append("uid", key);
                param.append("shardIndex", shardIndex);
                param.append("shardSize", shardSize);
                param.append("shardTotal", shardTotal);
                param.append("size", size);
                param.append("fileName", fileName);
                param.append("suffix", suffix);

*/

                let checkIndexData = await _this.check(key);//得到文件分片索引
                let checkIndex = checkIndexData.findex;

                //console.log(checkIndexData)
                if (checkIndex == -1) {
                    this.recursionUpload(param, file);
                } else if (checkIndex < shardTotal) {
                    param.shardIndex = param.shardIndex + 1;
                    this.recursionUpload(param, file);
                } else {
                    this.videoUrl = checkIndexData.fname;//把地址赋值给视频标签
                    this.$message({
                        message: '极速秒传成功。。。。。',
                        type: 'success'
                    });
                }


                //console.log('结果:', res)
            },

            getFileShard(shardIndex, shardSize, file) {
                let _this = this;
                let start = (shardIndex - 1) * shardSize;    //当前分片起始位置
                let end = Math.min(file.size, start + shardSize); //当前分片结束位置
                let fileShard = file.slice(start, end); //从文件中截取当前的分片数据
                return fileShard;
            },


        }
    }

</script>

<style scoped lang="less">
    .file-upload {
        .file-upload-el {

        }

    }
    .v-box-card{
        width: 50%;
    }
</style>
前端代码

源码参考地址:bigfileupload: springboot+vue大文件分片上传 (gitee.com)

posted @ 2024-09-29 14:36  Rolay  阅读(10)  评论(0编辑  收藏  举报