[Téléchargement de fichier Springboot] La solution et la réalisation élégante de la double ouverture des extrémités avant et arrière, deuxième transfert de gros fichiers et reprise des points interrompus

Image d'effet

Adresse de l'expérience de démonstration: http://easymall.ysqorz.top/file/upload (non garanti pour une validité à long terme)

Discussion d'idées et de solutions

Deuxième passage

Le «deuxième téléchargement» fait ici référence à: lorsque l'utilisateur choisit de télécharger un fichier, le serveur détecte si le fichier a déjà été téléchargé, et si le serveur stocke déjà le fichier (exactement le même), il revient immédiatement au front-end "Le fichier a été téléchargé avec succès". Le frontal mettra alors à jour la barre de progression à 100%. Cela donne aux utilisateurs le sentiment de "second passage".

Pour chaque ressource téléchargée sur le serveur, nous devons insérer un enregistrement dans la table db_file de la base de données. Chaque enregistrement contient la valeur MD5 du fichier, le nombre d'octets téléchargés, etc., et bien sûr d'autres champs d'informations sur le fichier. La structure du tableau sera détaillée ultérieurement.

Pour réaliser la deuxième transmission, nous devons considérer deux problèmes:

1. Comment identifier de manière unique un fichier?

Calculez la valeur MD5 du fichier entier. Ici, j'ai trouvé un outil en ligne pour calculer la valeur MD5 d'un fichier: http://www.metools.info/other/o21.html .

2. Comment calculer la valeur MD5 du fichier entier?

(1) Calculez d'abord la valeur MD5 du fichier entier. Ce travail doit être effectué par le front-end, car pour calculer la valeur MD5 du fichier par le serveur, le fichier entier doit d'abord être téléchargé sur le serveur. Cela ne viole-t-il pas l'idée de «deuxième passage»?

(2) Pour calculer la valeur MD5, nous devons utiliser un plugin js spark-md5.js. Adresse Github: https://github.com/satazor/js-spark-md5

Les outils en ligne ci-dessus doivent également utiliser le plugin js. Comment utiliser ce plugin? Une démo est fournie dans README.md de Github, et sa démo peut calculer la valeur MD5 d'un gros fichier (nombre de G) fichiers par bloc. J'ai essayé, s'il n'y a pas de bloc, il n'y a aucun moyen de calculer la valeur MD5 d'un gros fichier. Par conséquent, il suffit de copier sa démo et de la modifier.

Téléchargement avec reprise (téléchargement de point d'arrêt)

Quel est l'effet de la reprise du transfert? L'utilisateur télécharge un gros fichier et clique sur "Annuler" au milieu. La prochaine fois que vous téléchargez à nouveau le fichier, vous pouvez continuer le téléchargement là où vous vous étiez arrêté la dernière fois, au lieu de recommencer depuis le début. C'est un peu compliqué, la logique de mise en œuvre implique l'avant et l'arrière.

1. Le processus de mise en œuvre du téléchargement général de fichiers

Pour les téléchargements de fichiers généraux, le serveur utilise MultipartFile pour recevoir et le frontal utilise Ajax pour télécharger des fichiers de manière asynchrone. Si le fichier est très volumineux, par exemple pour atteindre un nombre de G, le serveur doit d'abord définir la taille de téléchargement maximale. Le téléchargement de fichiers est sans aucun doute une opération qui prend du temps, ce qui signifie que l'envoi de fichiers frontaux est une opération chronophage, et en même temps, la réception MultipartFile côté serveur est également une opération chronophage. Pendant le processus de réception, le serveur générera un fichier temporaire. Par défaut, il se trouvera dans un répertoire temporaire du serveur d'applications Web. Bien sûr, il peut également être spécifié. Lorsque le téléchargement du fichier est terminé ou qu'une erreur se produit pendant le processus de réception, le fichier temporaire sera automatiquement supprimé. Vous pouvez même le vérifier vous-même pour observer ce phénomène, je n'en dirai pas plus ici.

2. Comment annuler le téléchargement de fichiers?

Utilisez Ajax pour télécharger des fichiers de manière asynchrone. Si vous souhaitez mettre fin au téléchargement, vous ne pouvez appeler que la méthode abort () de XMLHttpRequest. Cette méthode interrompra directement la connexion entre le client et le serveur, provoquant l'exception de lecture de flux du serveur (levée par SpringMVC). Une fois l'exception levée, la couche contrôleur et la logique suivante ne seront pas exécutées. Les fichiers temporaires générés à mi-chemin du reçu seront également automatiquement supprimés. Cela signifie également: la progression du téléchargement ne peut pas être enregistrée dans la base de données!

3. Comment enregistrer la progression du téléchargement du fichier après l'annulation du fichier?

Si le téléchargement du fichier est interrompu, que ce soit via la méthode abort () de XMLHttpRequest ou que la page Web est soudainement fermée, déconnectée, etc., le front-end se déconnecte unilatéralement et le serveur lèvera une exception, provoquant le être supprimé et ne peut pas être enregistré Progression du téléchargement. Pour résoudre ce problème, nous pouvons utiliser la solution de téléchargement en blocs.

Sur le front-end, la partie non téléchargée du gros fichier entier est divisée en n blocs de taille égale via js, et la taille de chaque bloc est définie comme chunkSize (par exemple: 2 Mo). Si le dernier bloc est inférieur à chunkSize, les deux derniers blocs sont fusionnés en un seul. Une requête ajax est lancée pour chaque élément de téléchargement. Une fois que chaque élément a été téléchargé avec succès, le serveur ajoutera cet élément à la fin du fichier créé par lui-même via NIO, et en même temps mettra à jour les "octets téléchargés" du fichier. dans la base de données.

Si une requête Ajax est soudainement interrompue, cela entraînera uniquement l'échec du téléchargement de ce segment et n'affectera pas les segments précédemment téléchargés avec succès. Puis lors du téléchargement à nouveau la prochaine fois, le frontal reçoit le fichier "nombre d'octets uploadés" renvoyé par le serveur. Ensuite, les js frontaux pourront localiser la partie non téléchargée du fichier sur cette base, puis télécharger à nouveau la partie non téléchargée par blocs.

Description de l'interface back-end

Package de format de retour de données

   

Introduction de l'interface Méthode de demande Chemin de la demande Demander la description des paramètres Demander des remarques sur les paramètres Données renvoyées en cas de succès
Vérifiez s'il existe des ressources de fichiers sur le serveur Publier /contrôle de fichier

fileMd5: valeur MD5 du fichier entier

totalBytes: le nombre total d'octets dans le fichier entier

suffixe: le suffixe du fichier

Les 3 paramètres de demande sont obligatoires

FileCheckRspVo { 

uploadToken: Token (Jwt) émis par cette interface, requis pour le téléchargement

uploadBytes: le nombre d'octets du fichier téléchargé

}

Importer en morceaux Publier /téléchargement de fichiers

file: le segment à télécharger

uploadToken: le token qui doit être transporté lors du téléchargement

Les deux paramètres sont obligatoires Nombre d'octets téléchargés

Pourquoi les données renvoyées par l'interface / file / check (ci-après dénommée interface check) contiennent-elles un uploadToken? Pourquoi l'interface de demande / fichier / téléchargement (ci-après dénommée interface de téléchargement) contient le uploadToken émis par / file / check?

L'interface de téléchargement ne peut pas être demandée au hasard, elle doit l'être après avoir demandé l'interface de vérification! Afin de garantir cette commande, l'interface de téléchargement de la demande doit porter le jeton émis par l'interface de contrôle (jwt, Baidu peut être fait par vous-même). Il n'y a aucun moyen de falsifier ce jeton jwt. Si vous accédez à l'interface de téléchargement avec un jeton incorrect ou expiré, il sera extrait! ! !

Code clé

Les codes clés et les commentaires sont affichés ci-dessous, il suffit de faire attention à la logique spécifique de la mise en œuvre. Si vous avez besoin du code complet de la démo: xxx (veuillez l'ajouter plus tard)

Code frontal

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();
	}
}

Code serveur

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);
		}
	}

}

 

Je suppose que tu aimes

Origine blog.csdn.net/qq_43290318/article/details/109957084
conseillé
Classement