文件上传与下载(二)大文件上传

文件过大上传处理:文件过大时,推荐端侧上传即前端(调用三方分片上传或三方的其他优化方法)直接上传到云存储。本文主要针对服务端上传,即前端->服务端->云存储的过程优化。

一、图片压缩再上传:图片过大时上传会占用服务器内存,可以在前端进行压缩后上传,如图是我上传后下载的图片,原图为14M,压缩后只有270KB

<!-- @format -->
<template>
  <div >
    <div  ref="articalImg" class="articalImg">
            <span>
              <label class="button text-overflow" for="messageFile">
                <span>上传图片<i class="el-icon-plus"></i></span>
              </label>
              <form id="messageFileForm" class="upload-form">
                <input
                  ref="messageFile"
                  @change="fileChange($event)"
                  type="file"
                  id="messageFile"
                  style="position:absolute;clip:rect(0 0 0 0);"
                  accept="image/*"
                  class="upload-label"
                />
              </form>
            </span>
   </div>
  </div>
</template>

<script>
const MAX_IMAGE_WIDTH = 4096; // 图片最大宽度,超过了会裁剪
const MAX_IMAGE_PICK_SIZE = 20 * 1024 * 1024; // 选择图片最大大小,超过了不给旋转
const MAX_IMAGE_SIZE = 1 * 1024 * 1024; //超过1M压缩
export default {
  methods: {
    fileChange(ev) {
      let that = this;
      var fileList = ev.target.files;
      let file = fileList[0];
      let fileSize = file.size;
      if (fileSize > MAX_IMAGE_PICK_SIZE) {
        that.$Message.info('选择的图片最大不能超过20M');
        ev.target.value = ''; // 清除,否则上传同一文件不会触发
        return;
      }
      let quality = 1;
      if (fileSize > MAX_IMAGE_SIZE) {
        quality = MAX_IMAGE_SIZE / fileSize;
      }
      if (quality > 0.7) {
        quality = 0.7; //默认压缩比为7
      } else if (quality < 0.2) {
        quality = 0.2; //最小压缩比2
      }
      // 旋转方向为默认
      that.readImageFile(1, quality, file);
    },
	 readImageFile(origin, quality, file) {
      let that = this;
      let fileName = file.name;
      let reader = new FileReader();
      reader.onload = function(evt) {
        let base64File = evt.target.result;
        that.imageCompress(
          base64File,
          origin,
          {
            quality
          },
          function(result) {
            let blobFile = that.dataURLtoBlob(result);
            let compressFile = new window.File([blobFile], fileName, { type: file.type });
            that.uploadFile(compressFile);
          }
        );
      };
      reader.readAsDataURL(file);
    },
	 // 压缩图片
    imageCompress(path, Orientation, obj, callback) {
      let img = new Image();
      img.src = path;
      img.onload = function() {
        let that = this;
        // 默认按比例压缩
        let imgWidth = that.width;
        let imgHeight = that.height;
        let scale = imgWidth / imgHeight;
        if (imgWidth > MAX_IMAGE_WIDTH) {
          imgWidth = MAX_IMAGE_WIDTH;
          imgHeight = imgWidth / scale;
        }
        let quality = obj.quality || 0.7; // 默认图片质量为0.7
        // 生成canvas
        let canvas = document.createElement('canvas');
        let ctx = canvas.getContext('2d');
        let anw = document.createAttribute('width');
        let anh = document.createAttribute('height');

        anw.nodeValue = imgWidth;
        anh.nodeValue = imgHeight;
        canvas.setAttributeNode(anw);
        canvas.setAttributeNode(anh);
        ctx.drawImage(that, 0, 0, imgWidth, imgHeight);
        // quality值越小,所绘制出的图像越模糊
        let base64 = canvas.toDataURL('image/jpeg', quality);
        // 回调函数返回base64的值
        callback(base64);
      };
    },
    // 将base64转换为blob
    dataURLtoBlob(dataurl) {
      let arr = dataurl.split(',');
      let mime = arr[0].match(/:(.*?);/)[1];
      let bstr = atob(arr[1]);
      let n = bstr.length;
      let u8arr = new Uint8Array(n);
      while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
      }
      return new Blob([u8arr], {
        type: mime
      });
    },
   //上传api
    uploadFile(file) {
      let formData = new FormData();
      formData.append('file', file);
      //清空本次选择的文件
      this.$refs.messageFile.value = '';
      this.$api['file/upload'](formData, {
        headers: {
          'Content-Type': 'multipart/form-data'
        },
        timeout: 300000
      }).then(data => {
        if (data) {
         
        }
      });
    }
  }
};
</script>

二、大文件上传限制优化加快上传速度:

1、后端限制:大文件上传报错

org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (10486472) exceeds the configured maximum (10485760)

在springboot配置文件application.properties中加上配置:

(1)Spring Boot 1.4以下:

multipart.maxFileSize = 10Mb       //单个文件的大小
multipart.maxRequestSize=100Mb     //单次请求的文件的总大小

 (2)Spring Boot1.4版本后:

spring.http.multipart.maxFileSize = 10Mb 
spring.http.multipart.maxRequestSize=100Mb

(3)Spring Boot2.0之后:

spring.servlet.multipart.max-file-size=10000000
spring.servlet.multipart.max-request-size=100000000

 2、nginx限制:打开文件后前端一直转圈页面卡死,过一会甚至提示上传超时或文件过大,而在windows本地没有这种问题,这往往是nginx限制的原因,可以更改配置client_max_body_size nginx.conf 修改默认限制上传文件大小,修改后打开文件前端几乎无等待,快速进入后端接口。原因是nginx对上传文件大小有限制,而且默认是1M。如我限制最大可以上传1G,http块下加上:

client_max_body_size 1024M;

另外,若上传文件很大,还要适当调整上传超时时间:

#文件大小限制,默认1m。
#限制请求体的大小,若超过所设定的大小,返回413错误。
client_max_body_size     50m; 

#读取请求头的超时时间,若超过所设定的大小,返回408错误。
client_header_timeout    1m;

#读取请求实体的超时时间,若超过所设定的大小,返回413错误。
client_body_timeout      1m;

#http请求无法立即被容器(tomcat, netty等)处理,被放在nginx的待处理池中等待被处理。此参数为等待的最长时间,默认为60秒,官方推荐最长不要超过75秒。
proxy_connect_timeout     60s;

#http请求被容器(tomcat, netty等)处理后,nginx会等待处理结果,也就是容器返回的response。此参数即为服务器响应时间,默认60秒。
proxy_read_timeout      1m;

#http请求被服务器处理完后,把数据传返回给Nginx的用时,默认60秒。
proxy_send_timeout      1m;

3、后端卡顿:大文件上传占用带宽,上传一直不返回结果,页面则一直等待。这时候可以在后端异步上传,返回“上传中”结果给前端,在后端开启子线程进行上传;加上redis锁同一时间只能上传一个文件等方法来提升效率:

   private Long id; 
   
   @Override
    public Result<String> uploadFile(MultipartFile file) {
        if (redisClient.isExists(UPLOAD_FLAG)){
            Result<String> result = new Result<>();
            result.setCode("-1");
            result.setMessage("当前有文件在上传中,请稍后再试!");
            return result;
        }

        // 打上文件正在上传标识,防止并发上传文件
        redisClient.set(UPLOAD_FLAG,"true", 80000);

        try{
            UploadInfoDO uploadInfoDO = new UploadInfoDO();
			uploadInfoDO.setFileStatus("上传中");
            uploadInfoDO.setFileName(file.getOriginalFilename());
            fileMapper.inser(uploadInfoDO);
            id = uploadInfoDO.getId();
        } catch (Exception e){
            redisClient.expire(UPLOAD_FLAG, 0);
            Result<String> result = new Result<>();
            result.setCode("-1");
            result.setMessage("文件入库异常"+ e.getMessage());
            return result;
        }

        threadPoolTaskExecutor.execute(() -> upload(file));
        return new Result<>();
    }
	
	
	 //上传文件
	private void upload(MultipartFile file){
        String originalFilename = file.getOriginalFilename();
        try {
            //调用上传接口上传到服务器
			......
			//更新上传状态
            UploadInfoDO uploadInfoDO = new UploadInfoDO();
            uploadInfoDO.setId(id);
            uploadInfoDO.setUploadStatus("上传成功");
            fileMapper.updateVideoUploadInfoDO(UploadInfoDO);
        } catch (Exception e) {
            UploadInfoDO uploadInfoDO = new UploadInfoDO();
            uploadInfoDO.setId(id);
            uploadInfoDO.setUploadStatus("上传失败");
            fileMapper.update(uploadInfoDO);
        } finally {
            redisClient.expire(UPLOAD_FLAG, 0);
        }
    }

但是这种情况下,大文件单文件上传仍然很慢,这就需要分片上传。

三、前端分片上传到服务端:上传时在前端进行分片,后端保存分片,前端判断是最后一个分片时候调用合并接口,合并接口按照之前分片的顺序进行合并

1、前端:

<template>
  <div>
    <div class="upload-div">
      <span>
        <label class="button text-overflow" for="messageFile">
          <span class="upload-label">上传文件<i class="el-icon-plus"></i></span>
        </label>
        <form id="messageFileForm" class="upload-form">
          <input
            ref="messageFile"
            @change="cutFile($event)"
            type="file"
            id="messageFile"
            style="position: absolute; clip: rect(0 0 0 0)"
          />
        </form>
      </span>
    </div>
  </div>
</template>

<script>
import { cutFile, mergeFile } from "@/api/upload";
export default {
  methods: {
    cutFile(ev) {
      var fileList = ev.target.files;
      let file = fileList[0];
      ev.target.value = '';
      this.PostFile(file, 0);
    },
    //执行分片上传
    PostFile(file, i, uuid) {
      var name = file.name,                           //文件名
        size = file.size,                           //总大小shardSize = 2 * 1024 * 1024,
        shardSize = 1 * 1024 * 1024,                //以1MB为一个分片,每个分片的大小
        shardCount = Math.ceil(size / shardSize);   //总片数
      if (i >= shardCount) {
        return;
      }
      //判断uuid是否存在
      if (uuid == undefined || uuid == null) {
        uuid = this.guid();
      }
      //console.log(size,i+1,shardSize);  //文件总大小,第一次,分片大小//
      var start = i * shardSize;
      var end = start + shardSize;
      var packet = file.slice(start, end);  //将文件进行切片
      /*  构建form表单进行提交  */
      var form = new FormData();
      form.append("uuid", uuid);// 前端生成uuid作为标识符传个后台每个文件都是一个uuid防止文件串了
      form.append("data", packet); //slice方法用于切出文件的一部分
      form.append("name", name);
      form.append("totalSize", size);
      form.append("total", shardCount); //总片数
      form.append("index", i + 1); //当前是第几片
      cutFile(form)
        .then(res => {
          console.log(res);
          let msg = res.data
          /*  表示上一块文件上传成功,继续下一次  */
          if (msg.status == 201) {
            form = '';
            i++;
            this.PostFile(file, i, uuid);
          } else if (msg.status == 502) {
            form = '';
            /*  失败后,每2秒继续传一次分片文件  */
            setInterval(function () { PostFile(file, i, uuid) }, 2000);
          } else if (msg.status == 200) {
            this.merge(uuid, name)
            console.log("上传成功");
          } else if (msg.status == 500) {
            console.log('第' + msg.i + '次,上传文件有误!');
          } else {
            console.log('未知错误');
          }
        })
        .catch(() => { });
    },
    merge(uuid, fileName) {
      let file = { 'uuid': uuid, 'newFileName': fileName }
       mergeFile(file)
        .then(res => {
          alert("上传成功");
        })
        .catch(() => { });
    },
    guid() {
      return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        var r = Math.random() * 16 | 0,
          v = c == 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
      });
    }
  }
};
</script>
<style scoped >
.upload-div {
  margin: 20px 0;
}
.upload-label {
  background-color: #169bd5;
  color: #ffffff;
  padding: 8px 10px;
  font-weight: bold;
}
.upload-form {
  display: inline;
}
.tip {
  padding-left: 20px;
  color: #f59a23;
}
</style>

/api/upload.js: 

import request from '@/utils/request'

export function upload(formDatas) {
  return request({
    url: '/file/upload',
    method: 'post',
    upload: true,
    data: formDatas
  })
}

export function upload1(formDatas) {
  return request({
    url: '/file/upload1',
    method: 'post',
    upload: true,
    data: formDatas
  })
}

export function download() {
  return request({
    url: '/file/download',
    method: 'post'
  })
}

export function cutFile(formDatas) {
  return request({
    url: '/postFile/cutFile',
    method: 'post',
    upload: true,
    data: formDatas
  })
}

export function mergeFile(file) {
  const uuid = file.uuid
  const newFileName = file.newFileName
  return request({
    url: '/postFile/mergeFile',
    method: 'get',
    params: { uuid, newFileName }
  })
}

2、后端:

 配置文件支持最大1g文件上传:

server.port=9999
server.context-path=/demo
spring.redis.host=localhost
spring.redis.port=6379
#spring.redis.password=
spring.redis.database=1
spring.redis.pool.max-active=8
spring.redis.pool.max-wait=-1
spring.redis.pool.max-idle=500
spring.redis.pool.min-idle=0
spring.redis.timeout=0
#大文件上传
spring.http.multipart.enabled=true
spring.http.multipart.maxFileSize=1024MB
spring.http.multipart.maxRequestSize=1024MB

 分片上传和合并接口: 

package com.demo.rest;

import com.demo.dto.FileDTO;
import com.demo.dto.ResponseMessage;
import org.apache.tomcat.util.http.fileupload.FileUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.*;

@RestController
@RequestMapping("/postFile")
public class PostFileController {

    //分片目录
    private static String fileUploadTempDir = "D:/portalupload/fileuploaddir";
    //合并后的目录
    private static String fileUploadDir = "D:/portalupload/file";

    //保存分片的文件
    @RequestMapping("/cutFile")
    public ResponseMessage fragmentation(@ModelAttribute FileDTO fileDTO) {
        //resp.addHeader("Access-Control-Allow-Origin", "*");
        Map<String, Object> map = new HashMap<>();
        //MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) req;
        // 获得文件分片数据
        MultipartFile file = fileDTO.getData();
        //分片第几片
        int index = fileDTO.getIndex();
        //总片数
        int total = fileDTO.getTotal();
         //获取文件名
        String fileName = fileDTO.getName();
        String name = fileName.substring(0, fileName.lastIndexOf("."));
        String fileEnd = fileName.substring(fileName.lastIndexOf("."));
        //前端uuid,作为标识
        String uuid = fileDTO.getUuid();
        File uploadFile = new File(fileUploadTempDir + "/" + uuid, index + ".tem");
        if (!uploadFile.getParentFile().exists()) {
            uploadFile.getParentFile().mkdirs();
        }
        if (index < total) {
            try {
                file.transferTo(uploadFile);
                // 上传的文件分片名称
                map.put("status", 201);
                return ResponseMessage.success(map);
            } catch (IOException e) {
                e.printStackTrace();
                map.put("status", 502);
                return ResponseMessage.success(map);
            }
        } else {
            try {
                file.transferTo(uploadFile);
                // 上传的文件分片名称
                map.put("status", 200);
                return ResponseMessage.success(map);
            } catch (IOException e) {
                e.printStackTrace();
                map.put("status", 502);
                return ResponseMessage.success(map);
            }
        }
    }

    //合并文件
    @RequestMapping(value = "/mergeFile", method = RequestMethod.GET)
    public ResponseMessage merge(String uuid, String newFileName) {
        Map retMap = new HashMap();
        try {
            File dirFile = new File(fileUploadTempDir + "/" + uuid);
            if (!dirFile.exists()) {
                throw new RuntimeException("文件不存在!");
            }
            //分片上传的文件已经位于同一个文件夹下,方便寻找和遍历(当文件数大于十的时候记得排序用冒泡排序确保顺序是正确的)
            String[] fileNames = dirFile.list();
            //排序,排序不正确,读取的顺序不对,最后合并的文件会打不开
            List<Integer> sortFileIndexs = new ArrayList<>();
            for(String fileName :fileNames){
                sortFileIndexs.add(Integer.valueOf(fileName.replace(".tem","")));
            }
            Collections.sort(sortFileIndexs);
            //创建空的合并文件
            File targetFile = new File(fileUploadDir, newFileName);
            if(targetFile.exists()){
                targetFile.delete();
            }
            if (!targetFile.getParentFile().exists()) {
                targetFile.getParentFile().mkdirs();
            }
            targetFile.createNewFile();
            RandomAccessFile writeFile = new RandomAccessFile(targetFile, "rw");
            int position = 0;
            for (Integer fileIndex : sortFileIndexs) {
                String fileName = fileIndex+".tem";
                System.out.println(fileName);
                File sourceFile = new File(fileUploadTempDir + "/" + uuid, fileName);
                RandomAccessFile readFile = new RandomAccessFile(sourceFile, "rw");
                int chunksize = 1024 * 3;
                byte[] buf = new byte[chunksize];
                writeFile.seek(position);
                int byteCount = 0;
                while ((byteCount = readFile.read(buf)) != -1) {
                    if (byteCount != chunksize) {
                        byte[] tempBytes = new byte[byteCount];
                        System.arraycopy(buf, 0, tempBytes, 0, byteCount);
                        buf = tempBytes;
                    }
                    writeFile.write(buf);
                    position = position + byteCount;
                }
                readFile.close();
                //FileUtils.forceDelete(sourceFile);//删除缓存的临时文件
            }
            writeFile.close();
            retMap.put("code", "200");
        }catch (IOException e){
            e.printStackTrace();
            retMap.put("code", "500");
        }
        return ResponseMessage.success(retMap);
    }
}

3、测试:准备三个文件,一个小图片,一个较大的图片和一个视频文件

(1)点击页面上传6KB的小图片:

 因为我限制分片大小为1M,所以小图片只有一个分片:

上传成功,图片可以正常打开

(2)上传大图片:调用了多次分片接口:

图片可以正常打开:

(3)上传大的视频文件:280MB视频上传大概耗时半分钟

 视频可以正常打开:

四、服务端分片上传到云存储:同样是先分片再合并

//step2 循环上传每一片,可中断,下次从中断的片上传即可
		//分片的方法不止一种,可以自行选择
		try (FileInputStream in = (FileInputStream) file.getInputStream()) {
			try (FileChannel fcin = in.getChannel()) {
				ByteBuffer buffer = ByteBuffer.allocate(1024*1204);
				int chunk = 0;
				while (true) {
					buffer.clear();
					int flag = fcin.read(buffer);
					if (flag == -1) {
						break;
					}
					buffer.flip();
					byte[] bytes = new byte[buffer.remaining()];
					buffer.get(bytes, 0, bytes.length);
					//todo分片上传
					chunk += 1;
				}
				System.out.println(chunk);
			}
		} catch (FileNotFoundException e) {
//			e.printStackTrace();
		} catch (IOException e) {
//			e.printStackTrace();
		}
//todo合并

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

w_t_y_y

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值