第2-3-8章 分片上傳和分片合併的接口開發-文件存儲服務系統-nginx/fastDFS/minio/阿里雲oss/七牛雲oss

5.10 接口開發-分片上傳

第2-1-2章 傳統方式安裝FastDFS-附FastDFS常用命令
第2-1-3章 docker-compose安裝FastDFS,實現文件存儲服務
第2-1-5章 docker安裝MinIO實現文件存儲服務-springboot整合minio-minio全網最全的資料

全套代碼及資料全部完整提供,點此處下載

5.10.1 分片上傳介紹

前面我們已經實現了普通的附件服務和網盤服務,如果上傳的文件比較小,可以直接使用這兩個服務即可。如果上傳的文件比較大,例如要上傳一個500M或者1G的視頻文件(或者更大),這就需要分片上傳了。那麼什麼是分片上傳呢?

分片上傳就是把一個大文件進行分片,一片一片的上傳到服務端,最後由服務端進行分片的合併。

要實現分片上傳需要前端和後端配合來完成。在進行分片上傳時,一般是由前端對要上傳的大文件進行分片,然後分多次將這些分片上傳到服務端,所有分片都上傳到服務端後,在服務端將分片合併為原始的大文件。採用大文件分片並發上傳,可以極大的提高文件的上傳效率。

5.10.2 前端分片上傳插件webuploader

WebUploader是由Baidu WebFE(FEX)團隊開發的一個簡單的以HTML5為主,FLASH為輔的現代文件上傳組件。在現代的瀏覽器裏面能充分發揮HTML5的優勢,同時又不摒棄主流IE瀏覽器,沿用原來的FLASH運行時,兼容IE6+,iOS 6+, android 4+。

官網地址://fex.baidu.com/webuploader/

分片與並髮結合,將一個大文件分割成多塊,並發上傳,極大地提高大文件的上傳速度。

當網絡問題導致傳輸錯誤時,只需要重傳出錯分片,而不是整個文件。另外分片傳輸能夠更加實時的跟蹤上傳進度。

由於本文展示的主要為後端服務開發,所以前端部分不再開發,直接從資料中獲得使用即可。

資料位置:文件服務\資料\分片上傳\前端

直接打開index.html頁面,選擇要上傳的大文件,可以看到發送了多次請求,每次請求會上傳此大文件的一個分片:
在這裡插入圖片描述

註:由於目前後端服務還沒有開發,所以上傳會失敗。

5.10.3 後端代碼實現

5.10.3.1 接口文檔

在這裡插入圖片描述

在這裡插入圖片描述

5.10.3.2 代碼開發

第一步:創建FileChunkController並提供分片上傳方法uploadFile

package com.itheima.pinda.file.controller;

import com.itheima.pinda.base.BaseController;
import com.itheima.pinda.base.R;
import com.itheima.pinda.dozer.DozerUtils;
import com.itheima.pinda.file.domain.FileAttrDO;
import com.itheima.pinda.file.dto.chunk.FileChunksMergeDTO;
import com.itheima.pinda.file.dto.chunk.FileUploadDTO;
import com.itheima.pinda.file.entity.File;
import com.itheima.pinda.file.manager.WebUploader;
import com.itheima.pinda.file.properties.FileServerProperties;
import com.itheima.pinda.file.service.FileService;
import com.itheima.pinda.file.strategy.FileChunkStrategy;
import com.itheima.pinda.file.strategy.FileStrategy;
import com.itheima.pinda.file.utils.FileDataTypeUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
 * 分片上傳
 */
@RestController
@Slf4j
@RequestMapping("/chunk")
@CrossOrigin
@Api(value = "分片上傳", tags = "分片上傳,需要webuploder.js插件進行配合使用")
public class FileChunkController extends BaseController {
    @Autowired
    private FileServerProperties fileProperties;
    @Autowired
    private FileService fileService;
    @Autowired
    private FileStrategy fileStrategy;
    @Autowired
    private WebUploader webUploader;
    @Autowired
    private DozerUtils dozerUtils;
    /**
     * 分片上傳
     * @param fileUploadDTO
     * @param multipartFile
     * @return
     */
    @ApiOperation(value = "分片上傳", notes = "分片上傳")
    @PostMapping(value = "/upload")
    public R<FileChunksMergeDTO> uploadFile(FileUploadDTO fileUploadDTO,
                                            @RequestParam(value = "file", required = false) MultipartFile multipartFile) throws Exception {

        if (multipartFile == null || multipartFile.isEmpty()) {
            log.error("分片上傳分片為空");
            return fail("分片上傳分片為空");
        }

        //  存放分片文件的服務器絕對路徑 ,例如 D:\\uploadfiles\\2020\\04
        String uploadFolder = FileDataTypeUtil.getUploadPathPrefix(fileProperties.getStoragePath());

        if (fileUploadDTO.getChunks() == null || fileUploadDTO.getChunks() <= 0) {
            //沒有分片,按照普通文件上傳處理
            File file = fileStrategy.upload(multipartFile);
            file.setFileMd5(fileUploadDTO.getMd5());
            
            fileService.save(file);

            return success(null);
        } else {
            //為上傳的文件準備好對應的位置
            java.io.File targetFile = webUploader.getReadySpace(fileUploadDTO, uploadFolder);

            if (targetFile == null) {
                return fail("分片上傳失敗");
            }
            //保存上傳文件
            multipartFile.transferTo(targetFile);

            //封裝信息給前端,用於分片合併
            FileChunksMergeDTO mergeDTO = new FileChunksMergeDTO();
            mergeDTO.setSubmittedFileName(multipartFile.getOriginalFilename());
            dozerUtils.map(fileUploadDTO,mergeDTO);

            return success(mergeDTO);
        }
    }
}

第二步:在配置屬性類中添加storagePath屬性和對於的get、set方法

public String getStoragePath() {
    return storagePath;
}

public void setStoragePath(String storagePath) {
    this.storagePath = storagePath;
}

//指定分片上傳時臨時存放目錄
private String storagePath ;

第三步:創建WebUploader分片上傳工具類

package com.itheima.pinda.file.manager;

import com.itheima.pinda.file.dto.chunk.FileUploadDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.IOException;
/**
 * 分片上傳工具類
 */
@Service
@Slf4j
public class WebUploader2 {
    /**
     * 為上傳的文件創建對應的保存位置,若上傳的是分片,則會創建對應的文件夾結構和tmp文件
     *
     * @param fileUploadDTO 上傳文件的相關信息
     * @param path 文件保存根路徑
     * @return
     */
    public java.io.File getReadySpace(FileUploadDTO fileUploadDTO, String path) {
        //創建上傳文件所需的文件夾
        if (!this.createFileFolder(path, false)) {
            return null;
        }

        //將上傳的分片保存在此目錄中
        String fileFolder = fileUploadDTO.getName();

        if (fileFolder == null) {
            return null;
        }

        //文件上傳路徑更新為指定文件信息簽名後的臨時文件夾,用於後期合併
        path += "/" + fileFolder;

        if (!this.createFileFolder(path, true)) {
            return null;
        }

        //分片上傳,指定當前分片文件的文件名
        String newFileName = String.valueOf(fileUploadDTO.getChunk());
        return new java.io.File(path, newFileName);
    }

    /**
     * 創建存放分片上傳的文件的文件夾
     *
     * @param file   文件夾路徑
     * @param hasTmp 是否有臨時文件
     * @return
     */
    private boolean createFileFolder(String file, boolean hasTmp) {
        //創建存放分片文件的臨時文件夾
        java.io.File tmpFile = new java.io.File(file);
        if (!tmpFile.exists()) {
            try {
                tmpFile.mkdirs();
            } catch (SecurityException ex) {
                log.error("無法創建文件夾", ex);
                return false;
            }
        }

        if (hasTmp) {
            //創建臨時文件,用來記錄上傳分片文件的修改時間,用於清理長期未完成的垃圾分片
            tmpFile = new java.io.File(file + ".tmp");
            if (tmpFile.exists()) {
                return tmpFile.setLastModified(System.currentTimeMillis());
            } else {
                try {
                    tmpFile.createNewFile();
                } catch (IOException ex) {
                    log.error("無法創建tmp文件", ex);
                    return false;
                }
            }
        }
        return true;
    }
}

第四步:修改Nacos配置中心的pd-file-server.yml文件,加入storagePath配置項

5.10.3.3 接口測試

第一步:啟動Nacos配置中心

第二步:啟動Nginx服務

第三步:啟動文件服務

第四步:訪問分片上傳頁面,進行大文件上傳

可以看到,上傳完成後,對應的分片上傳所需目錄、臨時文件、分片文件都已經創建成功了:

在這裡插入圖片描述
在這裡插入圖片描述

5.11 接口開發-分片合併

前面我們已經完成了分片上傳的接口,本小節需要完成的是將這些分片文件合併為原始文件並按照配置文件配置的存儲策略保存到相應位置。由於不同的存儲方式對應的分片合併方式也不同,所以我們需要提供不同的分片合併處理策略。具體接口設計如下:

在這裡插入圖片描述

5.11.1 FileChunkStrategy

FileChunkStrategy是分片文件處理策略頂層接口,是對分片文件處理的頂層抽象,具體代碼如下:

package com.itheima.pinda.file.strategy;

import com.itheima.pinda.base.R;
import com.itheima.pinda.file.dto.chunk.FileChunksMergeDTO;
import com.itheima.pinda.file.entity.File;
/**
 * 文件分片處理策略接口
 */
public interface FileChunkStrategy {
    /**
     * 分片合併
     *
     * @param merge
     * @return
     */
    R<File> chunksMerge(FileChunksMergeDTO merge);
}

5.11.2 AbstractFileChunkStrategy

AbstractFileChunkStrategy是抽象分片策略處理類,實現了FileChunkStrategy接口。AbstractFileChunkStrategy實現主要的分片合併處理流程,例如:分片臨時存儲路徑獲取、分片數量的檢查、合併後臨時分片文件清理、合併後將文件信息保存到數據庫等,但是真正分片合併的處理過程需要其子類來完成,因為不同的存儲方案處理方式是不同的。

由於在進行分片合併處理過程中需要鎖,在資料中(文件服務\資料\分片上傳\後端)已經提供了工具類,直接導入項目使用即可。

AbstractFileChunkStrategy代碼如下:

package com.itheima.pinda.file.strategy.impl;

import com.itheima.pinda.base.R;
import com.itheima.pinda.file.dto.chunk.FileChunksMergeDTO;
import com.itheima.pinda.file.entity.File;
import com.itheima.pinda.file.enumeration.IconType;
import com.itheima.pinda.file.properties.FileServerProperties;
import com.itheima.pinda.file.service.FileService;
import com.itheima.pinda.file.strategy.FileChunkStrategy;
import com.itheima.pinda.file.utils.FileLock;
import com.itheima.pinda.file.utils.FileDataTypeUtil;
import com.itheima.pinda.utils.DateUtils;
import com.itheima.pinda.utils.NumberHelper;
import com.itheima.pinda.utils.StrPool;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.locks.Lock;
/**
 * 文件分片處理 抽象策略類
 */
@Slf4j
public abstract class AbstractFileChunkStrategy implements FileChunkStrategy {
    @Autowired
    protected FileService fileService;
    @Autowired
    protected FileServerProperties fileProperties;

    protected FileServerProperties.Properties properties;

    /**
     * 分片合併
     * @param info
     * @return
     */
    @Override
    public R<File> chunksMerge(FileChunksMergeDTO info) {
        //   570de89d476e6a5ba371f5fdd8d7920b.avi
        String filename = new StringBuilder(info.getName()).append(StrPool.DOT).append(info.getExt()).toString();
        //分片合併
        R<File> result = chunksMerge(info, filename);

        if (result.getIsSuccess() && result.getData() != null) {
            //文件名
            File filePo = result.getData();

            LocalDateTime now = LocalDateTime.now();
            filePo.setDataType(FileDataTypeUtil.getDataType(info.getContextType()))
                    .setCreateMonth(DateUtils.formatAsYearMonthEn(now))
                    .setCreateWeek(DateUtils.formatAsYearWeekEn(now))
                    .setCreateDay(DateUtils.formatAsDateEn(now))
                    .setSubmittedFileName(info.getSubmittedFileName())
                    .setIsDelete(false)
                    .setSize(info.getSize())
                    .setFileMd5(info.getMd5())
                    .setContextType(info.getContextType())
                    .setFilename(filename)
                    .setExt(info.getExt())
                    .setIcon(IconType.getIcon(info.getExt()).getIcon());

            //將上傳的文件信息保存到數據庫
            fileService.save(filePo);
            return R.success(filePo);
        }
        return result;
    }

    /**
     * 分片合併
     * @param info
     * @param fileName
     * @return
     */
    private R<File> chunksMerge(FileChunksMergeDTO info, String fileName) {
        //獲得分片文件存儲的路徑 D:\\chunks\\2020\\05
        String path = FileDataTypeUtil.getUploadPathPrefix(fileProperties.getStoragePath());
        int chunks = info.getChunks();
        String folder = info.getName();
        String md5 = info.getMd5();
        int chunksNum = this.getChunksNum(Paths.get(path, folder).toString());

        //檢查是否滿足合併條件:分片數量是否足夠
        if (chunks == chunksNum) {
            //同步指定合併的對象
            Lock lock = FileLock.getLock(folder);
            try {
                lock.lock();
                //檢查是否滿足合併條件:分片數量是否足夠
                List<java.io.File> files = new ArrayList<>(Arrays.asList(this.getChunks(Paths.get(path, folder).toString())));
                if (chunks == files.size()) {
                    //按照名稱排序文件,這裡分片都是按照數字命名的

                    //這裡存放的文件名一定是數字
                    files.sort((f1, f2) -> NumberHelper.intValueOf0(f1.getName()) - NumberHelper.intValueOf0(f2.getName()));

                    R<File> result = merge(files, fileName, info);

                    //清理:文件夾,tmp文件
                    this.cleanSpace(folder, path);
                    return result;
                }
            } catch (Exception ex) {
                log.error("數據分片合併失敗", ex);
                return R.fail("數據分片合併失敗");
            } finally {
                //解鎖
                lock.unlock();
                //清理鎖對象
                FileLock.removeLock(folder);
            }
        }

        log.error("文件[簽名:" + md5 + "]數據不完整,可能該文件正在合併中");
        return R.fail("數據不完整,可能該文件正在合併中, 也有可能是上傳過程中某些分片丟失");
    }

    /**
     * 子類實現具體的合併操作
     *
     * @param files    分片文件
     * @param fileName 唯一名 含後綴
     * @param info     分片信息
     * @return
     * @throws IOException
     */
    protected abstract R<File> merge(List<java.io.File> files,  String fileName, FileChunksMergeDTO info) throws IOException;

    /**
     * 清理分片上傳的相關數據
     * 文件夾,tmp文件
     *
     * @param folder 文件夾名稱
     * @param path   上傳文件根路徑
     * @return
     */
    protected boolean cleanSpace(String folder, String path) {
        //刪除分片文件夾
        java.io.File garbage = new java.io.File(Paths.get(path, folder).toString());
        if (!FileUtils.deleteQuietly(garbage)) {
            return false;
        }
        //刪除tmp文件
        garbage = new java.io.File(Paths.get(path, folder + ".tmp").toString());
        if (!FileUtils.deleteQuietly(garbage)) {
            return false;
        }
        return true;
    }

    /**
     * 獲取指定文件的分片數量
     *
     * @param folder 文件夾路徑
     * @return
     */
    private int getChunksNum(String folder) {
        java.io.File[] filesList = this.getChunks(folder);
        return filesList.length;
    }

    /**
     * 獲取指定文件的所有分片
     *
     * @param folder 文件夾路徑
     * @return
     */
    private java.io.File[] getChunks(String folder) {
        java.io.File targetFolder = new java.io.File(folder);
        return targetFolder.listFiles((file) -> {
            if (file.isDirectory()) {
                return false;
            }
            return true;
        });
    }
}

5.11.3 LocalChunkServiceImpl

LocalChunkServiceImpl是AbstractFileChunkStrategy的子類,負責處理存儲策略為本地時的分片文件合併操作。為了使程序能夠動態選擇具體的策略處理類,故將LocalChunkServiceImpl定義在LocalAutoConfigure配置類中,具體代碼如下:

/**
* 本地分片文件處理策略類
*/
@Service
public class LocalChunkServiceImpl extends AbstractFileChunkStrategy {
    /**
         *分片合併
         * @param files    分片文件
         * @param fileName 唯一名 含後綴
         * @param info     分片信息
         * @return
         * @throws IOException
     */
    @Override
    protected R<File> merge(List<java.io.File> files, String fileName, FileChunksMergeDTO info) throws IOException {
        properties = fileProperties.getLocal();

        //日期目錄
        String relativePath = Paths.get(LocalDate.now().format(DateTimeFormatter.ofPattern(DateUtils.DEFAULT_MONTH_FORMAT_SLASH))).toString();

        //合併後文件的存儲路徑 例如:D:\\uploadFiles\\oss-file-service\\2020\\05
        String path = Paths.get(properties.getEndpoint(), properties.getBucketName(), relativePath).toString();

        //上傳文件存放目錄,如果不存在則創建
        java.io.File uploadFolder = new java.io.File(path);
        if(!uploadFolder.exists()){
            uploadFolder.mkdirs();
        }

        //創建合併後的文件
        java.io.File outputFile = new java.io.File(Paths.get(path, fileName).toString());
        if (!outputFile.exists()) {
            boolean newFile = outputFile.createNewFile();
            if (!newFile) {
                return R.fail("創建文件失敗");
            }
            try (FileChannel outChannel = new FileOutputStream(outputFile).getChannel()) {
                //同步nio 方式對分片進行合併, 有效的避免文件過大導致內存溢出
                for (java.io.File file : files) {
                    try (FileChannel inChannel = new FileInputStream(file).getChannel()) {
                        inChannel.transferTo(0, inChannel.size(), outChannel);
                    } catch (FileNotFoundException ex) {
                        log.error("文件轉換失敗", ex);
                        return R.fail("文件轉換失敗");
                    }
                    //刪除分片
                    if (!file.delete()) {
                        log.error("分片[" + info.getName() + "=>" + file.getName() + "]刪除失敗");
                    }
                }
            } catch (FileNotFoundException e) {
                log.error("文件輸出失敗", e);
                return R.fail("文件輸出失敗");
            }

        } else {
            log.warn("文件[{}], fileName={}已經存在", info.getName(), fileName);
        }

        String url = new StringBuilder(properties.getUriPrefix()).
                    append(bucketName).append(StrPool.SLASH).
                    append(relativePath).append(StrPool.SLASH).
                    append(fileName).toString();
        File filePo = File.builder()
            .relativePath(relativePath)
            .url(StringUtils.replace(url, "\\", StrPool.SLASH))
            .build();
        return R.success(filePo);
    }
}

5.11.4 FastDfsChunkServiceImpl

FastDfsChunkServiceImpl是AbstractFileChunkStrategy的子類,負責處理存儲策略為FastDFS時的分片文件合併操作。為了使程序能夠動態選擇具體的策略處理類,故將FastDfsChunkServiceImpl定義在FastDfsAutoConfigure配置類中,具體代碼如下:

/**
* FastDfs分片文件處理策略類
*/
@Service
public class FastDfsChunkServiceImpl extends AbstractFileChunkStrategy {
    @Autowired
    protected AppendFileStorageClient storageClient;

    /**
         * 分片合併
         * @param files    分片文件
         * @param fileName 唯一名 含後綴
         * @param info     分片信息
         * @return
         * @throws IOException
    */
    @Override
    protected R<File> merge(List<java.io.File> files, String fileName, FileChunksMergeDTO info) throws IOException {
        StorePath storePath = null;

        for (int i = 0; i < files.size(); i++) {
            java.io.File file = files.get(i);

            FileInputStream in = FileUtils.openInputStream(file);
            if (i == 0) {
                storePath = storageClient.uploadAppenderFile(null, in,
                                                             file.length(), info.getExt());
            } else {
                storageClient.appendFile(storePath.getGroup(), storePath.getPath(),
                                         in, file.length());
            }
        }
        if (storePath == null) {
            return R.fail("上傳失敗");
        }

        String url = new StringBuilder(fileProperties.getUriPrefix())
            .append(storePath.getFullPath())
            .toString();
        File filePo = File.builder()
            .url(url)
            .group(storePath.getGroup())
            .path(storePath.getPath())
            .build();
        return R.success(filePo);
    }
}

5.11.5 AliChunkServiceImpl

AliChunkServiceImpl是AbstractFileChunkStrategy的子類,負責處理存儲策略為阿里雲OSS時的分片文件合併操作。為了使程序能夠動態選擇具體的策略處理類,故將AliChunkServiceImpl定義在AliOssAutoConfigure配置類中,具體代碼如下:

/**
* 阿里雲OSS分片文件處理策略類
*/
@Service
public class AliChunkServiceImpl extends AbstractFileChunkStrategy {
    private OSS buildClient() {
        properties = fileProperties.getAli();
        return new OSSClientBuilder().build(properties.getEndpoint(), properties.getAccessKeyId(),
                                            properties.getAccessKeySecret());
    }

    /**
         * 分片合併
         * @param files    分片文件
         * @param fileName 唯一名 含後綴
         * @param info     分片信息
         * @return
         * @throws IOException
    */
    @Override
    protected R<File> merge(List<java.io.File> files, String fileName, FileChunksMergeDTO info) throws IOException {
        OSS client = buildClient();
        String bucketName = properties.getBucketName();

        //日期文件夾
        String relativePath = LocalDate.now().format(DateTimeFormatter.ofPattern(DEFAULT_MONTH_FORMAT_SLASH));
        // web服務器存放的相對路徑
        String relativeFileName = relativePath + StrPool.SLASH + fileName;

        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentDisposition("attachment;fileName=" + info.getSubmittedFileName());
        metadata.setContentType(info.getContextType());
        //步驟1:初始化一個分片上傳事件。
        InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, relativeFileName, metadata);
        InitiateMultipartUploadResult result = client.initiateMultipartUpload(request);
        // 返回uploadId,它是分片上傳事件的唯一標識,您可以根據這個ID來發起相關的操作,如取消分片上傳、查詢分片上傳等。
        String uploadId = result.getUploadId();

        // partETags是PartETag的集合。PartETag由分片的ETag和分片號組成。
        List<PartETag> partETags = new ArrayList<PartETag>();
        for (int i = 0; i < files.size(); i++) {
            java.io.File file = files.get(i);
            FileInputStream in = FileUtils.openInputStream(file);

            UploadPartRequest uploadPartRequest = new UploadPartRequest();
            uploadPartRequest.setBucketName(bucketName);
            uploadPartRequest.setKey(relativeFileName);
            uploadPartRequest.setUploadId(uploadId);
            uploadPartRequest.setInputStream(in);
            // 設置分片大小。除了最後一個分片沒有大小限制,其他的分片最小為100KB。
            uploadPartRequest.setPartSize(file.length());
            // 設置分片號。每一個上傳的分片都有一個分片號,取值範圍是1~10000,如果超出這個範圍,OSS將返回InvalidArgument的錯誤碼。
            uploadPartRequest.setPartNumber(i + 1);

            // 每個分片不需要按順序上傳,甚至可以在不同客戶端上傳,OSS會按照分片號排序組成完整的文件。
            UploadPartResult uploadPartResult = client.uploadPart(uploadPartRequest);

            // 每次上傳分片之後,OSS的返回結果會包含一個PartETag。PartETag將被保存到partETags中。
            partETags.add(uploadPartResult.getPartETag());
        }

        /* 步驟3:完成分片上傳。 */
        // 排序。partETags必須按分片號升序排列。
        partETags.sort(Comparator.comparingInt(PartETag::getPartNumber));

        // 在執行該操作時,需要提供所有有效的partETags。OSS收到提交的partETags後,會逐一驗證每個分片的有效性。當所有的數據分片驗證通過後,OSS將把這些分片組合成一個完整的文件。
        CompleteMultipartUploadRequest completeMultipartUploadRequest =
            new CompleteMultipartUploadRequest(bucketName, relativeFileName, uploadId, partETags);

        CompleteMultipartUploadResult uploadResult = client.completeMultipartUpload(completeMultipartUploadRequest);

        String url = new StringBuilder(properties.getUriPrefix())
            .append(relativePath)
            .append(StrPool.SLASH)
            .append(fileName)
            .toString();
        File filePo = File.builder()
            .relativePath(relativePath)
            .group(uploadResult.getETag())
            .path(uploadResult.getRequestId())
            .url(StringUtils.replace(url, "\\", StrPool.SLASH))
            .build();

        // 關閉OSSClient。
        client.shutdown();
        return R.success(filePo);
    }
}

5.11.6 MinioChunkServiceImpl

MinioChunkServiceImpl是AbstractFileChunkStrategy的子類,負責處理存儲策略為MINIO時的分片文件合併操作。為了使程序能夠動態選擇具體的策略處理類,故將MinioChunkServiceImpll定義在MinioAutoConfigure配置類中,具體代碼如下:

    /**
     * 分片文件策略處理類
     */
    @Service
    public class MinioChunkServiceImpl extends AbstractFileChunkStrategy {

        /**
         * 分片合併抽象方法,需要子類實現
         *
         * @param files
         * @param fileName
         * @param fileChunksMergeDTO
         * @return
         */
        @Override
        protected R<File> merge(List<java.io.File> files, String fileName, FileChunksMergeDTO fileChunksMergeDTO) throws Exception {
            MinioAutoConfigure.this.buildClient(fileServerProperties);
            Vector<InputStream> streams = new Vector<>();
            //分片合併成功,需要封裝File對象相關屬性
            File fileResult = new File();

            for (java.io.File file : files) {//file對應的就是分片文件
                streams.add(new FileInputStream(file));
                new FileInputStream(file).available();
                //刪除當前分片
                file.delete();
            }

            //生成滿足要求的objectName和url
            String objectName = doReName(fileName, fileResult);
            //sequenceInputStream直接使用只能獲取第一個分片的數據,故先全部轉成輸出流再轉成輸入流
            //存在問題:
            //1.本身這個實現就不優雅
            //2.OutOfMemoryError: Java heap space,測試同時傳三個幾百M的文件會發生內存溢出
            //目前是分片文件上傳到服務器,再程序里合併後再上傳到minio,下面提供了很多minio的工具類,可以改成分片文件上傳到minio,利用minioClient合併文件,目前未實現
            try (SequenceInputStream sequenceInputStream = new SequenceInputStream(streams.elements());
                 ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
                byte[] bytes = new byte[sequenceInputStream.available()];
                int len;
                while ((len = sequenceInputStream.read(bytes)) != -1) {
                    outputStream.write(bytes, 0, len);
                }

                byte[] outBytes = outputStream.toByteArray();
                ByteBuffer buffer = ByteBuffer.wrap(outBytes);
                try (ByteArrayInputStream inputStream = new ByteArrayInputStream(buffer.array())) {
                    // 使用putObject上傳一個文件到存儲桶中
                    PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                            .bucket(bucketName)
                            .object(objectName)
                            .contentType(fileChunksMergeDTO.getContextType())
                            .stream(inputStream, inputStream.available(), ObjectWriteArgs.MIN_MULTIPART_SIZE).build();
                    minioClient.putObject(putObjectArgs);
                } catch (Exception ex) {
                    log.error("分片文件合併失敗");
                    return R.fail("分片文件合併失敗");
                }
            } catch (Exception ex) {
                log.error("分片文件合併失敗");
                return R.fail("分片文件合併失敗");
            }
            return R.success(fileResult);
        }
    }

5.11.7 分片合併接口

接口文檔:

在這裡插入圖片描述
在這裡插入圖片描述

在FileChunkController中提供分片合併方法,直接調用分片處理策略類完成分片合併操作:

@Autowired
private FileChunkStrategy fileChunkStrategy;//分片文件處理策略

/**
* 分片合併
* @param info
* @return
*/
@ApiOperation(value = "分片合併", notes = "所有分片上傳成功後,調用該接口對分片進行合併")
@PostMapping(value = "/merge")
public R<File> saveChunksMerge(FileChunksMergeDTO info) {
    log.info("info={}", info);

    return fileChunkStrategy.chunksMerge(info);
}

第2-1-2章 傳統方式安裝FastDFS-附FastDFS常用命令
第2-1-3章 docker-compose安裝FastDFS,實現文件存儲服務
第2-1-5章 docker安裝MinIO實現文件存儲服務-springboot整合minio-minio全網最全的資料

全套代碼及資料全部完整提供,點此處下載