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)

要实现文件上传需要完成以下几个步骤: 1. 在前端页面添加上传文件的表单,并绑定上传事件,将文件上传后端。 2. 在后端接收前端上传文件,并保存到服务器上。 下面是一个简单的示例,演示如何使用 Spring Boot + Vue.js + ElementUI 实现文件上传的功能。 前端代码: ```html <template> <div> <el-upload class="upload-demo" ref="upload" :action="uploadUrl" :on-success="handleSuccess" :before-upload="beforeUpload" :file-list="fileList"> <el-button size="small" type="primary">点击上传</el-button> <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div> </el-upload> </div> </template> <script> export default { data() { return { uploadUrl: "/upload", fileList: [] }; }, methods: { // 上传前的钩子函数 beforeUpload(file) { const isJPG = file.type === "image/jpeg" || file.type === "image/png"; const isLt500K = file.size / 1024 < 500; if (!isJPG) { this.$message.error("上传头像图片只能是 JPG/PNG 格式!"); } if (!isLt500K) { this.$message.error("上传头像图片大小不能超过 500KB!"); } return isJPG && isLt500K; }, // 上传成功的回调函数 handleSuccess(response, file, fileList) { this.fileList = fileList; this.$emit("upload-success", response); } } }; </script> ``` 在上面的代码中,我们使用了 ElementUI 的 Upload 组件,将上传文件的表单渲染到页面上。同时,我们还绑定了 beforeUpload 和 handleSuccess 两个事件,用于上传前的校验和上传成功后的回调。 注意:在使用 beforeUpload 钩子函数时,需要返回一个布尔值,表示是否允许上传。如果返回 false,上传将会被终止。 后端代码: ```java @RestController public class FileUploadController { @PostMapping("/upload") public String uploadFile(@RequestParam("file") MultipartFile file) throws Exception { if (file.isEmpty()) { return "上传文件为空!"; } // 获取文件名 String fileName = file.getOriginalFilename(); // 获取文件的后缀名 String suffixName = fileName.substring(fileName.lastIndexOf(".")); // 设置文件存储路径 String filePath = "/tmp/"; // 重新生成文件名 fileName = UUID.randomUUID() + suffixName; // 创建文件对象 File dest = new File(filePath + fileName); // 检测是否存在目录 if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); } // 保存文件 file.transferTo(dest); return "文件上传成功!"; } } ``` 在上面的代码中,我们使用了 Spring Boot 的 @PostMapping 注解,将上传文件的接口地址映射到 /upload 路径上。同时,我们还使用了 MultipartFile 类型的参数来接收前端上传文件。 在接收到文件后,我们首先判断文件是否为空。如果不为空,我们通过 MultipartFile 类型的方法获取文件名和后缀名,并将文件存储到指定的路径下。最后,我们返回一个字符串,表示上传成功。 需要注意的是,在保存文件时,我们使用了 transferTo 方法。该方法会将上传文件保存到指定的路径下,并自动关闭文件流。同时,我们还判断了目录是否存在,如果不存在,就创建一个新的目录。 综上所述,通过以上的代码,我们可以实现 Spring Boot + Vue.js + ElementUI文件上传功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值