Java大文件上传、分片上传、多文件上传、断点续传、上传文件minio、分片上传minio等解决方案

news2024/11/24 17:35:27

一、上传说明

         文件上传花样百出,根据不同场景使用不同方案进行实现尤为必要。通常开发过程中,文件较小,直接将文件转化为字节流上传到服务器,但是文件较大时,用普通的方法上传,显然效果不是很好,当文件上传一半中断再次上传时,发现需要重新开始,这种体验不是很爽,下面介绍几种好一点儿的上传方式。

        这里讲讲如何在Spring boot 编写上传代码,如有问题可以在下留言,我并在文章末尾附上Java上传源码供大家下载。

分片上传

分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分
隔成多个数据块(我们称之为Part)来进行分别上传,上传完之后再
由服务端对所有上传的文件进行汇总整合成原始的文件。

断点续传

断点续传是在下载/上传时,将下载/上传任务(一个文件或一个压缩
包)人为的划分为几个部分,每一个部分采用一个线程进行上传/下载,
如果碰到网络故障,可以从已经上传/下载的部分开始继续上传/下载
未完成的部分,而没有必要从头开始上传/下载。

二、Redis启动安装

Redis安装包分为 Windows 版和 Linux 版:
Windows版下载地址:https://github.com/microsoftarchive/redis/releases
Linux版下载地址: https://download.redis.io/releases/
我当前使用的Windows版本:

三、minio下载启动

windows版本可以参考我之前的文档:window10安装minio_minio windows安装-CSDN博客

启动会提示:

以上是密码设置问题需要修改如下:

set MINIO_ROOT_USER=admin
set MINIO_ROOT_PASSWORD=12345678

启动成功后会输出相应地址

四、上传后端Java代码

        后端采用Spring boot项目结构,主要代码如下:

  /**
     * 单文件上传
     * 直接将传入的文件通过io流形式直接写入(服务器)指定路径下
     *
     * @param file 上传的文件
     * @return
     */
    @Override
    public ResultEntity<Boolean> singleFileUpload(MultipartFile file) {
        //实际情况下,这些路径都应该是服务器上面存储文件的路径
        String filePath = System.getProperty("user.dir") + "\\file\\";
        File dir = new File(filePath);
        if (!dir.exists()) dir.mkdir();

        if (file == null) {
            return ResultEntity.error(false, "上传文件为空!");
        }
        InputStream fileInputStream = null;
        FileOutputStream fileOutputStream = null;
        try {
            String filename = file.getOriginalFilename();
            fileOutputStream = new FileOutputStream(filePath + filename);
            fileInputStream = file.getInputStream();

            byte[] buf = new byte[1024 * 8];
            int length;
            while ((length = fileInputStream.read(buf)) != -1) {//读取fis文件输入字节流里面的数据
                fileOutputStream.write(buf, 0, length);//通过fos文件输出字节流写出去
            }
            log.info("单文件上传完成!文件路径:{},文件名:{},文件大小:{}", filePath, filename, file.getSize());
            return ResultEntity.success(true, "单文件上传完成!");
        } catch (IOException e) {
            return ResultEntity.error(true, "单文件上传失败!");
        } finally {
            try {
                if (fileOutputStream != null) {
                    fileOutputStream.close();
                    fileOutputStream.flush();
                }
                if (fileInputStream != null) {
                    fileInputStream.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 多文件上传
     * 直接将传入的多个文件通过io流形式直接写入(服务器)指定路径下
     * 写入指定路径下是通过多线程进行文件写入的,文件写入线程执行功能就和上面单文件写入是一样的
     *
     * @param files 上传的所有文件
     * @return
     */
    @Override
    public ResultEntity<Boolean> multipleFileUpload(MultipartFile[] files) {
        //实际情况下,这些路径都应该是服务器上面存储文件的路径
        String filePath = System.getProperty("user.dir") + "\\file\\";
        File dir = new File(filePath);
        if (!dir.exists()) dir.mkdir();

        if (files.length == 0) {
            return ResultEntity.error(false, "上传文件为空!");
        }
        ArrayList<String> uploadFiles = new ArrayList<>();
        try {

            ArrayList<Future<String>> futures = new ArrayList<>();
            //使用多线程来完成对每个文件的写入
            for (MultipartFile file : files) {
                futures.add(partMergeTask.submit(new MultipleFileTaskExecutor(filePath, file)));
            }

            //这里主要用于监听各个文件写入线程是否执行结束
            int count = 0;
            while (count != futures.size()) {
                for (Future<String> future : futures) {
                    if (future.isDone()) {
                        uploadFiles.add(future.get());
                        count++;
                    }
                }
                Thread.sleep(1);
            }
            log.info("多文件上传完成!文件路径:{},文件信息:{}", filePath, uploadFiles);
            return ResultEntity.success(true, "多文件上传完成!");
        } catch (Exception e) {
            log.error("多文件分片上传失败!", e);
            return ResultEntity.error(true, "多文件上传失败!");
        }

    }

    /**
     * 单文件分片上传
     * 直接将传入的文件分片通过io流形式写入(服务器)指定临时路径下
     * 然后判断是否分片都上传完成,如果所有分片都上传完成的话,就把临时路径下的分片文件通过流形式读入合并并从新写入到(服务器)指定文件路径下
     * 最后删除临时文件和临时文件夹,临时文件夹是通过文件的uuid进行命名的
     *
     * @param filePart  分片文件
     * @param partIndex 当前分片值
     * @param partNum   所有分片数
     * @param fileName  当前文件名称
     * @param fileUid   当前文件uuid
     * @return
     */
    @Override
    public ResultEntity<Boolean> singleFilePartUpload(MultipartFile filePart, Integer partIndex, Integer partNum, String fileName, String fileUid) {
        //实际情况下,这些路径都应该是服务器上面存储文件的路径
        String filePath = System.getProperty("user.dir") + "\\file\\";//文件存放路径
        String tempPath = filePath + "temp\\" + fileUid;//临时文件存放路径
        File dir = new File(tempPath);
        if (!dir.exists()) dir.mkdirs();

        //生成一个临时文件名
        String tempFileNamePath = tempPath + "\\" + fileName + "_" + partIndex + ".part";
        try {
            //将分片存储到临时文件夹中
            filePart.transferTo(new File(tempFileNamePath));

            File tempDir = new File(tempPath);
            File[] tempFiles = tempDir.listFiles();

            one:
            if (partNum.equals(Objects.requireNonNull(tempFiles).length)) {
                //需要校验一下,表示已有异步程序正在合并了;如果是分布式这个校验可以加入redis的分布式锁来完成
                if (isMergePart.get(fileUid) != null) {
                    break one;
                }
                isMergePart.put(fileUid, tempFiles.length);
                System.out.println("所有分片上传完成,预计总分片:" + partNum + "; 实际总分片:" + tempFiles.length);

                FileOutputStream fileOutputStream = new FileOutputStream(filePath + fileName);
                //这里如果分片很多的情况下,可以采用多线程来执行
                for (int i = 0; i < partNum; i++) {
                    //读取分片数据,进行分片合并
                    FileInputStream fileInputStream = new FileInputStream(tempPath + "\\" + fileName + "_" + i + ".part");
                    byte[] buf = new byte[1024 * 8];//8MB
                    int length;
                    while ((length = fileInputStream.read(buf)) != -1) {//读取fis文件输入字节流里面的数据
                        fileOutputStream.write(buf, 0, length);//通过fos文件输出字节流写出去
                    }
                    fileInputStream.close();
                }
                fileOutputStream.flush();
                fileOutputStream.close();

                // 删除临时文件夹里面的分片文件 如果使用流操作且没有关闭输入流,可能导致删除失败
                for (int i = 0; i < partNum; i++) {
                    boolean delete = new File(tempPath + "\\" + fileName + "_" + i + ".part").delete();
                    File file = new File(tempPath + "\\" + fileName + "_" + i + ".part");
                }
                //在删除对应的临时文件夹
                if (Objects.requireNonNull(tempDir.listFiles()).length == 0) {
                    tempDir.delete();
                }
                isMergePart.remove(fileUid);
            }

        } catch (Exception e) {
            log.error("单文件分片上传失败!", e);
            return ResultEntity.error(false, "单文件分片上传失败");
        }
        //通过返回成功的分片值,来验证分片是否有丢失
        return ResultEntity.success(true, partIndex.toString());
    }

    /**
     * 多文件分片上传
     * 先将所有文件分片读入到(服务器)指定临时路径下,每个文件的分片文件的临时文件夹都是已文件的uuid进行命名的
     * 然后判断对已经上传所有分片的文件进行合并,此处是通过多线程对每一个文件的分片文件进行合并的
     * 最后对已经合并完成的分片临时文件和文件夹进行删除
     *
     * @param filePart  分片文件
     * @param partIndex 当前分片值
     * @param partNum   总分片数
     * @param fileName  当前文件名称
     * @param fileUid   当前文件uuid
     * @return
     */
    @Override
    public ResultEntity<String> multipleFilePartUpload(MultipartFile filePart, Integer partIndex, Integer partNum, String fileName, String fileUid) {
        //实际情况下,这些路径都应该是服务器上面存储文件的路径
        String filePath = System.getProperty("user.dir") + "\\file\\";//文件存放路径
        String tempPath = filePath + "temp\\" + fileUid;//临时文件存放路径
        File dir = new File(tempPath);
        if (!dir.exists()) dir.mkdirs();
        //生成一个临时文件名
        String tempFileNamePath = tempPath + "\\" + fileName + "_" + partIndex + ".part";
        try {
            filePart.transferTo(new File(tempFileNamePath));

            File tempDir = new File(tempPath);
            File[] tempFiles = tempDir.listFiles();
            //如果临时文件夹中分片数量和实际分片数量一致的时候,就需要进行分片合并
            one:
            if (partNum.equals(tempFiles.length)) {
                //需要校验一下,表示已有异步程序正在合并了;如果是分布式这个校验可以加入redis的分布式锁来完成
                if (isMergePart.get(fileUid) != null) {
                    break one;
                }
                isMergePart.put(fileUid, tempFiles.length);
                System.out.println(fileName + ":所有分片上传完成,预计总分片:" + partNum + "; 实际总分片:" + tempFiles.length);

                //使用多线程来完成对每个文件的合并
                Future<Integer> submit = partMergeTask.submit(new PartMergeTaskExecutor(filePath, tempPath, fileName, partNum));
                System.out.println("上传文件名:" + fileName + "; 总大小:" + submit.get());
                isMergePart.remove(fileUid);
            }
        } catch (Exception e) {
            log.error("{}:多文件分片上传失败!", fileName, e);
            return ResultEntity.error("", "多文件分片上传失败");
        }
        //通过返回成功的分片值,来验证分片是否有丢失
        return ResultEntity.success(partIndex.toString(), fileUid);
    }

    /**
     * 多文件(分片)秒传
     * 通过对比已有的文件分片md5值和需要上传文件分片的MD5值,
     * 在文件分片合并的时候,对已有的文件进行地址索引,对没有的文件进行临时文件写入
     * 最后合并的时候根据不同的文件分片进行文件读取写入
     *
     * @param filePart  上传没有的分片文件
     * @param fileInfo  当前分片文件相关信息
     * @param fileOther 已存在文件分片相关信息
     * @return
     */
    @Override
    public ResultEntity<String> multipleFilePartFlashUpload(MultipartFile filePart, String fileInfo, String fileOther) {
        DiskFileIndexVo upFileInfo = JSONObject.parseObject(fileInfo, DiskFileIndexVo.class);
        List<DiskFileIndexVo> notUpFileInfoList = JSON.parseArray(fileOther, DiskFileIndexVo.class);
        //实际情况下,这些路径都应该是服务器上面存储文件的路径
        String filePath = System.getProperty("user.dir") + "\\file\\";//文件存放路径
        //正常情况下,这个临时文件也应该放入(服务器)非临时文件夹中,这样方便下次其他文件上传查找是否曾经上传过类似的
        //当前demo是单独存放在临时文件夹中,文件合并完成之后直接删除的
        String tempPath = filePath + "temp\\" + upFileInfo.getFileUid();//临时文件存放路径

        File dir = new File(tempPath);
        if (!dir.exists()) dir.mkdirs();
        //生成一个临时文件名
        String tempFileNamePath = tempPath + "\\" + upFileInfo.getFileName() + "_" + upFileInfo.getPartIndex() + ".part";

        try {
            filePart.transferTo(new File(tempFileNamePath));

            File tempDir = new File(tempPath);
            File[] tempFiles = tempDir.listFiles();
            notUpFileInfoList = notUpFileInfoList.stream().filter(e ->
                    upFileInfo.getFileUid().equals(e.getFileUid())).collect(Collectors.toList());
            //如果临时文件夹中分片数量和实际分片数量一致的时候,就需要进行分片合并
            one:
            if ((upFileInfo.getPartNum() - notUpFileInfoList.size()) == tempFiles.length) {
                //需要校验一下,表示已有异步程序正在合并了;如果是分布式这个校验可以加入redis的分布式锁来完成
                if (isMergePart.get(upFileInfo.getFileUid()) != null) {
                    break one;
                }
                isMergePart.put(upFileInfo.getFileUid(), tempFiles.length);
                System.out.println(upFileInfo.getFileName() + ":所有分片上传完成,预计总分片:" + upFileInfo.getPartNum()
                        + "; 实际总分片:" + tempFiles.length + "; 已存在分片数:" + notUpFileInfoList.size());

                //使用多线程来完成对每个文件的合并
                Future<Integer> submit = partMergeTask.submit(
                        new PartMergeFlashTaskExecutor(filePath, upFileInfo, notUpFileInfoList));
                isMergePart.remove(upFileInfo.getFileUid());
            }
        } catch (Exception e) {
            log.error("{}:多文件(分片)秒传失败!", upFileInfo.getFileName(), e);
            return ResultEntity.error("", "多文件(分片)秒传失败!");
        }
        //通过返回成功的分片值,来验证分片是否有丢失
        return ResultEntity.success(upFileInfo.getPartIndex().toString(), upFileInfo.getFileUid());
    }

    /**
     * 根据传入需要上传的文件片段的md5值来对比服务器中的文件的md5值,将已有对应的md5值的文件过滤出来,
     * 通知前端或者自行出来这些文件,即为不需要上传的文件分片,并将已有的文件分片地址索引返回给前端进行出来
     *
     * @param upLoadFileListMd5 原本需要上传文件的索引分片信息
     * @return
     */
    @Override
    public ResultEntity<List<DiskFileIndexVo>> checkDiskFile(List<DiskFileIndexVo> upLoadFileListMd5) {
        List<DiskFileIndexVo> notUploadFile;
        try {
            //后端服务器已经存在的分片md5值集合
            List<DiskFileIndexVo> diskFileMd5IndexList = diskFileIndexVos;

            notUploadFile = upLoadFileListMd5.stream().filter(uf -> diskFileMd5IndexList.stream().anyMatch(
                    df -> {
                        if (df.getFileMd5().equals(uf.getFileMd5())) {
                            uf.setFileIndex(df.getFileName());//不需要上传文件的地址索引
                            return true;
                        }
                        return false;
                    })).collect(Collectors.toList());
            log.info("过滤出不需要上传的文件分片:{}", notUploadFile);
        } catch (Exception e) {
            log.error("上传文件检测异常!", e);
            return ResultEntity.error("上传文件检测异常!");
        }
        return ResultEntity.success(notUploadFile);
    }

    /**
     * 根据文件uuid(md5生成的)来判断此文件在服务器中是否未上传完整,
     * 如果没上传完整,则返回相关上传进度等信息
     *
     * @param pointFileIndexVo
     * @return
     */
    @Override
    public ResultEntity<PointFileIndexVo> checkUploadFileIndex(PointFileIndexVo pointFileIndexVo) {
        try {
            List<String> list = uploadProgress.get(pointFileIndexVo.getFileMd5());
            if (list == null) list = new ArrayList<>();
            pointFileIndexVo.setParts(list);
            System.out.println("已上传部分:" + list);
            return ResultEntity.success(pointFileIndexVo);
        } catch (Exception e) {
            log.error("上传文件检测异常!", e);
            return ResultEntity.error("上传文件检测异常!");
        }
    }

    /**
     * 单文件(分片)断点上传
     *
     * @param filePart 需要上传的分片文件
     * @param fileInfo 当前需要上传的分片文件信息,如uuid,文件名,文件总分片数量等
     * @return
     */
    @Override
    public ResultEntity<String> singleFilePartPointUpload(MultipartFile filePart, String fileInfo) {
        PointFileIndexVo pointFileIndexVo = JSONObject.parseObject(fileInfo, PointFileIndexVo.class);
        //实际情况下,这些路径都应该是服务器上面存储文件的路径
        String filePath = System.getProperty("user.dir") + "\\file\\";//文件存放路径
        String tempPath = filePath + "temp\\" + pointFileIndexVo.getFileMd5();//临时文件存放路径
        File dir = new File(tempPath);
        if (!dir.exists()) dir.mkdirs();

        //生成一个临时文件名
        String tempFileNamePath = tempPath + "\\" + pointFileIndexVo.getFileName() + "_" + pointFileIndexVo.getPartIndex() + ".part";
        try {
            //将分片存储到临时文件夹中
            filePart.transferTo(new File(tempFileNamePath));

            List<String> partIndex = uploadProgress.get(pointFileIndexVo.getFileMd5());
            if (Objects.isNull(partIndex)) {
                partIndex = new ArrayList<>();
            }
            partIndex.add(pointFileIndexVo.getPartIndex().toString());
            uploadProgress.put(pointFileIndexVo.getFileMd5(), partIndex);

            File tempDir = new File(tempPath);
            File[] tempFiles = tempDir.listFiles();

            one:
            if (pointFileIndexVo.getPartNum().equals(Objects.requireNonNull(tempFiles).length)) {
                //需要校验一下,表示已有异步程序正在合并了;如果是分布式这个校验可以加入redis的分布式锁来完成
                if (isMergePart.get(pointFileIndexVo.getFileMd5()) != null) {
                    break one;
                }
                isMergePart.put(pointFileIndexVo.getFileMd5(), tempFiles.length);
                System.out.println("所有分片上传完成,预计总分片:" + pointFileIndexVo.getPartNum() + "; 实际总分片:" + tempFiles.length);
                //读取分片数据,进行分片合并
                FileOutputStream fileOutputStream = new FileOutputStream(filePath + pointFileIndexVo.getFileName());
                //这里如果分片很多的情况下,可以采用多线程来执行
                for (int i = 0; i < pointFileIndexVo.getPartNum(); i++) {
                    FileInputStream fileInputStream = new FileInputStream(tempPath + "\\" + pointFileIndexVo.getFileName() + "_" + i + ".part");
                    byte[] buf = new byte[1024 * 8];//8MB
                    int length;
                    while ((length = fileInputStream.read(buf)) != -1) {//读取fis文件输入字节流里面的数据
                        fileOutputStream.write(buf, 0, length);//通过fos文件输出字节流写出去
                    }
                    fileInputStream.close();
                }
                fileOutputStream.flush();
                fileOutputStream.close();

                // 删除临时文件夹里面的分片文件 如果使用流操作且没有关闭输入流,可能导致删除失败
                for (int i = 0; i < pointFileIndexVo.getPartNum(); i++) {
                    boolean delete = new File(tempPath + "\\" + pointFileIndexVo.getFileName() + "_" + i + ".part").delete();
                    File file = new File(tempPath + "\\" + pointFileIndexVo.getFileName() + "_" + i + ".part");
                }
                //在删除对应的临时文件夹
                if (Objects.requireNonNull(tempDir.listFiles()).length == 0) {
                  tempDir.delete();
                }
                isMergePart.remove(pointFileIndexVo.getFileMd5());
                uploadProgress.remove(pointFileIndexVo.getFileMd5());
            }

        } catch (Exception e) {
            log.error("单文件分片上传失败!", e);
            return ResultEntity.error(pointFileIndexVo.getFileMd5(), "单文件分片上传失败");
        }
        //通过返回成功的分片值,来验证分片是否有丢失
        return ResultEntity.success(pointFileIndexVo.getFileMd5(), pointFileIndexVo.getPartIndex().toString());
    }

    /**
     * 获取(服务器)指定文件存储路径下所有文件MD5值
     * 实际情况下,每一个文件的md5值都是单独保存在数据库或者其他存储机制中的,
     * 不需要每次都去读取文件然后获取md5值,这样多次io读取很耗性能
     *
     * @return
     * @throws Exception
     */
    @Bean
    private List<DiskFileIndexVo> getDiskFileMd5Index() throws Exception {
        String filePath = System.getProperty("user.dir") + "\\file\\part\\";
        File saveFileDir = new File(filePath);
        if (!saveFileDir.exists()) saveFileDir.mkdirs();

        List<DiskFileIndexVo> diskFileIndexVoList = new ArrayList<>();
        File[] listFiles = saveFileDir.listFiles();
        if (listFiles == null) return diskFileIndexVoList;

        for (File listFile : listFiles) {
            String fileName = listFile.getName();
            FileInputStream fileInputStream = new FileInputStream(filePath + fileName);
            String md5DigestAsHex = DigestUtils.md5DigestAsHex(fileInputStream);

            DiskFileIndexVo diskFileIndexVo = new DiskFileIndexVo();
            diskFileIndexVo.setFileName(fileName);
            diskFileIndexVo.setFileMd5(md5DigestAsHex);
            diskFileIndexVoList.add(diskFileIndexVo);
            fileInputStream.close();
        }

        diskFileIndexVos = diskFileIndexVoList;
        log.info("服务器磁盘所有文件 {}", diskFileIndexVoList);
        return diskFileIndexVoList;
    }

 代码结构图:

五、前端代码

   整体的过程如下

前端将文件按照百分比进行计算,每次上传文件的百分之一(文件分片),给文件分片做上序号及文件uuid,然后在循环里面对文件片段上传的时候在将当前分片值一起传给后端。
后端将前端每次上传的文件,放入到缓存目录;
前端将全部的文件内容都上传完毕后,发送一个合并请求;
后端合并分片的之后对文件进行命名保存;
后端保存临时分片的时候命名索引,方便合并的时候按照分片索引进行合并;

vue模板代码:

      <!-- 单文件分片上传 -->
      <div class="fileUploadStyle">
        <h3>单文件分片上传</h3>
        <el-upload ref="upload" name="files" action="#" :on-change="selectSinglePartFile"
          :on-remove="removeSingleFilePart" :file-list="singleFilePart.fileList" :auto-upload="false">
          <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
          <el-button style="margin-left: 10px;" size="small" type="success"
            @click="singleFilePartUpload">点击进行单文件分片上传</el-button>
          <div slot="tip" class="el-upload__tip">主要用于测试单文件分片上传</div>
        </el-upload>
        <el-progress :text-inside="true" class="progress" :stroke-width="26" :percentage="singlePartFileProgress" />
      </div>
      <!-- 多文件分片上传 -->
      <div class="fileUploadStyle">
        <h3>多文件分片上传</h3>
        <el-upload ref="upload" name="files" action="#" :on-change="selectMultiplePartFile"
          :on-remove="removeMultiplePartFile" :file-list="multipleFilePart.fileList" :auto-upload="false">
          <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
          <el-button style="margin-left: 10px;" size="small" type="success"
            @click="multipleFilePartUpload">点击进行多文件分片上传</el-button>
          <div slot="tip" class="el-upload__tip">主要用于测试多文件分片上传</div>
        </el-upload>
      </div>
      <!-- 多文件(分片)秒传 -->
      <div class="fileUploadStyle">
        <h3>多文件(分片MD5值)秒传</h3>
        <el-upload ref="upload" name="files" action="#" :on-change="selectMultiplePartFlashFile"
          :on-remove="removeMultiplePartFlashFile" :file-list="multipleFilePartFlash.fileList" :auto-upload="false">
          <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
          <el-button style="margin-left: 10px;" size="small" type="success"
            @click="multipleFilePartFlashUpload">点击进行文件秒传</el-button>
          <div slot="tip" class="el-upload__tip">主要用于测试多文件(分片MD5值)秒传</div>
        </el-upload>
      </div>

js属性定义:

上传部分代码:

minio分片上传:

上传样式:

六、功能演示及源码

部分演示图: 这里就以上传minio为例,测试上传minio以分片方式上传

以8M进行分切 28M刚好分了四个区,我们使用redis客户工具查看

最后成功上传到minio中

而且看到上传文件大小为:28M

       文件上传代码其实功能也简单也很明确,先将一个大文件分成n个小文件,然后给后端检测这些分片是否曾经上传中断过,即对这些分片进行过滤出来,并将过滤出对应的分片定位值结果返回给前端处理出不需要上传的分片和需要上传的文件分片,这里主要还是区分到确定是这个文件的分区文件。

        这里,为了方便大家直接能够使用Java源码,本文所有都采用Spring boot框架模式,另外使用了第三方插件,如果大家使用中没有使用到minio可以不需要引入并把相关代码移除即可,代码使用了redis作为分区数量缓存,相对于Java内存更稳定些。

demo源码下载gitee地址(代码包含Java后端工程和vue2前端工程):java-file-upload-demo: java 多文件上传、多文件分片上传、多文件秒传、minio分片上传等功能

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

docker 拉取不到镜像的问题:拉取超时

error pulling image configuration: download failed after attempts6: dial tcp 31.13.94.10:443: i/o timeout 首先设置国内的镜像源&#xff1a;复制下面直接执行 sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-EOF{"registry-mirrors"…

k8s学习--Secret详细解释与应用

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 Secret什么是Secret?Secret四种类型及其特点Secret应用案例&#xff08;1&#xff09;将明文密码进行base64编码&#xff08;2&#xff09;编写创建secret的YAML文…

【人工智能003】图像识别算法模型常见术语简单总结(已更新)

1.熟悉、梳理、总结数据分析实战中的AI图像识别等实战研发知识体系&#xff0c;这块领域很大&#xff0c;需要耗费很多精力&#xff0c;逐步总结、更新到位&#xff0c;&#xff0c;&#xff0c; 2.欢迎点赞、关注、批评、指正&#xff0c;互三走起来&#xff0c;小手动起来&am…

心理咨询系统|心理咨询系统成品开发功能

心理咨询系统开发后端设计是一个复杂且精细的过程&#xff0c;涉及多个关键领域的专业知识和技术。本文将详细探讨心理咨询系统开发后端设计的各个方面&#xff0c;包括系统架构、数据库设计、接口开发、安全性保障以及性能优化等。 首先&#xff0c;我们来谈谈系统架构。在心理…

贝锐蒲公英异地组网:降低建筑工地远程视频监控成本、简化运维

中联建设集团股份有限公司是一家建筑行业的施工单位&#xff0c;专注于建筑施工&#xff0c;业务涉及市政公用工程施工总承包、水利水电工程施工总承包、公路工程施工总承包、城市园林绿化专业承包等&#xff0c;在全国各地开展有多个建筑项目&#xff0c;并且项目时间周期可能…

mac M1下安装PySide2

在M1下装不了PySide2, 是因为PySide2没有arm架构的包 1 先在M1上装qt5 安装qt主要是为了能用里面的Desinger, uic, rcc brew install qt5 我装完的路径在/opt/homebrew/opt/qt5 其中Designer就是用来设计界面的 rcc用resource compiler, 编绎rc资源文件的, 生成对应的py文件…

深入了解 C 语言 Bug

目录 一、引言二、Bug的定义三、Bug的由来四、Bug的影响五、应对 Bug 的方法六、结论 一、引言 1、在 C 语言的编程世界中&#xff0c;Bug 是一个我们无法回避的话题。 2、Bug&#xff0c;简单来说&#xff0c;就是程序中存在的错误或缺陷。它可以表现为程序运行结果的异常、崩…

ESP8266在阿里云上线(arduino)

电脑连接上ESP8266的板子 在arduino编写代码&#xff08;arduino按照之前的配置&#xff0c;已安装好esp的开发板和几个库ArduinoJson我选了5.的版本&#xff0c;PubSubclient,Crypto,AliyunIoTSDK并且修改pubsubclient的参数&#xff09; 在项目&#xff0c;加载库&#xff0c…

【ai】DeepStream 简介

NVIDIA Metropolis 平台。 NVIDIA 大都会 利用视觉 AI 将来自数万亿物联网设备的数据转化为有价值的见解。 NVIDIA Metropolis 是一个应用程序框架、一套开发工具和合作伙伴生态系统,它将视觉数据和 AI 结合在一起,以提高各行各业的运营效率和安全性。它有助于理解数万亿个…

【轻量化】YOLOv10: Real-Time End-to-End Object Detection

论文题目&#xff1a;YOLOv10: Real-Time End-to-End Object Detection 研究单位&#xff1a;清华大学 论文链接&#xff1a;http://arxiv.org/abs/2405.14458 代码链接&#xff1a;https://github.com/THU-MIG/yolov10 推荐测试博客&#xff1a;YOLOv10最全使用教程&#xff0…

可视化数据科学平台在信贷领域应用系列三:特征组合

现代各企业都提倡“降本增效”&#xff0c;所以越来越多优秀的工具诞生了。若想在特征加工这块工作上提升效率&#xff0c;建模人员也能有更多时间“偷懒”&#xff0c;都 “Sora”时代了&#xff0c;为啥不巧用工具呢&#xff1f;RapidMiner在信贷风控特征加工组合中是一把利器…

图像算法---自动曝光AE

一&#xff0c;自动曝光AE 自动曝光&#xff08;AE&#xff0c;全称Auto Exposure&#xff09;是一种在摄影和摄像中广泛使用的技术&#xff0c;它允许相机或摄像机根据环境光线条件自动调整曝光参数&#xff0c;以获得清晰、亮度适中的图像或视频。以下是关于自动曝光AE的详细…

LabVIEW的大气环境实时监测

LabVIEW的大气环境实时监测 设计并实现了一个基于LabVIEW的大气环境实时监测系统。通过使用高精度环境传感器采集温度、湿度、PM2.5、CO2等环境数据&#xff0c;利用LabVIEW进行数据处理、显示和存储。该系统能够实时监控环境参数&#xff0c;并通过阈值报警功能提示异常&…

60万路由器一夜瘫痪,美国遭遇神秘网络攻击大灾难;木马肆虐俄罗斯,关键信息基础设施惨遭毒手!Oracle漏洞被利用,CISA紧急行动!| 安全周报0606

新闻1&#xff1a;欧洲警报&#xff1a;俄GRU支持APT28发动HeadLace恶意软件攻击&#xff01; 俄罗斯GRU支持的威胁行为者APT28被认为在一系列活动中负责&#xff0c;这些活动使用HeadLace恶意软件和收集凭证的网页针对欧洲各地的网络。 APT28&#xff0c;也被称为BlueDelta、…

springboot undertow 文件上传文件过大异常

io.undertow.server.RequestTooBigException: UT000020 Connection terminated as request was larger than xxxx 修改yaml文件中关于undertow的配置项 server:undertow:# HTTP POST请求最大的大小# 默认0&#xff0c;无限制max-http-post-size: ${SERVER_UNDERTOW_MAX_HTTP_…

比较FITC-BSA与未标记BSA在生物活性方面的差异

牛血清白蛋白&#xff08;BSA&#xff09;作为一种诸多使用的生物化学试剂&#xff0c;在生物学和医学领域发挥着作用。然而&#xff0c;为了研究BSA在生物体内的行为以及与其他分子的相互作用&#xff0c;科学家们常常需要对其进行荧光标记&#xff0c;其中FITC-BSA便是常见的…

WEB-Wordlist-Generator:为扫描后的Web应用生成相关联的字典

关于WEB-Wordlist-Generator WEB-Wordlist-Generator是一款功能强大的字典生成工具&#xff0c;该工具旨在帮助广大研究人员扫描目标Web应用程序并生成与之相关联的字典文件&#xff0c;从而允许我们对相关的网络威胁行为执行预备性应对策略。 功能介绍 当前版本的WEB-Wordli…

vue页面上的form表单无法输入咋回事?

记录工作中遇到的问题… 如下图&#xff1a;下拉框选中无法回显&#xff0c;输入框无法输入 排查了原因&#xff0c;原来是接收的formData不是响应式的 const props defineProps({formData: {type: Object,default: ()> [{devices:[]}]},})formData将其放在模板中使用&am…

揭秘APP广告投放数据背后的秘密,Xinstall助你精准触达用户!

随着互联网的发展&#xff0c;App推广和运营的环境也在不断变化。传统的营销方式已难以满足多变的市场需求&#xff0c;如何确保在激烈的竞争中脱颖而出&#xff0c;成为了众多企业关注的焦点。Xinstall作为一款专业的App推广工具&#xff0c;致力于帮助企业解决推广和运营中的…

Linux网络服务之SSH(远程访问及控制)

ssh远程管理&#xff1a; ssh是一种安全通道协议&#xff0c;用来实现字符界面的远程登录。远程复制&#xff0c;远程文本传输。 ssh对通信双方的数据进行了加密 用户名和密码登录 密钥对认证方式&#xff08;可以实现免密登录&#xff09; ssh 22 网络层 传输层 数据传输…