yuupuu 发表于 2024-2-27 18:34

JavaScript+PHP实现视频文件分片上传源码

本帖最后由 yuupuu 于 2024-2-28 09:56 编辑

视频文件分片上传,整体思路是利用JavaScript将文件切片,然后循环调用上传接口 upload.php 将切片上传到服务器。
这样将由原来的一个大文件上传变为多个小文件同时上传,节省了上传时间,这就是文件分片上传的其中一个好处。



测试了一个100多M的视频,也能很快完成上传。



关键代码


index.html


<!DOCTYPE html>
<html lang="en">
        <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>视频文件分片上传</title>
                <style>
                  *{
                        padding: 0;
                        margin: 0;
                  }
                  body {
                        background: #eee;
                  }
                  .title {
                        text-align: center;
                        font-size: 25px;
                        margin-top: 50px;
                  }
                  .video_upload {
                        width: 500px;
                        height: 210px;
                        line-height: 60px;
                        background-color: #fff;
                        margin: 30px auto 0;
                        border: 2px dashed #ccc;
                        border-radius: 10px;
                        position: relative;
                        cursor: pointer;
                        text-align: center;
                        line-height: 200px;
                        font-size: 17px;
                  }
                  .upload_icon {
                        width: 30px;
                        height: 30px;
                        margin: 90px auto 0;
                        display: block;
                        opacity: 0.5;
                  }
                  #fileInput {
                        width: 100%;
                        height: 100%;
                        position: absolute;
                        left: 0;
                        top: 0;
                        opacity: 0;
                        cursor: pointer;
                  }
                  #uploadButton {
                        width: 500px;
                        height: 40px;
                        border: none;
                        outline: none;
                        border-radius: 5px;
                        font-size: 17px;
                        margin: 15px auto;
                        background: #39f;
                        color: #fff;
                        cursor: pointer;
                  }
                  .progress {
                        width: 500px;
                        margin: 15px auto;
                  }
                  .progress progress {
                        width: 500px;
                        height: 30px;
                  }
                  #ret {
                        text-align: center;
                        font-size: 16px;
                        margin-top: 20px;
                  }
                  #ret video {
                        width: 300px;
                  }
                  #retUrl {
                        text-align: center;
                        font-size: 16px;
                        margin-top: 20px;
                  }
                </style>
        </head>
        <body>
          
      <p class="title">JavaScript+PHP实现视频文件分片上传</p>
      <div class="video_upload">
            <img src="upload.png" class="upload_icon" />
            <input type="file" id="fileInput" accept="video/*">
      </div>
                <button id="uploadButton" style="display:none;">开始上传</button>
                <div class="progress"></div>
                <p id="ret"></p>
                <p id="retUrl"></p>

                <script>
               
                        // 定义全局变量
                        let videoFile = null;
                        let chunkSize = 1024 * 1024; // 1MB 分片大小
                       
                        // 当文件选择框的值改变时触发该函数
                        function handleFileSelect(event) {
                          const fileList = event.target.files;
                          if (fileList.length > 0) {
                                videoFile = fileList;
                                console.log("选择了文件: ", videoFile.name);
                                document.querySelector('.video_upload').innerHTML = videoFile.name;
                                document.querySelector('#uploadButton').style.display = 'block';
                          }
                        }
                       
                        // 分片并上传文件
                        async function uploadFile() {
                          
                if (!videoFile) {
                  console.error("请选择一个视频文件");
                  return;
                }
            
                const fileSize = videoFile.size;
                let start = 0;
                let end = Math.min(chunkSize, fileSize);
                let chunkIndex = 0;
                let totalChunks = Math.ceil(fileSize / chunkSize); // 总分片数
            
                // 获取文件名
                const fileName = videoFile.name;
            
                while (start < fileSize) {
                  const chunk = videoFile.slice(start, end); // 从文件中截取一个分片
            
                  // 使用FormData来构建multipart/form-data格式的请求体
                  const formData = new FormData();
                  formData.append('file', chunk);
                  formData.append('chunkIndex', chunkIndex);
                  formData.append('fileName', fileName); // 将文件名作为 formData 的一部分
            
                  try {
                        const response = await fetch('upload.php', {
                            method: 'POST',
                            body: formData
                        });
            
                        if (!response.ok) {
                            throw new Error('上传失败');
                        }
            
                        console.log('上传分片 ', chunkIndex, ' 成功');
            
                        // 计算并显示上传进度
                        let progress = Math.round(((chunkIndex + 1) / totalChunks) * 100);
                        document.querySelector('.video_upload').textContent = '上传进度 ' + progress + '%';
                        document.querySelector('#uploadButton').textContent = '正在上传...';
                        document.querySelector('.progress').innerHTML = '<progress value="' + progress + '" max="100"><span>' + progress + '</span>%</progress>';
                        console.log('上传进度:', progress + '%');
            
                  } catch (error) {
                        console.error('上传分片 ', chunkIndex, ' 失败: ', error.message);
                        return;
                  }
            
                  start = end;
                  end = Math.min(start + chunkSize, fileSize);
                  chunkIndex++;
                }
            
                console.log('文件上传完成');
            
                // 上传完成后发送通知给服务器进行合并
                notifyServerForMerge(fileName);
            }

                       
                        // 发送通知给服务器进行合并
                        async function notifyServerForMerge(fileName) {
                          try {
                                const response = await fetch('merge_chunks.php', {
                                    method: 'POST',
                                    headers: {
                                      'Content-Type': 'application/json'
                                    },
                                    body: JSON.stringify({ fileName: fileName })
                                });
                       
                                if (!response.ok) {
                                    throw new Error('无法通知服务器进行合并');
                                }
                                
                                const res_data = await response.json();
                       
                                console.log('已通知服务器进行合并');
                                document.querySelector('.video_upload').textContent = '上传完成!';
                                document.querySelector('#ret').innerHTML = '<video autoplay controls src="'+res_data.filePath+'"></video>';
                                document.querySelector('#retUrl').textContent = 'MP4视频直链:' + res_data.filePath;
                                document.querySelector('#uploadButton').style.display = 'none';
                          } catch (error) {
                                console.error('通知服务器进行合并时发生错误: ', error.message);
                          }
                        }
                       
                        // 注册文件选择框的change事件
                        document.getElementById('fileInput').addEventListener('change', handleFileSelect);
                       
                        // 注册上传按钮的click事件
                        document.getElementById('uploadButton').addEventListener('click', uploadFile);
                </script>

        </body>
</html>


upload.php


<?php

    // 设置允许跨域访问
    header("Access-Control-Allow-Origin: *");
    header("Access-Control-Allow-Methods: POST");
   
    // 检查是否接收到文件和分片索引
    if (isset($_FILES['file']['error']) && isset($_POST['chunkIndex']) && isset($_POST['fileName'])) {
      
      $error = $_FILES['file']['error'];
      $chunkIndex = $_POST['chunkIndex'];
      $fileName = $_POST['fileName']; // 获取文件名
      
      // 检查是否有错误
      if ($error !== UPLOAD_ERR_OK) {
            http_response_code(500);
            echo json_encode(array(
                'error' => '文件上传失败'
            ));
            exit();
      }
      
      // 检查分片是不是MP4
      if(pathinfo($fileName)['extension'] !== 'mp4') {
            
            http_response_code(400);
            echo json_encode(array(
                'error' => '文件类型不符合'
            ));
            exit();
      }
      
      // 设置存储目录和文件名
      $uploadDir = './uploads/';
      $filePath = $uploadDir . $fileName . '.' . $chunkIndex;
      
      // 将分片移动到指定的目录
      if (move_uploaded_file($_FILES['file']['tmp_name'], $filePath)) {
            
            echo json_encode(array(
                'success' => '分片上传成功'
            ));
      } else {
            
            http_response_code(500);
            echo json_encode(array(
                'error' => '分片上传失败'
            ));
      }
    } else {
      
      http_response_code(400);
      echo json_encode(array(
            'error' => '缺少文件、分片索引或文件名'
      ));
    }
   
?>



merge_chunks.php

<?php

    // 设置允许跨域访问
    header("Access-Control-Allow-Origin: *");
    header("Access-Control-Allow-Methods: POST");
    header("Content-Type: application/json");
   
    // 获取请求体中的文件名
    $data = json_decode(file_get_contents("php://input") , true);
    $fileName = isset($data['fileName']) ? $data['fileName'] : null;
    if ($fileName) {
      
      $uploadDir = './uploads/';
      $finalFilePath = $uploadDir . $fileName;
      $totalChunks = count(glob($uploadDir . $fileName . '.*'));
      
      // HTTP协议
      $protoCol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
      $videoUrl = $protoCol.'://'.$_SERVER['SERVER_NAME'].dirname($_SERVER["REQUEST_URI"]).'/uploads/'.$fileName;
      
      // 检查是否所有分片都已上传
      if ($totalChunks > 0) {
            
            // 所有分片都已上传,开始合并
            $finalFile = fopen($finalFilePath, 'wb');
            
            // 逐个读取分片并写入最终文件
            for ($i = 0; $i < $totalChunks; $i++) {
                $chunkFilePath = $uploadDir . $fileName . '.' . $i;
                $chunkFile = fopen($chunkFilePath, 'rb');
                stream_copy_to_stream($chunkFile, $finalFile);
                fclose($chunkFile);
                unlink($chunkFilePath); // 删除已合并的分片
               
            }
            
            fclose($finalFile);
            http_response_code(200);
            echo json_encode(array(
                'success' => '文件合并成功',
                'filePath' => $videoUrl
            ));
      } else {
            
            http_response_code(400);
            echo json_encode(array(
                'error' => '没有上传的分片'
            ));
      }
    } else {
      
      http_response_code(400);
      echo json_encode(array(
            'error' => '缺少文件名'
      ));
    }
?>


源码:https://likeyun.lanzout.com/ioDQ51pl8lkh

爱飞的猫 发表于 2024-2-28 02:04

进行了下简单的代码审计,可以发现合并/处理上传文件时并不会检查 `$finalFilePath` 文件的后缀名。

例如上传 `.php` 文件并访问上传后的地址,会导致服务器执行上传的 php 代码文件。

建议上传处理文件时,在后端(php 代码)对后缀名进行过滤,并拒绝含有非法字符(如 `/` 斜杠)的文件名。

Pwaerm 发表于 2024-2-27 18:38

好思路 学习了

柒呀柒 发表于 2024-2-27 19:00

学习学习

seawaycao 发表于 2024-2-27 19:02


学习学习,谢谢分享!

dingqh 发表于 2024-2-27 19:06

学习啥学习,拿来直接用。。。{:1_918:}

hjsen 发表于 2024-2-27 19:35

sxf_0328 发表于 2024-2-27 20:57

学习一下,谢谢分享

moruye 发表于 2024-2-27 21:03

meder 发表于 2024-2-27 21:04

感谢分享

xiajin 发表于 2024-2-27 21:20

支持一个,百度的 Webupload 也有同样的功能。
页: [1] 2 3
查看完整版本: JavaScript+PHP实现视频文件分片上传源码