文章目录
- 一、流程设计
- 1. 分片上传实现思路
- 2. 文件分片上传流程
- 3. 视频播放流程
- 二、代码实现
- 1. 后端代码
- 2. 文件上传前端代码
- 3. 视频播放前端代码
一、流程设计
1. 分片上传实现思路
2. 文件分片上传流程
3. 视频播放流程
二、代码实现
1. 后端代码
- pom.xml
<dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.5.5</version>
</dependency>
- application.yml
spring:servlet:multipart:max-file-size: 300MBmax-request-size: 300MBminio:endpoint: http://127.0.0.1:9000 #MinIO服务所在地址accessKey: admin #访问的keysecretKey: password #访问的秘钥bucketName: test #访问的存储桶名expiry: 86400 #过期时间
- com.example.web.dto.file.FileResp
package com.example.web.dto.file;import com.example.web.dto.CommResp;
import java.io.Serializable;/*** @description 文件处理返回消息*/
@lombok.Setter
@lombok.Getter
public class FileResp extends CommResp implements Serializable {private static final long serialVersionUID = 1L;// 文件处理代码private Integer code;// 文件名private String fileName;// 文件数量private Integer shardCount;// 文件MD5private String md5;// 文件访问路径private String fileUrl;public void setResp() {if (getCode()!=null && getCode()==200) setMsg("操作成功");if (getCode()!=null && getCode()==201) setMsg("分片上传成功");if (getCode()!=null && getCode()==202) setMsg("所有的分片均上传成功");if (getCode()!=null && getCode()==203) setMsg("系统异常");if (getCode()!=null && getCode()==204) setMsg("资源不存在");setPageNo(null);setPageSize(null);setTotals(null);}}
- com.example.web.dto.file.MinioObject
package com.example.web.dto.file;import java.io.Serializable;
import java.util.Map;@lombok.Setter
@lombok.Getter
public class MinioObject implements Serializable {private static final long serialVersionUID = 1L;private String bucket;private String region;private String object;private String etag;private long size;private boolean deleteMarker;private Map<String, String> userMetadata;}
- com.example.utils.FileMd5Util
package com.example.utils;import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;/*** @description 计算文件的Md5*/
public final class FileMd5Util {private static final int BUFFER_SIZE = 8 * 1024;private static final char[] HEX_CHARS ={'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};/*** 计算文件的输入流*/public static String calculateMd5(InputStream inputStream) {try {MessageDigest md5MessageDigest = MessageDigest.getInstance("MD5");try (BufferedInputStream bis = new BufferedInputStream(inputStream);DigestInputStream digestInputStream = new DigestInputStream(bis, md5MessageDigest)) {final byte[] buffer = new byte[BUFFER_SIZE];while (digestInputStream.read(buffer) > 0) {md5MessageDigest = digestInputStream.getMessageDigest();}return encodeHex(md5MessageDigest.digest());} catch (IOException ioException) {throw new IllegalArgumentException(ioException.getMessage());}} catch (NoSuchAlgorithmException e) {throw new IllegalArgumentException("no md5 found");}}/*** 转成的md5值为全小写*/private static String encodeHex(byte[] bytes) {char[] chars = new char[32];for (int i = 0; i < chars.length; i = i + 2) {byte b = bytes[i / 2];chars[i] = HEX_CHARS[(b >>> 0x4) & 0xf];chars[i + 1] = HEX_CHARS[b & 0xf];}return new String(chars);}
}
- com.example.utils.MinioFileUtil
package com.example.utils;import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import java.io.*;
import java.util.ArrayList;
import java.util.List;/*** @description minio文件操作*/
@lombok.Getter
@Component
public class MinioFileUtil {private static Log logger = LogFactory.getLog(MinioFileUtil.class);@Value("${minio.endpoint:1}")private String minioEndpoint;@Value("${minio.accessKey:1}")private String minioAccessKey;@Value("${minio.secretKey:1}")private String minioSecretKey;@Value("${minio.file-show-url:1}")private String showUrl;/*** @description 获取minioClient*/public MinioClient getMinioClient() {return MinioClient.builder().endpoint(minioEndpoint).credentials(minioAccessKey, minioSecretKey).build();}/*** @description 将分钟数转换为秒数* @Param expiry 过期时间(分钟)*/private int expiryHandle(Integer expiry) {expiry = expiry * 60;if (expiry > 604800) {return 604800;}return expiry;}/*** @description 文件上传至指定桶容器,并返回对象文件的存储路径加文件名* @param inputStream 文件流* @param bucketName 桶名称* @param directory 文件存储目录* @param objectName 文件名称*/@SneakyThrowspublic String uploadObject(InputStream inputStream, String bucketName, String directory, String objectName) {if (StringUtils.isNotEmpty(directory)) {objectName = directory + "/" + objectName;}getMinioClient().putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(inputStream, inputStream.available(), -1).build());return objectName;}/*** @description 获取访问对象的url地址* @param bucketName 桶名称* @param objectName 文件名称(包含存储目录)* @param expiry 过期时间(分钟) 最大为7天 超过7天则默认最大值*/@SneakyThrowspublic String getObjectUrl(String bucketName, String objectName, Integer expiry) {expiry = expiryHandle(expiry);String url = getMinioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().method(Method.GET).bucket(bucketName).object(objectName).expiry(expiry).build());if (!showUrl.equals("1") && showUrl.length()>2) {url = url.replace(minioEndpoint, showUrl);}return url;}/*** @description 获取某个文件* @param bucketName 桶名称* @param objectName 文件路径*/@SneakyThrowspublic StatObjectResponse getObjectInfo(String bucketName, String objectName) {return getMinioClient().statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());}/*** @description 删除一个对象文件* @param bucketName 桶名称* @param objectName 文件名称(包含存储目录)*/public boolean removeObject(String bucketName, String objectName) {try {getMinioClient().removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());return true;} catch (Exception e) {logger.error("removeObject error", e);return false;}}/*** @description 删除多个对象文件* @param bucketName 桶名称* @param objectNames 文件名称(包含存储目录)*/@SneakyThrowspublic List<String> removeObjects(String bucketName, List<String> objectNames) {if (!bucketExists(bucketName)) {return new ArrayList<>();}List<String> deleteErrorNames = new ArrayList<>();List<DeleteObject> deleteObjects = new ArrayList<>(objectNames.size());for (String objectName : objectNames) {deleteObjects.add(new DeleteObject(objectName));}Iterable<Result<DeleteError>> results = getMinioClient().removeObjects(RemoveObjectsArgs.builder().bucket(bucketName).objects(deleteObjects).build());for (Result<DeleteError> result : results) {DeleteError error = result.get();deleteErrorNames.add(error.objectName());}return deleteErrorNames;}/*** @description 判断bucket是否存在* @param bucketName 桶名称*/@SneakyThrowspublic boolean bucketExists(String bucketName) {boolean exists = false;BucketExistsArgs.Builder builder = BucketExistsArgs.builder();BucketExistsArgs build = builder.bucket(bucketName).build();exists = getMinioClient().bucketExists(build);return exists;}/*** @description 创建存储桶* minio 桶设置公共或私有,alioss统一设置成私有,可配置文件公共读或私有读* @param bucketName 桶名称*/@SneakyThrowspublic void makeBucket(String bucketName) {if (bucketExists(bucketName)) {return;}getMinioClient().makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());}/*** @description 获取文件* @param bucketName 桶名称* @param objectName 文件路径* @param offset 截取流的开始位置* @param length 截取长度*/@SneakyThrowspublic InputStream getObject(String bucketName, String objectName, Long offset, Long length) {return getMinioClient().getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).offset(offset).length(length).build());}/*** @description 获取文件* @param bucketName 桶名称* @param objectName 文件路径*/@SneakyThrowspublic InputStream getObject(String bucketName, String objectName) {return getMinioClient().getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());}/*** @description 上传分片文件* @param inputStream 输入流* @param objectName 文件路径* @param bucketName 桶名称*/@SneakyThrowspublic void putChunkObject(InputStream inputStream, String bucketName, String objectName) {try {getMinioClient().putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(inputStream, inputStream.available(), -1).build());} finally {if (inputStream != null) {inputStream.close();}}}/*** @description 删除空桶* @param bucketName 桶名称*/@SneakyThrowspublic void removeBucket(String bucketName) {removeObjects(bucketName, listObjectNames(bucketName));getMinioClient().removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());}/*** @description 查询桶中所有的文件* @param bucketName 桶名称*/@SneakyThrowspublic List<String> listObjectNames(String bucketName) {List<String> objectNameList = new ArrayList<>();if (bucketExists(bucketName)) {Iterable<Result<Item>> objects = getMinioClient().listObjects(ListObjectsArgs.builder().bucket(bucketName).recursive(true).build());for (Result<Item> result : objects) {Item item = result.get();objectNameList.add(item.objectName());}}return objectNameList;}/*** @description 文件合并* @param originBucketName 分块文件所在的桶* @param targetBucketName 合并文件生成文件所在的桶* @param objectName 存储于桶中的对象名*/@SneakyThrowspublic String composeObject(String originBucketName, String targetBucketName, String objectName) {Iterable<Result<Item>> results = getMinioClient().listObjects(ListObjectsArgs.builder().bucket(originBucketName).recursive(true).build());List<String> objectNameList = new ArrayList<>();for (Result<Item> result : results) {Item item = result.get();objectNameList.add(item.objectName());}if (ObjectUtils.isEmpty(objectNameList)) {throw new IllegalArgumentException(originBucketName + "桶中没有文件,请检查");}List<ComposeSource> composeSourceList = new ArrayList<>(objectNameList.size());// 对文件名集合进行升序排序objectNameList.sort((o1, o2) -> Integer.parseInt(o2) > Integer.parseInt(o1) ? -1 : 1);for (String object : objectNameList) {composeSourceList.add(ComposeSource.builder().bucket(originBucketName).object(object).build());}return composeObject(composeSourceList, targetBucketName, objectName);}/*** @description 文件合并* @param bucketName 合并文件生成文件所在的桶* @param objectName 原始文件名* @param sourceObjectList 分块文件集合*/@SneakyThrowspublic String composeObject(List<ComposeSource> sourceObjectList, String bucketName, String objectName) {getMinioClient().composeObject(ComposeObjectArgs.builder().bucket(bucketName).object(objectName).sources(sourceObjectList).build());return getObjectUrl(bucketName, objectName, 100);}
}
- com.example.blh.file.FileBlh
package com.example.blh.file;import com.alibaba.fastjson2.JSONObject;
import com.example.entity.CommBo;
import com.example.entity.file.FileBo;
import com.example.utils.FileMd5Util;
import com.example.utils.MinioFileUtil;
import com.example.web.dto.file.MinioObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.minio.StatObjectResponse;
import org.apache.catalina.connector.ClientAbortException;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;/*** @description 文件上传下载处理逻辑*/
@Component
public class FileBlh {private static Log logger = LogFactory.getLog(FileBlh.class);private static final String OBJECT_INFO_LIST = "minio.file.objects";private static final String MD5_KEY = "minio.file.md5s";private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMM");@Value("${minio-bucket-name:1}")private String bucketName;@Resourceprivate MinioFileUtil minioFileUtil;@Resourceprivate RedisTemplate redisTemplate;@Autowiredprivate ObjectMapper objectMapper;/*** @description 上传单个文件*/public void uploadFile(FileBo bo) {InputStream is = null;try {MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) bo.getReq();MultipartFile file = multipartRequest.getFile("file");is = file.getInputStream();String uuid = UUID.randomUUID().toString().replace("-","");String dir = dateFormat.format(new Date());minioFileUtil.uploadObject(is,bucketName,dir,uuid+"-"+file.getOriginalFilename());bo.setFileName(dir+"/"+uuid+"-"+file.getOriginalFilename());CommBo.setSuccessBo(bo);} catch (Exception e) {CommBo.setFailBo(bo, e);} finally {try {if (is!=null) is.close();} catch (IOException eis) {}}}/*** @description 获取文件路径*/public void getFileUrl(FileBo bo) {try {bo.setFileUrl(minioFileUtil.getObjectUrl(bucketName,bo.getFileName(),100));CommBo.setSuccessBo(bo);} catch (Exception e) {CommBo.setFailBo(bo, e);}}/*** @description 删除文件*/public void deleteFile(FileBo bo) {try {minioFileUtil.removeObject(bucketName,bo.getFileName());CommBo.setSuccessBo(bo);} catch (Exception e) {CommBo.setFailBo(bo, e);}}/*** @description 获取文件分片下载信息*/public void getSplitFileInfo(FileBo bo) {try {StatObjectResponse objectInfo = minioFileUtil.getObjectInfo(bucketName,bo.getFileName());bo.setShardCount((int)Math.ceil((double)objectInfo.size()/(1024*1024*5)));CommBo.setSuccessBo(bo);} catch (Exception e) {CommBo.setFailBo(bo, e);}}/*** @description 文件分片下载*/public void downSplitFile(FileBo bo) {try {StatObjectResponse objectInfo = minioFileUtil.getObjectInfo(bucketName,bo.getFileName());long fileSize = objectInfo.size();long startPos = (bo.getShardCount()-1) * (1024*1024*5);long endPos = bo.getShardCount() * (1024*1024*5);if (endPos>fileSize) {endPos = fileSize;}long rangLength = endPos - startPos;bo.getRes().addHeader("Content-Type", "*/*");BufferedOutputStream bos = new BufferedOutputStream(bo.getRes().getOutputStream());BufferedInputStream bis = new BufferedInputStream(minioFileUtil.getObject(bucketName, bo.getFileName(), startPos, rangLength));IOUtils.copy(bis, bos);} catch (Exception e) {CommBo.setFailBo(bo, e);}}/*** @description 根据文件大小和文件的md5校验文件是否存在*/public void checkSplitFile(FileBo bo) {try {if (ObjectUtils.isEmpty(bo.getMd5())) {bo.setCode(204);return;}String url = (String) redisTemplate.boundHashOps(MD5_KEY).get(bo.getMd5());if (ObjectUtils.isEmpty(url)) {bo.setCode(204); // 文件不存在return;}bo.setCode(200);bo.setFileUrl(url);} catch (Exception e) {CommBo.setFailBo(bo, e);}}/*** @description 文件上传,适合大文件,集成了分片上传*/public void uploadSplitFile(FileBo bo) {try {MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) bo.getReq();MultipartFile file = multipartRequest.getFile("data");if (file == null) {bo.setCode(203);return;}int index = Integer.parseInt(multipartRequest.getParameter("index")); // 第几片int total = Integer.parseInt(multipartRequest.getParameter("total")); // 总片数String fileName = multipartRequest.getParameter("name");String md5 = multipartRequest.getParameter("md5");minioFileUtil.makeBucket(md5);String objectName = String.valueOf(index);if (index < total) {try {logger.info("上传文件: " + md5 + " " + objectName);minioFileUtil.putChunkObject(file.getInputStream(), md5, objectName); // 上传文件bo.setCode(201); // 不是最后一片, 状态码为201} catch (Exception e) {logger.error(e.getMessage());bo.setCode(203);}} else {try {minioFileUtil.putChunkObject(file.getInputStream(), md5, objectName);bo.setCode(202); // 最后一片, 状态码为202bo.setFileName(objectName);} catch (Exception e) {logger.error(e.getMessage());bo.setCode(203);}}} catch (Exception e) {CommBo.setFailBo(bo, e);}}/*** @description 文件合并*/public void mergeSplitFile(FileBo bo) {logger.info("分片总数: " + bo.getShardCount());Map<String, Object> retMap = new HashMap<>();try {List<String> objectNameList = minioFileUtil.listObjectNames(bo.getMd5());if (bo.getShardCount() != objectNameList.size()) {bo.setCode(203);} else {// 开始合并请求String filenameExtension = StringUtils.getFilenameExtension(bo.getFileName());String uuid = UUID.randomUUID().toString();String dir = dateFormat.format(new Date());String objectName = dir+"/"+uuid+"-"+bo.getFileName();minioFileUtil.composeObject(bo.getMd5(), bucketName, objectName);// 合并成功之后删除对应的临时桶minioFileUtil.removeBucket(bo.getMd5());logger.info("创建文件 " + objectName + " ,删除桶 "+bo.getMd5()+" 成功");// 计算文件的md5String fileMd5 = null;try (InputStream inputStream = minioFileUtil.getObject(bucketName, objectName)) {fileMd5 = FileMd5Util.calculateMd5(inputStream);} catch (IOException e) {logger.error(e.getMessage());}// 计算文件真实的类型String type = null;if (!ObjectUtils.isEmpty(fileMd5) && fileMd5.equalsIgnoreCase(bo.getMd5())) {String url = minioFileUtil.getObjectUrl(bucketName, objectName, 100);redisTemplate.boundHashOps(MD5_KEY).put(fileMd5, objectName);bo.setCode(200);} else {minioFileUtil.removeObject(bucketName, objectName);redisTemplate.boundHashOps(MD5_KEY).delete(fileMd5);bo.setCode(203);}bo.setFileName(objectName);}} catch (Exception e) {logger.error(e.getMessage(), e);bo.setCode(203);}}/*** @description 文件播放*/public void videoPlay(FileBo bo) {logger.info("播放视频: " + bo.getFileName());// 设置响应报头String key = bucketName + "." + bo.getFileName();Object obj = redisTemplate.boundHashOps(OBJECT_INFO_LIST).get(key);// 记录视频文件的元数据MinioObject minioObject;if (obj == null) {StatObjectResponse objectInfo = null;try {objectInfo = minioFileUtil.getObjectInfo(bucketName,bo.getFileName());} catch (Exception e) {bo.getRes().setCharacterEncoding(StandardCharsets.UTF_8.toString());bo.getRes().setContentType("application/json;charset=utf-8");bo.getRes().setStatus(HttpServletResponse.SC_NOT_FOUND);try {JSONObject json = new JSONObject();json.put("operateSuccess",false);bo.getRes().getWriter().write(objectMapper.writeValueAsString(json));} catch (IOException ex) {throw new RuntimeException(ex);}return;}minioObject = new MinioObject();BeanUtils.copyProperties(objectInfo, minioObject);redisTemplate.boundHashOps(OBJECT_INFO_LIST).put(key, minioObject);} else {minioObject = (MinioObject) obj;}// 获取文件的长度long fileSize = minioObject.getSize();// Accept-Ranges: bytesbo.getRes().setHeader("Accept-Ranges", "bytes");// pos开始读取位置; last最后读取位置long startPos = 0;long endPos = fileSize - 1;String rangeHeader = bo.getReq().getHeader("Range");if (!ObjectUtils.isEmpty(rangeHeader) && rangeHeader.startsWith("bytes=")) {try {String numRang = bo.getReq().getHeader("Range").replaceAll("bytes=", "");if (numRang.startsWith("-")) {endPos = fileSize - 1;startPos = endPos - Long.parseLong(new String(numRang.getBytes(StandardCharsets.UTF_8), 1,numRang.length() - 1)) + 1;} else if (numRang.endsWith("-")) {endPos = fileSize - 1;startPos = Long.parseLong(new String(numRang.getBytes(StandardCharsets.UTF_8), 0,numRang.length() - 1));} else {String[] strRange = numRang.split("-");if (strRange.length == 2) {startPos = Long.parseLong(strRange[0].trim());endPos = Long.parseLong(strRange[1].trim());} else {startPos = Long.parseLong(numRang.replaceAll("-", "").trim());}}if (startPos < 0 || endPos < 0 || endPos >= fileSize || startPos > endPos) {// 要求的范围不满足bo.getRes().setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);return;}// 断点续传 状态码206bo.getRes().setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);} catch (NumberFormatException e) {logger.error(e.getMessage());startPos = 0;}}// 总共需要读取的字节long rangLength = endPos - startPos + 1;bo.getRes().setHeader("Content-Range", String.format("bytes %d-%d/%d", startPos, endPos, fileSize));bo.getRes().addHeader("Content-Length", String.valueOf(rangLength));bo.getRes().addHeader("Content-Type", "video/mp4");try (BufferedOutputStream bos = new BufferedOutputStream(bo.getRes().getOutputStream());BufferedInputStream bis = new BufferedInputStream(minioFileUtil.getObject(bucketName, bo.getFileName(), startPos, rangLength))) {IOUtils.copy(bis, bos);} catch (IOException e) {if (e instanceof ClientAbortException) {// ignore 这里不打印日志,这里的异常原因是用户在拖拽视频进度造成的} else {logger.error(e.getMessage());}}}
}
- com.example.web.rest.file.FileRest
package com.example.web.rest.file;import com.alibaba.fastjson2.JSONObject;
import com.example.blh.file.FileBlh;
import com.example.entity.file.FileBo;
import com.example.web.dto.CommResp;
import com.example.web.dto.file.FileResp;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** @description 文件管理*/
@RestController
@RequestMapping("/api/file")
public class FileRest {private static Log logger = LogFactory.getLog(FileRest.class);@Resourceprivate FileBlh fileBlh;/*** @description 直接上传文件, 入参:file, 出参:fileName*/@PostMapping(value="uploadFile")public FileResp uploadFileRest(HttpServletRequest req) {FileResp resp = new FileResp();FileBo bo = new FileBo();bo.setReq(req);fileBlh.uploadFile(bo);resp.setFileName(bo.getFileName());CommResp.setResp(resp,bo);resp.setResp();return resp;}/*** @description 获取文件下载Url, 入参:fileName, 出参:fileUrl*/@PostMapping(value="getFileUrl",consumes="application/json")public FileResp getFileUrlRest(@RequestBody JSONObject req) {FileResp resp = new FileResp();FileBo bo = new FileBo();bo.setFileName(req.getString("fileName"));fileBlh.getFileUrl(bo);resp.setFileUrl(bo.getFileUrl());CommResp.setResp(resp,bo);resp.setResp();return resp;}/*** @description 删除文件, 入参:fileName*/@PostMapping(value="deleteFile",consumes="application/json")public FileResp deleteFileRest(@RequestBody JSONObject req) {FileResp resp = new FileResp();FileBo bo = new FileBo();bo.setFileName(req.getString("fileName"));fileBlh.deleteFile(bo);CommResp.setResp(resp,bo);resp.setResp();return resp;}/*** @description 校验文件是否存在, 入参:md5*/@GetMapping(value = "checkSplitFile")public FileResp checkSplitFileRest(String md5) {FileResp resp = new FileResp();FileBo bo = new FileBo();bo.setMd5(md5);fileBlh.checkSplitFile(bo);resp.setFileUrl(bo.getFileUrl());resp.setCode(bo.getCode());CommResp.setResp(resp,bo);resp.setResp();return resp;}/*** @description 分片上传文件, 入参:data*/@PostMapping(value = "uploadSplitFile")public FileResp uploadSplitFileRest(HttpServletRequest req) {FileResp resp = new FileResp();FileBo bo = new FileBo();bo.setReq(req);fileBlh.uploadSplitFile(bo);resp.setCode(bo.getCode());CommResp.setResp(resp,bo);resp.setResp();return resp;}/*** @description 文件合并, 入参:shardCount/fileName/md5/fileType/fileSize*/@GetMapping(value = "mergeSplitFile")public FileResp mergeSplitFileRest(HttpServletRequest req) {FileResp resp = new FileResp();FileBo bo = new FileBo();bo.setShardCount(Integer.valueOf(req.getParameter("shardCount")));bo.setFileName(req.getParameter("fileName"));bo.setMd5(req.getParameter("md5"));bo.setFileType(req.getParameter("fileType"));bo.setFileSize(Long.valueOf(req.getParameter("fileSize")));fileBlh.mergeSplitFile(bo);resp.setCode(bo.getCode());resp.setFileName(bo.getFileName());CommResp.setResp(resp,bo);resp.setResp();return resp;}/*** @description 获取文件分片下载信息, 入参:fileName, 出参:shardCount*/@PostMapping(value="getSplitFileInfo",consumes="application/json")public FileResp getSplitFileInfoRest(@RequestBody JSONObject req) {FileResp resp = new FileResp();FileBo bo = new FileBo();bo.setFileName(req.getString("fileName"));fileBlh.getSplitFileInfo(bo);resp.setShardCount(bo.getShardCount());CommResp.setResp(resp,bo);resp.setResp();return resp;}/*** @description 文件分片下载, 入参:fileName/fileNo, 出参:文件流*/@GetMapping(value="downSplitFile")public void downSplitFileRest(HttpServletRequest req, HttpServletResponse res) {FileBo bo = new FileBo();bo.setFileName(req.getParameter("fileName"));bo.setShardCount(Integer.valueOf(req.getParameter("fileNo")));bo.setRes(res);fileBlh.downSplitFile(bo);}/*** @description 视频播放*/@GetMapping(value = "videoPlay")public void videoPlayRest(HttpServletRequest req, HttpServletResponse res) {FileBo bo = new FileBo();bo.setReq(req);bo.setRes(res);bo.setFileName(req.getParameter("video"));fileBlh.videoPlay(bo);}
}
2. 文件上传前端代码
- HTML效果
http://127.0.0.1:8081/test/upload.html
- resources/static/upload.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>upload</title><link rel="icon" href="data:;base64,="><script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script><script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
</head>
<body><input type="file" name="file" id="file"><text id="msgtext"></text>
</body>
<script>const baseUrl = "http://127.0.0.1:8081/test/api/file/"/*** 计算文件的md5值* @param file 文件*/function calculateFileMd5(file) {return calculateFileMd5Chunk(file, 2097152);}/*** 分片计算文件的md5值* @param file 文件* @param chunkSize 分片大小*/function calculateFileMd5Chunk(file, chunkSize) {return new Promise((resolve, reject) => {let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;let chunks = Math.ceil(file.size / chunkSize);let currentChunk = 0;let spark = new SparkMD5.ArrayBuffer();let fileReader = new FileReader();fileReader.onload = function (e) {spark.append(e.target.result);currentChunk++;if (currentChunk < chunks) {loadNext();} else {let md5 = spark.end();resolve(md5);}};fileReader.onerror = function (e) {reject(e);};function loadNext() {let start = currentChunk * chunkSize;let end = start + chunkSize;if (end > file.size) {end = file.size;}fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));}loadNext();});}/*** 获取文件的后缀名* @param fileName 文件名*/function getFileType(fileName) {return fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();}/*** 根据文件的md5值判断文件是否已经上传* @param md5 文件的md5* @param file 准备上传的文件*/function checkMd5(md5, file) {$.ajax({url: baseUrl + "checkSplitFile",type: "get",data: {md5: md5},async: true,dataType: "json",success: function (msg) {if (msg.code === 200) {console.log("文件已经存在")$('#msgtext').html('文件已存在: '+ msg.fileUrl);} else if (msg.code === 204) {console.log("文件不存在需要上传")postFile(file, 0, md5);} else {console.log('未知错误');}}})}/*** 分片上传* @param file 上传的文件* @param i 第几分片,从0开始* @param md5 文件的md5值*/function postFile(file, i, md5) {let name = file.name, // 文件名size = file.size, // 总大小shardSize = 2 * 1024 * 1024,shardSize = 5 * 1024 * 1024, // 以5MB为一个分片,每个分片的大小shardCount = Math.ceil(size / shardSize); // 总片数if (i >= shardCount) {return;}let start = i * shardSize;let end = start + shardSize;let packet = file.slice(start, end); // 将文件进行切片let form = new FormData();form.append("md5", md5); // 前端生成uuid作为标识符form.append("data", packet); // slice方法用于切出文件的一部分form.append("name", name);form.append("totalSize", size);form.append("total", shardCount); // 总片数form.append("index", i + 1); // 当前是第几片$.ajax({url: baseUrl + "uploadSplitFile",type: "post",data: form,async: true,dataType: "json",processData: false,contentType: false,success: function (msg) {if (msg.code === 201) {form = '';i++;postFile(file, i, md5);} else if (msg.code === 203) {form = '';setInterval(function () {postFile(file, i, md5)}, 2000);} else if (msg.code === 202) {merge(shardCount, name, md5, getFileType(file.name), file.size)console.log("上传成功");} else {console.log('未知错误');}}})}/*** 合并文件* @param shardCount 分片数* @param fileName 文件名* @param md5 文件md值* @param fileType 文件类型* @param fileSize 文件大小*/function merge(shardCount, fileName, md5, fileType, fileSize) {$.ajax({url: baseUrl + "mergeSplitFile",type: "get",data: {shardCount: shardCount,fileName: fileName,md5: md5,fileType: fileType,fileSize: fileSize},async: true,dataType: "json",success: function (msg) {$('#msgtext').html('文件上传成功: '+ msg.fileName);}})}// 浏览器加载文件后, 计算文件的md5值document.getElementById("file").addEventListener("change", function () {$('#msgtext').html('待上传');let file = this.files[0];calculateFileMd5(file).then(e => {let md5 = e;checkMd5(md5, file)}).catch(e => {console.error(e);});});$('#msgtext').html('待上传');
</script>
</html>
3. 视频播放前端代码
-
ckplayer
ckplayer是一款在网页上播放视频的软件,基于javascript和css,其特点是开源,不依赖其它插件。
视频播放插件下载、视频播放插件手册 -
HTML效果
http://127.0.0.1:8081/test/video.html?video=202312/f210299b-3988-4ad3-b9a3-fd6677936bda-test.mp4
-
resources/static/video.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>play</title><link rel="icon" href="data:;base64,="><script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script><script src="https://www.ckplayer.com/public/static/ckplayer-x3/js/ckplayer.js"></script><link rel="stylesheet" type="text/css" href="https://www.ckplayer.com/public/static/ckplayer-x3/css/ckplayer.css" />
</head>
<body><div class="video" style="width: 100%; height: 500px;max-width: 800px;"></div><p><button type="button" onclick="player.play()">播放</button><button type="button" onclick="player.pause()">暂停</button><button type="button" onclick="player.seek(20)">跳转</button><button type="button" onclick="player.volume(0.6)">修改音量</button><button type="button" onclick="player.muted()">静音</button><button type="button" onclick="player.exitMuted()">恢复音量</button><button type="button" onclick="player.full()">全屏</button></p><p id="state1"></p><p id="state2"></p>
</body>
<script>const queryString = window.location.search;const urlParams = new URLSearchParams(queryString);const videoObj = urlParams.get('video');const baseUrl = "http://127.0.0.1:8081/test/api/file/videoPlay"let videoObject = {container: '.video', // 视频容器的IDvolume: 0.8, // 默认音量,范围0-1video: baseUrl + '?video=' + videoObj, // 视频地址};let player = new ckplayer(videoObject) // 调用播放器并赋值给变量player
</script>
</html>