目錄
引言:
一.什么是 MinIO ?
二.MinIO 的安裝與部署:
三.Spring Cloud 集成 MinIO:
1.前提準備:
(1)安裝依賴:
(2)配置MinIO連接:
(3)修改bucket的訪問權限:?
2.測試上傳、刪除、下載文件:
?3.圖片操作:
(1)MinioConfig 配置類:
(2)Controller 接口定義:
(3)Service開發:
【1】根據擴展名獲取mimeType:
【2】將文件上傳到minio:
【3】獲取文件默認存儲目錄路徑 年/ 月/ 日:
【4】獲取文件的md5:
4.視頻操作:
(1)斷點上傳:
(2)測試文件分塊上傳與合并:
【1】分塊上傳:
【2】合并分塊文件:
?(3)使用MinIO合并分塊:
【1】將分塊文件上傳至minio:
【2】合并文件,要求分塊文件最小5M:
【3】清除分塊文件:
(4)三層架構——上傳分塊:
【1】檢查文件是否存在:
【2】文件上傳前檢查分塊文件是否存在:
【3】上傳分塊文件:
(4) 三層架構——清除分塊文件:
(5)三層架構——從MinIO下載文件:
(6)三層架構——合并分塊文件:
引言:
一個計算機無法存儲海量的文件,通過網絡將若干計算機組織起來共同去存儲海量的文件,去接收海量用戶的請求,這些組織起來的計算機通過網絡進行通信,如下圖:
好處:
1、一臺計算機的文件系統處理能力擴充到多臺計算機同時處理。
2、一臺計算機掛了還有另外副本計算機提供數據。
3、每臺計算機可以放在不同的地域,這樣用戶就可以就近訪問,提高訪問速度。
?市面上有哪些分布式文件系統的產品呢?
- NFS:在客戶端上映射NFS服務器的驅動器,客戶端通過網絡訪問NFS服務器的硬盤完全透明。
- GFS:采用主從結構,一個GFS集群由一個master和大量的chunkserver組成,master存儲了數據文件的元數據,一個文件被分成了若干塊存儲在多個chunkserver中。用戶從master中獲取數據元信息,向chunkserver存儲數據。
- HDFS:是Hadoop抽象文件系統的一種實現,高度容錯性的系統,適合部署在廉價的機器上。能提供高吞吐量的數據訪問,非常適合大規模數據集上的應用,HDFS的文件分布在集群機器上,同時提供副本進行容錯及可靠性保證。例如客戶端寫入讀取文件的直接操作都是分布在集群各個機器上的,沒有單點性能壓力。
- 阿里云對象存儲服務OSS:對象存儲 OSS_云存儲服務阿里云對象存儲 OSS 是一款海量、安全、低成本、高可靠的云存儲服務,提供 99.995 % 的服務可用性和多種存儲類型,適用于數據湖存儲,數據遷移,企業數據管理,數據處理等多種場景,可對接多種計算分析平臺,直接進行數據處理與分析,打破數據孤島,優化存儲成本,提升業務價值。
https://www.aliyun.com/product/oss
- 百度對象存儲BOS:對象存儲BOS_百度智能云百度智能云對象存儲BOS提供穩定、安全、高效、高可擴展的云存儲服務。您可以將任意數量和形式的非結構化數據存入對象存儲BOS,BOS支持標準、低頻、冷和歸檔存儲等多種存儲類型,適用于數據遷移、企業數據管理、數據處理、數據湖存儲等多種場景。
https://cloud.baidu.com/product/bos.html?
一.什么是 MinIO ?
MinIO是一個高性能、分布式對象存儲系統,專為大規模數據基礎設施而設計,它兼容亞馬遜 S3 云存儲服務接口,非常適合于存儲大容量非結構化的數據,例如圖片、視頻、日志文件、備份數據和容器/虛擬機鏡像等。
它一大特點就是輕量,使用簡單,功能強大,支持各種平臺,單個文件最大5TB,兼容 Amazon S3接口,提供了 Java、Python、GO等多版本SDK支持。
官網:https://min.io
中文:https://www.minio.org.cn/,http://docs.minio.org.cn/docs/
MinIO的主要特點包括:
- ?高性能?:作為世界上最快的對象存儲之一,MinIO可以支持高達每秒數百GB的吞吐量
- ?簡單易用?:簡單的命令行和Web界面,幾分鐘內即可完成安裝和配置
- ?云原生?:從公有云到私有云再到邊緣計算,MinIO都能完美運行
- ?開源?:采用Apache V2開源協議,可以自由使用和修改
- ?輕量級?:單個二進制文件即可運行,沒有外部依賴
MinIO集群采用去中心化共享架構,每個結點是對等關系,通過Nginx可對MinIO進行負載均衡訪問。
去中心化有什么好處?
在大數據領域,通常的設計理念都是無中心和分布式。Minio分布式模式可以幫助你搭建一個高可用的對象存儲服務,你可以使用這些存儲設備,而不用考慮其真實物理位置。
它將分布在不同服務器上的多塊硬盤組成一個對象存儲服務。由于硬盤分布在不同的節點上,分布式Minio避免了單點故障。如下圖:
Minio使用糾刪碼技術來保護數據,它是一種恢復丟失和損壞數據的數學算法,它將數據分塊冗余的分散存儲在各各節點的磁盤上,所有的可用磁盤組成一個集合,上圖由8塊硬盤組成一個集合,當上傳一個文件時會通過糾刪碼算法計算對文件進行分塊存儲,除了將文件本身分成4個數據塊,還會生成4個校驗塊,數據塊和校驗塊會分散的存儲在這8塊硬盤上。
使用糾刪碼的好處是即便丟失一半數量(N / 2)的硬盤,仍然可以恢復數據。 比如上邊集合中有4個以內的硬盤損害仍可保證數據恢復,不影響上傳和下載,如果多于一半的硬盤壞了則無法恢復。
二.MinIO 的安裝與部署:
下邊在本機演示MinIO恢復數據的過程,在本地創建4個目錄表示4個硬盤。
下載minio,下載地址在?Minio下載地址?:
隨后CMD進入有minio.exe的目錄,運行下邊的命令:( 替換自己的安裝地址)
minio.exe server E:\minio_data\data1 E:\minio_data\data2 E:\minio_data\data3 E:\minio_data\data4
啟動結果如下:
說明如下:
WARNING: MINIO_ACCESS_KEY and MINIO_SECRET_KEY are deprecated.
?????????Please use MINIO_ROOT_USER and MINIO_ROOT_PASSWORD
Formatting 1st pool, 1 set(s), 4 drives per set.
WARNING: Host local has more than 2 drives of set. A host failure will result in data becoming unavailable.
WARNING: Detected default credentials 'minioadmin:minioadmin', we recommend that you change these values with 'MINIO_ROOT_USER' and 'MINIO_ROOT_PASSWORD' environment variables1)老版本使用的MINIO_ACCESS_KEY 和 MINIO_SECRET_KEY不推薦使用,推薦使用MINIO_ROOT_USER 和MINIO_ROOT_PASSWORD設置賬號和密碼。
2)pool即minio節點組成的池子,當前有一個pool和4個硬盤組成的set集合
3)因為集合是4個硬盤,大于2的硬盤損壞數據將無法恢復。
4)賬號和密碼默認為minioadmin、minioadmin,可以在環境變量中設置通過'MINIO_ROOT_USER' and 'MINIO_ROOT_PASSWORD' 進行設置。
下邊輸入?http://192.168.88.1:9000 進行登錄(看自己的地址),賬號和密碼均為為:minioadmin
?輸入bucket的名稱,點擊“CreateBucket”,創建成功:
隨后就可以進行上傳、刪除等操作了。
三.Spring Cloud 集成 MinIO:
1.前提準備:
(1)安裝依賴:
<dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.5.2</version>
</dependency>
<dependency><groupId>com.squareup.okhttp3</groupId><artifactId>okhttp</artifactId><version>4.12.0</version>
</dependency>
<!--根據擴展名取mimetype-->
<dependency><groupId>com.j256.simplemagic</groupId><artifactId>simplemagic</artifactId><version>1.17</version>
</dependency>
(2)配置MinIO連接:
因為我們要上傳普通文件與視頻文件,所以創建 mediafiles(普通文件) 與 video(視頻文件) 兩個 buckets 。
在?bootstrap.yml
中添加配置:
minio:endpoint: http://192.168.56.1:9000accessKey: minioadminsecretKey: minioadminbucket:files: mediafilesvideofiles: video
需要三個參數才能連接到minio服務。
參數 | 說明 |
Endpoint | 對象存儲服務的URL |
Access Key | Access key就像用戶ID,可以唯一標識你的賬戶。 |
Secret Key | Secret key是你賬戶的密碼。 |
隨后也可以添加對上傳文件的限制:
spring:servlet:multipart:max-file-size: 50MBmax-request-size: 50MB
max-file-size:單個文件的大小限制
Max-request-size: 單次請求的大小限制
(3)修改bucket的訪問權限:?
點擊“Manage”修改bucket的訪問權限:
選擇public權限:
2.測試上傳、刪除、下載文件:
首先初始化?minioClient:
MinioClient minioClient =MinioClient.builder().endpoint("http://192.168.56.1:9000").credentials("minioadmin", "minioadmin").build();
隨后設置contentType可以通過com.j256.simplemagic.ContentType枚舉類查看常用的mimeType(媒體類型)。通過擴展名得到mimeType,代碼如下:
// 根據擴展名取出mimeType
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(".mp4");
String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;// 通用mimeType,字節流
校驗文件的完整性,對文件計算出md5值,比較原始文件的md5和目標文件的md5,一致則說明完整:
//校驗文件的完整性對文件的內容進行md5
FileInputStream fileInputStream1 = new FileInputStream(new File("D:\\develop\\upload\\1.mp4"));
String source_md5 = DigestUtils.md5Hex(fileInputStream1);
FileInputStream fileInputStream = new FileInputStream(new File("D:\\develop\\upload\\1a.mp4"));
String local_md5 = DigestUtils.md5Hex(fileInputStream);
if(source_md5.equals(local_md5)){System.out.println("下載成功");
}
?下面是完整的測試代碼:
package com.xuecheng.media;import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import io.minio.*;
import org.apache.commons.codec.digest.DigestUtils;import org.apache.commons.compress.utils.IOUtils;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import java.io.*;public class MinioTest {// 初始化minioClientMinioClient minioClient =MinioClient.builder().endpoint("http://192.168.56.1:9000").credentials("minioadmin", "minioadmin").build();@Testpublic void test_upload() throws Exception {// 通過擴展名得到媒體資源類型 mimeType// 根據擴展名取出mimeTypeContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(".mp4");String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;// 通用mimeType,字節流if(extensionMatch != null){mimeType = extensionMatch.getMimeType();}// 上傳文件的參數信息UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder().bucket("testbucket")// 桶.filename("C:\\Users\\Eleven\\Videos\\4月16日.mp4") // 指定本地文件路徑
// .object("1.mp4")// 對象名在桶下存儲該文件.object("test/01/1.mp4")// 對象名 放在子目錄下.contentType(mimeType)// 設置媒體文件類型.build();// 上傳文件minioClient.uploadObject(uploadObjectArgs);}// 刪除文件@Testpublic void test_delete() throws Exception {//RemoveObjectArgsRemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder().bucket("testbucket").object("test/01/1.mp4").build();// 刪除文件minioClient.removeObject(removeObjectArgs);}// 查詢文件 從minio中下載@Testpublic void test_getFile() throws Exception {GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket("testbucket").object("test/01/1.mp4").build();//查詢遠程服務獲取到一個流對象FilterInputStream inputStream = minioClient.getObject(getObjectArgs);//指定輸出流FileOutputStream outputStream = new FileOutputStream(new File("D:\\develop\\upload\\1a.mp4"));IOUtils.copy(inputStream,outputStream);//校驗文件的完整性對文件的內容進行md5FileInputStream fileInputStream1 = new FileInputStream(new File("D:\\develop\\upload\\1.mp4"));String source_md5 = DigestUtils.md5Hex(fileInputStream1);FileInputStream fileInputStream = new FileInputStream(new File("D:\\develop\\upload\\1a.mp4"));String local_md5 = DigestUtils.md5Hex(fileInputStream);if(source_md5.equals(local_md5)){System.out.println("下載成功");}}
}
?3.圖片操作:
上傳課程圖片總體上包括兩部分:
1、上傳課程圖片前端請求媒資管理服務將文件上傳至分布式文件系統,并且在媒資管理數據庫保存文件信息。
2、上傳圖片成功保存圖片地址到課程基本信息表中。
詳細流程如下:
1、前端進入上傳圖片界面
2、上傳圖片,請求媒資管理服務。
3、媒資管理服務將圖片文件存儲在MinIO。
4、媒資管理記錄文件信息到數據庫。
5、前端請求內容管理服務保存課程信息,在內容管理數據庫保存圖片地址。
?
?首先在minio配置bucket,bucket名稱為:mediafiles,并設置bucket的權限為公開。
在nacos配置中minio的相關信息,進入media-service-dev.yaml:
配置信息如下:
minio:endpoint: http://192.168.56.1:9000accessKey: minioadminsecretKey: minioadminbucket:files: mediafilesvideofiles: video
(1)MinioConfig 配置類:
package com.xuecheng.media.config;import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** @author eleven* @version 1.0* @description TODO* @date 2025/6/4 15:00*/
@Configuration
public class MinioConfig {@Value("${minio.endpoint}")private String endpoint;@Value("${minio.accessKey}")private String accessKey;@Value("${minio.secretKey}")private String secretKey;@Beanpublic MinioClient minioClient() {MinioClient minioClient = MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build();return minioClient;}
}
(2)Controller 接口定義:
根據需求分析,下邊進行接口定義,此接口定義為一個通用的上傳文件接口,可以上傳圖片或其它文件。
首先分析接口:
請求地址:/media/upload/coursefile
請求內容:Content-Type: multipart/form-data;
因為無法直接獲取上傳文件的本地路徑,所以創建臨時文件作為中轉,臨時文件名以"minio"為前綴,".temp"為后綴。
package com.xuecheng.media.api;import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.media.model.dto.QueryMediaParamsDto;
import com.xuecheng.media.model.dto.UploadFileParamsDto;
import com.xuecheng.media.model.dto.UploadFileResultDto;
import com.xuecheng.media.model.po.MediaFiles;
import com.xuecheng.media.service.MediaFileService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;import java.io.File;
import java.io.IOException;/*** @description 媒資文件管理接口* @author eleven* @version 1.0*/@Tag(name = "媒資文件管理接口",description = "媒資文件管理接口")@RestController
public class MediaFilesController {@AutowiredMediaFileService mediaFileService;@Operation(summary = "上傳圖片")@RequestMapping(value = "/upload/coursefile",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)public UploadFileResultDto upload(@RequestPart("filedata") MultipartFile filedata) throws IOException {// 準備上傳文件的信息UploadFileParamsDto uploadFileParamsDto = new UploadFileParamsDto();// 原始文件名稱uploadFileParamsDto.setFilename(filedata.getOriginalFilename());// 文件大小uploadFileParamsDto.setFileSize(filedata.getSize());// 文件類型uploadFileParamsDto.setFileType("001001"); // 自定義的字典,001001代表圖片// 因為無法直接獲得該文件的路徑,所以創建一個臨時文件File tempFile = File.createTempFile("minio", ".temp");filedata.transferTo(tempFile); // 拷貝文件Long companyId = 1232141425L;// 文件路徑String localFilePath = tempFile.getAbsolutePath();//調用service上傳圖片UploadFileResultDto uploadFileResultDto = mediaFileService.uploadFile(companyId, uploadFileParamsDto, localFilePath);return uploadFileResultDto;}
}
(3)Service開發:
這里分幾個方法進行開發:
【1】根據擴展名獲取mimeType:
/*** 根據擴展名獲取mimeType*/
private String getMimeType(String extension){if(extension == null){extension = "";}// 根據擴展名取出mimeTypeContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//通用mimeType,字節流if(extensionMatch != null){mimeType = extensionMatch.getMimeType();}return mimeType;
}
【2】將文件上傳到minio:
/*** 將文件上傳到minio* @param localFilePath 文件本地路徑* @param mimeType 媒體類型* @param bucket 桶* @param objectName 對象名* @return*/
public boolean addMediaFilesToMinIO(String localFilePath,String mimeType,String bucket, String objectName){try {UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder().bucket(bucket)//桶.filename(localFilePath) //指定本地文件路徑.object(objectName)//對象名 放在子目錄下.contentType(mimeType)//設置媒體文件類型.build();//上傳文件minioClient.uploadObject(uploadObjectArgs);log.debug("上傳文件到minio成功,bucket:{},objectName:{}",bucket,objectName);return true;} catch (Exception e) {e.printStackTrace();log.error("上傳文件出錯,bucket:{},objectName:{},錯誤信息:{}",bucket,objectName,e.getMessage());}return false;
}
【3】獲取文件默認存儲目錄路徑 年/ 月/ 日:
/*** 獲取文件默認存儲目錄路徑 年/月/日*/
private String getDefaultFolderPath() {SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");String folder = sdf.format(new Date()).replace("-", "/")+"/";return folder;
}
【4】獲取文件的md5:
/*** 獲取文件的md5*/
private String getFileMd5(File file) {try (FileInputStream fileInputStream = new FileInputStream(file)) {String fileMd5 = DigestUtils.md5Hex(fileInputStream);return fileMd5;} catch (Exception e) {e.printStackTrace();return null;}
}
隨后 MediaFileServiceImpl 類創建方法實現上傳圖片并校驗是否成功上傳:
如果在uploadFile方法上添加@Transactional,當調用uploadFile方法前會開啟數據庫事務,如果上傳文件過程時間較長那么數據庫的事務持續時間就會變長,這樣數據庫鏈接釋放就慢,最終導致數據庫鏈接不夠用。
我們只將addMediaFilesToDb方法添加事務控制即可,將該方法提取出來在 MediaFileTransactionalServiceImpl 中創建方法。
@Autowired
MediaFilesMapper mediaFilesMapper;@Autowired
MinioClient minioClient;@Autowired
private MediaFileTransactionalServiceImpl transactionalService; // 事務,操作數據庫//存儲普通文件
@Value("${minio.bucket.files}")
private String bucket_mediafiles;//存儲視頻
@Value("${minio.bucket.videofiles}")
private String bucket_video;@Override
public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath) {// 文件名String filename = uploadFileParamsDto.getFilename();// 先得到擴展名String extension = filename.substring(filename.lastIndexOf("."));// 根據擴展名得到mimeTypeString mimeType = getMimeType(extension);// 子目錄String defaultFolderPath = getDefaultFolderPath();// 文件的md5值String fileMd5 = getFileMd5(new File(localFilePath));String objectName = defaultFolderPath+fileMd5+extension;// 上傳文件到minioboolean result = addMediaFilesToMinIO(localFilePath, mimeType, bucket_mediafiles, objectName);if(!result){XueChengPlusException.cast("上傳文件失敗");}try {// 調用事務方法MediaFiles mediaFiles = transactionalService.addMediaFilesToDbWithTransaction(companyId, fileMd5, uploadFileParamsDto, bucket_mediafiles, objectName);UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);return uploadFileResultDto;} catch (Exception e) {// 如果事務失敗,嘗試刪除已上傳的MinIO文件try {minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucket_mediafiles).object(objectName).build());} catch (Exception ex) {log.error("回滾時刪除MinIO文件失敗", ex);}throw e;}
}
而為了回滾數據庫,我們在新建的 MediaFileTransactionalServiceImpl 類中創建:
package com.xuecheng.media.service.impl;import com.xuecheng.base.exception.XueChengPlusException;
import com.xuecheng.media.mapper.MediaFilesMapper;
import com.xuecheng.media.model.dto.UploadFileParamsDto;
import com.xuecheng.media.model.po.MediaFiles;
import com.xuecheng.media.service.MediaFileTransactionalService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;@Slf4j
@Service
public class MediaFileTransactionalServiceImpl implements MediaFileTransactionalService {@Autowiredprivate MediaFilesMapper mediaFilesMapper;/*** @description 將文件信息添加到文件表* @param companyId 機構id* @param fileMd5 文件md5值* @param uploadFileParamsDto 上傳文件的信息* @param bucket 桶* @param objectName 對象名稱* @return com.xuecheng.media.model.po.MediaFiles* @author Mr.M* @date 2022/10/12 21:22*/@Override@Transactional(rollbackFor = Exception.class)public MediaFiles addMediaFilesToDbWithTransaction(Long companyId, String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket, String objectName) {// 原addMediaFilesToDb方法內容MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);if(mediaFiles == null){mediaFiles = new MediaFiles();BeanUtils.copyProperties(uploadFileParamsDto,mediaFiles);mediaFiles.setId(fileMd5);mediaFiles.setCompanyId(companyId);mediaFiles.setBucket(bucket);mediaFiles.setFilePath(objectName);mediaFiles.setFileId(fileMd5);mediaFiles.setUrl("/" + bucket + "/" + objectName);mediaFiles.setCreateDate(LocalDateTime.now());mediaFiles.setStatus("1");mediaFiles.setAuditStatus("002003");int insert = mediaFilesMapper.insert(mediaFiles);if(insert <= 0){log.error("向數據庫保存文件失敗,bucket:{},objectName:{}",bucket,objectName);throw new XueChengPlusException("保存文件信息失敗"); // 觸發回滾}}return mediaFiles;}
}
4.視頻操作:
(1)斷點上傳:
通常視頻文件都比較大,所以對于媒資系統上傳文件的需求要滿足大文件的上傳要求。http協議本身對上傳文件大小沒有限制,但是客戶的網絡環境質量、電腦硬件環境等參差不齊,如果一個大文件快上傳完了網斷了沒有上傳完成,需要客戶重新上傳,用戶體驗非常差,所以對于大文件上傳的要求最基本的是斷點續傳。
什么是斷點續傳?
????????引用百度百科:斷點續傳指的是在下載或上傳時,將下載或上傳任務(一個文件或一個壓縮包)人為的劃分為幾個部分,每一個部分采用一個線程進行上傳或下載,如果碰到網絡故障,可以從已經上傳或下載的部分開始繼續上傳下載未完成的部分,而沒有必要從頭開始上傳下載,斷點續傳可以提高節省操作時間,提高用戶體驗性。
斷點續傳流程如下圖:
流程如下:
1、前端上傳前先把文件分成塊
2、一塊一塊的上傳,上傳中斷后重新上傳,已上傳的分塊則不用再上傳
3、各分塊上傳完成最后在服務端合并文件
(2)測試文件分塊上傳與合并:
為了更好的理解文件分塊上傳的原理,下邊用java代碼測試文件的分塊與合并。
文件分塊的流程如下:
1、獲取源文件長度
2、根據設定的分塊文件的大小計算出塊數
3、從源文件讀數據依次向每一個塊文件寫數據。
而為了實現文件分塊,需要使用 RandomAccessFile。
RandomAccessFile
?是 Java 提供的 ?隨機訪問文件? 類,允許對文件進行 ?任意位置讀寫,適用于大文件分塊、斷點續傳、數據庫索引等場景。構造方法:
// 模式: // "r" : 只讀 // "rw": 讀寫(文件不存在則自動創建) // "rws": 讀寫 + 同步寫入元數據(強制刷盤) // "rwd": 讀寫 + 同步寫入文件內容(強制刷盤) RandomAccessFile raf = new RandomAccessFile(File file, String mode); RandomAccessFile raf = new RandomAccessFile(String path, String mode);
操作指針:?
方法 作用 long getFilePointer()
返回當前指針位置 void seek(long pos)
移動指針到指定位置 long length()
返回文件長度 void setLength(long newLength)
擴展/截斷文件 讀寫數據:
方法 說明 int read()
讀取1字節(返回? 0~255
,失敗返回?-1
)int read(byte[] b)
讀取數據到字節數組 readInt()
,?readDouble()
讀取基本類型 write(byte[] b)
寫入字節數組 writeInt()
,?writeUTF()
寫入基本類型或字符串
【1】分塊上傳:
流程分析:
①初始化階段
- 設置源文件路徑和分塊存儲目錄,自動創建不存在的目錄
- 定義每個分塊大小為1MB,并根據文件總大小計算所需分塊數量
- 初始化1KB的讀寫緩沖區:byte[] b = new byte[1024];
②文件讀取準備
- 使用 RandomAccessFile 以只讀模式(r)打開源文件
- 文件指針自動記錄讀取位置,確保連續性
③分塊處理核心流程
- 循環創建每個分塊文件,先刪除已存在的舊文件
- 為每個分塊創建新的 RandomAccessFile 寫入流(rw)
- 通過緩沖區循環讀取源文件數據,寫入分塊文件
- 實時檢查分塊文件大小,達到1MB立即切換下一個分塊
④收尾工作
- 每個分塊寫入完成后立即關閉文件流
- 所有分塊處理完畢后關閉源文件流
package com.xuecheng.media;import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.*;/*** @author eleven* @version 1.0* @description 大文件處理測試*/
public class BigFileTest {/*** 測試文件分塊方法*/@Testpublic void testChunk() throws IOException {File sourceFile = new File("d:/develop/bigfile_test/nacos.mp4");String chunkPath = "d:/develop/bigfile_test/chunk/";File chunkFolder = new File(chunkPath); // 分塊地址文件if (!chunkFolder.exists()) {chunkFolder.mkdirs(); // 不存在則創建}// 分塊大小long chunkSize = 1024 * 1024 * 1;// 分塊數量 (向上取整)long chunkNum = (long) Math.ceil(sourceFile.length() * 1.0 / chunkSize);System.out.println("分塊總數:" + chunkNum);// 緩沖區大小byte[] b = new byte[1024];// 使用流從源文件中讀數據,向分塊文件中寫數據// 使用RandomAccessFile訪問文件RandomAccessFile raf_read = new RandomAccessFile(sourceFile, "r"); // r:允許對文件進行讀操作// 分塊for (int i = 0; i < chunkNum; i++) {// 創建分塊文件File file = new File(chunkPath + i);if(file.exists()){file.delete(); // 確保每個文件都是重新生成}boolean newFile = file.createNewFile(); // 在指定路徑下創建一個空的物理文件// 如果文件已存在(盡管前面有 file.delete(),但極端情況下可能刪除失敗)// createNewFile() 會返回 false,防止后續 RandomAccessFile 仍會覆蓋寫入if (newFile) {// 在 RandomAccessFile 中,文件指針(File Pointer) 會自動記錄當前讀寫位置// 確保每次 read() 或 write() 操作都會從上次結束的位置繼續。// 創建分塊文件寫入流,向分塊文件中寫數據RandomAccessFile raf_write = new RandomAccessFile(file, "rw"); // rw:允許對文件進行讀寫操作int len = -1;// 從源文件(raf_read)讀取數據到緩沖區 byte[] b,每次最多讀取 1024 字節(緩沖區大小)// len 返回實際讀取的字節數,如果 len = -1 表示源文件已讀完while ((len = raf_read.read(b)) != -1) {// 將緩沖區 b 中的數據讀取并寫入目標文件(分塊文件file),寫入范圍是 [0, len),確保只寫入有效數據raf_write.write(b, 0, len);// 確保每個分塊文件不超過指定大小chunkSizeif (file.length() >= chunkSize) {break;}}raf_write.close();System.out.println("完成分塊"+i);}}raf_read.close();}
}
【2】合并分塊文件:
流程分析:
- ?初始化階段?:檢查并創建合并文件,初始化寫入流和緩沖區(1KB),獲取所有分塊文件并按文件名數字排序,確保按原始順序合并。
- ?文件合并階段?:
- 遍歷每個分塊文件,使用
RandomAccessFile
讀取數據到緩沖區- 通過
seek(0)
確保每次從分塊文件頭部讀取- 將緩沖區數據寫入合并文件,循環直到當前分塊讀取完畢
- ?資源釋放?:每個分塊處理完后立即關閉流,全部合并后關閉寫入流。
- ?完整性校驗?:
- 使用
FileInputStream
讀取原始文件和合并文件的二進制內容- 通過
DigestUtils.md5Hex()
計算MD5哈希值比對- 完全一致則判定合并成功
package com.xuecheng.media;import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.*;/*** @author eleven* @version 1.0* @description 大文件處理測試*/
public class BigFileTest {/*** 測試文件合并方法*/@Testpublic void testMerge() throws IOException {// 塊文件目錄File chunkFolder = new File("d:/develop/bigfile_test/chunk/");// 原始文件File originalFile = new File("d:/develop/bigfile_test/nacos.mp4");// 合并文件File mergeFile = new File("d:/develop/bigfile_test/nacos01.mp4");if (mergeFile.exists()) {mergeFile.delete();}// 創建新的合并文件mergeFile.createNewFile();// 用于寫文件RandomAccessFile raf_write = new RandomAccessFile(mergeFile, "rw");// 指針指向文件頂端raf_write.seek(0);// 緩沖區byte[] b = new byte[1024];// 分塊列表File[] fileArray = chunkFolder.listFiles();// 轉成集合,便于排序List<File> fileList = Arrays.asList(fileArray);// 從小到大排序Collections.sort(fileList, new Comparator<File>() {@Overridepublic int compare(File o1, File o2) {return Integer.parseInt(o1.getName()) - Integer.parseInt(o2.getName());}});// 合并文件for (File chunkFile : fileList) {RandomAccessFile raf_read = new RandomAccessFile(chunkFile, "rw");int len = -1;while ((len = raf_read.read(b)) != -1) {raf_write.write(b, 0, len);}raf_read.close();}raf_write.close();//校驗文件try (FileInputStream fileInputStream = new FileInputStream(originalFile);FileInputStream mergeFileStream = new FileInputStream(mergeFile);) {//取出原始文件的md5String originalMd5 = DigestUtils.md5Hex(fileInputStream);//取出合并文件的md5進行比較String mergeFileMd5 = DigestUtils.md5Hex(mergeFileStream);if (originalMd5.equals(mergeFileMd5)) {System.out.println("合并文件成功");} else {System.out.println("合并文件失敗");}}}
}
?(3)使用MinIO合并分塊:
【1】將分塊文件上傳至minio:
// 測試方法:將本地分塊文件上傳至MinIO對象存儲
@Test
public void uploadChunk() {// 1. 初始化分塊文件目錄String chunkFolderPath = "D:\\develop\\upload\\chunk\\"; // 本地分塊文件存儲路徑File chunkFolder = new File(chunkFolderPath); // 創建文件對象表示該目錄// 2. 獲取所有分塊文件File[] files = chunkFolder.listFiles(); // 列出目錄下所有文件(分塊文件)// 3. 遍歷并上傳每個分塊文件for (int i = 0; i < files.length; i++) {try {// 3.1 構建上傳參數對象UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder().bucket("testbucket") // 設置目標存儲桶名稱.object("chunk/" + i) // 設置對象存儲路徑(格式:chunk/0, chunk/1...).filename(files[i].getAbsolutePath()) // 設置本地文件絕對路徑.build(); // 構建上傳參數// 3.2 執行上傳操作minioClient.uploadObject(uploadObjectArgs); // 調用MinIO客戶端上傳文件// 3.3 打印上傳成功日志System.out.println("上傳分塊成功" + i); // 標識當前上傳的分塊序號} catch (Exception e) {// 3.4 捕獲并打印上傳異常e.printStackTrace(); // 打印異常堆棧(如網絡問題、權限不足等)}}
}
【2】合并文件,要求分塊文件最小5M:
//合并文件,要求分塊文件最小5M
@Test
public void test_merge() throws Exception {List<ComposeSource> sources =Stream.iterate(0, i -> ++i) // 從0開始生成無限遞增序列.limit(6) // 限制取前6個元素(0-5).map(i -> ComposeSource.builder() // 將每個整數映射為ComposeSource對象.bucket("testbucket") // 設置存儲桶名.object("chunk/" + i) // 設置分塊對象路徑.build()) // 構建ComposeSource.collect(Collectors.toList()); // 收集為List// 合并操作構建對象ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder().bucket("testbucket").object("merge01.mp4") // 合并后的文件名.sources(sources).build(); // 要合并的分塊文件列表minioClient.composeObject(composeObjectArgs);
}
【3】清除分塊文件:
// 測試方法:清除MinIO中的分塊文件
@Test
public void test_removeObjects() {// 1. 準備待刪除的分塊文件列表// 使用Stream API生成0-5的序列,構建DeleteObject列表List<DeleteObject> deleteObjects = Stream.iterate(0, i -> ++i) // 生成無限遞增序列(0,1,2...).limit(6) // 限制只處理前6個分塊(0-5).map(i -> new DeleteObject( // 將每個數字轉為DeleteObject"chunk/".concat(Integer.toString(i)) // 構造分塊路徑格式:chunk/0, chunk/1...)).collect(Collectors.toList()); // 收集為List// 2. 構建刪除參數對象RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket("testbucket") // 設置目標存儲桶.objects(deleteObjects) // 設置要刪除的對象列表.build(); // 構建參數對象// 3. 執行批量刪除操作// 返回一個包含刪除結果的Iterable對象(可能包含成功/失敗信息)Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);// 4. 處理刪除結果(檢查是否有刪除失敗的記錄)results.forEach(r -> {DeleteError deleteError = null;try {// 獲取單個刪除操作的結果(如果刪除失敗會拋出異常)deleteError = r.get();// 如果deleteError不為null,表示對應文件刪除失敗} catch (Exception e) {// 打印刪除過程中出現的異常(如網絡問題、權限不足等)e.printStackTrace();}});
}
(4)三層架構——上傳分塊:
?下圖是上傳視頻的整體流程:
1、前端對文件進行分塊。
2、前端上傳分塊文件前請求媒資服務檢查文件是否存在,如果已經存在則不再上傳。
3、如果分塊文件不存在則前端開始上傳
4、前端請求媒資服務上傳分塊。
5、媒資服務將分塊上傳至MinIO。
6、前端將分塊上傳完畢請求媒資服務合并分塊。
7、媒資服務判斷分塊上傳完成則請求MinIO合并文件。
8、合并完成校驗合并后的文件是否完整,如果不完整則刪除文件。
其實整體實現無外乎就是將邏輯由一個文件的操作變為多文件操作。
【1】檢查文件是否存在:
/?**?* 文件上傳前檢查文件是否存在(基于文件MD5值)* @param fileMd5 文件的MD5哈希值(用于唯一標識文件)* @return RestResponse<Boolean> 封裝檢查結果(true=文件已存在,false=文件不存在)*/
@Override
public RestResponse<Boolean> checkFile(String fileMd5) {// 1. 根據文件MD5查詢數據庫中的文件記錄MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);// 2. 如果數據庫中存在該文件記錄,則進一步檢查MinIO存儲中是否真實存在該文件if (mediaFiles != null) {// 從數據庫記錄中獲取MinIO存儲的桶名稱String bucket = mediaFiles.getBucket();// 從數據庫記錄中獲取MinIO中的文件路徑(對象鍵)String filePath = mediaFiles.getFilePath();// 3. 初始化文件輸入流(用于檢查文件是否存在)InputStream stream = null;try {// 4. 通過MinIO客戶端API獲取文件對象// - 使用GetObjectArgs構建獲取對象的參數// - .bucket(bucket) 指定存儲桶// - .object(filePath) 指定對象路徑stream = minioClient.getObject(GetObjectArgs.builder().bucket(bucket).object(filePath).build());// 5. 如果成功獲取到輸入流(不為null),說明文件確實存在于MinIO中if (stream != null) {// 文件已存在,返回成功響應(true)return RestResponse.success(true);}} catch (Exception e) {// 6. 捕獲并處理可能發生的異常(如網絡問題、MinIO服務不可用、權限問題等)// - 使用自定義異常處理器拋出業務異常// - 異常信息會包含具體的錯誤詳情XueChengPlusException.cast(e.getMessage());} finally {// 7. 資源清理:確保輸入流被正確關閉(防止資源泄漏)if (stream != null) {try {stream.close();} catch (IOException e) {// 關閉流時的異常可以記錄日志,但不需要中斷業務流程log.error("關閉MinIO文件流失敗", e);}}}}// 8. 如果數據庫中沒有記錄 或 MinIO中不存在文件,則返回文件不存在return RestResponse.success(false);
}
【2】文件上傳前檢查分塊文件是否存在:
首先我們保存分塊文件的路徑格式如下:
假設?
fileMd5 = "d41d8cd98f00b204e9800998ecf8427e"
(一個標準的32位MD5值),生成的路徑會是:d/4/d41d8cd98f00b204e9800998ecf8427e/chunk/
?即:
- 第1級目錄:
d
(MD5的第1個字符)- 第2級目錄:
4
(MD5的第2個字符)- 第3級目錄:完整的MD5值(
d41d8cd98f00b204e9800998ecf8427e
)- 第4級目錄:固定字符串
chunk
最終路徑示例:
d/4/d41d8cd98f00b204e9800998ecf8427e/chunk/
所以獲取路徑代碼為:
// 得到分塊文件的目錄 private String getChunkFileFolderPath(String fileMd5) {return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/"; }
下面為檢查分塊文件是否存在代碼:
/?**?* 文件上傳前檢查指定分塊文件是否存在(用于大文件分片上傳的斷點續傳/秒傳功能)* @param fileMd5 文件的MD5值(用于唯一標識整個文件)* @param chunkIndex 當前分塊的序號(從0開始或從1開始,需與前端約定一致)* @return RestResponse<Boolean> 封裝檢查結果(true=分塊已存在,false=分塊不存在)*/
@Override
public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {// 1. 根據文件MD5生成分塊存儲目錄路徑// 例如:/chunks/{fileMd5}/ 這樣的目錄結構,用于按文件分組存儲分塊String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);// 2. 拼接完整的分塊文件路徑// 例如:/chunks/{fileMd5}/1 表示文件MD5為{fileMd5}的第1個分塊String chunkFilePath = chunkFileFolderPath + chunkIndex;// 3. 初始化文件輸入流(用于檢查分塊是否存在)InputStream fileInputStream = null;try {// 4. 通過MinIO客戶端API嘗試獲取分塊對象// - 使用GetObjectArgs構建獲取對象的參數// - .bucket(bucket_videofiles) 指定存儲桶(視頻文件專用桶)// - .object(chunkFilePath) 指定分塊對象路徑fileInputStream = minioClient.getObject(GetObjectArgs.builder().bucket(bucket_videofiles).object(chunkFilePath).build());// 5. 如果成功獲取到輸入流(不為null),說明分塊確實存在于MinIO中if (fileInputStream != null) {// 分塊已存在,返回成功響應(true)return RestResponse.success(true);}} catch (Exception e) {// 6. 捕獲并處理可能發生的異常// - NoSuchKeyException:分塊不存在(MinIO特定異常)// - 其他異常:可能是網絡問題、MinIO服務不可用、權限問題等// 當前實現只是打印堆棧跟蹤,建議:// 1. 使用日志框架記錄異常(如SLF4J)// 2. 區分不同類型的異常返回更精確的響應e.printStackTrace();} finally {// 7. 資源清理:確保輸入流被正確關閉(防止資源泄漏)if (fileInputStream != null) {try {fileInputStream.close();} catch (IOException e) {// 關閉流時的異常可以記錄日志,但不需要中斷業務流程e.printStackTrace();}}}// 8. 如果MinIO中不存在分塊(或發生異常),返回文件不存在return RestResponse.success(false);
}
【3】上傳分塊文件:
首先根據擴展名獲取mimeType:
如果傳入的extension為空,那么就使用通用的mimeType字節流:
String APPLICATION_OCTET_STREAM_VALUE = "application/octet-stream";
/*** 根據擴展名獲取mimeType*/
private String getMimeType(String extension){if(extension == null){extension = "";}// 根據擴展名取出mimeTypeContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;// 通用mimeType,字節流if(extensionMatch != null){mimeType = extensionMatch.getMimeType();}return mimeType;
}
?隨后編寫 addMediaFilesToMinIO 上傳文件方法:
/*** 將文件上傳到minio* @param localFilePath 文件本地路徑* @param mimeType 媒體類型* @param bucket 桶* @param objectName 對象名* @return*/
public boolean addMediaFilesToMinIO(String localFilePath,String mimeType,String bucket, String objectName){try {UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder().bucket(bucket) // 桶.filename(localFilePath) // 指定本地文件路徑.object(objectName) // 對象名 放在子目錄下.contentType(mimeType) // 設置媒體文件類型.build();// 上傳文件minioClient.uploadObject(uploadObjectArgs);log.debug("上傳文件到minio成功,bucket:{},objectName:{}",bucket,objectName);return true;} catch (Exception e) {e.printStackTrace();log.error("上傳文件出錯,bucket:{},objectName:{},錯誤信息:{}",bucket,objectName,e.getMessage());}return false;
}
整體調用:?
@Override
public RestResponse uploadChunk(String fileMd5, int chunk, String localChuckFilePath) {// 得到分塊文件的目錄路徑String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);// 得到分塊文件的路徑String chunkFilePath = chunkFileFolderPath + chunk;String mimeType = getMimeType(null);boolean b = addMediaFilesToMinIO(localChuckFilePath, mimeType, bucket_mediafiles, chunkFilePath);if(!b){return RestResponse.validfail(false,"上傳分塊文件失敗");}return RestResponse.success(true);
}
(4) 三層架構——清除分塊文件:
/*** 清除分塊文件* @param chunkFileFolderPath 分塊文件路徑* @param chunkTotal 分塊文件總數*/
private void clearChunkFiles(String chunkFileFolderPath, int chunkTotal) {try {// 使用Stream生成從0到chunkTotal-1的整數序列// 每個整數代表一個分塊文件的序號List<DeleteObject> deleteObjects = Stream.iterate(0, i -> ++i)// 限制流的大小為chunkTotal,即只生成chunkTotal個序號.limit(chunkTotal)// 將每個序號轉換為對應的DeleteObject對象// 文件名格式為:chunkFileFolderPath + 序號(轉換為字符串).map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i))))// 將所有DeleteObject對象收集到一個List中.collect(Collectors.toList());// 構建刪除對象的參數// 指定存儲桶名稱為"video"// 設置要刪除的對象列表RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket("video").objects(deleteObjects).build();// 執行批量刪除操作,返回一個包含刪除結果的IterableIterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);// 遍歷刪除結果results.forEach(r -> {DeleteError deleteError = null;try {// 獲取刪除操作的錯誤信息(如果有)deleteError = r.get();if (deleteError != null) {log.error("清除分塊文件失敗,objectname:{}", deleteError.objectName(), deleteError);} else {log.error("清除分塊文件失敗,但未獲取到具體的錯誤信息");}} catch (Exception e) {// 如果獲取錯誤信息時發生異常,打印堆棧并記錄錯誤日志e.printStackTrace();// 記錄錯誤日志,包含出錯的對象名和異常信息log.error("清楚分塊文件失敗,objectname:{}", deleteError.objectName(), e);}});} catch (Exception e) {// 如果整個刪除過程中發生異常,打印堆棧并記錄錯誤日志e.printStackTrace();// 記錄錯誤日志,包含分塊文件路徑和異常信息log.error("清楚分塊文件失敗,chunkFileFolderPath:{}", chunkFileFolderPath, e);}
}
(5)三層架構——從MinIO下載文件:
/*** 從MinIO下載文件* @param bucket 桶名稱* @param objectName 對象在桶中的名稱* @return 下載后的文件(臨時文件),如果下載失敗則返回null*/
public File downloadFileFromMinIO(String bucket, String objectName) {// 創建臨時文件用于存儲下載的內容File minioFile = null;// 使用try-with-resources確保InputStream和FileOutputStream都能正確關閉// 這樣可以避免資源泄漏,無需在finally塊中手動關閉try (// 從MinIO獲取對象(文件)的輸入流InputStream stream = minioClient.getObject(GetObjectArgs.builder().bucket(bucket) // 指定桶名稱.object(objectName) // 指定對象名稱.build());// 創建文件輸出流,用于將下載的內容寫入臨時文件FileOutputStream outputStream = new FileOutputStream(minioFile)) {// 創建臨時文件// 文件名前綴為"minio",后綴為".merge"minioFile = File.createTempFile("minio", ".merge");// 使用IOUtils工具類將輸入流的內容復制到輸出流// 這樣可以高效地將文件內容從MinIO傳輸到本地臨時文件IOUtils.copy(stream, outputStream);// 返回下載的臨時文件return minioFile;} catch (Exception e) {// 如果下載過程中發生任何異常,打印堆棧跟蹤e.printStackTrace();// 可以添加更詳細的日志記錄log.error("從MinIO下載文件失敗,bucket: {}, objectName: {}", bucket, objectName, e);// 下載失敗,返回nullreturn null;}// 注意:由于使用了try-with-resources,不再需要finally塊來手動關閉資源// 資源會在try塊結束時自動關閉
}
(6)三層架構——合并分塊文件:
首先得到合并后的地址:
/*** 得到合并后的文件的地址* @param fileMd5 文件id即md5值* @param fileExt 文件擴展名* @return*/
private String getFilePathByMd5(String fileMd5,String fileExt){return fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/" +fileMd5 +fileExt;
}
?隨后調用方法:
/?**?* 合并文件分塊為完整文件(用于大文件分片上傳的最終合并階段)* * @param companyId 公司ID(用于業務關聯)* @param fileMd5 文件的MD5值(用于唯一標識整個文件)* @param chunkTotal 分塊總數(用于確定需要合并的分塊數量)* @param uploadFileParamsDto 文件上傳參數DTO(包含文件名等信息)* @return RestResponse<Boolean> 封裝合并結果(true=合并成功,false=合并失敗)*/
@Override
public RestResponse mergechunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {// =====獲取分塊文件路徑=====// 根據文件MD5生成分塊存儲的目錄路徑(如:/chunks/{fileMd5}/)String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);// 組成將分塊文件路徑組成 List<ComposeSource>// 使用Stream生成從0到chunkTotal-1的分塊索引列表// 為每個分塊構建ComposeSource對象(包含bucket和object路徑信息)List<ComposeSource> sourceObjectList = Stream.iterate(0, i -> ++i).limit(chunkTotal).map(i -> ComposeSource.builder().bucket(bucket_videofiles) // 指定存儲桶(視頻文件專用桶).object(chunkFileFolderPath.concat(Integer.toString(i))) // 構建分塊路徑(如:/chunks/{fileMd5}/0).build()).collect(Collectors.toList());// =====合并=====// 從DTO中獲取原始文件名(如:"example.mp4")String fileName = uploadFileParamsDto.getFilename();// 提取文件擴展名(如:".mp4")String extName = fileName.substring(fileName.lastIndexOf("."));// 根據文件MD5和擴展名生成合并后的文件存儲路徑(如:/videos/{fileMd5}.mp4)String mergeFilePath = getFilePathByMd5(fileMd5, extName);try {// 調用MinIO的composeObject方法合并分塊// 參數說明:// - bucket: 存儲桶名稱// - object: 合并后的文件路徑// - sources: 待合并的分塊列表ObjectWriteResponse response = minioClient.composeObject(ComposeObjectArgs.builder().bucket(bucket_videofiles).object(mergeFilePath) .sources(sourceObjectList).build());// 記錄合并成功的日志log.debug("合并文件成功:{}", mergeFilePath);} catch (Exception e) {// 合并失敗的異常處理log.debug("合并文件失敗,fileMd5:{},異常:{}", fileMd5, e.getMessage(), e);return RestResponse.validfail(false, "合并文件失敗。");}// ====驗證md5====// 從MinIO下載合并后的文件到本地臨時文件File minioFile = downloadFileFromMinIO(bucket_videofiles, mergeFilePath);if (minioFile == null) {// 下載失敗的處理log.debug("下載合并后文件失敗,mergeFilePath:{}", mergeFilePath);return RestResponse.validfail(false, "下載合并后文件失敗。");}try (InputStream newFileInputStream = new FileInputStream(minioFile)) {// 計算下載文件的MD5值(用于校驗文件完整性)String md5Hex = DigestUtils.md5Hex(newFileInputStream);// 比較計算出的MD5與原始MD5是否一致// 不一致說明文件在合并過程中可能損壞或不完整if (!fileMd5.equals(md5Hex)) {return RestResponse.validfail(false, "文件合并校驗失敗,最終上傳失敗。");}// 設置文件大小到DTO中(用于后續入庫)uploadFileParamsDto.setFileSize(minioFile.length());} catch (Exception e) {// 文件校驗過程中的異常處理log.debug("校驗文件失敗,fileMd5:{},異常:{}", fileMd5, e.getMessage(), e);return RestResponse.validfail(false, "文件合并校驗失敗,最終上傳失敗。");} finally {// 確保臨時文件被刪除(避免磁盤空間泄漏)if (minioFile != null) {minioFile.delete();}}// 文件入庫// 將文件元數據(包括公司ID、MD5、文件參數等)事務性保存到數據庫mediaFileTransactionalService.addMediaFilesToDbWithTransaction(companyId, fileMd5, uploadFileParamsDto, bucket_videofiles, mergeFilePath);// =====清除分塊文件=====// 合并完成后刪除所有分塊文件(釋放存儲空間)clearChunkFiles(chunkFileFolderPath, chunkTotal);// 返回成功響應return RestResponse.success(true);
}
5.視頻轉碼:
視頻上傳成功后需要對視頻進行轉碼處理。
什么是視頻編碼?查閱百度百科如下:
首先我們要分清文件格式和編碼格式:
文件格式:是指.mp4、.avi、.rmvb等這些不同擴展名的視頻文件的文件格式 ?,視頻文件的內容主要包括視頻和音頻,其文件格式是按照一定的編碼格式去編碼,并且按照該文件所規定的封裝格式將視頻、音頻、字幕等信息封裝在一起,播放器會根據它們的封裝格式去提取出編碼,然后由播放器解碼,最終播放音視頻。
音視頻編碼格式:通過音視頻的壓縮技術,將視頻格式轉換成另一種視頻格式,通過視頻編碼實現流媒體的傳輸。比如:一個.avi的視頻文件原來的編碼是a,通過編碼后編碼格式變為b,音頻原來為c,通過編碼后變為d。
音視頻編碼格式各類繁多,主要有幾下幾類:
MPEG系列:
(由ISO[國際標準組織機構]下屬的MPEG[運動圖象專家組]開發 )視頻編碼方面主要是Mpeg1(vcd用的就是它)、Mpeg2(DVD使用)、Mpeg4(的DVDRIP使用的都是它的變種,如:divx,xvid等)、Mpeg4 AVC(正熱門);音頻編碼方面主要是MPEG Audio Layer 1/2、MPEG Audio Layer 3(大名鼎鼎的mp3)、MPEG-2 AAC 、MPEG-4 AAC等等。注意:DVD音頻沒有采用Mpeg的。
H.26X系列:
(由ITU[國際電傳視訊聯盟]主導,側重網絡傳輸,注意:只是視頻編碼)
包括H.261、H.262、H.263、H.263+、H.263++、H.264(就是MPEG4 AVC-合作的結晶)
目前最常用的編碼標準是視頻H.264,音頻AAC。
我們將視頻錄制完成后,使用視頻編碼軟件對視頻進行編碼,本項目 使用FFmpeg對視頻進行編碼 。下載:FFmpeg Download FFmpeg
測試是否正常:cmd運行 ffmpeg -v:
安裝成功,作下簡單測試
將一個.avi文件轉成mp4、mp3、gif等。
比如我們將nacos.avi文件轉成mp4,運行如下命令:
D:\soft\ffmpeg\ffmpeg.exe -i 1.avi 1.mp4
可以將ffmpeg.exe配置到環境變量path中,進入視頻目錄直接運行:ffmpeg.exe -i 1.avi 1.mp4
轉成mp3:ffmpeg -i nacos.avi nacos.mp3
轉成gif:ffmpeg -i nacos.avi nacos.gif
官方文檔(英文):ffmpeg Documentation
Mp4VideoUtil類是用于將視頻轉為mp4格式,是我們項目要使用的工具類。我們要通過ffmpeg對視頻轉碼,Java程序調用ffmpeg,使用java.lang.ProcessBuilder去完成。
package com.xuecheng.base.utils;import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;public class Mp4VideoUtil extends VideoUtil {String ffmpeg_path = "D:\\Program Files\\ffmpeg-20180227-fa0c9d6-win64-static\\bin\\ffmpeg.exe";//ffmpeg的安裝位置String video_path = "D:\\BaiduNetdiskDownload\\test1.avi";String mp4_name = "test1.mp4";String mp4folder_path = "D:/BaiduNetdiskDownload/Movies/test1/";public Mp4VideoUtil(String ffmpeg_path, String video_path, String mp4_name, String mp4folder_path){super(ffmpeg_path);this.ffmpeg_path = ffmpeg_path;this.video_path = video_path;this.mp4_name = mp4_name;this.mp4folder_path = mp4folder_path;}//清除已生成的mp4private void clear_mp4(String mp4_path){//刪除原來已經生成的m3u8及ts文件File mp4File = new File(mp4_path);if(mp4File.exists() && mp4File.isFile()){mp4File.delete();}}/*** 視頻編碼,生成mp4文件* @return 成功返回success,失敗返回控制臺日志*/public String generateMp4(){//清除已生成的mp4
// clear_mp4(mp4folder_path+mp4_name);clear_mp4(mp4folder_path);/*ffmpeg.exe -i lucene.avi -c:v libx264 -s 1280x720 -pix_fmt yuv420p -b:a 63k -b:v 753k -r 18 .\lucene.mp4*/List<String> commend = new ArrayList<String>();//commend.add("D:\\Program Files\\ffmpeg-20180227-fa0c9d6-win64-static\\bin\\ffmpeg.exe");commend.add(ffmpeg_path);commend.add("-i");
// commend.add("D:\\BaiduNetdiskDownload\\test1.avi");commend.add(video_path);commend.add("-c:v");commend.add("libx264");commend.add("-y");//覆蓋輸出文件commend.add("-s");commend.add("1280x720");commend.add("-pix_fmt");commend.add("yuv420p");commend.add("-b:a");commend.add("63k");commend.add("-b:v");commend.add("753k");commend.add("-r");commend.add("18");
// commend.add(mp4folder_path + mp4_name );commend.add(mp4folder_path );String outstring = null;try {ProcessBuilder builder = new ProcessBuilder();builder.command(commend);//將標準輸入流和錯誤輸入流合并,通過標準輸入流程讀取信息builder.redirectErrorStream(true);Process p = builder.start();outstring = waitFor(p);} catch (Exception ex) {ex.printStackTrace();}
// Boolean check_video_time = this.check_video_time(video_path, mp4folder_path + mp4_name);Boolean check_video_time = this.check_video_time(video_path, mp4folder_path);if(!check_video_time){return outstring;}else{return "success";}}public static void main(String[] args) throws IOException {//ffmpeg的路徑String ffmpeg_path = "tools/ffmpeg.exe";//ffmpeg的安裝位置//源avi視頻的路徑String video_path = "D:\\develop\\bigfile_test\\nacos01.avi";//轉換后mp4文件的名稱String mp4_name = "nacos01.mp4";//轉換后mp4文件的路徑String mp4_path = "D:\\develop\\bigfile_test\\";//創建工具類對象Mp4VideoUtil videoUtil = new Mp4VideoUtil(ffmpeg_path,video_path,mp4_name,mp4_path);//開始視頻轉換,成功將返回successString s = videoUtil.generateMp4();System.out.println(s);}
}