[Springboot file upload] The solution and elegant realization of double opening of front and back ends, second transfer of large files and resuming of interrupted points

Effect picture

Demo experience address: http://easymall.ysqorz.top/file/upload (not guaranteed for long-term validity)

Discussion of ideas and solutions

Second pass

The "second upload" here refers to: when the user chooses to upload a file, the server detects whether the file has been uploaded before, and if the server already stores the file (exactly the same), it immediately returns to the front end "file has been Uploaded successfully". The front end will then update the progress bar to 100%. This gives users the feeling of "second pass".

For each resource uploaded to the server, we need to insert a record into the db_file table of the database. Each record contains the MD5 value of the file, the number of bytes uploaded, etc., and of course other file information fields. The structure of the table will be fully given later.

To realize the second transmission, we need to consider two issues:

1. How to uniquely identify a file?

Calculate the MD5 value of the entire file. Here, I found an online tool for calculating the MD5 value of a file: http://www.metools.info/other/o21.html .

2. How to calculate the MD5 value of the entire file?

(1) First calculate the MD5 value of the entire file. This work must be done by the front end, because to calculate the MD5 value of the file by the server, the entire file must first be uploaded to the server. Doesn't this violate the idea of ​​"second pass"?

(2) To calculate the MD5 value, we need to use a js plugin spark-md5.js. Github address: https://github.com/satazor/js-spark-md5

The above online tools should also use the js plugin. How to use this plugin? A demo is provided in Github's README.md, and its demo can calculate the MD5 value of a large file (number of G) files by partitioning. I have tried, if there is no block, there is no way to calculate the MD5 value of a large file. Therefore, we only need to copy its demo and change it.

Resumable upload (breakpoint upload)

What is the effect of resuming the transfer? The user is uploading a large file and clicked "Cancel" in the middle. Next time you upload the file again, you can continue uploading from where you left off last time, instead of starting from the beginning. This is a bit complicated, and the implementation logic involves the front and back ends.

1. The implementation process of general file upload

For general file uploads, the server uses MultipartFile to receive, and the front end uses Ajax to upload files asynchronously. If the file is very large, such as reaching a number of G, the server must first set the maximum upload size. File upload is undoubtedly a time-consuming operation, which means that the front-end file sending is a time-consuming operation, and the MultipartFile reception on the server side is also a time-consuming operation. During the receiving process, the server will generate a temporary file. By default, it will be in a temporary directory of the web application server. Of course, it can also be specified. When the file upload is completed or an error occurs during the receiving process, the temporary file will be automatically deleted. You can even verify it yourself to observe this phenomenon, I won't say more here.

2. How to cancel file upload?

To upload files asynchronously with Ajax, if you want to terminate the upload, you can only call the abort() method of XMLHttpRequest. This method will directly interrupt the connection between the client and the server, causing the server's stream reading exception (thrown by SpringMVC). After the exception is thrown, the controller layer and subsequent logic will not be executed. Temporary files generated halfway through the receipt will also be automatically deleted. This also means: upload progress cannot be saved to the database!

3. How to save the file upload progress after canceling the file?

If the file upload is terminated, whether it is through the abort() method of XMLHttpRequest or the webpage is suddenly closed, disconnected, etc., the front-end unilaterally disconnects, and the server will throw an exception, causing the temporary file to be deleted and cannot be saved Upload progress. To solve this problem, we can use the solution of uploading in blocks.

On the front end, the unuploaded part of the entire large file is divided into n blocks of equal size through js, and the size of each block is defined as chunkSize (for example: 2 MB). If the last block is less than chunkSize, the last two blocks are merged into one. An ajax request is initiated for each piece of upload. After each piece is successfully uploaded, the server will append this piece to the end of the file created by itself through NIO, and at the same time update the "uploaded bytes" of the file in the database.

If an Ajax request is suddenly interrupted, it will only cause the upload of this segment to fail, and will not affect the previously successfully uploaded segments. Then when uploading again next time, the front end receives the file "number of bytes uploaded" returned by the server. Then the front-end js will be able to locate the unuploaded part of the file based on this, and then upload the unuploaded part again in blocks.

Back-end interface description

Data return format package

   

Interface introduction Request method Request path Request parameter description Request parameter remarks Data returned on success
Check if there are file resources in the server post /file/check

fileMd5: MD5 value of the entire file

totalBytes: the total number of bytes in the entire file

suffix: the suffix of the file

All 3 request parameters are required

FileCheckRspVo { 

uploadToken: Token (Jwt) issued by this interface, required for upload

uploadedBytes: the number of bytes of the file uploaded

}

Upload in chunks post /file/upload

file: the segment to be uploaded

uploadToken: the token that needs to be carried when uploading

Both parameters are required Number of bytes uploaded

Why does the data returned by the /file/check interface (hereinafter referred to as the check interface) contain an uploadToken? Why does the request /file/upload interface (hereinafter referred to as the upload interface) carry the uploadToken issued by /file/check?

The upload interface cannot be requested randomly, it must be after requesting the check interface! In order to ensure this order, the request upload interface must carry the token issued by the check interface (jwt, Baidu can be done by yourself). There is no way to forge this jwt token. If you access the upload interface with an incorrect or expired token, it will be checked out! ! !

Key code

The key codes and comments are posted below, just pay attention to the specific logic of the implementation. If you need the complete code of the Demo: xxx (please add it later)

Front-end code

upload.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
	<head>
		<!-- 不设置的话,手机端不会进行响应式布局 -->
		<meta http-equiv="X-UA-Compatible" content="IE=edge">
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<title>大文件断点续传</title>
		
		<!-- 引入Bootstrap核心样式文件(必须) -->
		<link rel="stylesheet" href="/lib/bootstrap/css/bootstrap.min.css">
		
		<!-- 你自己的样式或其他文件 -->
		<link rel="stylesheet" href="/css/upload.css">
		
		<!--站点图标-->
		<!-- ... -->
	</head>
	<body>
		<div class="container">
			<div class="progress progress-top">
			  <div id="progressbar" class="progress-bar progress-bar-success" role="progressbar" 
			  aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="min-width: 2em;">
			    0%
			  </div>
			</div>
			<div class="form-group">
			   <div class="input-group">
					<input id='location' class="form-control" onclick="$('#i-file').click();">
					<div class="input-group-btn">
						<input type="button" id="i-check" value="选择" class="btn btn-default" onclick="$('#i-file').click();">
						<input type="button" value="上传"  onClick="upload()" class="btn btn-default" >
						<input type="button" value="取消"  onClick="cancel()" class="btn btn-default" >
						<input type="button" value="下载二维码"  onClick="downloadQRCode()" class="btn btn-default" >
					</div>
			   </div>
			   <input type="file" name="file" id='i-file' onchange="$('#location').val($('#i-file').val());" style="display: none">
				<p class="help-block proccess-msg" id="proccess-msg"></p>
			</div>
			<img id="downloadQRcode" src="" />
		</div>
		
		<script src="/lib/jquery/jquery.min.js"></script>
		<!-- 引入所有的Bootstrap的JS插件 -->
		<script src="/lib/bootstrap/js/bootstrap.min.js"></script>
		<script src="/lib/spark-md5.min.js"></script>
		<script src="/js/upload.js"></script>
	</body>
</html>

upload.js


// 真正上传文件的ajax请求
var uploadAjax = null;
var fileMd5; // 文件md5值

function downloadQRCode() {
	if (fileMd5 != null) {
		var url = '/file/qrcode/generate?fileMd5=' + fileMd5 + '&seconds=900';
		$('#downloadQRcode').attr('src', url);
	}
}

function upload() {
	// 文件限制检查
	var file = $('#i-file')[0].files[0];
	if (file == null) {
		return;
	}
	
	var suffix = file.name.substr(file.name.lastIndexOf('.'));
	var type = file.type;
	var totalBytes = file.size;
	var blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
	console.log(suffix, type, totalBytes);
	
	// 开始。通过回调函数,进行链式调用
	calculteFileMd5();
	
	// 计算文件的MD5值,分块计算,支持大文件
	function calculteFileMd5() {
		var chunkSize = 2097152,    // Read in chunks of 2MB 。每一块的大小
	        chunks = Math.ceil(file.size / chunkSize), // 整个文件可分为多少块,向下取整
	        currentChunk = 0,	// 当前加载的块。初始化为0
	        spark = new SparkMD5.ArrayBuffer(),
	        fileReader = new FileReader();
		
		// fileReader加载文件数据到内存之后会执行此回调函数
		fileReader.onload = function (e) {
			refreshMsg('read chunk nr ' + (currentChunk + 1) + ' of ' + chunks);
			spark.append(e.target.result);                   // Append array buffer
			currentChunk++;
	
			if (currentChunk < chunks) {
				loadNext();
			} else {
				refreshMsg('finished loading');
				// 计算出文件的md5值
				fileMd5 = spark.end();
				refreshMsg('computed hash: ' + fileMd5);  // Compute hash
				
				// 服务器检查文件是否存在
				requestCheckFile();
			}
		};
		
		// 开始计算
		loadNext();
		
		function loadNext() {
			var start = currentChunk * chunkSize,
				end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
		
			fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
		}
	}
	
	// 请求服务器验证文件
	function requestCheckFile() {
		$.ajax({
			url: '/file/check',    // 提交到controller的url路径
			type: "POST",    // 提交方式
			dataType: "json",    
			data: {
				fileMd5: fileMd5,
				totalBytes: totalBytes,
				suffix: suffix
			}, 
			success: function (res) {    
				console.log(res);
				if (res.code === 2000) {
					var percentage = parseFloat(res.data.uploadedBytes) / totalBytes * 100; 
					refreshStatus(percentage);
					if (res.data.uploadedBytes < totalBytes) {
						requestRealUpload(res.data);
					}
				} 
			}
		});
	}
	
	// 分块上传
	function requestRealUpload(params) {
		var chunkSize = 2097152;    // 每一块的大小。2 M
	    //var chunks = Math.ceil((totalBytes - params.uploadedBytes) / chunkSize); // 尚未上传的部分可分为几块,取下整
	    //var currentChunk = 0;	// 当前加载的块。初始化为0

		uploadChunk(params.uploadedBytes);
		
		// 请求服务端,上传一块
		function uploadChunk(uploadedBytes) {
			var formData = new FormData();
			var start = uploadedBytes;
			var end = Math.min(start + chunkSize, totalBytes);
			console.log(start, end);
			formData.append('file', blobSlice.call(file, start, end)); // [start, end)
			formData.append('uploadToken', params.uploadToken); // 携带token
			var preLoaded = 0; // 当前块的上一次加载的字节数,用于计算速度
			var preTime = new Date().getTime(); // 上一次回调进度的时间
			uploadAjax = $.ajax({
			    url:  '/file/upload',
			    type: "POST",
			    data: formData,
				cache: false,
			    contentType: false,  // 必须 不设置内容类型
			    processData: false,  // 必须 不处理数据
			    xhr: function() {
			        //获取原生的xhr对象
			        var xhr = $.ajaxSettings.xhr();
			        if (xhr.upload) {
			            //添加 progress 事件监听
						//console.log(xhr.upload);
						xhr.upload.onprogress = function(e) {
							// e.loaded 应该是指当前块,已经加载到内存的字节数
							// 这里的上传进度是整个文件的上传进度,并不是指当前这一块
							var percentage = (start + e.loaded) / totalBytes * 100; 
							refreshStatus(percentage); // 更新百分比
							
							// 计算速度
							var now = new Date().getTime();
							var duration = now - preTime; // 毫秒
							var speed = ((e.loaded - preLoaded) / duration).toFixed(2); // KB/s
							preLoaded = e.loaded;
							preTime = now;
							//if (duration > 1000) {
								// 隔1秒才更新速度
								refreshMsg('正在上传:' + speed + ' KB/s');
							//}
						};
						xhr.upload.onabort = function() {
							refreshMsg('已取消上传,服务端已保存上传完成的分块,下次重传可续传');
						};
			        }
			        return xhr;
			    },
			    success: function(res) {
			        //成功回调 
					console.log(res);   
					if (res.code === 2000) {
						if (res.data < totalBytes) {
							uploadChunk(res.data); // 上传下一块
						} else {
							refreshMsg('上传完成!'); //所有块上传完成
						}
					} else {
						refreshMsg(res.msg); // 当前块上传失败,提示错误,后续块停止上传
					}
			    }
			});		
		}
	}
	
	// 刷新进度条
	function refreshStatus(percentage) {
		var per = (percentage).toFixed(2);
		console.log(per);
		$('#progressbar').text(per + '%');
		$('#progressbar').css({
			width: per + '%'
		});
	}
	// 更新提示信息
	function refreshMsg(msg) {
		$('#proccess-msg').text(msg);
	}
}

// 直接终端上传的ajax请求,后端会抛出异常
function cancel() {
	if (uploadAjax != null) {
		console.log(uploadAjax);
		uploadAjax.abort();
	}
}

Server code

FileController.java

package net.ysq.easymall.controller;

import java.io.IOException;

import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import com.auth0.jwt.interfaces.DecodedJWT;

import net.ysq.easymall.common.JwtUtils;
import net.ysq.easymall.common.ResultModel;
import net.ysq.easymall.common.StatusCode;
import net.ysq.easymall.po.DbFile;
import net.ysq.easymall.po.User;
import net.ysq.easymall.service.FileService;
import net.ysq.easymall.vo.FileCheckRspVo;

/**
 * @author	passerbyYSQ
 * @date	2020-11-13 17:42:40
 */
@Controller
@RequestMapping("/file")
public class FileController {
	
	@Autowired
	private FileService fileService;
	
	@GetMapping("/upload")
	public String uploadPage() {
		return "upload";
	}
	
	@PostMapping("/check")
	@ResponseBody
	public ResultModel<FileCheckRspVo> checkFileExist(String fileMd5, long totalBytes, 
			String suffix, HttpSession session) {
		// 简单的参数检查,之后再全局处理优化
		if (StringUtils.isEmpty(fileMd5) || totalBytes <= 0
				|| StringUtils.isEmpty(suffix)) {
			return ResultModel.error(StatusCode.PARAM_IS_INVALID);
		}
		
		/*
		// 检查大小
        DataSize size = DataSize.of(totalBytes, DataUnit.BYTES);
        // 限制100 M
        DataSize limit = DataSize.of(100, DataUnit.MEGABYTES);
        if (size.compareTo(limit) > 0) {
            String msg = String.format("当前文件大小为 %d MB,最大允许大小为 %d MB",
                    size.toMegabytes(), limit.toMegabytes());
            return ResultModel.error(StatusCode.FILE_SIZE_EXCEEDED.getCode(), msg);
        }
        */
		User user = (User) session.getAttribute("user");
		
		// 根据md5去数据库查询是否已存在文件
		DbFile dbFile = fileService.checkFileExist(fileMd5);
		// 如果不存在,则创建文件,并插入记录。如果已存在,返回结果
		if (ObjectUtils.isEmpty(dbFile)) {
			dbFile = fileService.createFile(fileMd5, totalBytes, suffix, user);
		}
		
		FileCheckRspVo fileCheckRspVo = new FileCheckRspVo();
		fileCheckRspVo.setUploadedBytes(dbFile.getUploadedBytes());
		if (dbFile.getUploadedBytes() < dbFile.getTotalBytes()) { // 未上传完,返回token
			String uploadToken = fileService.generateUploadToken(user.getEmail(), dbFile);
			fileCheckRspVo.setUploadToken(uploadToken);
		}
		
		return ResultModel.success(fileCheckRspVo);
	}
	
	@PostMapping("/upload")
	@ResponseBody
	public ResultModel<Long> uploadFile(MultipartFile file, String uploadToken) {
		
		// 解析过程可能会抛出异常,全局进行捕获
		DecodedJWT decodedJWT = JwtUtils.verifyJwt(uploadToken);
		String fileMd5 = decodedJWT.getClaim("fileMd5").asString();
		// 如果token验证通过(没有异常抛出),则肯定能找得到
		DbFile dbFile = fileService.checkFileExist(fileMd5);
		
		// 上传文件
		long uploadedBytes = fileService.transfer(file, dbFile);
		System.out.println("已上传:" + uploadedBytes);
		//System.out.println("总大小:" + dbFile.getTotalBytes());
		
		return ResultModel.success(uploadedBytes);
	}
	
	@GetMapping("/qrcode/generate")
	public void downloadByQrcode(String fileMd5, long seconds, 
			HttpServletResponse response) throws IOException, Exception {
		if (ObjectUtils.isEmpty(fileMd5)) {
			throw new Exception("fileMd5为空");
		}
		if (ObjectUtils.isEmpty(seconds) || seconds <= 0) {
			seconds = 60 * 15; // 15分钟
		}
		DbFile dbFile = fileService.checkFileExist(fileMd5);
		if (ObjectUtils.isEmpty(dbFile)) {
			throw new Exception("fileMd5错误");
		}
		
		fileService.generateDownloadQRCode(seconds, dbFile, response.getOutputStream());
	}
	
	@GetMapping("/qrcode/download")
	public void downloadByQrcode(String downloadToken, HttpSession session, 
			HttpServletResponse response) {
		System.out.println("download!!");
		
		DecodedJWT decodedJWT = JwtUtils.verifyJwt(downloadToken);
		String fileMd5 = decodedJWT.getClaim("fileMd5").asString();
		DbFile dbFile = fileService.checkFileExist(fileMd5);
		
		// 设置响应头
		response.setHeader("Content-Type", "application/x-msdownload");
		response.setHeader("Content-Disposition", "attachment; filename=" + dbFile.getRandName());
		
		fileService.download(dbFile, response);
	}
}

FileService.java

package net.ysq.easymall.service;

import java.io.OutputStream;

import javax.servlet.http.HttpServletResponse;

import org.springframework.web.multipart.MultipartFile;

import net.ysq.easymall.po.DbFile;
import net.ysq.easymall.po.User;

/**
 * @author	passerbyYSQ
 * @date	2020-11-13 17:54:06
 */
public interface FileService {
	// 下载
	void download(DbFile dbFile, HttpServletResponse response);
	
	// 生成下载的token
	void generateDownloadQRCode(long seconds, DbFile dbFile, OutputStream outStream) throws Exception;
	
	// 根据id查找
	DbFile findById(Integer fileId);
	
	// 根据fileMd5检查文件是否已存在
	DbFile checkFileExist(String fileMd5);
	
	// 在磁盘上创建文件,并将记录插入数据库
	DbFile createFile(String fileMd5, long totalBytes, String suffix, User user);
	
	// 生成上传文件的token
	String generateUploadToken(String email, DbFile dbFile); 
	
	// 复制到目标目录
	long transfer(MultipartFile file, DbFile dbFile);
}

FileServiceImpl.java

package net.ysq.easymall.service.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.ResourceUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import net.ysq.easymall.common.CloseUtils;
import net.ysq.easymall.common.JwtUtils;
import net.ysq.easymall.common.QRCodeUtils;
import net.ysq.easymall.dao.DbFileMapper;
import net.ysq.easymall.po.DbFile;
import net.ysq.easymall.po.User;
import net.ysq.easymall.service.FileService;

/**
 * @author	passerbyYSQ
 * @date	2020-11-13 17:55:09
 */
@Service
public class FileServiceImpl implements FileService {
	
	@Autowired
	private DbFileMapper dbFileMapper;
	
	@Override
	public DbFile findById(Integer fileId) {
		DbFile record = new DbFile();
		record.setId(fileId);
		return dbFileMapper.selectOne(record);
	}

	@Override
	public DbFile checkFileExist(String fileMd5) {
		DbFile record = new DbFile();
		// 设置查询条件
		record.setFileMd5(fileMd5);
		// 找不到返回null
		DbFile dbFile = dbFileMapper.selectOne(record);
		//System.out.println(dbFile);
		return dbFile;
	}

	@Override
	public DbFile createFile(String fileMd5, long totalBytes, String suffix, User user) {
		try {
			// 创建目标目录
			File classpath = ResourceUtils.getFile("classpath:");
	        File destDir = new File(classpath, "upload/" + user.getEmail());
	        if (!destDir.exists()) {
	            destDir.mkdirs(); // 递归创建创建多级
	            System.out.println("创建目录成功:" + destDir.getAbsolutePath());
	        }
	        // 利用UUID生成随机文件名
	     	String randName = UUID.randomUUID().toString().replace("-", "") + suffix;
			File destFile = new File(destDir, randName);
			// 创建目标
			destFile.createNewFile();
			
			String path = user.getEmail() + "/" + randName;
			DbFile dbFile = new DbFile();
			dbFile.setFileMd5(fileMd5);
			dbFile.setRandName(randName);
			dbFile.setPath(path);
			dbFile.setTotalBytes(totalBytes);
			dbFile.setUploadedBytes(0L);
			dbFile.setCreatorId(user.getId());
			int count = dbFileMapper.insertSelective(dbFile);
			
			return count == 1 ? dbFile : null;
		} catch (IOException e) {
			e.printStackTrace();
		}
		return null;
	}

	@Override
	public String generateUploadToken(String email, DbFile dbFile) {
		Map<String, String> claims = new HashMap<>();
		claims.put("fileMd5", dbFile.getFileMd5());
		// 5分钟后过期
		String jwt = JwtUtils.generateJwt(claims, 60 * 1000 * 5);
		return jwt;
	}
	
	@Override
	public void generateDownloadQRCode(long seconds, DbFile dbFile, OutputStream outStream) throws Exception {
		Map<String, String> claims = new HashMap<>();
		claims.put("fileMd5", dbFile.getFileMd5());
		long millis = Duration.ofSeconds(seconds).toMillis();
		String downloadToken = JwtUtils.generateJwt(claims, millis);
		String downloadUrl = ServletUriComponentsBuilder
	        .fromCurrentContextPath()
	        .path("/file/qrcode/download")
	        .queryParam("downloadToken", downloadToken)
	        .toUriString();
		
		QRCodeUtils.encode(downloadUrl, outStream);
		//QRCodeUtil.generateWithStr(downloadUrl, outStream);
	}

	@Override
	public long transfer(MultipartFile file, DbFile dbFile) {
		
		InputStream inStream = null;
		ReadableByteChannel inChannel = null;
		FileOutputStream outStream = null;
		FileChannel outChannel = null;
		try {
			inStream = file.getInputStream();
			inChannel = Channels.newChannel(inStream);
			
			File classpath = ResourceUtils.getFile("classpath:");
			File destFile = new File(classpath, "upload/" + dbFile.getPath());
			outStream = new FileOutputStream(destFile, true); // 注意,第二个参数为true,否则无法追加
			outChannel = outStream.getChannel();
			
			long count = outChannel.transferFrom(inChannel, outChannel.size(), file.getSize());
			//long count = inChannel.transferTo(dbFile.getUploadedBytes(), inChannel.size(), outChannel);
			
			DbFile record = new DbFile();
			record.setId(dbFile.getId());
			record.setUploadedBytes(dbFile.getUploadedBytes() + count);
			// 更新已上传的字节数到数据库
			dbFileMapper.updateByPrimaryKeySelective(record);
			
			return record.getUploadedBytes();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			CloseUtils.close(inChannel, inStream, outChannel, outStream);
		}
		return dbFile.getUploadedBytes();
	}

	@Override
	public void download(DbFile dbFile, HttpServletResponse response) {
		FileInputStream inStream = null;
		FileChannel inChannel = null;
		OutputStream outStream = null;
		WritableByteChannel outChannel = null;
		try {
			File classpath = ResourceUtils.getFile("classpath:");
			File destFile = new File(classpath, "upload/" + dbFile.getPath());
			inStream = new FileInputStream(destFile);
	        inChannel = inStream.getChannel();
	        outStream = response.getOutputStream();
	        outChannel = Channels.newChannel(outStream);
	        inChannel.transferTo(0, inChannel.size(), outChannel);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			CloseUtils.close(outChannel, outStream, inChannel, inStream);
		}
	}

}

 

Guess you like

Origin blog.csdn.net/qq_43290318/article/details/109957084