文件切片上传--后端

一、文件切片上传

前端使用vue-simple-uploader组件

业务需求
  • 文件秒传

    如果该文件曾经上传过,或者服务器存在该文件,则立即上传成功,并返回文件地址

  • 断点续传

    如果文件上传的过程中,因某种原因中断,则已上传的内容不在继续上传

核心内容–保存文件
  1. 上传流程

    • 第一步:获取RandomAccessFile,随机访问文件类的对象
    • 第二步:调用RandomAccessFile的getChannel()方法,打开文件通道 FileChannel
    • 第三步:获取当前是第几个分块,计算文件的最后偏移量
    • 第四步:获取当前文件分块的字节数组,用于获取文件字节长度
    • 第五步:使用文件通道FileChannel类的 map()方法创建直接字节缓冲器 MappedByteBuffer
    • 第六步:将分块的字节数组放入到当前位置的缓冲区内 mappedByteBuffer.put(byte[] b);
    • 第七步:释放缓冲区
  2. 实现过程

    @Resource
    private Semaphore semaphore;
    
    /**
      * MultipartFileDto multipartFile 参数
      * chunkNumber:当前切片序号
      * chunkSize:切片大小
      * currentChunkSize:当前切片大小
      * totalSize:文件总大小
      * identifier:切片的唯一标识,MD5
      * fileName:文件名
      * relativePath:临时文件名
      * totalChunks:总切片数
      * file:文件
      */
    public void saveFile(MultipartFileDto multipartFile) throws IOException {
        MappedByteBuffer mappedByteBuffer = null;
        FileChannel fileChannel = null;
        RandomAccessFile randomAccessFile = null;
        try {
            // 获取信号量
            semaphore.acquire();
            String filePath = "c:/" + multipartFile.getRelativePath();
            // 第一步 获取RandomAccessFile,随机访问文件类的对象
            randomAccessFile = new RandomAccessFile(filePath, "rw");
            // 第二步 调用RandomAccessFile的getChannel()方法,打开文件通道 FileChannel
            fileChannel = randomAccessFile.getChannel();
            // 第三步 获取当前是第几个分块,计算文件的最后偏移量
            long offset = (multipartFile.getChunkNumber() - 1) * multipartFile.getChunkSize();
            // 第四步 获取当前文件分块的字节数组,用于获取文件字节长度
            byte[] fileData = multipartFile.getFile().getBytes();
            // 第五步 使用文件通道FileChannel类的 map()方法创建直接字节缓冲器  MappedByteBuffer
            mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);
            // 第六步 将分块的字节数组放入到当前位置的缓冲区内  mappedByteBuffer.put(byte[] b)
            mappedByteBuffer.put(fileData);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 第七步 释放缓冲区
            freeMappedByteBuffer(mappedByteBuffer);
            if (fileChannel != null) {
                fileChannel.close();
            }
            if (randomAccessFile != null) {
                randomAccessFile.close();
            }
            // 释放信号量
            semaphore.release();
        }
    }
    
    /**
      * 用于释放MappedByteBuffer
      */
    private void freeMappedByteBuffer(final MappedByteBuffer mappedByteBuffer) {
        try {
            if (mappedByteBuffer == null) {
                return;
            }
            mappedByteBuffer.force();
            AccessController.doPrivileged(new PrivilegedAction<Object>() {
                @Override
                public Object run() {
                    try {
                        Method getCleanerMethod = mappedByteBuffer.getClass().getMethod("cleaner", new Class[0]);
                        //可以访问private的权限
                        getCleanerMethod.setAccessible(true);
                        //在具有指定参数的 方法对象上调用此 方法对象表示的底层方法
                        sun.misc.Cleaner cleaner = (sun.misc.Cleaner) getCleanerMethod.invoke(mappedByteBuffer, new Object[0]);
                        cleaner.clean();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    return null;
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    

    信号量的作用:每一次请求都会开辟一个对磁盘空间操作的方法,为防止磁盘压力过大,通过信号量,控制同时对磁盘操作的个数

功能完善

mongodb

判断文件是否上传完成的过程中,会大量的访问数据库,采用mongodb可提高访问速度

mongodb中的集合

  • MultipartFilePo

    @Data
    @Document("multipart_file")
    public class MultipartFilePo implements Serializable {
    
        // 表示主键
        @Id
        private ObjectId id;
    
        // 切片的唯一标识,MD5
        @Indexed(unique = true)
        @Field("identifier")
        private String identifier;
    
        // 当前上传文件大小
        @Field("uploader_size")
        private Long uploaderSize;
    
        // 切片序号
        @Field("chunk_number")
        private Integer chunkNumber;
    }
    
  • CompleteFilePo

    @Data
    @Document("complete_file")
    public class CompleteFilePo implements Serializable {
    
        // 表示主键
        @Id
        private ObjectId id;
    
        // 切片的唯一标识,MD5
        @Indexed(unique = true)
        @Field("identifier")
        private String identifier;
    
        // 总文件大小
        @Field("total_size")
        private Long totalSize;
    
        // 文件切片总数
        @Field("total_chunk")
        private Integer totalChunk;
    
        // 是否已经全部上传
        @Field("upload_success")
        private Boolean uploadSuccess;
    
        // 文件地址
        @Field("url")
        private String url;
    }
    

具体实现过程

  • 返回vo文件

    @Data
    public class MultipartFileVo {
    
        // 上传记录
        private Boolean needMerge;
        // 已上传的切片
        private List<Integer> uploaded;
    
        // 是否秒传
        private Boolean skipUpload;
        // 秒传地址
        private String url;
    }
    
  1. 判断是否上传过文件

    // 判断是否上传过文件
    @Transactional
    public MultipartFileVo isUploadFile(MultipartFileDto multipartFile) {
        MultipartFileVo multipartFileVo = new MultipartFileVo();
        CompleteFilePo completeFilePo = completeRepository.findByIdentifier(multipartFile.getIdentifier());
        if (completeFilePo == null) {
            // 首次上传
            completeFilePo = new CompleteFilePo();
            completeFilePo.setIdentifier(multipartFile.getIdentifier());
            completeFilePo.setTotalSize(multipartFile.getTotalSize());
            completeFilePo.setTotalChunk(multipartFile.getTotalChunks());
            completeFilePo.setUploadSuccess(false);
            completeFilePo.setUrl(filePathDir + multipartFile.getRelativePath());
            completeRepository.insert(completeFilePo);
            multipartFileVo.setNeedMerge(false);
        } else if (!completeFilePo.getUploadSuccess()) {
            // 断点续传
            List<MultipartFilePo> multipartFilePoList = multipartRepository.findByIdentifier(multipartFile.getIdentifier());
            List<Integer> chunkIdList = multipartFilePoList.stream().map(MultipartFilePo::getChunkNumber).sorted().collect(Collectors.toList());
            multipartFileVo.setUploaded(chunkIdList);
            multipartFileVo.setNeedMerge(true);
            multipartFileVo.setSkipUpload(false);
        } else {
            // 秒传
            multipartFileVo.setSkipUpload(true);
            multipartFileVo.setUrl(completeFilePo.getUrl());
        }
        return multipartFileVo;
    }
    
  2. 判断文件是否上传完成

    // 判断是否上传完成
    private MultipartFileVo isUploadOver(MultipartFileDto multipartFile) {
        MultipartFileVo multipartFileVo = new MultipartFileVo();
        List<MultipartFilePo> multipartFilePoList = multipartRepository.findByIdentifier(multipartFile.getIdentifier());
        long totalChunkSize = multipartFilePoList.stream().mapToLong(MultipartFilePo::getUploaderSize).sum();
        if (totalChunkSize == multipartFile.getTotalSize()) {
            // 更新文件状态
            CompleteFilePo completeFilePo = completeRepository.findByIdentifier(multipartFile.getIdentifier());
            completeFilePo.setUploadSuccess(true);
            completeRepository.save(completeFilePo);
            // 返回前端
            multipartFileVo.setSkipUpload(true);
            multipartFileVo.setUrl(completeFilePo.getUrl());
        } else {
            multipartFileVo.setNeedMerge(true);
            multipartFileVo.setSkipUpload(false);
        }
        return multipartFileVo;
    }
    
  3. 保存已上传的文件信息

    // 保存上传文件
    @Transactional
    public MultipartFileVo uploadFile(MultipartFileDto multipartFileDto) {
        MultipartFilePo multipartFilePo = new MultipartFilePo();
        multipartFilePo.setIdentifier(multipartFileDto.getIdentifier());
        multipartFilePo.setUploaderSize(multipartFileDto.getCurrentChunkSize());
        multipartFilePo.setChunkNumber(multipartFileDto.getChunkNumber());
        // 将该切片信息保存到mongodb中
        multipartRepository.insert(multipartFilePo);
        return isUploadOver(multipartFileDto);
    }
    
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值