亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

springboot斷點(diǎn)上傳、續(xù)傳、秒傳實(shí)現(xiàn)方式

 更新時(shí)間:2024年07月17日 08:56:59   作者:Mr-Wanter  
這篇文章主要介紹了springboot斷點(diǎn)上傳、續(xù)傳、秒傳實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教

前言

  • springboot 斷點(diǎn)上傳、續(xù)傳、秒傳實(shí)現(xiàn)。
  • 保存方式提供本地上傳(單機(jī))和minio上傳(可集群)
  • 本文主要是后端實(shí)現(xiàn)方案,數(shù)據(jù)庫(kù)持久化采用jpa

一、實(shí)現(xiàn)思路

  • 前端生成文件md5,根據(jù)md5檢查文件塊上傳進(jìn)度或秒傳
  • 需要上傳分片的文件上傳分片文件
  • 分片合并后上傳服務(wù)器

二、數(shù)據(jù)庫(kù)表對(duì)象

說(shuō)明:

  • AbstractDomainPd<String>為公共字段,如id,創(chuàng)建人,創(chuàng)建時(shí)間等,根據(jù)自己框架修改即可。
  • clientId 應(yīng)用id用于隔離不同應(yīng)用附件,非必須
  • 附件表:上傳成功的附件信息
@Entity
@Table(name = "gsdss_file", schema = "public")
@Data
public class AttachmentPO extends AbstractDomainPd<String> implements Serializable {
    /**
     * 相對(duì)路徑
     */
    private String path;
    /**
     * 文件名
     */
    private String fileName;
    /**
     * 文件大小
     */
    private String size;
    /**
     * 文件MD5
     */
    private String fileIdentifier;
}

分片信息表:記錄當(dāng)前文件已上傳的分片數(shù)據(jù)

@Entity
@Table(name = "gsdss_file_chunk", schema = "public")
@Data
public class ChunkPO extends AbstractDomainPd<String> implements Serializable {
    
    /**
     * 應(yīng)用id
     */
    private String clientId;
    /**
     * 文件塊編號(hào),從1開(kāi)始
     */
    private Integer chunkNumber;
    /**
     * 文件標(biāo)識(shí)MD5
     */
    private String fileIdentifier;
    /**
     * 文件名
     */
    private String fileName;
    /**
     * 相對(duì)路徑
     */
    private String path;
    
}

三、業(yè)務(wù)入?yún)?duì)象

檢查文件塊上傳進(jìn)度或秒傳入?yún)?duì)象

package com.gsafety.bg.gsdss.file.manage.model.req;

import io.swagger.v3.oas.annotations.Hidden;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;

import javax.validation.constraints.NotNull;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ChunkReq {
    
    /**
     * 文件塊編號(hào),從1開(kāi)始
     */
    @NotNull
    private Integer chunkNumber;
    /**
     * 文件標(biāo)識(shí)MD5
     */
    @NotNull
    private String fileIdentifier;
    /**
     * 相對(duì)路徑
     */
    @NotNull
    private String path;
    /**
     * 塊內(nèi)容
     */
    @Hidden
    private MultipartFile file;
    /**
     * 應(yīng)用id
     */
    @NotNull
    private String clientId;
    /**
     * 文件名
     */
    @NotNull
    private String fileName;
}

上傳分片入?yún)?/p>

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CheckChunkReq {
    
    /**
     * 應(yīng)用id
     */
    @NotNull
    private String clientId;
    /**
     * 文件名
     */
    @NotNull
    private String fileName;
    
    /**
     * md5
     */
    @NotNull
    private String fileIdentifier;
}

分片合并入?yún)?/p>

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class FileReq {
    
    @Hidden
    private MultipartFile file;
    /**
     * 文件名
     */
    @NotNull
    private String fileName;
    /**
     * 文件大小
     */
    @NotNull
    private Long fileSize;
    /**
     * eg:data/plan/
     */
    @NotNull
    private String path;
    /**
     * md5
     */
    @NotNull
    private String fileIdentifier;
    /**
     * 應(yīng)用id
     */
    @NotNull
    private String clientId;
}

檢查文件塊上傳進(jìn)度或秒傳返回結(jié)果

@Data
public class UploadResp implements Serializable {
    
    /**
     * 是否跳過(guò)上傳(已上傳的可以直接跳過(guò),達(dá)到秒傳的效果)
     */
    private boolean skipUpload = false;
    
    /**
     * 已經(jīng)上傳的文件塊編號(hào),可以跳過(guò),斷點(diǎn)續(xù)傳
     */
    private List<Integer> uploadedChunks;
    
    /**
     * 文件信息
     */
    private AttachmentResp fileInfo;
    
}

四、本地上傳實(shí)現(xiàn)

    @Resource
    private S3OssProperties properties;
    @Resource
    private AttachmentService attachmentService;
    @Resource
    private ChunkDao chunkDao;
    @Resource
    private ChunkMapping chunkMapping;
    
    /**
     * 上傳分片文件
     *
     * @param req
     */
    @Override
    public boolean uploadChunk(ChunkReq req) {
        BizPreconditions.checkArgumentNoStack(!req.getFile().isEmpty(), "上傳分片不能為空!");
        BizPreconditions.checkArgumentNoStack(req.getPath().endsWith("/"), "url參數(shù)必須是/結(jié)尾");
        //文件名-1
        String fileName = req.getFileName().concat("-").concat(req.getChunkNumber().toString());
        //分片文件上傳服務(wù)器的目錄地址 文件夾地址/chunks/文件md5
        String filePath = properties.getPath().concat(req.getClientId()).concat(File.separator).concat(req.getPath())
                .concat("chunks").concat(File.separator).concat(req.getFileIdentifier()).concat(File.separator);
        try {
            Path newPath = Paths.get(filePath);
            Files.createDirectories(newPath);
            //文件夾地址/md5/文件名-1
            newPath = Paths.get(filePath.concat(fileName));
            if (Files.notExists(newPath)) {
                Files.createFile(newPath);
            }
            Files.write(newPath, req.getFile().getBytes(), StandardOpenOption.CREATE);
        } catch (IOException e) {
            log.error(" 附件存儲(chǔ)失敗 ", e);
            throw new BusinessCheckException("附件存儲(chǔ)失敗");
        }
        // 存儲(chǔ)分片信息
        chunkDao.save(chunkMapping.req2PO(req));
        return true;
    }
    
    /**
     * 檢查文件塊
     */
    @Override
    public UploadResp checkChunk(CheckChunkReq req) {
        UploadResp result = new UploadResp();
        //查詢數(shù)據(jù)庫(kù)記錄
        //先判斷整個(gè)文件是否已經(jīng)上傳過(guò)了,如果是,則告訴前端跳過(guò)上傳,實(shí)現(xiàn)秒傳
        AttachmentResp resp = attachmentService.findByFileIdentifierAndClientId(req.getFileIdentifier(), req.getClientId());
        if (resp != null) {
            //當(dāng)前文件信息另存
            AttachmentResp newResp = attachmentService.save(AttachmentReq.builder()
                    .fileName(req.getFileName()).origin(AttachmentConstants.TYPE.LOCAL_TYPE)
                    .clientId(req.getClientId()).path(resp.getPath()).size(resp.getSize())
                    .fileIdentifier(req.getFileIdentifier()).build());
            result.setSkipUpload(true);
            result.setFileInfo(newResp);
            return result;
        }
        
        //如果完整文件不存在,則去數(shù)據(jù)庫(kù)判斷當(dāng)前哪些文件塊已經(jīng)上傳過(guò)了,把結(jié)果告訴前端,跳過(guò)這些文件塊的上傳,實(shí)現(xiàn)斷點(diǎn)續(xù)傳
        List<ChunkPO> chunkList = chunkDao.findByFileIdentifierAndClientId(req.getFileIdentifier(), req.getClientId());
        //將已存在的塊的chunkNumber列表返回給前端,前端會(huì)規(guī)避掉這些塊
        if (!CollectionUtils.isEmpty(chunkList)) {
            List<Integer> collect = chunkList.stream().map(ChunkPO::getChunkNumber).collect(Collectors.toList());
            result.setUploadedChunks(collect);
        }
        return result;
    }
    
    /**
     * 分片合并
     *
     * @param req
     */
    @Override
    public boolean mergeChunk(FileReq req) {
        String filename = req.getFileName();
        String date = DateUtil.localDateToString(LocalDate.now());
        //附件服務(wù)器存儲(chǔ)合并后的文件存放地址
        String file = properties.getPath().concat(req.getClientId()).concat(File.separator).concat(req.getPath())
                .concat(date).concat(File.separator).concat(filename);
        //服務(wù)器分片文件存放地址
        String folder = properties.getPath().concat(req.getClientId()).concat(File.separator).concat(req.getPath())
                .concat("chunks").concat(File.separator).concat(req.getFileIdentifier());
        //合并文件到本地目錄,并刪除分片文件
        boolean flag = mergeFile(file, folder, filename);
        if (!flag) {
            return false;
        }
        
        //保存文件記錄
        AttachmentResp resp = attachmentService.findByFileIdentifierAndClientId(req.getFileIdentifier(), req.getClientId());
        if (resp == null) {
            attachmentService.save(AttachmentReq.builder().fileName(filename).origin(AttachmentConstants.TYPE.LOCAL_TYPE)
                    .clientId(req.getClientId()).path(file).size(FileUtils.changeFileFormat(req.getFileSize()))
                    .fileIdentifier(req.getFileIdentifier()).build());
        }
        
        //插入文件記錄成功后,刪除chunk表中的對(duì)應(yīng)記錄,釋放空間
        chunkDao.deleteAllByFileIdentifierAndClientId(req.getFileIdentifier(), req.getClientId());
        return true;
    }
    
    /**
     * 文件合并
     *
     * @param targetFile 要形成的文件地址
     * @param folder     分片文件存放地址
     * @param filename   文件的名稱
     */
    private boolean mergeFile(String targetFile, String folder, String filename) {
        try {
            //先判斷文件是否存在
            if (FileUtils.fileExists(targetFile)) {
                //文件已存在
                return true;
            }
            Path newPath = Paths.get(StringUtils.substringBeforeLast(targetFile, File.separator));
            Files.createDirectories(newPath);
            Files.createFile(Paths.get(targetFile));
            Files.list(Paths.get(folder))
                    .filter(path -> !path.getFileName().toString().equals(filename))
                    .sorted((o1, o2) -> {
                        String p1 = o1.getFileName().toString();
                        String p2 = o2.getFileName().toString();
                        int i1 = p1.lastIndexOf("-");
                        int i2 = p2.lastIndexOf("-");
                        return Integer.valueOf(p2.substring(i2)).compareTo(Integer.valueOf(p1.substring(i1)));
                    })
                    .forEach(path -> {
                        try {
                            //以追加的形式寫(xiě)入文件
                            Files.write(Paths.get(targetFile), Files.readAllBytes(path), StandardOpenOption.APPEND);
                            //合并后刪除該塊
                            Files.delete(path);
                        } catch (IOException e) {
                            log.error(e.getMessage(), e);
                            throw new BusinessException("文件合并失敗");
                        }
                    });
            //刪除空文件夾
            FileUtils.delDir(folder);
        } catch (IOException e) {
            log.error("文件合并失敗: ", e);
            throw new BusinessException("文件合并失敗");
        }
        return true;
    }

五、minio上傳實(shí)現(xiàn)

    @Resource
    private MinioTemplate minioTemplate;
    @Resource
    private AttachmentService attachmentService;
 	@Resource
    private ChunkDao chunkDao;
    @Resource
    private ChunkMapping chunkMapping;
    
    /**
     * 上傳分片文件
     */
    @Override
    public boolean uploadChunk(ChunkReq req) {
        String fileName = req.getFileName();
        BizPreconditions.checkArgumentNoStack(!req.getFile().isEmpty(), "上傳分片不能為空!");
        BizPreconditions.checkArgumentNoStack(req.getPath().endsWith(separator), "url參數(shù)必須是/結(jié)尾");
        String newFileName = req.getPath().concat("chunks").concat(separator).concat(req.getFileIdentifier()).concat(separator)
                + fileName.concat("-").concat(req.getChunkNumber().toString());
        try {
            minioTemplate.putObject(req.getClientId(), newFileName, req.getFile());
        } catch (Exception e) {
            e.printStackTrace();
            throw new BusinessException("文件上傳失敗");
        }
        // 存儲(chǔ)分片信息
        chunkDao.save(chunkMapping.req2PO(req));
        return true;
    }
    
    /**
     * 檢查文件塊
     */
    @Override
    public UploadResp checkChunk(CheckChunkReq req) {
        UploadResp result = new UploadResp();
        //查詢數(shù)據(jù)庫(kù)記錄
        //先判斷整個(gè)文件是否已經(jīng)上傳過(guò)了,如果是,則告訴前端跳過(guò)上傳,實(shí)現(xiàn)秒傳
        AttachmentResp resp = attachmentService.findByFileIdentifierAndClientId(req.getFileIdentifier(), req.getClientId());
        if (resp != null) {
            //當(dāng)前文件信息另存
            AttachmentResp newResp = attachmentService.save(AttachmentReq.builder()
                    .fileName(req.getFileName()).origin(AttachmentConstants.TYPE.MINIO_TYPE)
                    .clientId(req.getClientId()).path(resp.getPath()).size(resp.getSize())
                    .fileIdentifier(req.getFileIdentifier()).build());
            result.setSkipUpload(true);
            result.setFileInfo(newResp);
            return result;
        }
        
        //如果完整文件不存在,則去數(shù)據(jù)庫(kù)判斷當(dāng)前哪些文件塊已經(jīng)上傳過(guò)了,把結(jié)果告訴前端,跳過(guò)這些文件塊的上傳,實(shí)現(xiàn)斷點(diǎn)續(xù)傳
        List<ChunkPO> chunkList = chunkDao.findByFileIdentifierAndClientId(req.getFileIdentifier(), req.getClientId());
        //將已存在的塊的chunkNumber列表返回給前端,前端會(huì)規(guī)避掉這些塊
        if (!CollectionUtils.isEmpty(chunkList)) {
            List<Integer> collect = chunkList.stream().map(ChunkPO::getChunkNumber).collect(Collectors.toList());
            result.setUploadedChunks(collect);
        }
        return result;
    }
    
    /**
     * 分片合并
     *
     * @param req
     */
    @Override
    public boolean mergeChunk(FileReq req) {
        String filename = req.getFileName();
        //合并文件到本地目錄
        String chunkPath = req.getPath().concat("chunks").concat(separator).concat(req.getFileIdentifier()).concat(separator);
        List<Item> chunkList = minioTemplate.getAllObjectsByPrefix(req.getClientId(), chunkPath, false);
        String fileHz = filename.substring(filename.lastIndexOf("."));
        String newFileName = req.getPath() + UUIDUtil.uuid() + fileHz;
        try {
            List<ComposeSource> sourceObjectList = chunkList.stream()
                    .sorted(Comparator.comparing(Item::size).reversed())
                    .map(l -> ComposeSource.builder()
                            .bucket(req.getClientId())
                            .object(l.objectName())
                            .build())
                    .collect(Collectors.toList());
            ObjectWriteResponse response = minioTemplate.composeObject(req.getClientId(), newFileName, sourceObjectList);
            //刪除分片bucket及文件
            minioTemplate.removeObjects(req.getClientId(), chunkPath);
        } catch (Exception e) {
            e.printStackTrace();
            throw new BusinessException("文件合并失敗");
        }
        //保存文件記錄
        AttachmentResp resp = attachmentService.findByFileIdentifierAndClientId(req.getFileIdentifier(), req.getClientId());
        if (resp == null) {
            attachmentService.save(AttachmentReq.builder().fileName(filename).origin(AttachmentConstants.TYPE.MINIO_TYPE)
                    .clientId(req.getClientId()).path(newFileName).size(FileUtils.changeFileFormat(req.getFileSize()))
                    .fileIdentifier(req.getFileIdentifier()).build());
        }
        
        //插入文件記錄成功后,刪除chunk表中的對(duì)應(yīng)記錄,釋放空間
        chunkDao.deleteAllByFileIdentifierAndClientId(req.getFileIdentifier(), req.getClientId());
        return true;
    }

MinioTemplate 參考

總結(jié)

1.檢查文件塊上傳進(jìn)度或秒傳

  • 根據(jù)文件md5查詢附件信息表,如果存在,直接返回附件信息。
  • 不存在查詢分片信息表,查詢當(dāng)前文件分片上傳進(jìn)度,返回已經(jīng)上傳過(guò)的分片編號(hào)

2.上傳分片

  • 分片文件上傳地址需要保證唯一性,可用文件MD5作為隔離
  • 上傳后保存分片上傳信息
  • minio對(duì)合并分片文件有大小限制,除最后一個(gè)分片外,其他分片文件大小不得小于5MB,所以minio分片上傳需要分片大小最小為5MB,并且獲取分片需要按照分片文件大小排序,將最后一個(gè)分片放到最后進(jìn)行合并

3.分片合并

  • 將分片文件合并為新文件到最終文件存放地址并刪除分片文件
  • 保存最終文件信息到附件信息表
  • 刪除對(duì)應(yīng)分片信息表數(shù)據(jù)

總結(jié)

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • 詳解Java如何實(shí)現(xiàn)百萬(wàn)數(shù)據(jù)excel導(dǎo)出功能

    詳解Java如何實(shí)現(xiàn)百萬(wàn)數(shù)據(jù)excel導(dǎo)出功能

    這篇文章主要為大家詳細(xì)介紹了Java如何實(shí)現(xiàn)百萬(wàn)數(shù)據(jù)excel導(dǎo)出功能,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,需要的可以參考一下
    2023-02-02
  • 詳解Spring Boot 使用Spring security 集成CAS

    詳解Spring Boot 使用Spring security 集成CAS

    本篇文章主要介紹了詳解Spring Boot 使用Spring security 集成CAS,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-05-05
  • java web手寫(xiě)實(shí)現(xiàn)分頁(yè)功能

    java web手寫(xiě)實(shí)現(xiàn)分頁(yè)功能

    這篇文章主要為大家詳細(xì)介紹了java web手寫(xiě)實(shí)現(xiàn)分頁(yè)功能的方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2020-02-02
  • java BigDecimal精度丟失及常見(jiàn)問(wèn)分析

    java BigDecimal精度丟失及常見(jiàn)問(wèn)分析

    這篇文章主要為大家介紹了java BigDecimal精度丟失及常見(jiàn)問(wèn)分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-02-02
  • Java實(shí)現(xiàn)RSA加密工具類

    Java實(shí)現(xiàn)RSA加密工具類

    這篇文章主要介紹了Java如何實(shí)現(xiàn)RSA加密工具類,幫助大家更好的理解和使用Java,感興趣的朋友可以了解下
    2020-09-09
  • 淺談Java設(shè)置PPT幻燈片背景——純色、漸變、圖片背景

    淺談Java設(shè)置PPT幻燈片背景——純色、漸變、圖片背景

    這篇文章主要介紹了Java設(shè)置PPT幻燈片背景——純色、漸變、圖片背景,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-03-03
  • Spring Security如何使用URL地址進(jìn)行權(quán)限控制

    Spring Security如何使用URL地址進(jìn)行權(quán)限控制

    這篇文章主要介紹了Spring Security如何使用URL地址進(jìn)行權(quán)限控制,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2019-12-12
  • 在idea中將java項(xiàng)目中的單個(gè)類打包成jar包操作

    在idea中將java項(xiàng)目中的單個(gè)類打包成jar包操作

    這篇文章主要介紹了在idea中將java項(xiàng)目中的單個(gè)類打包成jar包操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2020-08-08
  • SpringBoot如何優(yōu)雅的處理重復(fù)請(qǐng)求

    SpringBoot如何優(yōu)雅的處理重復(fù)請(qǐng)求

    對(duì)于一些用戶請(qǐng)求,在某些情況下是可能重復(fù)發(fā)送的,如果是查詢類操作并無(wú)大礙,但其中有些是涉及寫(xiě)入操作的,一旦重復(fù)了,可能會(huì)導(dǎo)致很嚴(yán)重的后果,所以本文給大家介紹了SpringBoot優(yōu)雅的處理重復(fù)請(qǐng)求的方法,需要的朋友可以參考下
    2023-12-12
  • 70行Java代碼實(shí)現(xiàn)深度神經(jīng)網(wǎng)絡(luò)算法分享

    70行Java代碼實(shí)現(xiàn)深度神經(jīng)網(wǎng)絡(luò)算法分享

    這篇文章主要介紹了70行Java代碼實(shí)現(xiàn)深度神經(jīng)網(wǎng)絡(luò)算法分享,涉及神經(jīng)網(wǎng)絡(luò)的計(jì)算過(guò)程,神經(jīng)網(wǎng)絡(luò)的算法程序?qū)崿F(xiàn),多層神經(jīng)網(wǎng)絡(luò)完整程序?qū)崿F(xiàn)等相關(guān)內(nèi)容,具有一定參考價(jià)值,需要的朋友可以參考下。
    2017-11-11

最新評(píng)論