国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

minio&前后端分離上傳視頻/上傳大文件——前后端分離斷點(diǎn)續(xù)傳&minio分片上傳實(shí)現(xiàn)

這篇具有很好參考價(jià)值的文章主要介紹了minio&前后端分離上傳視頻/上傳大文件——前后端分離斷點(diǎn)續(xù)傳&minio分片上傳實(shí)現(xiàn)。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

????????分布式文件系統(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ù)傳流程如下圖:

后端上傳視頻,# java開發(fā)實(shí)戰(zhàn)知識(shí),java,minio,斷點(diǎn)續(xù)傳,分片上傳,前后端分離

流程如下:

  1. 前端上傳前先把文件分成塊。
  2. 一塊一塊的上傳,上傳中斷后重新上傳,已上傳的分塊則不用再上傳。
  3. 各分塊上傳完成最后在服務(wù)端合并文件。

2.分塊與合并測(cè)試

為了更好的理解文件分塊上傳的原理,下邊用java代碼測(cè)試文件的分塊與合并。

2.1 分塊測(cè)試

2.1.1 流程分析

文件分塊的流程如下:

  1. 獲取源文件長(zhǎng)度
  2. 根據(jù)設(shè)定的分塊文件的大小計(jì)算出塊數(shù)
  3. 從源文件讀數(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 流程分析

  1. 找到要合并的文件并按文件合并的先后進(jìn)行排序。
  2. 創(chuàng)建合并文件。
  3. 依次從合并的文件中讀取數(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.前后端分離上傳視頻流程分析

后端上傳視頻,# java開發(fā)實(shí)戰(zhàn)知識(shí),java,minio,斷點(diǎn)續(xù)傳,分片上傳,前后端分離

  1. 前端上傳文件前請(qǐng)求媒資接口層檢查文件是否存在,如果已經(jīng)存在則不再上傳。
  2. 如果文件在系統(tǒng)不存在則前端開始上傳,首先對(duì)視頻文件進(jìn)行分塊。
  3. 前端分塊進(jìn)行上傳,上傳前首先檢查分塊是否上傳,如已上傳則不再上傳,如果未上傳則開始上傳分塊。
  4. 前端請(qǐng)求媒資管理接口層請(qǐng)求上傳分塊。
  5. 接口層請(qǐng)求服務(wù)層上傳分塊。
  6. 服務(wù)端將分塊信息上傳到MinIO。
  7. 前端將分塊上傳完畢請(qǐng)求接口層合并分塊。
  8. 接口層請(qǐng)求服務(wù)層合并分塊。
  9. 服務(wù)層根據(jù)文件信息找到MinIO中的分塊文件,下載到本地臨時(shí)目錄,將所有分塊下載完畢后開始合并 。
  10. 合并完成將合并后的文件上傳到MinIO。

4.實(shí)戰(zhàn)開發(fā) - 思路分析

  1. 前端準(zhǔn)備上傳一個(gè)視頻文件,需要先發(fā)送請(qǐng)求——檢查文件是否已存在,攜帶參數(shù)為文件的Md5十六進(jìn)制值
  2. 后端根據(jù) md5十六進(jìn)制值 去數(shù)據(jù)庫查詢?cè)撐募欠翊嬖凇?
    • 如果存在,則將文件信息加密返回,前端拿到加密信息再發(fā)送其它請(qǐng)求保存到數(shù)據(jù)庫如課程資源表中。流程結(jié)束。
    • 如果不存在,就提醒前端不存在該文件,前端接下來就需要進(jìn)行分塊上傳處理。
  3. 前端發(fā)現(xiàn)不存在該文件,就將文件進(jìn)行分塊,每上傳一個(gè)分塊文件前,都需要發(fā)送請(qǐng)求——檢查當(dāng)前分塊文件是否存在,攜帶參數(shù)為文件的Md5十六進(jìn)制值以及當(dāng)前分塊索引(第幾塊,從0開始)
  4. 后端就根據(jù)參數(shù)去 minio 中查找是否存在該分塊文件。
    • 如果存在了,就告訴前端不需要上傳該分塊文件了。
    • 如果不存在,就告訴前端需要發(fā)送請(qǐng)求上傳分塊文件。
  5. 前端發(fā)現(xiàn)不存在該分塊文件,就發(fā)送請(qǐng)求——上傳分塊文件,攜帶參數(shù)為文件的分塊文件,Md5十六進(jìn)制值以及當(dāng)前分塊索引(第幾塊,從0開始)。
  6. 后端根據(jù)參數(shù)將 分塊文件 保存在 minio 中。
  7. 所有分塊文件上傳完畢,前端發(fā)送請(qǐng)求——合并分塊文件與上傳合并后的文件,攜帶參數(shù)為文件的Md5十六進(jìn)制值,文件名,文件標(biāo)簽,文件塊總數(shù)。
  8. 后端就根據(jù)參數(shù)進(jìn)行合并與上傳處理:
    1. 從 minio 下載所有原文件的分塊文件。
    2. 將分塊文件進(jìn)行合并處理。
    3. 計(jì)算合并后文件的md5值,如果和前端參數(shù)中的md5值一致,則說明正確合并。否則上傳失敗,流程結(jié)束。
    4. 再將合并后的文件斷點(diǎn)續(xù)傳到 minio。
    5. 將文件信息保存至數(shù)據(jù)庫中。
    6. 最后將文件信息加密返回,前端拿到加密信息再發(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

  1. MinIO開源,使用簡(jiǎn)單,功能強(qiáng)大。
  2. MinIO使用糾刪碼算法,只要不超過一半的節(jié)點(diǎn)壞掉整個(gè)文件系統(tǒng)就可以使用。
  3. 如果將壞的節(jié)點(diǎn)重新啟動(dòng),自動(dòng)恢復(fù)沒有上傳成功的文件。

7.3 怎么樣構(gòu)建這個(gè)獨(dú)立文件服務(wù)?

  1. 我們項(xiàng)目中有很多要上傳文件的地方,比如上傳圖片、上傳文檔、上傳視頻等,所以我們要構(gòu)建一 個(gè)獨(dú)立的文件服務(wù)負(fù)責(zé)上傳、下載等功能,負(fù)責(zé)對(duì)文件進(jìn)行統(tǒng)一管理。

  2. 創(chuàng)建單獨(dú)的文件服務(wù),提供以下接口:

  • 上傳接口
  • 下載接口
  • 我的圖庫接口
  • 我的文件庫接口
  • 刪除文件接口
  1. 文件的存儲(chǔ)和下載使用MinIO實(shí)現(xiàn)。

MinIO是一個(gè)分布式的文件系統(tǒng),性能高,擴(kuò)展強(qiáng)。

  1. 使用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)上傳過的不再上傳。

  1. 前端對(duì)文件分塊。
  2. 前端使用多線程一塊-塊上傳,上傳前給服務(wù)端發(fā)一 個(gè)消息校驗(yàn)該分塊是否上傳,如果已上傳則不再上傳。
  3. 等所有分塊上傳完畢,服務(wù)端合并所有分塊,校驗(yàn)文件的完整性。因?yàn)榉謮K全部上傳到了服務(wù)器,服務(wù)器將所在分塊按順序進(jìn)行合并,就是寫每個(gè)分塊文件內(nèi)容按順序依次寫入一個(gè)文件中。(使用字節(jié)流去讀寫文件)
  4. 前端給服務(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

  1. 在數(shù)據(jù)庫中有一張文件表記錄minio中存儲(chǔ)的文件信息。
  2. 文件開始上傳時(shí)會(huì)寫入文件表,狀態(tài)為,上傳中,上傳完成會(huì)更新狀態(tài)為上傳完成。
  3. 當(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)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • 前端文件上傳(文件上傳,分片上傳,斷點(diǎn)續(xù)傳)

    前端文件上傳(文件上傳,分片上傳,斷點(diǎn)續(xù)傳)

    普通文件上傳 思路: 首先獲取用戶選擇的文件對(duì)象,并將其添加到一個(gè) FormData 對(duì)象中。然后,使用 axios 的 post 方法將 FormData 對(duì)象發(fā)送到服務(wù)器。在 then 和 catch 中,我們分別處理上傳成功和失敗的情況,并輸出相應(yīng)的信息。 需要注意,在使用 axios 進(jìn)行文件上傳時(shí),必須將

    2024年02月22日
    瀏覽(30)
  • spring boot 阿里云oss 文件分片上傳、斷點(diǎn)續(xù)傳

    spring boot 阿里云oss 文件分片上傳、斷點(diǎn)續(xù)傳

    文章目錄 前言 一、申請(qǐng)阿里云oss 二、上代碼 總結(jié) ? ? ? 阿里云對(duì)象存儲(chǔ)OSS(Object Storage Service)是一款海量、安全、低成本、高可靠的云存儲(chǔ)服務(wù),可提供99.9999999999%(12個(gè)9)的數(shù)據(jù)持久性,99.995%的數(shù)據(jù)可用性。多種存儲(chǔ)類型供選擇,全面優(yōu)化存儲(chǔ)成本。 ? ? 您可以使用阿

    2024年02月07日
    瀏覽(27)
  • 大文件上傳阿里云oss,分片、斷點(diǎn)續(xù)傳進(jìn)度條展示

    大文件上傳阿里云oss,分片、斷點(diǎn)續(xù)傳進(jìn)度條展示

    前端頁面展示 大文件如果不采用分片上傳會(huì)導(dǎo)致卡死、內(nèi)存占用過高導(dǎo)致程序奔潰等一些列問題。 通常在文件大于100 MB的情況下,建議采用分片上傳的方法,通過斷點(diǎn)續(xù)傳和重試,提高上傳成功率。如果在文件小于100 MB的情況下使用分片上傳,且partSize設(shè)置不合理的情況下,

    2024年02月11日
    瀏覽(26)
  • 【SpringBoot整合系列】SpringBoot 實(shí)現(xiàn)大文件分片上傳、斷點(diǎn)續(xù)傳及秒傳

    【SpringBoot整合系列】SpringBoot 實(shí)現(xiàn)大文件分片上傳、斷點(diǎn)續(xù)傳及秒傳

    小文件(圖片、文檔、視頻)上傳可以直接使用很多ui框架封裝的上傳組件,或者自己寫一個(gè)input 上傳,利用FormData 對(duì)象提交文件數(shù)據(jù),后端使用spring提供的MultipartFile進(jìn)行文件的接收,然后寫入即可。 但是對(duì)于比較大的文件,比如上傳2G左右的文件(http上傳),就需要將文件

    2024年04月16日
    瀏覽(37)
  • 使用MinIO文件存儲(chǔ)系統(tǒng)【完成視頻斷點(diǎn)續(xù)傳】業(yè)務(wù)邏輯

    使用MinIO文件存儲(chǔ)系統(tǒng)【完成視頻斷點(diǎn)續(xù)傳】業(yè)務(wù)邏輯

    目錄 視頻上傳 接口一:檢查該視頻/媒資文件是否已經(jīng)上傳完成 接口二:檢查視頻分塊是否已經(jīng)在minio中已經(jīng)存在 接口三:上傳分塊文件到minio中(已經(jīng)上傳的分塊會(huì)在接口二進(jìn)行校驗(yàn)) 接口四:合并上傳的分塊文件保存文件合并后的文件信息 視頻上傳流程圖 接口一:檢查

    2024年02月16日
    瀏覽(25)
  • springboot整合vue2-uploader文件分片上傳、秒傳、斷點(diǎn)續(xù)傳

    springboot整合vue2-uploader文件分片上傳、秒傳、斷點(diǎn)續(xù)傳

    vue-simple-uploader 是基于 simple-uploader.js 封裝的vue上傳插件。它的優(yōu)點(diǎn)包括且不限于以下幾種: 支持文件、多文件、文件夾上傳;支持拖拽文件、文件夾上傳 可暫停、繼續(xù)上傳 錯(cuò)誤處理 支持“秒傳”,通過文件判斷服務(wù)端是否已存在從而實(shí)現(xiàn)“秒傳” 分片上傳 支持進(jìn)度、預(yù)估

    2024年02月06日
    瀏覽(35)
  • 基于vue-simple-uploader封裝文件分片上傳、秒傳及斷點(diǎn)續(xù)傳的全局上傳

    基于vue-simple-uploader封裝文件分片上傳、秒傳及斷點(diǎn)續(xù)傳的全局上傳

    1. 前言 文件上傳 小文件(圖片、文檔、視頻)上傳可以直接使用很多ui框架封裝的上傳組件,或者自己寫一個(gè)input 上傳,利用FormData 對(duì)象提交文件數(shù)據(jù),后端使用spring提供的MultipartFile進(jìn)行文件的接收,然后寫入即可。但是對(duì)于比較大的文件,比如上傳2G左右的文件(http上傳

    2024年02月06日
    瀏覽(22)
  • 大文件切片上傳+斷點(diǎn)續(xù)傳解決方案-前后端實(shí)現(xiàn)(附源碼)

    上傳文件大家應(yīng)該都做過,前端直接把file文件傳給后端就ok了,但是大文件這樣傳就會(huì)造成頁面假死,體驗(yàn)極差。如果遇到網(wǎng)絡(luò)不穩(wěn)定的時(shí)候,中途上傳失敗的話,又要從頭開始傳,本來文件就大,還慢。所以今天我們用一種新方法-切片上傳+斷點(diǎn)續(xù)傳 頁面上很簡(jiǎn)單,我就放

    2024年02月09日
    瀏覽(32)
  • 【萬字長(zhǎng)文】Vue+SpringBoot實(shí)現(xiàn)大文件秒傳、斷點(diǎn)續(xù)傳和分片上傳完整教程(提供Gitee源碼)

    前言:最近在實(shí)際項(xiàng)目中碰到一個(gè)需求,客戶可能會(huì)上傳比較大的文件,如果采用傳統(tǒng)的文件上傳方案可能會(huì)存在服務(wù)器壓力大、資源浪費(fèi)甚至內(nèi)存溢出的一些安全風(fēng)險(xiǎn),所以為了解決一系列問題,需要采用新的技術(shù)方案來實(shí)現(xiàn)大文件的上傳;空閑的時(shí)候參考了網(wǎng)上的一些相

    2024年02月12日
    瀏覽(24)
  • Spring Boot實(shí)現(xiàn)HTTP大文件斷點(diǎn)續(xù)傳分片下載-大視頻分段漸進(jìn)式播放

    服務(wù)端如何將一個(gè)大視頻文件做切分,分段響應(yīng)給客戶端,讓瀏覽器可以漸進(jìn)式地播放。 Spring Boot實(shí)現(xiàn)HTTP分片下載斷點(diǎn)續(xù)傳,從而實(shí)現(xiàn)H5頁面的大視頻播放問題,實(shí)現(xiàn)漸進(jìn)式播放,每次只播放需要播放的內(nèi)容就可以了,不需要加載整個(gè)文件到內(nèi)存中。 文件的斷點(diǎn)續(xù)傳、文件多

    2024年02月14日
    瀏覽(31)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包