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

来源:https://www.cnblogs.com/gitBook/archive/2022/11/21/16910229.html
-Advertisement-
Play Games

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


目錄

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+。

官網地址:http://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全網最全的資料

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Linux的文件系統分層結構: Lunux文件系統常用的目錄如下: 根目錄(/):根目錄是整個系統最重要的一個目錄,因為不但所有的目錄都是由根目錄衍生出來的,同時根目錄也與開機、還原、系統修複等操作有關。 執行文件目錄(/bin):所有用戶使用的基本命令:不能關聯至獨立分區,OS啟動即會用到的程式 ...
  • 本文章沒有任何宣傳產品等盈利性質,所有的陳述均為博主真實的日常使用。 關於電腦 現在很多學電腦的同學總是抱怨自己的電腦卡卡卡,打開一看360,金山等安全軟體在瘋狂打架,桌面各種文件堆得也是稀碎,再打開文件資源管理器一看,好家伙,C盤都紅了,各種文件堆在盤裡也是亂七八糟。清理垃圾本身沒多煩,21世紀 ...
  • 一、安裝VMware Workstation虛擬機 下載VMware Workstation 16 PRO虛擬機 https://www.vmware.com/cn/products/workstation-pro/workstation-pro-evaluation.html,下載後安裝即可,安裝 ...
  • 1.虛擬機下載 官網下載地址:https://www.kali.org/get-kali/#kali-virtual-machines 選擇VMware版本下載,並解壓 2.打開虛擬機 選擇打開虛擬機,瀏覽到剛纔壓縮包解壓路徑,選擇.vmx文件打開 開啟此虛擬機 用戶名密碼都是kali 3.設置ro ...
  • 世界上的開源許可證(Open Source License)大概有上百種,而我們常用的開源軟體協議大致有GPL、BSD、MIT、Mozilla、Apache和LGPL。 從下圖中可以看出幾種開源軟體協議的區別。 以下是上述協議的簡單介紹: GPL GNU是GNU General Public Lic ...
  • GreatSQL社區原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。 GreatSQL是MySQL的國產分支版本,使用上與MySQL一致。 作者: 如常 Debezium Incremental snapshotting Introduction CDC(Change-Data-Captur ...
  • 資料庫面試測試題(一) 簡述當前主流RDBMS軟體有哪些?開源且跨平臺的資料庫軟體有哪些? 參考答案 當前主流的資料庫伺服器軟體有: Oracle 、 DB2 、 SQL SERVER 、MySQL 等 ,其中只有MySQL是既開源又跨平臺的資料庫服務軟體。 簡述MySQL資料庫的服務進程名、預設端 ...
  • 1、文本類指令 {{}}、v-text 都是用於綁定節點的文本; 二者區別:{{}}這種綁定值的方式在頁面會出現“{{}}”一閃而過的效果 解決{{}}在頁面出現一閃而過的辦法: // css: [v-cloak] { display: 'none' }// html <h1 v-cloak>{{m ...
一周排行
    -Advertisement-
    Play Games
  • 1、預覽地址:http://139.155.137.144:9012 2、qq群:801913255 一、前言 隨著網路的發展,企業對於信息系統數據的保密工作愈發重視,不同身份、角色對於數據的訪問許可權都應該大相徑庭。 列如 1、不同登錄人員對一個數據列表的可見度是不一樣的,如數據列、數據行、數據按鈕 ...
  • 前言 上一篇文章寫瞭如何使用RabbitMQ做個簡單的發送郵件項目,然後評論也是比較多,也是準備去學習一下如何確保RabbitMQ的消息可靠性,但是由於時間原因,先來說說設計模式中的簡單工廠模式吧! 在瞭解簡單工廠模式之前,我們要知道C#是一款面向對象的高級程式語言。它有3大特性,封裝、繼承、多態。 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 介紹 Nodify是一個WPF基於節點的編輯器控制項,其中包含一系列節點、連接和連接器組件,旨在簡化構建基於節點的工具的過程 ...
  • 創建一個webapi項目做測試使用。 創建新控制器,搭建一個基礎框架,包括獲取當天日期、wiki的請求地址等 創建一個Http請求幫助類以及方法,用於獲取指定URL的信息 使用http請求訪問指定url,先運行一下,看看返回的內容。內容如圖右邊所示,實際上是一個Json數據。我們主要解析 大事記 部 ...
  • 最近在不少自媒體上看到有關.NET與C#的資訊與評價,感覺大家對.NET與C#還是不太瞭解,尤其是對2016年6月發佈的跨平臺.NET Core 1.0,更是知之甚少。在考慮一番之後,還是決定寫點東西總結一下,也回顧一下.NET的發展歷史。 首先,你沒看錯,.NET是跨平臺的,可以在Windows、 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 添加節點(nodes) 通過上一篇我們已經創建好了編輯器實例現在我們為編輯器添加一個節點 添加model和viewmode ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...
  • 類型檢查和轉換:當你需要檢查對象是否為特定類型,並且希望在同一時間內將其轉換為那個類型時,模式匹配提供了一種更簡潔的方式來完成這一任務,避免了使用傳統的as和is操作符後還需要進行額外的null檢查。 複雜條件邏輯:在處理複雜的條件邏輯時,特別是涉及到多個條件和類型的情況下,使用模式匹配可以使代碼更 ...
  • 在日常開發中,我們經常需要和文件打交道,特別是桌面開發,有時候就會需要載入大批量的文件,而且可能還會存在部分文件缺失的情況,那麼如何才能快速的判斷文件是否存在呢?如果處理不當的,且文件數量比較多的時候,可能會造成卡頓等情況,進而影響程式的使用體驗。今天就以一個簡單的小例子,簡述兩種不同的判斷文件是否... ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...