当前位置: 首页 > news >正文

文件分片上传

1前端

<input
       type="file"
       accept=".mp4"
       ref="videoInput"
       @change="handleVideoChange"
       style="display: none;">

2生成hash

// 根据整个文件的文件名和大小组合的字符串生成hash值,大概率确定文件的唯一性
        fhash(file) {
            // console.log("哈希字段: ", file.name+file.size.toString());
            return new Promise(resolve => {
                const spark = new SparkMD5();
                spark.append(file.name+file.size.toString());
                resolve(spark.end());
            })
        },
 

3生成切片

// 生成切片
        createChunks(file) {
            const result = [];
            for (let i = 0; i < file.size; i += this.chunkSize) {
                result.push(file.slice(i, i + this.chunkSize));
            }
            return result;
        },

4查询未上传切片的hash,断点续传

// 获取当前还没上传的序号 断点续传
        async askCurrentChunk(hash) {
            return await this.$get("/video/ask-chunk", {
                params: { hash: hash },
                headers: { Authorization: "Bearer " + localStorage.getItem("teri_token") }
            });
        },

5上传分片

 // 上传分片
        async uploadChunk(formData) {
            return await this.$post("/video/upload-chunk", formData, {
                headers: {
                    'Content-Type': 'multipart/form-data',
                    Authorization: "Bearer " + localStorage.getItem("teri_token"),
                }
            })
        },

6上传文件

async upload() {
            const chunks = this.createChunks(this.selectedVideo);
            // 向服务器查询还没上传的下一个分片序号
            const result = await this.askCurrentChunk(this.hash);
            this.current = result.data.data;
            // 逐个上传分片
            for (this.current; this.current < chunks.length; this.current++) {
                const chunk = chunks[this.current];
                const formData = new FormData();
                formData.append('chunk', chunk); // 将当前分片作为单独的文件上传
                formData.append('hash', this.hash);
                formData.append('index', this.current); // 传递分片索引
                
                // 发送分片到服务器
                try {
                    const res = await this.uploadChunk(formData);
                    if (res.data.code !== 200) {
                        // ElMessage.error("分片上传失败");
                        this.isFailed = true;
                        this.isPause = true;
                    }
                } catch {
                    // ElMessage.error("分片上传失败");
                    this.isFailed = true;
                    this.isPause = true;
                    return;
                }
                // 暂停上传
                if (this.isPause) {
                    // 取消上传彻底删除已上传分片
                    if (this.isCancel) {
                        await this.cancelUpload(this.hash);
                        this.isCancel = false;
                    }
                    return;
                }
                this.progress = Math.round(((this.current + 1) / chunks.length) * 100); // 实时改进度条
            }
            this.progress = 100;    // 上传完成再次确认为100%
        },

后端

1查询hash

     /**
     * 获取视频下一个还没上传的分片序号
     * @param hash 视频的hash值
     * @return CustomResponse对象
     */
    public CustomResponse askCurrentChunk(String hash) {
        CustomResponse customResponse = new CustomResponse();

        // 查询本地
        // 获取分片文件的存储目录
        File chunkDir = new File(CHUNK_DIRECTORY);
        // 获取存储在目录中的所有分片文件
        File[] chunkFiles = chunkDir.listFiles((dir, name) -> name.startsWith(hash + "-"));
        // 返回还没上传的分片序号
        if (chunkFiles == null) {
            customResponse.setData(0);
        } else {
            customResponse.setData(chunkFiles.length);
        }

        // 查询OSS当前存在的分片数量,即前端要上传的分片序号,建议分布式系统才使用OSS存储分片,单体系统本地存储分片效率更高
//        int counts = ossUploadUtil.countFiles("chunk/", hash + "-");
//        customResponse.setData(counts);

        return customResponse;
    }

2上传分片

/**
     * 上传单个视频分片,当前上传到阿里云对象存储
     * @param chunk 分片文件
     * @param hash  视频的hash值
     * @param index 当前分片的序号
     * @return  CustomResponse对象
     * @throws IOException
     */
    @Override
    public CustomResponse uploadChunk(MultipartFile chunk, String hash, Integer index) throws IOException {
        CustomResponse customResponse = new CustomResponse();
        // 构建分片文件名
        String chunkFileName = hash + "-" + index;

        // 存储到本地
        // 构建分片文件的完整路径
        String chunkFilePath = Paths.get(CHUNK_DIRECTORY, chunkFileName).toString();
        // 检查是否已经存在相同的分片文件
        File chunkFile = new File(chunkFilePath);
        if (chunkFile.exists()) {
            log.warn("分片 " + chunkFilePath + " 已存在");
            customResponse.setCode(500);
            customResponse.setMessage("已存在分片文件");
            return customResponse;
        }
        // 保存分片文件到指定目录
        chunk.transferTo(chunkFile);

        // 存储到OSS,建议分布式系统才使用OSS存储分片,单体系统本地存储分片效率更高
//        try {
//            boolean flag = ossUploadUtil.uploadChunk(chunk, chunkFileName);
//            if (!flag) {
//                log.warn("分片 " + chunkFileName + " 已存在");
//                customResponse.setCode(500);
//                customResponse.setMessage("已存在分片文件");
//            }
//        } catch (IOException ioe) {
//            log.error("读取分片文件数据流时出错了");
//        }

        // 返回成功响应
        return customResponse;
    }

3合并

    /**
     * 合并分片并将投稿信息写入数据库
     * @param vui 存放投稿信息的 VideoUploadInfo 对象
     */
    @Transactional
    public void mergeChunks(VideoUploadInfoDTO vui) throws IOException {
        String url; // 视频最终的URL

        // 合并到本地
//        // 获取分片文件的存储目录
//        File chunkDir = new File(CHUNK_DIRECTORY);
//        // 获取当前时间戳
//        long timestamp = System.currentTimeMillis();
//        // 构建最终文件名,将时间戳加到文件名开头
//        String finalFileName = timestamp + vui.getHash() + ".mp4";
//        // 构建最终文件的完整路径
//        String finalFilePath = Paths.get(VIDEO_DIRECTORY, finalFileName).toString();
//        // 创建最终文件
//        File finalFile = new File(finalFilePath);
//        // 获取所有对应分片文件
//        File[] chunkFiles = chunkDir.listFiles((dir, name) -> name.startsWith(vui.getHash() + "-"));
//        if (chunkFiles != null && chunkFiles.length > 0) {
//            // 使用流操作对文件名进行排序,防止出现先合并 10 再合并 2
//            List<File> sortedChunkFiles = Arrays.stream(chunkFiles)
//                    .sorted(Comparator.comparingInt(file -> Integer.parseInt(file.getName().split("-")[1])))
//                    .collect(Collectors.toList());
//            try {
                System.out.println("正在合并视频");
//                // 合并分片文件
//                for (File chunkFile : sortedChunkFiles) {
//                    byte[] chunkBytes = FileUtils.readFileToByteArray(chunkFile);
//                    FileUtils.writeByteArrayToFile(finalFile, chunkBytes, true);
//                    chunkFile.delete(); // 删除已合并的分片文件
//                }
                System.out.println("合并完成!");
//                // 获取绝对路径,仅限本地服务器
//                url = finalFile.getAbsolutePath();
                System.out.println(url);
//            } catch (IOException e) {
//                // 处理合并失败的情况 重新入队等
//                log.error("合并视频失败");
//                throw e;
//            }
//        } else {
//            // 没有找到分片文件 发通知用户投稿失败
//            log.error("未找到分片文件 " + vui.getHash());
//            return;
//        }

        // 合并到OSS,并返回URL地址
        url = ossUtil.appendUploadVideo(vui.getHash());
        if (url == null) {
            return;
        }

        // 存入数据库
        Date now = new Date();
        Video video = new Video(
                null,
                vui.getUid(),
                vui.getTitle(),
                vui.getType(),
                vui.getAuth(),
                vui.getDuration(),
                vui.getMcId(),
                vui.getScId(),
                vui.getTags(),
                vui.getDescr(),
                vui.getCoverUrl(),
                url,
                0,
                now,
                null
        );
        videoMapper.insert(video);
        VideoStats videoStats = new VideoStats(video.getVid(),0,0,0,0,0,0,0,0);
        videoStatsMapper.insert(videoStats);
        esUtil.addVideo(video);
        CompletableFuture.runAsync(() -> redisUtil.setExObjectValue("video:" + video.getVid(), video), taskExecutor);
        CompletableFuture.runAsync(() -> redisUtil.addMember("video_status:0", video.getVid()), taskExecutor);
        CompletableFuture.runAsync(() -> redisUtil.setExObjectValue("videoStats:" + video.getVid(), videoStats), taskExecutor);

        // 其他逻辑 (发送消息通知写库成功)
    }

4oss追加上传

    /**
     * 将本地的视频分片文件追加合并上传到OSS
     * @param hash  视频的hash值,用于检索对应分片
     * @return  视频在OSS的URL地址
     * @throws IOException
     */
    public String appendUploadVideo(@NonNull String hash) throws IOException {
        // 生成文件名
        String uuid = System.currentTimeMillis() + UUID.randomUUID().toString().replace("-", "");
        String fileName = uuid + ".mp4";
        // 完整路径名
        String date = new SimpleDateFormat("yyyy-MM-dd").format(new Date()).replace("-", "");
        String filePathName = date + "/video/" + fileName;
        ObjectMetadata meta = new ObjectMetadata();
        // 设置内容类型为MP4视频
        meta.setContentType("video/mp4");
        int chunkIndex = 0;
        long position = 0; // 追加位置
        while (true) {
            File chunkFile = new File(CHUNK_DIRECTORY + hash + "-" + chunkIndex);
            if (!chunkFile.exists()) {
                if (chunkIndex == 0) {
                    log.error("没找到任何相关分片文件");
                    return null;
                }
                break;
            }
            // 读取分片数据
            FileInputStream fis = new FileInputStream(chunkFile);
            byte[] buffer = new byte[(int) chunkFile.length()];
            fis.read(buffer);
            fis.close();
            // 追加上传分片数据
            try {
                AppendObjectRequest appendObjectRequest = new AppendObjectRequest(OSS_BUCKET, filePathName, new ByteArrayInputStream(buffer), meta);
                appendObjectRequest.setPosition(position);
                AppendObjectResult appendObjectResult = ossClient.appendObject(appendObjectRequest);
                position = appendObjectResult.getNextPosition();
            } catch (OSSException oe) {
                log.error("OSS出错了:" + oe.getErrorMessage());
                throw oe;
            } catch (ClientException ce) {
                log.error("OSS连接出错了:" + ce.getMessage());
                throw ce;
            }
            chunkFile.delete(); // 上传完后删除分片
            chunkIndex++;
        }
        return OSS_BUCKET_URL + filePathName;
    }

相关文章:

  • 【Linux】ELF、BIN、PBP、MAP文件查看
  • 基于SpringBoot的失物招领平台(源码+数据库)
  • 基于python大数据的商品数据可视化分析系统
  • 网红酒店|基于java+vue的网红酒店预定系统(源码+数据库+文档)
  • SpringCloud-Nacos
  • 【Sa-Token】学习笔记 01 - SaToken介绍快速上手
  • C语言水仙花续集2
  • 打车APP订单系统逻辑梳理与实现
  • Spring IOC实战:解密Bean的九种诞生方式
  • 消息队列篇--通信协议篇--理解HTTP、TLS和TCP如何协同工作
  • HarmonyOSNext_API16_媒体查询
  • [Python] 贪心算法简单版
  • MySQL-5.7.37安装配置(Windows)
  • 从Web到桌面:深入解析Electron的技术架构与应用实践
  • Node.js 路由 - 初识 Express 中的路由
  • Java面试黄金宝典21
  • 关于ESP系列MCU的UART download原理
  • C# 中 WebSocket 的详细讲解与实现
  • spring 常用注解区别及使用场景
  • vscode通过root远程连接wsl
  • 做动物网站的素材/百度一下网页版浏览器百度
  • 中山网站建设文化报价/友情链接的网站图片
  • 如何把自己做的网站/惠州网站建设
  • 广州网站开发公司哪家好/口碑优化
  • 做网站赚广告/关键词组合工具
  • 西安网站自然排名优化/企业网站搭建