一、流程设计
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: 300MB
max-request-size: 300MB
minio:
endpoint: http://127.0.0.1:9000 #MinIO服务所在地址
accessKey: admin #访问的key
secretKey: 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;
// 文件MD5
private 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 文件名称
*/
@SneakyThrows
public 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天则默认最大值
*/
@SneakyThrows
public 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 文件路径
*/
@SneakyThrows
public 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 文件名称(包含存储目录)
*/
@SneakyThrows
public 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 桶名称
*/
@SneakyThrows
public 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 桶名称
*/
@SneakyThrows
public void makeBucket(String bucketName) {
if (bucketExists(bucketName)) {
return;
}
getMinioClient().makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
/**
* @description 获取文件
* @param bucketName 桶名称
* @param objectName 文件路径
* @param offset 截取流的开始位置
* @param length 截取长度
*/
@SneakyThrows
public 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 文件路径
*/
@SneakyThrows
public InputStream getObject(String bucketName, String objectName) {
return getMinioClient().getObject(
GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
}
/**
* @description 上传分片文件
* @param inputStream 输入流
* @param objectName 文件路径
* @param bucketName 桶名称
*/
@SneakyThrows
public 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 桶名称
*/
@SneakyThrows
public void removeBucket(String bucketName) {
removeObjects(bucketName, listObjectNames(bucketName));
getMinioClient().removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
}
/**
* @description 查询桶中所有的文件
* @param bucketName 桶名称
*/
@SneakyThrows
public 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 存储于桶中的对象名
*/
@SneakyThrows
public 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 分块文件集合
*/
@SneakyThrows
public 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;
@Resource
private MinioFileUtil minioFileUtil;
@Resource
private RedisTemplate redisTemplate;
@Autowired
private 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); // 最后一片, 状态码为202
bo.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()+" 成功");
// 计算文件的md5
String 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: bytes
bo.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;
}
// 断点续传 状态码206
bo.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);
@Resource
private 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
文章来源:https://www.toymoban.com/news/detail-756510.html -
resources/static/video.html文章来源地址https://www.toymoban.com/news/detail-756510.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', // 视频容器的ID
volume: 0.8, // 默认音量,范围0-1
video: baseUrl + '?video=' + videoObj, // 视频地址
};
let player = new ckplayer(videoObject) // 调用播放器并赋值给变量player
</script>
</html>
到了这里,关于09 视频分片上传Minio和播放的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!