spring-boot中实现分片上传文件

一、上传文件基本实现

  • 1、前端效果图展示,这里使用element-ui plus来展示样式效果

在这里插入图片描述

  • 2、基础代码如下

    <template>
      <div
        >
        <el-upload
          ref="uploadRef"
          class="upload-demo"
          :limit="1"
          :on-change="handleExceed"
          :auto-upload="false"
        >
          <template #trigger>
            <el-button type="primary">选择文件</el-button>
          </template>
          <el-button style="margin-left: 30px" type="success" @click="submitUpload"> 上传 </el-button>
        </el-upload>
      </div>
    </template>
    
    <script setup>
      import { ref } from 'vue';
    
      import axios from 'axios';
    
      const fileRef = ref(null);
    
      const handleExceed = (files) => {
        console.log(files);
        fileRef.value = files;
      };
    
      const submitUpload = () => {
        console.log('开始上传文件', JSON.stringify(fileRef.value));
        const formData = new FormData();
        formData.append('file', fileRef.value.raw);
        axios.post('http://localhost:9002/file/upload', formData).then((res) => {
          console.log(fileRef.value, '??');
          console.log('上传成功');
        });
      };
    </script>
    
    <style lang="scss" scoped></style>
    
    
  • 3、定义后端接口,并且处理好跨域(关于跨域处理,自己百度处理)

    package com.course.file.controller;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.multipart.MultipartFile;
    
    @RestController
    public class UploadController {
        private static final Logger LOG = LoggerFactory.getLogger(UploadController.class);
        @PostMapping("/upload")
        public String uploadApi(@RequestParam MultipartFile file) {
            this.LOG.info("上传文件开始");
            this.LOG.info(file.getOriginalFilename());
            this.LOG.info(String.valueOf(file.getSize()));
            return "上传成功";
        }
    }
    
  • 4、保存文件到本地文件

    package com.course.file.controller;
    
    import com.course.file.utils.UuidUtil;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.multipart.MultipartFile;
    
    import java.io.File;
    import java.io.IOException;
    
    @RestController
    public class UploadController {
        private static final Logger LOG = LoggerFactory.getLogger(UploadController.class);
    
        @PostMapping("/upload")
        public String uploadApi(@RequestParam MultipartFile file) throws IOException {
            this.LOG.info("上传文件开始");
            this.LOG.info(file.getOriginalFilename());
            this.LOG.info(String.valueOf(file.getSize()));
            // 保存文件到本地
            String fileName = file.getOriginalFilename();
            String key = UuidUtil.getShortUuid();
            // 需要先本地创建一个file文件夹
            String fullPath = "E:/file/" + key + "-" + fileName;
            File dest = new File(fullPath);
            file.transferTo(dest);
            return "上传成功";
        }
    }
    

二、配置静态目录

  • 1、在FileApplication.java旁边添加一个SpringMvcConfig.java的文件

    package com.course.file;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class SpringMvcConfig implements WebMvcConfigurer {
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("/f/**").addResourceLocations("file:E:/file/");
        }
        // http://localhost:9002/file/f/FdXMQJdF-xx.png
    }
    
  • 2、直接浏览器上直接访问上面的地址

  • 3、上传地址返回给前端

    @RestController
    public class UploadController {
        private static final Logger LOG = LoggerFactory.getLogger(UploadController.class);
    
        @PostMapping("/upload")
        public String uploadApi(@RequestParam MultipartFile file) throws IOException {
            this.LOG.info("上传文件开始");
            this.LOG.info(file.getOriginalFilename());
            this.LOG.info(String.valueOf(file.getSize()));
            // 保存文件到本地
            String fileName = file.getOriginalFilename();
            String key = UuidUtil.getShortUuid();
            // 需要先本地创建一个file文件夹
            String fullPath = "E:/file/" + key + "-" + fileName;
            File dest = new File(fullPath);
            file.transferTo(dest);
            return "http://localhost:9002/file/f/" + key + "-" + fileName;
        }
    }
    
  • 4、将上面几个固定的配置写到配置文件中

    file.path=E:/file/
    file.domain=http://localhost:9002/file/
    # 修改上传文件大小(不设置大文件可能上传失败)
    spring.servlet.multipart.max-file-size=50MB
    spring.servlet.multipart.max-request-size=50MB
    
  • 5、在控制器中使用

    public class UploadController {
        private static final Logger LOG = LoggerFactory.getLogger(UploadController.class);
        @Value("${file.path}")
        private String FILE_PATH;
    
        @Value("${file.domain}")
        private String FILE_DOMAIN;
    }
    

三、断点续传

  • 1、主要原理

    • 前端将大文件根据文件大小来切割成小片段,使用递归的方式调用后端接口,将文件上传到服务器端
    • 服务器端接收到前端上传片段,存储到服务器上
    • 等前端最后一个上传完成后,将全部的文件合并成一个文件
    • 合并完成后,返回一个url地址,将之前的分片上传的文件删除
  • 2、手动演示前端分段上传

    const submitUpload = () => {
        console.log('开始上传文件', JSON.stringify(fileRef.value));
        const formData = new FormData();
        const file = fileRef.value.raw;
        // 文件分配
        let shardSize = 5 * 1024 * 1024; // 以5MB为一个分片
        let shardIndex = 0; // 分片索引
        let start = shardIndex * shardSize; // 开始位置
        let end = Math.min(file.size, start + shardSize); // 结束位置
        let fileShard = file.slice(start, end); // 每次上传的分片数据
        formData.append('file', fileShard);
        axios.post('http://localhost:9002/file/upload', formData).then((res) => {
          console.log(fileRef.value, '??');
          console.log('上传成功');
        });
      };
    
  • 3、第二次的时候将shardIndex改为1

  • 4、查看本地文件夹下的文件

    在这里插入图片描述

  • 5、手动创建一个接口来尝试合并文件

    @GetMapping("merge")
        public String merge() throws FileNotFoundException {
            // 最终合成后的视频文件名称
            File newFile = new File(FILE_PATH + "test.mp4");
            FileOutputStream outputStream = new FileOutputStream(newFile, true);
            FileInputStream fileInputStream = null;
            byte[] bytes = new byte[5 * 1024 * 1024];
            int len;
            try {
                // 读取第一段
                fileInputStream = new FileInputStream(new File(FILE_PATH + "/pN0EoOny-blob"));
                while ((len = fileInputStream.read(bytes)) != -1) {
                    outputStream.write(bytes, 0, len);
                }
                // 读取第二段
                fileInputStream = new FileInputStream(new File(FILE_PATH + "/f5oeIEDW-blob"));
                while ((len = fileInputStream.read(bytes)) != -1) {
                    outputStream.write(bytes, 0, len);
                }
                // 读取第三段
                fileInputStream = new FileInputStream(new File(FILE_PATH + "/qsm8n03q-blob"));
                while ((len = fileInputStream.read(bytes)) != -1) {
                    outputStream.write(bytes, 0, len);
                }
            } catch (IOException e) {
                LOG.error("合并分片失败", e);
            } finally {
                try {
                    if (fileInputStream != null) {
                        fileInputStream.close();
                    }
                    outputStream.close();
                    LOG.info("IO流关闭");
                } catch (IOException e) {
                    LOG.error("IO流关闭", e);
                }
            }
            return "合并视频成功";
        }
    

四、使用数据库来实现分片上传

  • 1、数据表字段

    在数据库中涉及key只跟文件有关,跟上传多少片没关系的,当已经上传的分片数和分片总数一样的时候就合并文件

    drop table if exists `file`;
    create table `file` (
      `id` int(11) not null PRIMARY key auto_increment comment '主键id',
      `path` varchar(100) not null comment '相对路径',
      `name` varchar(100) comment '文件名',
      `suffix` varchar(10) comment '后缀',
      `size` int(11) comment '大小|字节B',
    	`shard_index` int(11) DEFAULT 0 COMMENT '已上传分片',
    	`shard_size` int(11) DEFAULT 0 COMMENT '分片大小',
    	`shard_total` int(11) DEFAULT 0 COMMENT '分片总数',
    	`key` VARCHAR(100) DEFAULT NULL COMMENT '文件标识',
      `created_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间',
      `updated_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '更新时间',
      `deleted_at` timestamp(6) NULL DEFAULT NULL COMMENT '软删除时间'
    ) engine=innodb default charset=utf8mb4 comment='文件';
    
  • 2、在pom.xml文件中添加mybatis-plus的依赖包

    <!--    配置连接到数据库    -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.26</version>
    </dependency>
    <!--   mybatis plus 依赖包  -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.2.0</version>
    </dependency>
    
    <!--    mybatis 代码生成器    -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-generator</artifactId>
        <version>3.2.0</version>
    </dependency>
    
  • 3、application.properties添加配置

    # mysql数据库链接
    spring.datasource.url=jdbc:mysql://localhost:3306/beego?characterEncoding=utf-8&serverTimezone=GMT%2B8
    spring.datasource.username=root
    spring.datasource.password=123456
    
    # 配置mybatis-plus
    # 开启下划线转驼峰
    mybatis-plus.configuration.map-underscore-to-camel-case=true 
    mybatis-plus.configuration.auto-mapping-behavior=full
    mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
    # mapping的路径
    mybatis-plus.mapper-locations=classpath*:mapper/**/*Mapper.xml
    #主键类型  AUTO:"数据库ID自增", INPUT:"用户输入ID", ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
    mybatis-plus.global-config.db-config.id-type=AUTO
    # 逻辑删除(软删除)
    mybatis-plus.global-config.db-config.logic-delete-value=NOW()
    mybatis-plus.global-config.db-config.logic-not-delete-value=NULL
    
  • 4、使用模板生成器生成代码

  • 5、前端使用递归的方式来分片上传文件

    <script setup>
      import { ref } from 'vue';
      import { genFileId } from 'element-plus';
      import axios from 'axios';
      import md5 from 'js-md5';
    
      const fileRef = ref(null);
    
      const handleExceed = (files) => {
        console.log(files);
        fileRef.value = files;
      };
    
      const updateFile = (shardIndex) => {
        const formData = new FormData();
        const file = fileRef.value.raw;
        // 文件分配
        let shardSize = 5 * 1024 * 1024; // 以5MB为一个分片
        // let shardIndex = 0; // 分片索引
        let start = (shardIndex - 1) * shardSize; // 开始位置
        let end = Math.min(file.size, start + shardSize); // 结束位置
        let fileShard = file.slice(start, end); // 每次上传的分片数据
        // 前端多上传参数
        let size = file.size;
        let shardTotal = Math.ceil(size / shardSize); // 总片数
        const suffix = file.name.substring(file.name.lastIndexOf('.') + 1);
        formData.append('shard', fileShard);
        formData.append('shardIndex', shardIndex);
        formData.append('shardSize', shardSize);
        formData.append('shardTotal', shardTotal);
        formData.append('name', file.name);
        formData.append('size', size);
        formData.append('suffix', suffix); 
        formData.append('key', md5(`${file.name}_${file.size}_${file.type}`));
    
        axios.post('http://localhost:9002/file/upload1', formData).then((res) => {
          // 判断如果当前的shardIndex < shardTotal的时候递归上传
          if (shardIndex < shardTotal) {
            updateFile(++shardIndex);
          } else {
            console.log('上传成功');
          }
        });
      };
      const submitUpload = () => {
        console.log('开始上传文件', JSON.stringify(fileRef.value));
        // 开始上传文件
        updateFile(1);
      };
    </script>
    
  • 6、后端对文件上传处理,存储到本地和入库操作

    package com.course.file.controller;
    
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.course.file.model.FileEntity;
    import com.course.file.service.IFileService;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.multipart.MultipartFile;
    
    import java.io.File;
    import java.io.IOException;
    
    @RestController
    public class FileController {
        @Autowired
        private IFileService fileService;
        private static final Logger LOG = LoggerFactory.getLogger(FileController.class);
        @Value("${file.path}")
        private String FILE_PATH;
    
        @Value("${file.domain}")
        private String FILE_DOMAIN;
    
        @PostMapping("upload1")
        public String upload1(
                @RequestParam MultipartFile shard,
                Integer shardIndex,
                Integer shardSize,
                Integer shardTotal,
                String name,
                String suffix,
                Integer size,
                String key
        ) throws IOException {
            this.LOG.info("开始上传文件");
            System.out.println("当前分片:" + shardIndex);
            System.out.println("当前分片大小:" + shardSize);
            System.out.println("当前分片总数:" + shardTotal);
            System.out.println("文件名称:" + name);
            System.out.println("文件后缀名:" + suffix);
            System.out.println("文件大小:" + size);
            System.out.println("文件唯一的key:" + key);
            // 文件保存到本地目录下
            String localPath = this.FILE_PATH + key + "." + suffix + "." + shardIndex;
            File dest = new File(localPath);
            shard.transferTo(dest);
            LOG.info(dest.getAbsolutePath());
            // 数据入库操作
            LambdaQueryWrapper<FileEntity> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(FileEntity::getFileKey, key);
            FileEntity fileEntity = new FileEntity();
            if (this.fileService.getOne(queryWrapper) != null) {
                // 说明不是第一次上传,需要更新当前上传的分片数量
                fileEntity.setShardIndex(shardIndex);
                if (this.fileService.update(fileEntity, queryWrapper)) {
                    return "上传成功";
                } else {
                    return "上传失败";
                }
            } else {
                // 第一次上传创建
                String path = this.FILE_PATH + key + "." + suffix;
                fileEntity.setFileKey(key);
                fileEntity.setName(name);
                fileEntity.setPath(path);
                fileEntity.setShardIndex(shardIndex);
                fileEntity.setSuffix(suffix);
                fileEntity.setShardSize(shardSize);
                fileEntity.setShardTotal(shardTotal);
                fileEntity.setSize(size);
                if (this.fileService.save(fileEntity)) {
                    return "上传成功";
                } else {
                    return "上传失败";
                }
            }
        }
    }
    
  • 7、查看本地目录是否生成分片文件

    在这里插入图片描述

  • 8、对本地分片文件合并操作,当shardIndex=shardTotal的时候进行合并操作

    @RestController
    public class FileController {
        @Autowired
        private IFileService fileService;
        private static final Logger LOG = LoggerFactory.getLogger(FileController.class);
        @Value("${file.path}")
        private String FILE_PATH;
    
        @Value("${file.domain}")
        private String FILE_DOMAIN;
    
        @PostMapping("upload1")
        public String upload1(
                @RequestParam MultipartFile shard,
                Integer shardIndex,
                Integer shardSize,
                Integer shardTotal,
                String name,
                String suffix,
                Integer size,
                String key
        ) throws IOException {
            this.LOG.info("开始上传文件");
            System.out.println("当前分片:" + shardIndex);
            System.out.println("当前分片大小:" + shardSize);
            System.out.println("当前分片总数:" + shardTotal);
            System.out.println("文件名称:" + name);
            System.out.println("文件后缀名:" + suffix);
            System.out.println("文件大小:" + size);
            System.out.println("文件唯一的key:" + key);
            // 文件保存到本地目录下
            String localPath = this.FILE_PATH + key + "." + suffix + "." + shardIndex;
            File dest = new File(localPath);
            shard.transferTo(dest);
            LOG.info(dest.getAbsolutePath());
            // 合并文件操作
            if (Objects.equals(shardIndex, shardTotal)) {
                this.merge(key, suffix, shardTotal);
            }
    
            // 数据入库操作
            LambdaQueryWrapper<FileEntity> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(FileEntity::getFileKey, key);
            FileEntity fileEntity = new FileEntity();
            if (this.fileService.getOne(queryWrapper) != null) {
                // 说明不是第一次上传,需要更新当前上传的分片数量
                fileEntity.setShardIndex(shardIndex);
    
                if (this.fileService.update(fileEntity, queryWrapper)) {
                    return "上传成功";
                } else {
                    return "上传失败";
                }
            } else {
                // 第一次上传创建
                String path = this.FILE_PATH + key + "." + suffix;
                fileEntity.setFileKey(key);
                fileEntity.setName(name);
                fileEntity.setPath(path);
                fileEntity.setShardIndex(shardIndex);
                fileEntity.setSuffix(suffix);
                fileEntity.setShardSize(shardSize);
                fileEntity.setShardTotal(shardTotal);
                fileEntity.setSize(size);
                if (this.fileService.save(fileEntity)) {
                    return "上传成功";
                } else {
                    return "上传失败";
                }
            }
        }
    	/**
         * 对上传的文件片段合并操作
         * @param key
         * @param suffix
         * @param shardTotal
         * @throws FileNotFoundException
         */
        private void merge(String key, String suffix, Integer shardTotal) throws FileNotFoundException {
            this.LOG.info("=====开始合并切片操作=====");
            String path = this.FILE_PATH + key + "." + suffix;
            File newFile = new File(path);
            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++) {
                    fileInputStream = new FileInputStream(new File(this.FILE_PATH + key + "." + suffix + "." + (i + 1)));
                    while ((len = fileInputStream.read(byt)) != -1) {
                        outputStream.write(byt, 0, len);
                    }
                }
            } catch (Exception e) {
                this.LOG.error("分片合并异常", e);
            } finally {
                try {
                    if (fileInputStream != null) {
                        fileInputStream.close();
                    }
                    outputStream.close();
                    LOG.info("IO流关闭");
                } catch (Exception e) {
                    LOG.error("IO流关闭", e);
                }
            }
            this.LOG.info("=====结束合并切片操作=====");
        }
    }
    
  • 9、当合并后可以对之前的分片进行删除操作,避免占用更的磁盘空间

    private void merge(String key, String suffix, Integer shardTotal) throws FileNotFoundException, InterruptedException {
        // ...
        this.LOG.info("=====结束合并切片操作=====");
        System.gc();
        Thread.sleep(1000);
    
        // 删除分片
        LOG.info("删除分片开始");
        for (int i = 0; i < shardTotal; i++) {
            String filePath = path + "." + (i + 1);
            File file = new File(filePath);
            boolean result = file.delete();
            this.LOG.info("删除{},{}", filePath, result ? "成功" : "失败");
        }
        LOG.info("删除分片结束");
    }
    

五、前端使用BS64提交数据

  • 1、使用bs64提交后端可以直接定义一个字符串接收数据,将接收到的数据转换为图片

  • 2、前端将上传的文件转换为bs64

    // 使用BS64上传
      const bs64UploadHandler = () => {
        const file = fileRef.value.raw;
        let fileReader = new FileReader();
        fileReader.onload = function (e) {
          const base64 = e.target.result;
          console.log('base64', base64);
          const suffix = file.name.substring(file.name.lastIndexOf('.') + 1);
          axios
            .post('http://localhost:9002/file/bs64Upload', {
              fileName: md5(`${file.name}_${file.size}_${file.type}`) + '.' + suffix,
              fileBs64: base64,
            })
            .then((res) => {
              console.log(res);
            });
        };
        fileReader.readAsDataURL(file);
      };
    
  • 3、后端定义一个方法,将字符串转换为MultipartFile数据类型

    package com.course.file.utils;
    
    
    import org.springframework.web.multipart.MultipartFile;
    import sun.misc.BASE64Decoder;
    
    import java.io.*;
    
    public class Base64ToMultipartFile implements MultipartFile {
    
        private final byte[] imgContent;
        private final String header;
    
        public Base64ToMultipartFile(byte[] imgContent, String header) {
            this.imgContent = imgContent;
            this.header = header.split(";")[0];
        }
    
        @Override
        public String getName() {
            // TODO - implementation depends on your requirements
            return System.currentTimeMillis() + Math.random() + "." + header.split("/")[1];
        }
    
        @Override
        public String getOriginalFilename() {
            // TODO - implementation depends on your requirements
            return System.currentTimeMillis() + (int) Math.random() * 10000 + "." + header.split("/")[1];
        }
    
        @Override
        public String getContentType() {
            // TODO - implementation depends on your requirements
            return header.split(":")[1];
        }
    
        @Override
        public boolean isEmpty() {
            return imgContent == null || imgContent.length == 0;
        }
    
        @Override
        public long getSize() {
            return imgContent.length;
        }
    
        @Override
        public byte[] getBytes() throws IOException {
            return imgContent;
        }
    
        @Override
        public InputStream getInputStream() throws IOException {
            return new ByteArrayInputStream(imgContent);
        }
    
        @Override
        public void transferTo(File dest) throws IOException, IllegalStateException {
            new FileOutputStream(dest).write(imgContent);
        }
    
        public static MultipartFile base64ToMultipart(String base64) {
            try {
                String[] baseStrs = base64.split(",");
    
                BASE64Decoder decoder = new BASE64Decoder();
                byte[] b = new byte[0];
                b = decoder.decodeBuffer(baseStrs[1]);
    
                for(int i = 0; i < b.length; ++i) {
                    if (b[i] < 0) {
                        b[i] += 256;
                    }
                }
    
                return new Base64ToMultipartFile(b, baseStrs[0]);
            } catch (IOException e) {
                e.printStackTrace();
                return null;
            }
        }
    }
    
  • 4、直接保存文件,这里就不做分片上传

    @PostMapping("bs64Upload")
    public String bs64Upload(@RequestBody FileDTO req) throws IOException {
        // 1.将上传的bs64转为图片
        String bs64 = req.getFileBs64();
        MultipartFile shard = Base64ToMultipartFile.base64ToMultipart(bs64);
        // 图片保存到本地
        String localPath = this.FILE_PATH + req.getFileName() ;
        File dest = new File(localPath);
        shard.transferTo(dest);
        LOG.info(dest.getAbsolutePath());
        return this.FILE_DOMAIN + req.getFileName();
    }
    

六、断点续传

  • 1、断点续传主要原谅,前端在点击上传按钮的时候先调用后端一个接口,判断之前是否有上传过记录,如果有就返回之前上传的分片shardIndex,前端就继续以这个分片来上传,如果没有就返回0表示从0开始上传

  • 2、后端定义一个接口根据key来查询数据库是否已经有上传过记录

    @GetMapping("{key}")
    public Integer getShardIndexByKeyApi(@PathVariable String key) {
        LambdaQueryWrapper<FileEntity> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(FileEntity::getFileKey, key).select(FileEntity::getShardIndex);
        FileEntity fileEntity = this.fileService.getOne(queryWrapper);
        return fileEntity.getShardIndex();
    }
    
  • 3、前端在使用上传前先调用上面的接口

     const submitUpload = () => {
        console.log('开始上传文件', JSON.stringify(fileRef.value));
        const file = fileRef.value.raw;
        const key = md5(`${file.name}_${file.size}_${file.type}`);
        axios.get(`http://localhost:9002/file/${key}`).then((response) => {
          if (response.data > 0) {
            // 历史上传
            updateFile(response.data + 1);
          } else {
            // 首次上传
            updateFile(1);
          }
        });
     };
    

七、秒传功能

  • 1、当前端使用MD5加密后提交的名字在数据库已经存在,则直接拼接url返回就可以,不需要再次上传

八、上传到阿里OSS

  • 1、配置依赖包

    <!--    阿里云oss存储    -->
    <dependency>
        <groupId>com.aliyun.oss</groupId>
        <artifactId>aliyun-sdk-oss</artifactId>
        <version>3.10.2</version>
    </dependency>
    
  • 2、封装方法使用

    
    import com.aliyun.oss.OSS;
    import com.aliyun.oss.OSSClientBuilder;
    import com.aliyun.oss.model.ObjectMetadata;
    import com.aliyun.oss.model.PutObjectResult;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    import org.springframework.web.multipart.MultipartFile;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.URL;
    import java.util.Date;
    import java.util.List;
    import java.util.UUID;
    
    /**
     * 阿里云上传文件
     */
    @Component
    public class OssUtil {
    
        @Value("${aliyun.oss.endpoint}")
        private String endpoint;
        @Value("${aliyun.oss.accessKeyId}")
        private String accessKeyId;
        @Value("${aliyun.oss.accessKeySecret}")
        private String accessKeySecret;
        @Value("${aliyun.oss.bucketName}")
        private String bucketName;
    
        //文件存储目录(自定义阿里云的)
        private String fileDir = "clouFile/";
    
        /**
         * 单个文件上传
         *
         * @param file
         * @return
         */
        public String uploadFile(MultipartFile file) {
            // 调用封装好的上传文件方法
            String fileUrl = uploadImg2Oss(file);
            // 返回完整的路径
            String str = getFileUrl(fileUrl);
            return str.trim();
        }
    
        /**
         * 单个文件上传(指定文件名需要后缀名)
         *
         * @param file
         * @param fileName
         * @return
         */
        public String uploadFile(MultipartFile file, String fileName) {
            try {
                InputStream inputStream = file.getInputStream();
                this.uploadFile2OSS(inputStream, fileName);
                return fileName;
            } catch (Exception e) {
                return "上传失败";
            }
        }
    
        /**
         * 多个文件的上传,返回路径用,分割
         *
         * @param fileList
         * @return
         */
        public String uploadFile(List<MultipartFile> fileList) {
            String fileUrl = "";
            String str = "";
            String photoUrl = "";
            for (int i = 0; i < fileList.size(); i++) {
                fileUrl = uploadImg2Oss(fileList.get(i));
                str = getFileUrl(fileUrl);
                if (i == 0) {
                    photoUrl = str;
                } else {
                    photoUrl += "," + str;
                }
            }
            return photoUrl.trim();
        }
    
        /**
         * 获取完整的路径名
         *
         * @param fileUrl
         * @return
         */
        private String getFileUrl(String fileUrl) {
            if (fileUrl != null && fileUrl.length() > 0) {
                String[] split = fileUrl.split("/");
                String url = this.getUrl(this.fileDir + split[split.length - 1]);
                return url;
            }
            return null;
        }
    
        /**
         * 获取去掉参数的完整路径
         *
         * @param url
         * @return
         */
        private String getShortUrl(String url) {
            String[] imgUrls = url.split("\\?");
            return imgUrls[0].trim();
        }
    
        /**
         * 获取url地址
         *
         * @param key
         * @return
         */
        private String getUrl(String key) {
            // 设置URL过期时间为20年  3600l* 1000*24*365*20
            Date expiration = new Date(new Date().getTime() + 3600l * 1000 * 24 * 365 * 20);
            // 生成URL
            OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
            URL url = ossClient.generatePresignedUrl(bucketName, key, expiration);
            if (url != null) {
                return getShortUrl(url.toString());
            }
            return null;
        }
    
        private String uploadImg2Oss(MultipartFile file) {
            //1、限制最大文件为20M
            if (file.getSize() > 1024 * 1024 * 20) {
                return "图片太大";
            }
            //2、重命名文件
            String fileName = file.getOriginalFilename();
            String suffix = fileName.substring(fileName.lastIndexOf(".")).toLowerCase(); //文件后缀
            String uuid = UUID.randomUUID().toString();
            String name = uuid + suffix;
    
            try {
                InputStream inputStream = file.getInputStream();
                this.uploadFile2OSS(inputStream, name);
                return name;
            } catch (Exception e) {
                return "上传失败";
            }
        }
    
        /**
         * 使用阿里云上传文件
         *
         * @param inStream 输入流
         * @param fileName 文件名称
         * @return
         */
        private String uploadFile2OSS(InputStream inStream, String fileName) {
            String ret = "";
            try {
                //创建上传Object的Metadata
                ObjectMetadata objectMetadata = new ObjectMetadata();
                objectMetadata.setContentLength(inStream.available());
                objectMetadata.setCacheControl("no-cache");
                objectMetadata.setHeader("Pragma", "no-cache");
                objectMetadata.setContentType(getContentType(fileName.substring(fileName.lastIndexOf("."))));
                objectMetadata.setContentDisposition("inline;filename=" + fileName);
                //上传文件
                OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
                PutObjectResult putResult = ossClient.putObject(bucketName, fileDir + fileName, inStream, objectMetadata);
                ret = putResult.getETag();
            } catch (IOException e) {
                System.out.println(e.getMessage());
            } finally {
                try {
                    if (inStream != null) {
                        inStream.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return ret;
        }
    
        /**
         * 获取文件类型
         *
         * @param FilenameExtension
         * @return
         */
        private static String getContentType(String FilenameExtension) {
            if (FilenameExtension.equalsIgnoreCase(".bmp")) {
                return "image/bmp";
            }
            if (FilenameExtension.equalsIgnoreCase(".gif")) {
                return "image/gif";
            }
            if (FilenameExtension.equalsIgnoreCase(".jpeg") ||
                    FilenameExtension.equalsIgnoreCase(".jpg") ||
                    FilenameExtension.equalsIgnoreCase(".png")) {
                return "image/jpeg";
            }
            if (FilenameExtension.equalsIgnoreCase(".html")) {
                return "text/html";
            }
            if (FilenameExtension.equalsIgnoreCase(".txt")) {
                return "text/plain";
            }
            if (FilenameExtension.equalsIgnoreCase(".vsd")) {
                return "application/vnd.visio";
            }
            if (FilenameExtension.equalsIgnoreCase(".pptx") ||
                    FilenameExtension.equalsIgnoreCase(".ppt")) {
                return "application/vnd.ms-powerpoint";
            }
            if (FilenameExtension.equalsIgnoreCase(".docx") ||
                    FilenameExtension.equalsIgnoreCase(".doc")) {
                return "application/msword";
            }
            if (FilenameExtension.equalsIgnoreCase(".xml")) {
                return "text/xml";
            }
            //PDF
            if (FilenameExtension.equalsIgnoreCase(".pdf")) {
                return "application/pdf";
            }
            return "image/jpeg";
        }
    }
    

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/118824.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

还在按键进入BIOS?其实进入BIOS有更便捷的方法

在能够配置各种硬件参数之前,首先必须学习进入ASUS BIOS实用程序的正确方法。无论你面对的是PRIME、ROG、TUF GAMING还是任何其他型号,所涉及的步骤都是相同的。 华硕主板的BIOS访问键通常是“F2”或“Del”键。在启动或启动屏幕期间,只要ASUS徽标出现,就重复按下此键以进…

为什么说制造企业需要部署MES管理系统

在数字化浪潮席卷的今天&#xff0c;每个企业都期望通过新技术、新模式来优化自身的运营。这其中&#xff0c;MES管理系统成为了不少企业的首选。那么&#xff0c;为何企业需要部署MES管理系统&#xff1f;又该如何搭建MES管理系统呢&#xff1f; 一、企业缘何钟情于MES系统&am…

apache-maven-3.6.3 安装配置教程

链接&#xff1a;https://pan.baidu.com/s/1RkMXipnvac9EKcZyUStfGQ?pwdl32m 提取码&#xff1a;l32m 1. 将 maven 压缩包解压至指定文件夹 2. 配置环境变量 &#xff08;1&#xff09;打开此电脑-> 鼠标右键选择属性->点击高级系统设置 &#xff08;2&#xff09;点…

易云维®工厂能耗管理平台系统方案,保证运营质量,推动广东制造企业节能减排

我国《关于完整准确全面贯彻新发展理念推进碳达峰碳中和工作的实施意见》出台&#xff0c;提出了推进碳达峰碳中和工作的总体目标。到2025年&#xff0c;广东具备条件的地区、行业和企业率先实现碳达峰&#xff0c;为全省实现碳达峰、碳中和奠定坚实基础&#xff1b;2030年前实…

Qt 继承QAbstractListModel实现自定义ListModel

1.简介 QAbstractListModel类提供了一个抽象模型&#xff0c;可以将其子类化以创建一维列表模型。 QAbstractListModel为将其数据表示为简单的非层次项目序列的模型提供了一个标准接口。它不直接使用&#xff0c;但必须进行子类化。 由于该模型提供了比QAbstractItemModel更…

【广州华锐互动】3D全景虚拟旅游在文旅行业的应用场景

随着科技的不断发展&#xff0c;3D全景虚拟旅游正在成为一种新兴的旅游体验方式&#xff0c;它可以帮助旅游者更加深入地了解旅游信息&#xff0c;提升旅游体验。下面我们将详细介绍3D全景虚拟旅游可以应用于哪些场景。 一、旅游规划 3D全景虚拟旅游可以帮助旅游者更加直观地进…

Tomcat 9.0.x 源码编译

文章目录 一、克隆源码二、构建 Maven1&#xff09;在项目根目录中新建 pom.xml 文件2&#xff09;然后 Add Maven Projects 三、在目录中增加 home 目录四、增加启动配置五、其它问题1&#xff09;控制台乱码解决 2&#xff09;启动后访问 localhost:8080 报错解决 一、克隆源…

046_第三代软件开发-虚拟屏幕键盘

第三代软件开发-虚拟屏幕键盘 文章目录 第三代软件开发-虚拟屏幕键盘项目介绍虚拟屏幕键盘 关键字&#xff1a; Qt、 Qml、 虚拟键盘、 qtvirtualkeyboard、 自定义 项目介绍 欢迎来到我们的 QML & C 项目&#xff01;这个项目结合了 QML&#xff08;Qt Meta-Object L…

2014年亚太杯APMCM数学建模大赛A题无人机创造安全环境求解全过程文档及程序

2014年亚太杯APMCM数学建模大赛 A题 无人机创造安全环境 原题再现 20 国集团&#xff0c;又称 G20&#xff0c;是一个国际经济合作论坛。2016 年第 11 届 20 国集团峰会将在中国召开&#xff0c;这是继 APEC 后中国将举办的另一个大型峰会。此类大型峰会&#xff0c;举办城市…

L4级自动驾驶前装量产车型来了,小马智行与丰田联合打造

11月5日&#xff0c;小马智行与丰田汽车联合发布的首款纯电自动驾驶出租车&#xff08;Robotaxi&#xff09;概念车在第六届进博会亮相&#xff0c;该车型基于广汽丰田生产的bZ4X纯电车辆平台打造&#xff0c;将搭载小马智行研发的第七代L4级自动驾驶乘用车软硬件系统。 今年8月…

SpringBoot整合JUnit

1.创建新项目 说明&#xff1a;创建springboot_04_junit项目&#xff0c;选择对应的版本。 2.接口类 说明&#xff1a;新建BookDao接口。 package com.forever.dao;public interface BookDao {public void save(); }3.实现类 说明&#xff1a; 新建BookDaoImpl实现类。 pa…

餐饮加盟信息展示预约小程序的内容如何

餐饮业规模持续增加&#xff0c;相关从业者逐渐增多&#xff0c;对中等规模以上的餐饮品牌来说&#xff0c;当有一定规模后除了开多家直营店外&#xff0c;还会开放招商加盟&#xff0c;扩展品牌、提升营收等。 由于餐饮加盟属于准属性业务&#xff0c;因此传统线下方式不太适…

HomeAssistant添加HACS插件并实现公网控制米家,HomeKit等智能家居

HomeAssistant添加HACS插件并实现公网控制米家&#xff0c;HomeKit等智能家居 文章目录 HomeAssistant添加HACS插件并实现公网控制米家&#xff0c;HomeKit等智能家居基本条件一、下载HACS源码二、添加HACS集成三、绑定米家设备 ​ 上文介绍了如何实现群晖Docker部署HomeAssist…

3.27每日一题(常系数线性非齐次方程的特解)

常系数非齐次线性方程的特解如何假设&#xff08;两种&#xff09;形式&#xff1a; 1、题目中 e 的 x 次幂以及 1&#xff0c;都是第一种&#xff1a;1可以看成为e的0次幂 注&#xff1a;题目给的多项式是特殊的形式&#xff0c;我们要设为一般的形式的多项式 2、题目中sin…

【MySQL基本功系列】第一篇 先熟悉MySQL的运行逻辑

​ 我将推出一系列关于MySQL的博客文章&#xff0c;涵盖了从入门到深入底层的原理。这些文章将包括MySQL的运行逻辑、InnoDB存储引擎、SQL优化、undo log、bin log等多个方面的知识。希望这些文章能为你提供宝贵的信息和洞见&#xff0c;并帮助你更好地理解和应用MySQL。同时&a…

gma 1.x 气候气象指数计算源代码(分享)

本模块的主要内建子模块如下&#xff1a; 如何获得完整代码&#xff1a; 回复博主 或者 留言/私信 。 注意&#xff1a;本代码完全开源&#xff0c;可随意修改使用。 但如果您的成果使用或参考了本段代码&#xff0c;给予一定的引用说明&#xff08;非强制&#xff09;&#xf…

客户案例 | 思腾合力助力深度图灵生成式AI应用平台建设

近年来&#xff0c;娱乐行业发展迅猛&#xff0c;市场容量不断扩大。从娱乐产业发展来看&#xff0c;用户对于娱乐内容和体验的需求不断攀升&#xff0c;如何将生成式AI更好的应用于照片修复、创意摄影、漫画创作、图片生成等场景中是对娱乐行业各科技公司的挑战和考验&#xf…

Kubernetes 的四个网络挑战

Kubernetes 的主要职责之一是在应用程序之间共享节点。由于这些应用程序需要相互通信并与外部世界通信&#xff0c;因此网络是一个基本的需求。 Kubernetes 托管的分布式应用程序架构 来自 Kubernetes 集群外部的请求通常通过负责将它们代理到适当服务的路由器或 API 网关进行…

Python笔记——linux/ubuntu下安装mamba,安装bob.learn库

Python笔记——linux/ubuntu下安装mamba&#xff0c;安装bob.learn库 一、安装/卸载anaconda二、安装mamba1. 命令行安装&#xff08;大坑&#xff0c;不推荐&#xff09;2. 命令行下载guihub上的安装包并安装&#xff08;推荐&#xff09;3. 网站下载安装包并安装&#xff08;…

STM32创建工程步骤

以创建led工程为例&#xff1a; 新建一个led文件夹 新建一个以led命名的工程&#xff08;用keil_uVision5&#xff09;并添加三个组。 Library文件夹里放置库函数文件。 User&#xff1a; 点亮led灯的程序&#xff1b; 直接给寄存器赋值 调用库函数。 #include "stm…
最新文章