????????分布式文件系統(tǒng)-minio:
- 第一章:分布式文件系統(tǒng)介紹與minio介紹與使用(附minio java client 使用)
- 第二章:minio&前后端分離上傳視頻/上傳大文件——前后端分離斷點(diǎn)續(xù)傳&minio分片上傳實(shí)現(xiàn)
1.斷點(diǎn)續(xù)傳
- 斷點(diǎn)續(xù)傳指的是在下載或上傳時(shí),將下載或上傳任務(wù)(一個(gè)文件或一個(gè)壓縮包)人為的劃分為幾個(gè)部分,每一個(gè)部分采用一個(gè)線程進(jìn)行上傳或下載,如果碰到網(wǎng)絡(luò)故障,可以從已經(jīng)上傳或下載的部分開始繼續(xù)上傳下載未完成的部分,而沒有必要從頭開始上傳下載,斷點(diǎn)續(xù)傳可以提高節(jié)省操作時(shí)間,提高用戶體驗(yàn)性。
- 通常視頻文件都比較大,所以對(duì)于媒資系統(tǒng)上傳文件的需求要滿足大文件的上傳要求。http協(xié)議本身對(duì)上傳文件大小沒有限制,但是客戶的網(wǎng)絡(luò)環(huán)境質(zhì)量、電腦硬件環(huán)境等參差不齊,如果一個(gè)大文件快上傳完了網(wǎng)斷了沒有上傳完成,需要客戶重新上傳,用戶體驗(yàn)非常差,所以對(duì)于大文件上傳的要求最基本的是斷點(diǎn)續(xù)傳。
斷點(diǎn)續(xù)傳流程如下圖:
流程如下:
- 前端上傳前先把文件分成塊。
- 一塊一塊的上傳,上傳中斷后重新上傳,已上傳的分塊則不用再上傳。
- 各分塊上傳完成最后在服務(wù)端合并文件。
2.分塊與合并測(cè)試
為了更好的理解文件分塊上傳的原理,下邊用java代碼測(cè)試文件的分塊與合并。
2.1 分塊測(cè)試
2.1.1 流程分析
文件分塊的流程如下:
- 獲取源文件長(zhǎng)度
- 根據(jù)設(shè)定的分塊文件的大小計(jì)算出塊數(shù)
- 從源文件讀數(shù)據(jù)依次向每一個(gè)塊文件寫數(shù)據(jù)。
2.1.2 代碼實(shí)現(xiàn)
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
/**
* 分塊文件測(cè)試
*
* @author 狐貍半面添
* @create 2023-02-07 17:14
*/
public class BigFileChunkDemo {
public static void main(String[] args) throws IOException {
// 1.指定要進(jìn)行分塊的源文件
File sourceFile = new File("D:\\SystemDefault\\video\\不為誰而作的歌.mp4");
// 2.指定分塊文件存儲(chǔ)路徑
File chunkFolderPath = new File("D:\\SystemDefault\\video\\chunk\\");
// chunk文件夾不存在則創(chuàng)建
if (!chunkFolderPath.exists()) {
chunkFolderPath.mkdirs();
}
// 3.分塊的大小 - 10MB
int chunkSize = 1024 * 1024 * 10;
// 4.根據(jù)分塊大小得到源文件的分塊數(shù)量(向上轉(zhuǎn)型)
long chunkNum = (long) Math.ceil(sourceFile.length() * 1.0 / chunkSize);
/*
分塊思路:使用流對(duì)象讀取源文件,向分塊文件寫數(shù)據(jù),達(dá)到分塊大小不再寫
*/
// 5.使用流對(duì)象 rafRead 讀取源文件
RandomAccessFile rafRead = new RandomAccessFile(sourceFile, "r");
// 6.設(shè)置每次讀取的緩沖區(qū)大小
byte[] b = new byte[1024];
RandomAccessFile rafWrite;
// 7.開始分塊
for (long i = 0; i < chunkNum; i++) {
// 7.1 指定分塊文件
File file = new File("D:\\SystemDefault\\video\\chunk\\" + i);
// 7.2 如果分塊文件存在,則刪除
if (file.exists()) {
file.delete();
}
// 7.3 創(chuàng)建一個(gè)空的分塊文件
boolean newFile = file.createNewFile();
if (newFile) {
// 7.4 向分塊文件寫數(shù)據(jù)流對(duì)象
rafWrite = new RandomAccessFile(file, "rw");
int len = -1;
// 7.5 讀取源文件,每次讀取的大小為設(shè)置的緩沖區(qū)的大小
while ((len = rafRead.read(b)) != -1) {
// 7.6 將緩沖區(qū)的數(shù)據(jù)寫入到分塊文件中
rafWrite.write(b, 0, len);
// 7.7 達(dá)到分塊大小不再寫了,繼續(xù)下一次循環(huán),將后面的數(shù)據(jù)寫入新的分塊文件中
if (file.length() >= chunkSize) {
break;
}
}
// 7.8 關(guān)閉該分塊文件流,釋放資源
rafWrite.close();
}
}
// 8.分塊完成,關(guān)閉源文件流,釋放資源
rafRead.close();
System.out.println("分塊文件完成");
}
}
2.2 合并測(cè)試
2.2.1 流程分析
- 找到要合并的文件并按文件合并的先后進(jìn)行排序。
- 創(chuàng)建合并文件。
- 依次從合并的文件中讀取數(shù)據(jù)向合并文件寫入數(shù)。
2.2.2 代碼實(shí)現(xiàn)
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.13</version>
</dependency>
import org.apache.commons.codec.digest.DigestUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Arrays;
import java.util.List;
/**
* 合并文件測(cè)試
*
* @author 狐貍半面添
* @create 2023-02-07 17:37
*/
public class BigFileMergeDemo {
public static void main(String[] args) throws IOException {
// 1.指定源文件 - 目的是后面比較合并后的文件與源文件是否相同
File sourceFile = new File("D:\\SystemDefault\\video\\不為誰而作的歌.mp4");
// 2.指定分塊文件存儲(chǔ)路徑
File chunkFolderPath = new File("D:\\SystemDefault\\video\\chunk\\");
// 3.指定并創(chuàng)建合并后的文件
File mergeFile = new File("D:\\SystemDefault\\video\\不為誰而作的歌(合并).mp4");
boolean success = mergeFile.createNewFile();
if (!success) {
System.out.println("error: 文件創(chuàng)建失敗");
return;
}
/*
思路:使用流對(duì)象讀取分塊文件,按順序?qū)⒎謮K文件依次向合并文件寫數(shù)據(jù)
*/
// 4.獲取分塊文件列表,按文件名升序排序
// 4.1 獲取分塊文件列表
File[] chunkFiles = chunkFolderPath.listFiles();
if (chunkFiles == null) {
System.out.println("error: 分塊文件列表為空");
return;
}
// 4.2 由數(shù)組變?yōu)閘ist集合
List<File> chunkFileList = Arrays.asList(chunkFiles);
// 4.3 按文件名升序排序
chunkFileList.sort((o1, o2) -> Integer.parseInt(o1.getName()) - Integer.parseInt(o2.getName()));
// 5.創(chuàng)建合并文件的流對(duì)象
RandomAccessFile rafWrite = new RandomAccessFile(mergeFile, "rw");
// 6.設(shè)置每次讀取的緩沖區(qū)大小
byte[] b = new byte[1024];
// 7.逐一讀取分塊文件,將數(shù)據(jù)寫入到合并文件中
for (File file : chunkFileList) {
// 7.1 獲取讀取分塊文件的流對(duì)象
RandomAccessFile rafRead = new RandomAccessFile(file, "r");
int len = -1;
// 7.2 讀取當(dāng)前分塊文件,每次讀取的大小為設(shè)置的緩沖區(qū)的大小,直到將當(dāng)前分塊文件讀取完畢
while ((len = rafRead.read(b)) != -1) {
// 7.3 將緩沖區(qū)數(shù)據(jù)寫入到合并文件中
rafWrite.write(b, 0, len);
}
// 7.4 關(guān)閉分塊文件流對(duì)象,釋放資源
rafRead.close();
}
// 8.關(guān)閉流對(duì)象,釋放資源
rafWrite.close();
// 9.校驗(yàn)合并后的文件是否正確
FileInputStream sourceFileStream = new FileInputStream(sourceFile);
FileInputStream mergeFileStream = new FileInputStream(mergeFile);
String sourceMd5Hex = DigestUtils.md5Hex(sourceFileStream);
String mergeMd5Hex = DigestUtils.md5Hex(mergeFileStream);
if (sourceMd5Hex.equals(mergeMd5Hex)) {
System.out.println("合并成功");
}
}
}
3.前后端分離上傳視頻流程分析
- 前端上傳文件前請(qǐng)求媒資接口層檢查文件是否存在,如果已經(jīng)存在則不再上傳。
- 如果文件在系統(tǒng)不存在則前端開始上傳,首先對(duì)視頻文件進(jìn)行分塊。
- 前端分塊進(jìn)行上傳,上傳前首先檢查分塊是否上傳,如已上傳則不再上傳,如果未上傳則開始上傳分塊。
- 前端請(qǐng)求媒資管理接口層請(qǐng)求上傳分塊。
- 接口層請(qǐng)求服務(wù)層上傳分塊。
- 服務(wù)端將分塊信息上傳到MinIO。
- 前端將分塊上傳完畢請(qǐng)求接口層合并分塊。
- 接口層請(qǐng)求服務(wù)層合并分塊。
- 服務(wù)層根據(jù)文件信息找到MinIO中的分塊文件,下載到本地臨時(shí)目錄,將所有分塊下載完畢后開始合并 。
- 合并完成將合并后的文件上傳到MinIO。
4.實(shí)戰(zhàn)開發(fā) - 思路分析
- 前端準(zhǔn)備上傳一個(gè)視頻文件,需要先發(fā)送請(qǐng)求——檢查文件是否已存在,攜帶參數(shù)為文件的Md5十六進(jìn)制值。
- 后端根據(jù) md5十六進(jìn)制值 去數(shù)據(jù)庫查詢?cè)撐募欠翊嬖凇?
- 如果存在,則將文件信息加密返回,前端拿到加密信息再發(fā)送其它請(qǐng)求保存到數(shù)據(jù)庫如課程資源表中。流程結(jié)束。
- 如果不存在,就提醒前端不存在該文件,前端接下來就需要進(jìn)行分塊上傳處理。
- 前端發(fā)現(xiàn)不存在該文件,就將文件進(jìn)行分塊,每上傳一個(gè)分塊文件前,都需要發(fā)送請(qǐng)求——檢查當(dāng)前分塊文件是否存在,攜帶參數(shù)為文件的Md5十六進(jìn)制值以及當(dāng)前分塊索引(第幾塊,從0開始)。
- 后端就根據(jù)參數(shù)去 minio 中查找是否存在該分塊文件。
- 如果存在了,就告訴前端不需要上傳該分塊文件了。
- 如果不存在,就告訴前端需要發(fā)送請(qǐng)求上傳分塊文件。
- 前端發(fā)現(xiàn)不存在該分塊文件,就發(fā)送請(qǐng)求——上傳分塊文件,攜帶參數(shù)為文件的分塊文件,Md5十六進(jìn)制值以及當(dāng)前分塊索引(第幾塊,從0開始)。
- 后端根據(jù)參數(shù)將 分塊文件 保存在 minio 中。
- 所有分塊文件上傳完畢,前端發(fā)送請(qǐng)求——合并分塊文件與上傳合并后的文件,攜帶參數(shù)為文件的Md5十六進(jìn)制值,文件名,文件標(biāo)簽,文件塊總數(shù)。
- 后端就根據(jù)參數(shù)進(jìn)行合并與上傳處理:
- 從 minio 下載所有原文件的分塊文件。
- 將分塊文件進(jìn)行合并處理。
- 計(jì)算合并后文件的md5值,如果和前端參數(shù)中的md5值一致,則說明正確合并。否則上傳失敗,流程結(jié)束。
- 再將合并后的文件斷點(diǎn)續(xù)傳到 minio。
- 將文件信息保存至數(shù)據(jù)庫中。
- 最后將文件信息加密返回,前端拿到加密信息再發(fā)送其它請(qǐng)求保存到數(shù)據(jù)庫如課程資源表中。流程結(jié)束。
5.實(shí)戰(zhàn)開發(fā) - 準(zhǔn)備工作
5.1 數(shù)據(jù)庫設(shè)計(jì)
CREATE TABLE service_media_file(
`id` BIGINT UNSIGNED PRIMARY KEY COMMENT '主鍵id(雪花算法)',
`file_name` VARCHAR(255) NOT NULL COMMENT '文件名稱',
`file_type` CHAR(2) NOT NULL COMMENT '文件類型:文本,圖片,音頻,視頻,其它',
`file_format` VARCHAR(128) NOT NULL COMMENT '文件格式',
`tag` VARCHAR(32) NOT NULL COMMENT '標(biāo)簽',
`bucket` VARCHAR(32) NOT NULL COMMENT '存儲(chǔ)桶',
`file_path` VARCHAR(512) NOT NULL COMMENT '文件存儲(chǔ)路徑',
`file_md5` CHAR(32) NOT NULL UNIQUE COMMENT '文件的md5值',
`file_byte_size` BIGINT UNSIGNED NOT NULL COMMENT '文件的字節(jié)大小',
`file_format_size` VARCHAR(24) NOT NULL COMMENT '文件的格式大小',
`user_id` BIGINT NOT NULL COMMENT '上傳人id',
`create_time` DATETIME NOT NULL COMMENT '創(chuàng)建時(shí)間(上傳時(shí)間)',
`update_time` DATETIME NOT NULL COMMENT '修改時(shí)間'
)ENGINE = INNODB CHARACTER SET = utf8mb4 COMMENT '第三方服務(wù)-媒資文件表';
5.2 核心 pom.xml
<!--根據(jù)擴(kuò)展名取mimetype-->
<dependency>
<groupId>com.j256.simplemagic</groupId>
<artifactId>simplemagic</artifactId>
<version>1.17</version>
</dependency>
<!--對(duì)象存儲(chǔ)服務(wù)-->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.2.1</version>
</dependency>
<dependency>
<groupId>me.tongfei</groupId>
<artifactId>progressbar</artifactId>
<version>0.5.3</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.8.1</version>
</dependency>
<!--生成文件對(duì)象的md5十六進(jìn)制值-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.13</version>
</dependency>
<!--用于文件類型判斷-->
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>2.4.0</version>
</dependency>
5.3 application.yaml核心配置
spring:
servlet:
multipart:
max-file-size: 3MB
max-request-size: 5MB
minio:
# 指定連接的ip和端口
endpoint: http://192.168.65.129:9000
# 指定 訪問秘鑰(也稱用戶id)
accessKey: minioadmin
# 指定 私有秘鑰(也稱密碼)
secretKey: minioadmin
5.4 MinioConfig.java配置類
package com.zhulang.waveedu.service.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 狐貍半面添
* @create 2023-02-08 16:26
*/
@Configuration
public class MinioConfig {
/**
* 連接的ip和端口
*/
@Value("${minio.endpoint}")
private String endpoint;
/**
* 訪問秘鑰(也稱用戶id)
*/
@Value("${minio.accessKey}")
private String accessKey;
/**
* 私有秘鑰(也稱密碼)
*/
@Value("${minio.secretKey}")
private String secretKey;
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}
5.5 MinioClientUtils工具類
package com.zhulang.waveedu.common.util;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import io.minio.GetObjectArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.UploadObjectArgs;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.http.MediaType;
import java.io.*;
/**
* 操作minio的工具類
*
* @author 狐貍半面添
* @create 2023-02-08 22:08
*/
public class MinioClientUtils {
private final MinioClient minioClient;
public MinioClientUtils(MinioClient minioClient) {
this.minioClient = minioClient;
}
/**
* 獲取minio文件的輸入流對(duì)象
*
* @param bucket 桶
* @param filePath 文件路徑
* @return 輸入流
* @throws Exception 異常
*/
public InputStream getObject(String bucket, String filePath) throws Exception {
return minioClient.getObject(GetObjectArgs.builder().bucket(bucket).object(filePath).build());
}
/**
* 將分塊文件上傳到分布式文件系統(tǒng)
*
* @param bytes 文件的字節(jié)數(shù)組
* @param bucket 桶
* @param filePath 存儲(chǔ)在桶中的文件路徑
*/
public void uploadChunkFile(byte[] bytes, String bucket, String filePath) throws Exception {
// 1.指定資源的媒體類型為未知二進(jìn)制流,以分片形式上傳至minio
try (
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes)
) {
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucket)
.object(filePath)
// InputStream stream, long objectSize 對(duì)象大小, long partSize 分片大小(-1表示5M,最大不要超過5T,最多10000)
.stream(byteArrayInputStream, byteArrayInputStream.available(), -1)
.contentType(MediaType.APPLICATION_OCTET_STREAM_VALUE)
.build()
);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 將文件上傳到分布式文件系統(tǒng)
*
* @param bytes 文件的字節(jié)數(shù)組
* @param bucket 桶
* @param filePath 存儲(chǔ)在桶中的文件路徑
*/
public void uploadFile(byte[] bytes, String bucket, String filePath) throws Exception {
// 1.指定資源的媒體類型,默認(rèn)未知二進(jìn)制流
String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
// 2.判斷是否有后綴,有后綴則根據(jù)后綴推算出文件類型,否則使用默認(rèn)的未知二進(jìn)制流
if (filePath.contains(".")) {
// 取objectName中的擴(kuò)展名
String extension = filePath.substring(filePath.lastIndexOf("."));
ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
if (extensionMatch != null) {
contentType = extensionMatch.getMimeType();
}
}
// 3.以分片形式上傳至minio
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.bucket(bucket)
.object(filePath)
// InputStream stream, long objectSize 對(duì)象大小, long partSize 分片大小(-1表示5M,最大不要超過5T,最多10000)
.stream(byteArrayInputStream, byteArrayInputStream.available(), -1)
.contentType(contentType)
.build();
// 上傳
minioClient.putObject(putObjectArgs);
}
/**
* 根據(jù)文件路徑將文件上傳到文件系統(tǒng)
*
* @param naiveFilePath 本地文件路徑
* @param bucket 桶
* @param minioFilePath 保存到minio的文件路徑位置
* @throws Exception 異常
*/
public void uploadChunkFile(String naiveFilePath, String bucket, String minioFilePath) throws Exception {
UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
.bucket(bucket)
.object(minioFilePath)
.filename(naiveFilePath)
.build();
minioClient.uploadObject(uploadObjectArgs);
}
/**
* 下載文件保存至本地臨時(shí)文件中
*
* @param tempFilePrefix 臨時(shí)文件的前綴
* @param tempFileSuffix 臨時(shí)文件的后綴
* @param bucket 桶
* @param filePath 文件路徑
* @return 攜帶數(shù)據(jù)的臨時(shí)文件
* @throws Exception 異常信息
*/
public File downloadFile(String tempFilePrefix, String tempFileSuffix, String bucket, String filePath) throws Exception {
// 1.創(chuàng)建空文件,臨時(shí)保存下載下來的分塊文件數(shù)據(jù)
File tempFile = File.createTempFile(tempFilePrefix, tempFileSuffix);
try (
// 2.獲取目標(biāo)文件的輸入流對(duì)象
InputStream inputStream = getObject(bucket, filePath);
// 3.獲取臨時(shí)空文件的輸出流對(duì)象
FileOutputStream outputStream = new FileOutputStream(tempFile);
) {
// 4.進(jìn)行數(shù)據(jù)拷貝
IOUtils.copy(inputStream, outputStream);
// 5.返回保存了數(shù)據(jù)的臨時(shí)文件
return tempFile;
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
// public File downloadFile(String tempFilePrefix, String tempFileSuffix, String bucket, String filePath) throws Exception {
// // 1.創(chuàng)建空文件,臨時(shí)保存下載下來的分塊文件數(shù)據(jù)
// File tempFile = File.createTempFile(tempFilePrefix, tempFileSuffix);
// try {
// Long start = System.currentTimeMillis();
// minioClient.downloadObject(
// DownloadObjectArgs.builder()
// // 指定 bucket 存儲(chǔ)桶
// .bucket(bucket)
// // 指定 哪個(gè)文件
// .object(filePath)
// // 指定存放位置與名稱
// .filename(tempFile.getPath())
// .build());
// Long end = System.currentTimeMillis();
// System.out.println("下載分塊時(shí)間:"+(end-start)+"ms");
// // 5.返回保存了數(shù)據(jù)的臨時(shí)文件
// return tempFile;
// } catch (Exception e) {
// throw new RuntimeException(e.getMessage());
// }
// }
}
5.6 FileTypeUtils工具類
package com.zhulang.waveedu.common.util;
import org.apache.tika.metadata.HttpHeaders;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.mime.MediaType;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.Parser;
import org.xml.sax.helpers.DefaultHandler;
import java.io.File;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.Map;
/**
* 文件類型工具類
*
* @author 狐貍半面添
* @create 2023-02-09 0:13
*/
public class FileTypeUtils {
private static final Map<String, String> contentType = new HashMap<>();
/**
* 獲取文件的 mime 類型
*
* @param file 文件
* @return mime類型
*/
public static String getMimeType(File file) {
AutoDetectParser parser = new AutoDetectParser();
parser.setParsers(new HashMap<MediaType, Parser>());
Metadata metadata = new Metadata();
metadata.add(TikaCoreProperties.RESOURCE_NAME_KEY, file.getName());
try (InputStream stream = Files.newInputStream(file.toPath())) {
parser.parse(stream, new DefaultHandler(), metadata, new ParseContext());
} catch (Exception e) {
throw new RuntimeException();
}
return metadata.get(HttpHeaders.CONTENT_TYPE);
}
/**
* 根據(jù) mimetype 獲取文件的簡(jiǎn)單類型
*
* @param mimeType mime類型
* @return 簡(jiǎn)單類型:文本,圖片,音頻,視頻,其它
*/
public static String getSimpleType(String mimeType) {
String simpleType = mimeType.split("/")[0];
switch (simpleType) {
case "text":
return "文本";
case "image":
return "圖片";
case "audio":
return "音頻";
case "video":
return "視頻";
case "application":
return "其它";
default:
throw new RuntimeException("mimeType格式錯(cuò)誤");
}
}
// 測(cè)試
public static void main(String[] args) {
File file = new File("D:\\location語法規(guī)則.docx");
String mimeType = getMimeType(file);
System.out.println(mimeType);
System.out.println(getSimpleType(mimeType));
}
5.7 FileFormatUtils工具類
package com.zhulang.waveedu.common.util;
import java.text.DecimalFormat;
/**
* 文件格式工具
*
* @author 狐貍半面添
* @create 2023-02-09 19:43
*/
public class FileFormatUtils {
/**
* 將字節(jié)單位的文件大小轉(zhuǎn)為格式化的文件大小表示
*
* @param fileLength 文件字節(jié)大小
* @return 格式化文件大小表示
*/
public static String formatFileSize(long fileLength) {
DecimalFormat df = new DecimalFormat("#.00");
String fileSizeString = "";
String wrongSize = "0B";
if (fileLength == 0) {
return wrongSize;
}
if (fileLength < 1024) {
fileSizeString = df.format((double) fileLength) + " B";
} else if (fileLength < 1048576) {
fileSizeString = df.format((double) fileLength / 1024) + " KB";
} else if (fileLength < 1073741824) {
fileSizeString = df.format((double) fileLength / 1048576) + " MB";
} else {
fileSizeString = df.format((double) fileLength / 1073741824) + " GB";
}
return fileSizeString;
}
}
5.8 CipherUtils加密解密工具類
package com.zhulang.waveedu.common.util;
import com.alibaba.fastjson.JSON;
import org.apache.ibatis.logging.stdout.StdOutImpl;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
/**
* AES加密解密工具
*
* @author 狐貍半面添
* @create 2023-01-18 20:34
*/
public class CipherUtils {
private static final String SECRET_KEY = "tangyulang5201314";
private static final String AES = "AES";
private static final String CHARSET_NAME = "UTF-8";
/**
* 生成密鑰 key
*
* @param password 加密密碼
* @return
* @throws Exception
*/
private static SecretKeySpec generateKey(String password) throws Exception {
// 1.構(gòu)造密鑰生成器,指定為AES算法,不區(qū)分大小寫
KeyGenerator keyGenerator = KeyGenerator.getInstance(AES);
// 2. 因?yàn)锳ES要求密鑰的長(zhǎng)度為128,我們需要固定的密碼,因此隨機(jī)源的種子需要設(shè)置為我們的密碼數(shù)組
// 生成一個(gè)128位的隨機(jī)源, 根據(jù)傳入的字節(jié)數(shù)組
/*
* 這種方式 windows 下正常, Linux 環(huán)境下會(huì)解密失敗
* keyGenerator.init(128, new SecureRandom(password.getBytes()));
*/
// 兼容 Linux
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
random.setSeed(password.getBytes());
keyGenerator.init(128, random);
// 3.產(chǎn)生原始對(duì)稱密鑰
SecretKey original_key = keyGenerator.generateKey();
// 4. 根據(jù)字節(jié)數(shù)組生成AES密鑰
return new SecretKeySpec(original_key.getEncoded(), AES);
}
/**
* 加密
*
* @param content 加密的內(nèi)容
* @param password 加密密碼
* @return
*/
private static String aESEncode(String content, String password) {
try {
// 根據(jù)指定算法AES自成密碼器
Cipher cipher = Cipher.getInstance(AES);
// 基于加密模式和密鑰初始化Cipher
cipher.init(Cipher.ENCRYPT_MODE, generateKey(password));
// 單部分加密結(jié)束, 重置Cipher, 獲取加密內(nèi)容的字節(jié)數(shù)組(這里要設(shè)置為UTF-8)防止解密為亂碼
byte[] bytes = cipher.doFinal(content.getBytes(CHARSET_NAME));
// 將加密后的字節(jié)數(shù)組轉(zhuǎn)為字符串返回
return Base64.getUrlEncoder().encodeToString(bytes);
} catch (Exception e) {
// 如果有錯(cuò)就返回 null
return null;
}
}
/**
* 解密
*
* @param content 解密內(nèi)容
* @param password 解密密碼
* @return
*/
private static String AESDecode(String content, String password) {
try {
// 將加密并編碼后的內(nèi)容解碼成字節(jié)數(shù)組
byte[] bytes = Base64.getUrlDecoder().decode(content);
// 這里指定了算法為AES
Cipher cipher = Cipher.getInstance(AES);
// 基于解密模式和密鑰初始化Cipher
cipher.init(Cipher.DECRYPT_MODE, generateKey(password));
// 單部分加密結(jié)束,重置Cipher
byte[] result = cipher.doFinal(bytes);
// 將解密后的字節(jié)數(shù)組轉(zhuǎn)成 UTF-8 編碼的字符串返回
return new String(result, CHARSET_NAME);
} catch (Exception e) {
// 如果有錯(cuò)就返回 null
return null;
}
}
/**
* 加密
*
* @param content 加密內(nèi)容
* @return 加密結(jié)果
*/
public static String encrypt(String content) {
return aESEncode(content, SECRET_KEY);
}
/**
* 解密
*
* @param content 解密內(nèi)容
* @return 解密結(jié)果
*/
public static String decrypt(String content) {
try {
return AESDecode(content, SECRET_KEY);
} catch (Exception e) {
return null;
}
}
}
5.9 MediaFile.java
package com.zhulang.waveedu.service.po;
import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* <p>
* 第三方服務(wù)-媒資文件表
* </p>
*
* @author 狐貍半面添
* @since 2023-02-08
*/
@TableName("service_media_file")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MediaFile implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主鍵id(雪花算法)
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 文件名稱
*/
private String fileName;
/**
* 文件類型:文本,圖片,音頻,視頻,其它
*/
private String fileType;
/**
* 文件格式
*/
private String fileFormat;
/**
* 標(biāo)簽
*/
private String tag;
/**
* 存儲(chǔ)桶
*/
private String bucket;
/**
* 文件存儲(chǔ)路徑
*/
private String filePath;
/**
* 文件的md5值
*/
private String fileMd5;
/**
* 文件字節(jié)大小
*/
private Long fileByteSize;
/**
* 文件格式化大小
*/
private String fileFormatSize;
/**
* 上傳人id
*/
private Long userId;
/**
* 創(chuàng)建時(shí)間(上傳時(shí)間)
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 修改時(shí)間
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
6.實(shí)戰(zhàn)開發(fā) - 業(yè)務(wù)代碼
6.1 檢查文件是否已存在
6.1.1 MediaFileController
package com.zhulang.waveedu.service.controller;
import com.alibaba.fastjson.JSONObject;
import com.zhulang.waveedu.common.entity.Result;
import com.zhulang.waveedu.service.service.MediaFileService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.stereotype.Controller;
import javax.annotation.Resource;
/**
* <p>
* 第三方服務(wù)-媒資文件表 前端控制器
* </p>
*
* @author 狐貍半面添
* @since 2023-02-08
*/
@Controller
@RequestMapping("/media-file")
public class MediaFileController {
@Resource
private MediaFileService mediaFileService;
/**
* 文件上傳前檢查文件是否存在
*
* @param object 需要上傳的文件的md5值
* @return 是否存在, false-不存在 true-存在
*/
@PostMapping("/upload/checkFile")
public Result checkFile(@RequestBody JSONObject object) {
return mediaFileService.checkFile(object.getString("fileMd5"));
}
}
6.1.2 MediaFileService
package com.zhulang.waveedu.service.service;
import com.zhulang.waveedu.common.entity.Result;
import com.zhulang.waveedu.service.po.MediaFile;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 第三方服務(wù)-媒資文件表 服務(wù)類
* </p>
*
* @author 狐貍半面添
* @since 2023-02-08
*/
public interface MediaFileService extends IService<MediaFile> {
/**
* 文件上傳前檢查文件是否存在
*
* @param fileMd5 需要上傳的文件的md5值
* @return 是否存在,false-不存在 true-存在
*/
Result checkFile(String fileMd5);
}
6.1.3 MediaFileServiceImpl
package com.zhulang.waveedu.service.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.zhulang.waveedu.common.constant.HttpStatus;
import com.zhulang.waveedu.common.entity.Result;
import com.zhulang.waveedu.common.util.RegexUtils;
import com.zhulang.waveedu.service.po.MediaFile;
import com.zhulang.waveedu.service.dao.MediaFileMapper;
import com.zhulang.waveedu.service.service.MediaFileService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import io.minio.GetObjectArgs;
import io.minio.MinioClient;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.InputStream;
/**
* <p>
* 第三方服務(wù)-媒資文件表 服務(wù)實(shí)現(xiàn)類
* </p>
*
* @author 狐貍半面添
* @since 2023-02-08
*/
@Service
public class MediaFileServiceImpl extends ServiceImpl<MediaFileMapper, MediaFile> implements MediaFileService {
@Resource
private MediaFileMapper mediaFileMapper;
@Resource
private MinioClientUtils minioClientUtils;
@Override
public Result checkFile(String fileMd5) {
HashMap<String, Object> resultMap = new HashMap<>();
// 1.校驗(yàn) fileMd5 合法性
if (RegexUtils.isMd5HexInvalid(fileMd5)) {
return Result.error(HttpStatus.HTTP_BAD_REQUEST.getCode(), "文件md5格式錯(cuò)誤");
}
// 2.在文件表存在,并且在文件系統(tǒng)存在,此文件才存在
// 2.1 判斷是否在文件表中存在
LambdaQueryWrapper<MediaFile> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MediaFile::getFileMd5, fileMd5);
MediaFile mediaFile = mediaFileMapper.selectOne(wrapper);
if (mediaFile == null) {
resultMap.put("exist", false);
return Result.ok(resultMap);
}
// 2.2 判斷是否在文件系統(tǒng)存在
try {
InputStream inputStream = minioClientUtils.getObject(mediaFile.getBucket(), mediaFile.getFilePath());
if (inputStream == null) {
// 文件不存在
resultMap.put("exist", false);
return Result.ok(resultMap);
}
} catch (Exception e) {
// 文件不存在
resultMap.put("exist", false);
return Result.ok(resultMap);
}
// 3.走到這里說明文件已存在,返回true
resultMap.put("exist", true);
// 4.封裝文件信息
resultMap.put("info", encodeFileInfo(mediaFile));
// 5.返回結(jié)果
return Result.ok(resultMap);
}
/**
* 將 部分信息進(jìn)行封裝加密
*
* @param mediaFile 媒資對(duì)象
* @return 加密結(jié)果
*/
private String encodeFileInfo(MediaFile mediaFile) {
HashMap<String, Object> fileMap = new HashMap<>(5);
fileMap.put("fileType", mediaFile.getFileType());
fileMap.put("filePath", mediaFile.getBucket() + "/" + mediaFile.getFilePath());
fileMap.put("fileFormat", mediaFile.getFileFormat());
fileMap.put("fileByteSize", mediaFile.getFileByteSize());
fileMap.put("fileFormatSize", mediaFile.getFileFormatSize());
return CipherUtils.encrypt(JSON.toJSONString(fileMap));
}
}
6.2 檢查分塊文件是否已存在
6.2.1 MediaFileController
/**
* 分塊文件上傳前檢測(cè)分塊文件是否已存在
*
* @param chunkFileVO 分塊文件的源文件md5和該文件索引
* @return 是否存在, false-不存在 true-存在
* @throws Exception
*/
@PostMapping("/upload/checkChunk")
public Result checkChunk(@Validated @RequestBody ChunkFileVO chunkFileVO) throws Exception {
return mediaFileService.checkChunk(chunkFileVO.getFileMd5(),chunkFileVO.getChunkIndex());
}
6.2.2 MediaFileService
/**
* 分塊文件上傳前檢測(cè)分塊文件是否已存在
*
* @param fileMd5 分塊文件的源文件md5
* @param chunkIndex 分塊文件索引
* @return 是否存在, false-不存在 true-存在
*/
Result checkChunk(String fileMd5, Integer chunkIndex);
6.2.3 MediaFileServiceImpl
@Resource
private MinioClientUtils minioClientUtils;
@Value("${minio.bucket}")
private String bucket;
@Override
public Result checkChunk(String fileMd5, Integer chunkIndex) {
// 1.得到分塊文件所在目錄
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
// 2.分塊文件的路徑
String chunkFilePath = chunkFileFolderPath + chunkIndex;
// 3.查看是否在文件系統(tǒng)存在(注意關(guān)閉流)
try (
InputStream inputStream = minioClientUtils.getObject(bucket, chunkFilePath)
) {
if (inputStream == null) {
//文件不存在
return Result.ok(false);
}
} catch (Exception e) {
//文件不存在
return Result.ok(false);
}
// 4.走到這里說明文件已存在,返回true
return Result.ok(true);
}
/**
* 得到分塊文件的目錄
*
* @param fileMd5 文件的md5值
* @return 分塊文件所在目錄
*/
private String getChunkFileFolderPath(String fileMd5) {
return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
}
6.3 上傳分塊文件
6.3.1 MediaFileController
/**
* 上傳分塊文件
*
* @param file 分塊文件
* @param fileMd5 原文件md5值
* @param chunkIndex 分塊文件索引
* @return 上傳情況
*/
@PostMapping("/upload/uploadChunk")
public Result uploadChunk(@RequestParam("file") MultipartFile file,
@RequestParam("fileMd5") @Pattern(regexp = RegexUtils.RegexPatterns.MD5_HEX_REGEX, message = "文件md5格式錯(cuò)誤") String fileMd5,
@RequestParam("chunkIndex") @Min(value = 0, message = "索引必須大于等于0") Integer chunkIndex) throws Exception {
return mediaFileService.uploadChunk(fileMd5, chunkIndex, file.getBytes());
}
6.3.2 MediaFileService
/**
* 上傳分塊文件
*
* @param fileMd5 原文件md5值
* @param chunkIndex 分塊文件索引
* @param bytes 分塊文件的字節(jié)數(shù)組形式
* @return 上傳情況
*/
Result uploadChunk(String fileMd5, Integer chunkIndex, byte[] bytes);
6.3.3 MediaFileServiceImpl
@Resource
private MinioClientUtils minioClientUtils;
@Value("${minio.bucket}")
private String bucket;
@Override
public Result uploadChunk(String fileMd5, Integer chunkIndex, byte[] bytes) {
// 1.得到分塊文件所在目錄
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
// 2.分塊文件的路徑
String chunkFilePath = chunkFileFolderPath + chunkIndex;
try {
// 3.將分塊上傳到文件系統(tǒng)
minioClientUtils.uploadChunkFile(bytes, bucket, chunkFilePath);
// 4.上傳成功
return Result.ok();
} catch (Exception e) {
// 上傳失敗
return Result.error();
}
}
/**
* 得到分塊文件的目錄
*
* @param fileMd5 文件的md5值
* @return 分塊文件所在目錄
*/
private String getChunkFileFolderPath(String fileMd5) {
return fileMd5.charAt(0) + "/" + fileMd5.charAt(1) + "/" + fileMd5 + "/" + "chunk" + "/";
}
6.4 合并前下載分塊文件(多線程下載)
合并分塊前要檢查分塊文件是否全部上傳完成,如果完成則將已經(jīng)上傳的分塊文件從minio下載下來,然后再進(jìn)行合并。
/**
* 創(chuàng)建10個(gè)線程數(shù)量的線程池
*/
private final ExecutorService threadPool = Executors.newFixedThreadPool(10);
/**
* 下載所有的塊文件
*
* @param fileMd5 源文件的md5值
* @param chunkTotal 塊總數(shù)
* @return 所有塊文件
*/
private File[] downloadChunkFilesFromMinio(String fileMd5, int chunkTotal) throws Exception {
// 1.得到分塊文件所在目錄
String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
// 2.分塊文件數(shù)組
File[] chunkFiles = new File[chunkTotal];
// 3.設(shè)置計(jì)數(shù)器
CountDownLatch countDownLatch = new CountDownLatch(chunkTotal);
// 4.開始逐個(gè)下載
for (int i = 0; i < chunkTotal; i++) {
int index = i;
threadPool.execute(() -> {
// 4.1 得到分塊文件的路徑
String chunkFilePath = chunkFileFolderPath + index;
// 4.2 下載分塊文件
try {
chunkFiles[index] = minioClientUtils.downloadFile("chunk", null, bucket, chunkFilePath);
} catch (Exception e) {
// 計(jì)數(shù)器減1
countDownLatch.countDown();
throw new RuntimeException(e);
}
// 計(jì)數(shù)器減1
countDownLatch.countDown();
});
}
/*
阻塞到任務(wù)執(zhí)行完成,當(dāng)countDownLatch計(jì)數(shù)器歸零,這里的阻塞解除等待,
給一個(gè)充裕的超時(shí)時(shí)間,防止無限等待,到達(dá)超時(shí)時(shí)間還沒有處理完成則結(jié)束任務(wù)
*/
countDownLatch.await(30, TimeUnit.MINUTES);
// 5.返回所有塊文件
return chunkFiles;
}
6.5 合并分塊文件并上傳
6.5.1 MediaFileController
/**
* 合并分塊文件
*
* @param fileMd5 文件的md5十六進(jìn)制值
* @param fileName 文件名
* @param tag 文件標(biāo)簽
* @param chunkTotal 文件塊總數(shù)
* @return 合并與上傳情況
*/
@PostMapping("/upload/uploadMergeChunks")
public Result uploadMergeChunks(@RequestParam("fileMd5") @Pattern(regexp = RegexUtils.RegexPatterns.MD5_HEX_REGEX, message = "文件md5格式錯(cuò)誤") String fileMd5,
@RequestParam("fileName") @Pattern(regexp = RegexUtils.RegexPatterns.FILE_NAME_REGEX, message = "文件名最多255個(gè)字符") String fileName,
@RequestParam("tag") @Pattern(regexp = RegexUtils.RegexPatterns.FILE_TAG_REGEX, message = "文件標(biāo)簽最多32個(gè)字符") String tag,
@RequestParam("chunkTotal") @Min(value = 1, message = "塊總數(shù)必須大于等于1") Integer chunkTotal) {
return mediaFileService.uploadMergeChunks(fileMd5, fileName, tag, chunkTotal);
}
6.5.2 MediaFileService
/**
* 合并分塊文件
*
* @param fileMd5 文件的md5十六進(jìn)制值
* @param fileName 文件名
* @param tag 文件標(biāo)簽
* @param chunkTotal 文件塊總數(shù)
* @return 合并與上傳情況
*/
Result uploadMergeChunks(String fileMd5, String fileName, String tag, Integer chunkTotal);
6.5.3 MediaFileServiceImpl
/**
* 將 部分信息進(jìn)行封裝加密
*
* @param mediaFile 媒資對(duì)象
* @return 加密結(jié)果
*/
private String encodeFileInfo(MediaFile mediaFile) {
HashMap<String, Object> fileMap = new HashMap<>(5);
fileMap.put("fileType", mediaFile.getFileType());
fileMap.put("filePath", mediaFile.getBucket() + "/" + mediaFile.getFilePath());
fileMap.put("fileFormat", mediaFile.getFileFormat());
fileMap.put("fileByteSize", mediaFile.getFileByteSize());
fileMap.put("fileFormatSize", mediaFile.getFileFormatSize());
return CipherUtils.encrypt(JSON.toJSONString(fileMap));
}
@Override
public Result uploadMergeChunks(String fileMd5, String fileName, String tag, Integer chunkTotal) {
try {
// 1.下載分塊
File[] chunkFiles = downloadChunkFilesFromMinio(fileMd5, chunkTotal);
// 2.根據(jù)文件名得到合并后文件的擴(kuò)展名
int index = fileName.lastIndexOf(".");
String extension = index != -1 ? fileName.substring(index) : "";
File tempMergeFile = null;
try {
try {
// 3.創(chuàng)建一個(gè)臨時(shí)文件作為合并文件
tempMergeFile = File.createTempFile("merge", extension);
} catch (IOException e) {
return Result.error(HttpStatus.HTTP_INTERNAL_ERROR.getCode(), "創(chuàng)建臨時(shí)合并文件出錯(cuò)");
}
// 4.創(chuàng)建合并文件的流對(duì)象
try (RandomAccessFile rafWrite = new RandomAccessFile(tempMergeFile, "rw")) {
byte[] b = new byte[1024];
for (File file : chunkFiles) {
// 5.讀取分塊文件的流對(duì)象
try (RandomAccessFile rafRead = new RandomAccessFile(file, "r");) {
int len = -1;
while ((len = rafRead.read(b)) != -1) {
// 6.向合并文件寫數(shù)據(jù)
rafWrite.write(b, 0, len);
}
}
}
} catch (IOException e) {
return Result.error(HttpStatus.HTTP_INTERNAL_ERROR.getCode(), "合并文件過程出錯(cuò)");
}
// 7.校驗(yàn)合并后的文件是否正確
try (
// 7.1 獲取合并后文件的流對(duì)象
FileInputStream mergeFileStream = new FileInputStream(tempMergeFile);
) {
// 7.2 獲取合并文件的md5十六進(jìn)制值
String mergeMd5Hex = DigestUtils.md5Hex(mergeFileStream);
// 7.3 校驗(yàn)
if (!fileMd5.equals(mergeMd5Hex)) {
return Result.error(HttpStatus.HTTP_BAD_REQUEST.getCode(), "合并文件校驗(yàn)不通過");
}
} catch (IOException e) {
return Result.error(HttpStatus.HTTP_INTERNAL_ERROR.getCode(), "合并文件校驗(yàn)出錯(cuò)");
}
// 8.得到 mimetype
String mimeType = FileTypeUtils.getMimeType(tempMergeFile);
// 9.拿到合并文件在minio的存儲(chǔ)路徑
String mergeFilePath = getFilePathByMd5(fileMd5, extension);
// 10.將合并后的文件上傳到文件系統(tǒng)
minioClientUtils.uploadFile(tempMergeFile.getAbsolutePath(), bucket, mergeFilePath);
// 11.設(shè)置需要入庫的文件信息
MediaFile mediaFile = new MediaFile();
// 11.1 文件名
mediaFile.setFileName(WaveStrUtils.removeBlank(fileName));
// 11.2 文件類型
mediaFile.setFileType(FileTypeUtils.getSimpleType(mimeType));
// 11.3 文件格式
mediaFile.setFileFormat(mimeType);
// 11.4 文件標(biāo)簽
mediaFile.setTag(WaveStrUtils.removeBlank(tag));
// 11.5 存儲(chǔ)桶
mediaFile.setBucket(bucket);
// 11.6 存儲(chǔ)路徑
mediaFile.setFilePath(mergeFilePath);
// 11.7 設(shè)置md5值
mediaFile.setFileMd5(fileMd5);
// 11.8 設(shè)置合并文件大?。▎挝唬鹤止?jié))
mediaFile.setFileByteSize(tempMergeFile.length());
// 11.9 設(shè)置文件格式大小
mediaFile.setFileFormatSize(FileFormatUtils.formatFileSize(mediaFile.getFileByteSize()));
// 11.10 設(shè)置上傳者
mediaFile.setUserId(UserHolderUtils.getUserId());
// 12.保存至數(shù)據(jù)庫
this.save(mediaFile);
// 15.返回成功
return Result.ok(encodeFileInfo(mediaFile));
} finally {
// 13.刪除臨時(shí)分塊文件
for (File chunkFile : chunkFiles) {
if (chunkFile.exists()) {
chunkFile.delete();
}
}
// 14.刪除合并的臨時(shí)文件
if (tempMergeFile != null) {
tempMergeFile.delete();
}
}
} catch (Exception e) {
return Result.error(HttpStatus.HTTP_INTERNAL_ERROR.getCode(), e.getMessage());
}
}
/**
* 得到合并文件的路徑
*
* @param fileMd5 文件的md5十六進(jìn)制值
* @param fileExt 文件的擴(kuò)展名
* @return 合并文件路徑
*/
private String getFilePathByMd5(String fileMd5, String fileExt) {
return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + fileMd5 + fileExt;
}
7.補(bǔ)充-面試題
7.1 MinIO是什么?
MinIO一個(gè)輕量級(jí)的分布式文件系統(tǒng),由多個(gè)個(gè)MinIO節(jié)點(diǎn)連接組成,可根據(jù)文件規(guī)模進(jìn)行擴(kuò)展,適用于海量文件的存儲(chǔ)與訪問。
7.2 為什么用MinIO
- MinIO開源,使用簡(jiǎn)單,功能強(qiáng)大。
- MinIO使用糾刪碼算法,只要不超過一半的節(jié)點(diǎn)壞掉整個(gè)文件系統(tǒng)就可以使用。
- 如果將壞的節(jié)點(diǎn)重新啟動(dòng),自動(dòng)恢復(fù)沒有上傳成功的文件。
7.3 怎么樣構(gòu)建這個(gè)獨(dú)立文件服務(wù)?
-
我們項(xiàng)目中有很多要上傳文件的地方,比如上傳圖片、上傳文檔、上傳視頻等,所以我們要構(gòu)建一 個(gè)獨(dú)立的文件服務(wù)負(fù)責(zé)上傳、下載等功能,負(fù)責(zé)對(duì)文件進(jìn)行統(tǒng)一管理。
-
創(chuàng)建單獨(dú)的文件服務(wù),提供以下接口:
- 上傳接口
- 下載接口
- 我的圖庫接口
- 我的文件庫接口
- 刪除文件接口
- 文件的存儲(chǔ)和下載使用MinIO實(shí)現(xiàn)。
MinIO是一個(gè)分布式的文件系統(tǒng),性能高,擴(kuò)展強(qiáng)。
- 使用Nginx+MinIO組成一個(gè)文件服務(wù)器。通過訪問Nginx,由nginx代理將請(qǐng)求轉(zhuǎn)發(fā)到MinIO去瀏覽、下載文件。
7.4 斷點(diǎn)續(xù)傳是怎么做的?
我們是基于分塊上傳的模式實(shí)現(xiàn)斷點(diǎn)續(xù)傳的需求,當(dāng)文件上傳一部分?jǐn)嗑W(wǎng)后前邊已經(jīng)上傳過的不再上傳。文章來源:http://www.zghlxwxcb.cn/news/detail-780000.html
- 前端對(duì)文件分塊。
- 前端使用多線程一塊-塊上傳,上傳前給服務(wù)端發(fā)一 個(gè)消息校驗(yàn)該分塊是否上傳,如果已上傳則不再上傳。
- 等所有分塊上傳完畢,服務(wù)端合并所有分塊,校驗(yàn)文件的完整性。因?yàn)榉謮K全部上傳到了服務(wù)器,服務(wù)器將所在分塊按順序進(jìn)行合并,就是寫每個(gè)分塊文件內(nèi)容按順序依次寫入一個(gè)文件中。(使用字節(jié)流去讀寫文件)
- 前端給服務(wù)傳了一個(gè)md5值,服務(wù)端合并文件后計(jì)算合并后文件的md5是否和前端傳的一樣,如果一樣則說文件完整,如果不一樣說明可能由于網(wǎng)絡(luò)丟包導(dǎo)致文件不完整,這時(shí)上傳失敗需要重新上傳。
7.5 分塊文件清理問題
上傳一個(gè)文件進(jìn)行分塊上傳,上傳一半不傳了, 之前上傳到minio的分塊文件要清理嗎?怎么做的?文章來源地址http://www.zghlxwxcb.cn/news/detail-780000.html
- 在數(shù)據(jù)庫中有一張文件表記錄minio中存儲(chǔ)的文件信息。
- 文件開始上傳時(shí)會(huì)寫入文件表,狀態(tài)為,上傳中,上傳完成會(huì)更新狀態(tài)為上傳完成。
- 當(dāng)一個(gè)文件傳了一半不再上傳了說明該文件沒有上傳完成,會(huì)有定時(shí)任務(wù)去查詢文件表中的記錄,如果文件未上傳完成則刪除minio中沒有上傳成功的文件目錄。
到了這里,關(guān)于minio&前后端分離上傳視頻/上傳大文件——前后端分離斷點(diǎn)續(xù)傳&minio分片上傳實(shí)現(xiàn)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!