php大文件切片/分片上传思路+代码[支持断点续传]

 大概思路:[序号1、2需要同步执行]

  1. 创建上传任务 [存储一些文件的重要信息, 如果之前有存储 就判断是否完成或者需要断点续传]
  2. 开始上传. [取出第一步存储的文件信息,进行单片上传->上传完成进行合并->返回上传成功的url路径.]

1.创建上传任务

/**
     * 描述:创建上传任务
     * 函数名: createUploadTask
     * 创建人:wenhao
     * 创建时间:2020/11/19 下午3:21
     * @param Request $request
     * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response
     * @throws CustomException
     * @throws \App\Exceptions\IncorrectParamsException
     */
    public function createUploadTask(Request $request)
    {
        // 文件md5
        $md5 = $request->post('md5', "");
        //文件名(不带后缀)
        $fileName = $request->post('fileName', "");
        //文件后缀
        $ext = $request->post('ext', "");
        //文件夹id(缺省或为空这默认去主文件夹)
        $folderId   = $request->post('folderId', "");                    //文件mime类型
        $mimeType   = $request->post('mimeType', "");                   //文件大小(字节)
        $fileSize   = $request->post('fileSize', 0);                    //上传类型
        $uploadType = $request->post('uploadType', "");
        //视频缩略图
        $vodCover = (string)$request->post('vodCover', "");
        //视频时长。
        $vodDuration = (int)$request->post('vodDuration', 0);

        if (empty($md5) || empty($fileName) || empty($ext) || empty($mimeType) || empty($fileSize) || empty($uploadType)) {
            throw new IncorrectParamsException();
        }

        $result = UploadService::createUploadTask(self::$userId, $md5, $fileName, $ext, $uploadType, $mimeType, $fileSize, $folderId, $vodCover, $vodDuration);
        return $this->success(UploadResources::make($result));
    }
/**
     * 描述:创建上传任务服务.
     * 函数名: createUploadTask
     * 创建人:wenhao
     * 创建时间:2020/11/18 下午3:37
     * @param int $userId 用户id
     * @param string $fileMd5 文件md5
     * @param string $fileName 文件名字
     * @param string $ext 文件后缀
     * @param string $mimeType 文件mime 类型
     * //     * @param int $chunkSize 分片大小(字节)
     * //     * @param int $chunkNum 分片个数
     * @param int $fileSize 文件大小(字节)
     * @param string $folderId 文件夹id(缺省或为空这默认去主文件夹)
     * @param string $vodCover 视频缩略图
     * @param string $vodDuration 视频时长
     * @return array
     * @throws CustomException
     * @throws IncorrectParamsException
     */
    public static function createUploadTask(int $userId, string $fileMd5, string $fileName, string $ext, string $uploadType, string $mimeType, int $fileSize, string $folderId, string $vodCover, int $vodDuration): array
    {
        /**************参数处理区域****************/
        //检测上传类型是否存在
        if (!in_array($uploadType, self::$uploadType) || empty($fileMd5)) {
            throw new IncorrectParamsException();
        }
        //检测用户是否有权限上传[容量是否足够等]
        switch ($uploadType) {
            //如果是网盘文件
            case "panFile":
                $canUseSpace = UserService::getUserCanUseSpace($userId);
                if ($canUseSpace < $fileSize) {
                    throw new CustomException(405024);
                }
                break;
        }
        //传入文件夹为空值,则获取主目录.
        if (empty($folderId)) {
            $folderId = FolderService::getHomeFolderIdByUser();
        }
        $chunkSize = 5242880; //分片大小为5M
        //分片个数 分片大小 由服务端控制.
        $chunkNum = ceil($fileSize / $chunkSize);
        /**************参数处理区域结束****************/


        //定义返回值:
        //status=1的时候用到的字段有fileMd5
        //status=2的时候用到的字段有fileMd5、notUploadChunks
        //status=3的时候用到的字段有fileMd5、fileUrl
        //status=4的时候用到的字段有fileMd5
        $returnResult = [
            'status'          => 0,  //状态:1=文件任务创建完成状态 2=文件任务可以断点续传状态 3=文件整体上传完成 此状态返回可供访问的文件url 4=文件切片上传完成
            'fileMd5'         => $fileMd5, //文件的唯一标识[md5]
            'notUploadChunks' => [], //未上传的分片 片数
            'fileUrl'         => '', //文件的url
            'chunkSize'       => $chunkSize, //分片大小
            'chunkNum'        => $chunkNum, //分片个数
            'folderId'        => $folderId, //文件夹id
            'uploadType'      => $uploadType //文件夹id
        ];

        //检查前端是否是小文件上传.
        if ($chunkNum == 1) {
            $uploadStatus  = Pan::SMALL_FILE_TASK_CREATEOK_STATUS;
            $saveCacheData = [];
            //定义缓存存储信息
            $saveCacheData['uploadTaskInfo'] = json_encode([
                'file_name'     => $fileName,
                'folder_id'     => $folderId,
                'user_id'       => $userId,
                'ext'           => $ext,
                'md5'           => $fileMd5,
                'size'          => $fileSize,
                'type'          => self::getFileTypeByExt($ext),
                'mime'          => $mimeType,
                'upload_type'   => $uploadType,
                'vod_cover'     => $vodCover,
                'vod_duration'  => $vodDuration,
                'upload_status' => $uploadStatus
            ]);
            Upload::hashSetUploadTask($saveCacheData, $userId, $fileMd5);
            //如果文件大小小于切片大小 那么就引导前端走小文件上传接口.
            $returnResult['status'] = $uploadStatus;
            return $returnResult;
        }


        /**************检查是否可以断点续传和上传完毕待合并区域****************/
        //获取任务信息.
        $uploadTaskHashInfo = Upload::hashGetUoploadTask($userId, $fileMd5);
        $uploadTaskInfo     = $uploadTaskHashInfo['uploadTaskInfo'] ?? [];
        $chunkUploadInfo    = $uploadTaskHashInfo['chunkUploadInfo'] ?? [];
        //获取已经分好的片段数量
        $chunkOkCount = count($chunkUploadInfo);

        if (!empty($uploadTaskInfo) && $chunkOkCount > 0 && $uploadTaskInfo['chunk_num'] != $chunkOkCount) {
            //可以断点续传

            //计算出1-N的数字到数组中
            $completeChunkNum = [];
            for ($i = 1; $i <= $uploadTaskInfo['chunk_num']; $i++) {
                array_unshift($completeChunkNum, $i);
            }
            //已经完成的切片
            $alreadyOk = [];
            foreach ($chunkUploadInfo as $k => $v) {
                array_unshift($alreadyOk, intval($v));
            }
            //未完成的切片[取差集]
            $noOk = array_diff($completeChunkNum, $alreadyOk);

            $returnResult['status']          = Pan::FILE_TASK_DDXC_STATUS;
            $returnResult['notUploadChunks'] = $noOk;
            return $returnResult;
        } else if (!empty($uploadTaskInfo) && $uploadTaskInfo['chunk_num'] == $chunkOkCount) {
            //全部上传完成 待合并
            $url = self::chunkUploadFinishReport($userId, $uploadTaskInfo['md5'], $uploadType);

            $returnResult['status']  = Pan::FILE_TASK_UPLOAD_OK;
            $returnResult['fileUrl'] = $url;
            return $returnResult;
        }
        /**************检查是否可以断点续传和上传完毕待合并区域结束****************/


        if (empty($uploadTaskInfo)) {
            $kkyResult = KkyOssService::createMultipartUpload($ext, $mimeType);

            $uploadStatus = Pan::FILE_TASK_CREATEOK_STATUS;
            $info         = pathinfo($kkyResult['key']);

            $saveCacheData = [];
            //定义缓存存储信息
            $saveCacheData['uploadTaskInfo'] = json_encode([
                'file_name'     => $fileName,
                'folder_id'     => $folderId,
                'user_id'       => $userId,
                'ext'           => $ext,
                'md5'           => $fileMd5,
                'size'          => $fileSize,
                'type'          => self::getFileTypeByExt($ext),
                'mime'          => $mimeType,
                'upload_id'     => $kkyResult['uploadId'],
                'chunk_num'     => $chunkNum,
                'chunk_size'    => $chunkSize,
                'store_name'    => $info['filename'],
                'store_path'    => $info['dirname'] . '/',
                'upload_type'   => $uploadType,
                'vod_cover'     => $vodCover,
                'vod_duration'  => $vodDuration,
                'upload_status' => $uploadStatus
            ]);


            Upload::hashSetUploadTask($saveCacheData, $userId, $fileMd5);
        }


        //定义返回值
        $returnResult['status'] = Pan::FILE_TASK_CREATEOK_STATUS;
        return $returnResult;

    }
public static function hashSetUploadTask(array $saveData,int $userId,string $fileMd5)
    {
        $redisKey = sprintf(RedisKey::KEY_UPLOAD_TASK_USER_FILEMD5, $userId, $fileMd5);
        $result = RedisService::hmset($redisKey,$saveData);
        RedisService::expire($redisKey, 36000);
        return $result;
    }

2.文件上传[需要完成第一步的创建上传任务.] 

/**
     * 描述:文件上传[切片上传|小文件上传]
     * 函数名: upload
     * 创建人:wenhao
     * 创建时间:2020/11/19 下午3:21
     * @param Request $request
     * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response
     * @throws CustomException
     * @throws ResoursesNotExistException
     * @throws \App\Exceptions\ServerErrorException
     * @throws \App\Exceptions\InvalidParamsException
     */
    public function upload(Request $request)
    {
        //文件的md5
        $fileMd5 = $request->post('fileMd5');
        //1.先检查该文件是否有创建其上传任务;
        //获取上传任务hash缓存.
        $uploadTaskHashInfo = Upload::hashGetUoploadTask(self::$userId, $fileMd5);
        //上传任务信息
        $uploadTaskInfo = $uploadTaskHashInfo['uploadTaskInfo'] ?? [];
        if (empty($uploadTaskInfo)) {
            throw new CustomException('405023');
        }
        //上传的状态 从创建上传任务接口拿
        $uploadStatus = (int)$uploadTaskInfo['upload_status'];
        //文件夹id
        $folderId = $uploadTaskInfo['folder_id'];
        //第几个分片
        $index      = (int)$request->post('index', 1);
        $uploadFile = $request->file('file');
        //上传类型
        $uploadType = $uploadTaskInfo['upload_type'];
        //视频缩略图
        $vodCover = $uploadTaskInfo['vod_cover'] ?? '';
        //视频时长。
        $vodDuration = intval($uploadTaskInfo['vod_duration'] ?? 0);

        if (empty($uploadStatus) || empty($index) || empty($uploadType) || empty($uploadFile)) {
            throw new IncorrectParamsException();
        }
        switch ($uploadStatus) {
            case 5:
                //小文件上传.
                $result = UploadService::smallFileUpload(self::$userId, $folderId, $uploadType, $uploadFile, $vodCover, $vodDuration,$fileMd5);
                break;
            default:
                $result = UploadService::chunkUpload(self::$userId, $fileMd5, $index, $uploadType, $uploadFile);
        }
        return $this->success(UploadResources::make($result));
    }

 

/**
     * 描述:分片上传服务
     * 函数名: chunkUpload
     * 创建人:wenhao
     * 创建时间:2020/11/18 下午3:46
     * @param int $userId 用户id
     * @param string $fileMd5 文件md5
     * @param int $index 当前上传到第几片
     * @param string $uploadType 上传类型
     * @param $file
     * @return mixed
     * @throws CustomException
     * @throws ResoursesNotExistException
     * @throws \App\Exceptions\ServerErrorException
     * @throws InvalidParamsException
     */
    public static function chunkUpload(int $userId, string $fileMd5, int $index, string $uploadType, $file)
    {
        //获取上传任务hash缓存.
        $uploadTaskHashInfo = Upload::hashGetUoploadTask($userId, $fileMd5);
        //上传任务信息
        $uploadTaskInfo = $uploadTaskHashInfo['uploadTaskInfo'] ?? [];
        //切片信息
        $chunkUploadInfo = $uploadTaskHashInfo['chunkUploadInfo'] ?? [];
        if (empty($uploadTaskInfo)) {
            throw new ResoursesNotExistException();
        }
        //检测$index是否大于总切片数量
        if ($index > $uploadTaskInfo['chunk_num']) {
            throw new InvalidParamsException();
        }

        //定义该服务的统一返回格式
        $returnResult = [
            'status'  => 0, //状态:1=文件任务创建完成状态 2=文件任务可以断点续传状态 3=文件整体上传完成 此状态返回可供访问的文件url 4=文件切片上传完成
            'fileUrl' => '',
        ];

        //获取已经分好的片段数量
        $chunkOkCount = count($chunkUploadInfo);

        //上传前检测切片是否可以合并
        if ($chunkOkCount == $uploadTaskInfo['chunk_num']) {
            $url = self::chunkUploadFinishReport($userId, $fileMd5, $uploadType);

            $returnResult['status']  = Pan::FILE_TASK_UPLOAD_OK;
            $returnResult['fileUrl'] = $url;
            return $returnResult;
        }


        $path  = $file->store('files');
        $chunk = storage_path('app') . '/' . $path;


        $key      = $uploadTaskInfo['store_path'] . $uploadTaskInfo['store_name'] . '.' . $uploadTaskInfo['ext'];
        $uploadId = $uploadTaskInfo['upload_id'];

        // 上传分片
        $result = KkyOssService::uploadChunk($chunk, $key, $uploadId, $index);


        $chunkUploadData = [];
        if ($chunkOkCount > 0) {
            //如果有切片信息.那么就修改
            $chunkUploadInfo[$result['etag']]   = $index;
            $chunkUploadData['chunkUploadInfo'] = json_encode($chunkUploadInfo);
        } else {
            $chunkUploadData['chunkUploadInfo'] = json_encode([$result['etag'] => $index]);
        }

        Upload::hashSetUploadTask($chunkUploadData, $userId, $fileMd5);
        // 删除临时分片
        Storage::delete($path);


        //检测如果分片全部分好 就去提交合并
        if ($chunkOkCount + 1 == $uploadTaskInfo['chunk_num']) {
            $url = self::chunkUploadFinishReport($userId, $fileMd5, $uploadType);

            $returnResult['status']  = Pan::FILE_TASK_UPLOAD_OK;
            $returnResult['fileUrl'] = $url;
            return $returnResult;
        }


        $returnResult['status'] = Pan::FILE_TASK_CHUNK_UPLOAD_OK;
        return $returnResult;
    }
/**
     * 描述:获取上传任务信息hash key值
     * 此key有两个键: 分别是 uploadTaskInfo 和 chunkUploadInfo
     * 函数名: hashGetUoploadTask
     * 创建人:wenhao
     * 创建时间:2020/11/23 上午10:58
     * @param int $userId
     * @param string $fileMd5
     * @return mixed
     */
    public static function hashGetUoploadTask(int $userId,string $fileMd5)
    {
        $redisKey = sprintf(RedisKey::KEY_UPLOAD_TASK_USER_FILEMD5, $userId, $fileMd5);
        $result = RedisService::hgetAll($redisKey);
        if(!empty($result['uploadTaskInfo'])){
            $result['uploadTaskInfo'] = json_decode($result['uploadTaskInfo'],true);
        }
        if(!empty($result['chunkUploadInfo'])){
            $result['chunkUploadInfo'] = json_decode($result['chunkUploadInfo'],true);
        }
        return $result;
    }

 

/**
     * 描述:分片完成上报合并服务
     * 函数名: chunkUploadFinishReport
     * 创建人:wenhao
     * 创建时间:2020/11/18 下午3:46
     * @param int $userId 用户id
     * @param string $fileMd5 文件md5
     * @param string $uploadType 上传类型
     * @return mixed
     * @throws CustomException
     * @throws ResoursesNotExistException
     * @throws \App\Exceptions\ServerErrorException
     */
    private static function chunkUploadFinishReport(int $userId, string $fileMd5, string $uploadType)
    {
        //获取上传任务hash缓存.
        $uploadTaskHashInfo = Upload::hashGetUoploadTask($userId, $fileMd5);
        //上传任务信息
        $uploadTaskInfo = $uploadTaskHashInfo['uploadTaskInfo'] ?? [];
        //切片信息
        $chunkUploadInfo = $uploadTaskHashInfo['chunkUploadInfo'] ?? [];
        if (empty($uploadTaskInfo)) {
            throw new ResoursesNotExistException();
        }
        //获取分段片段组成数组;
        $etags = [];
        foreach ($chunkUploadInfo as $k => $v) {
            $etags[intval($v)] = $k;
        }
        // 片段合并
        if ($etags && count($etags) > 0) {
            $result = KkyOssService::completeMultipartUpload($uploadTaskInfo['store_path'] . $uploadTaskInfo['store_name'] . '.' . $uploadTaskInfo['ext'], $uploadTaskInfo['upload_id'], $etags);
        } else {
            throw new CustomException('600001', '没有可合并的片段');
        }

        switch ($uploadType) {
            //如果是网盘文件就要进行存储
            case "panFile":
                //获取文件详情
                $fileInfo = OssService::getFileInfo($result['key']);
                //合并完成之后添加文件.
                //重新组合file 数据库的添加数据
                $saveFileData = [
                    'file_name'    => $uploadTaskInfo['file_name'],
                    'folder_id'    => $uploadTaskInfo['folder_id'],
                    'user_id'      => $uploadTaskInfo['user_id'],
                    'ext'          => $uploadTaskInfo['ext'],
                    'md5'          => $uploadTaskInfo['md5'],
                    'size'         => $fileInfo['file_size'],
                    'type'         => $uploadTaskInfo['type'],
                    'mime'         => $fileInfo['contentType'],
                    'store_name'   => $uploadTaskInfo['store_name'],
                    'store_path'   => $uploadTaskInfo['store_path'],
                    //....此处写你自己的存储数据
                    'status'       => 1,
                ];
                $saveFileId   = (new File)->insertData($saveFileData);
                if (!$saveFileId) {
                    throw new CustomException(600001, '文件上传失败');
                }
                break;
        }


        //删除上传任务缓存
        Upload::delUploadTask($userId, $fileMd5);

        return $result['key'];
    }

 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
好的,以下是一个使用 Node.js 实现大文件切片上传断点续传的功能示例: ```javascript const http = require('http'); const fs = require('fs'); const path = require('path'); const PORT = 8000; const UPLOAD_DIR = path.resolve(__dirname, './uploads'); // 创建上传目录 if (!fs.existsSync(UPLOAD_DIR)) { fs.mkdirSync(UPLOAD_DIR); } // 上传文件处理函数 const handleUpload = (req, res) => { const { filename, chunkIndex, totalChunks } = req.headers; const chunkDir = path.resolve(UPLOAD_DIR, filename); // 如果是第一个分片,创建文件夹 if (chunkIndex === '0') { fs.mkdirSync(chunkDir); } // 获取上传分片数据 const chunksData = []; req.on('data', (chunk) => { chunksData.push(chunk); }); req.on('end', () => { const buffer = Buffer.concat(chunksData); // 写入分片文件 fs.writeFileSync(path.resolve(chunkDir, chunkIndex), buffer); // 如果当前分片是最后一个分片,则合并文件 if (Number(chunkIndex) === Number(totalChunks) - 1) { const filePath = path.resolve(UPLOAD_DIR, filename); const chunks = fs.readdirSync(chunkDir); const writeStream = fs.createWriteStream(filePath); chunks.forEach((chunk) => { const chunkPath = path.resolve(chunkDir, chunk); const chunkBuffer = fs.readFileSync(chunkPath); fs.unlinkSync(chunkPath); // 删除分片文件 writeStream.write(chunkBuffer); }); writeStream.end(() => { res.end('upload success'); }); } else { res.end('chunk upload success'); } }); }; // 断点续传处理函数 const handleResumeUpload = (req, res) => { const { filename } = req.headers; const filePath = path.resolve(UPLOAD_DIR, filename); const fileStat = fs.statSync(filePath); res.setHeader('Content-Length', fileStat.size); res.setHeader('Content-Type', 'application/octet-stream'); res.setHeader('Accept-Ranges', 'bytes'); const range = req.headers.range || 'bytes=0-'; const positions = range.replace(/bytes=/, '').split('-'); const start = parseInt(positions[0], 10); const end = positions[1] ? parseInt(positions[1], 10) : fileStat.size - 1; const chunkSize = end - start + 1; res.setHeader('Content-Range', `bytes ${start}-${end}/${fileStat.size}`); res.setHeader('Cache-Control', 'no-cache'); const readStream = fs.createReadStream(filePath, { start, end }); readStream.on('open', () => { readStream.pipe(res); }); readStream.on('error', () => { res.end('Error'); }); }; // 创建 HTTP 服务器 const server = http.createServer((req, res) => { if (req.url === '/upload' && req.method === 'POST') { handleUpload(req, res); } else if (req.url === '/resume-upload' && req.method === 'GET') { handleResumeUpload(req, res); } else { res.end('Hello World!'); } }); // 启动服务器 server.listen(PORT, () => { console.log(`Server is listening on port ${PORT}`); }); ``` 使用示例: 1. 开启服务器:`node server.js` 2. 上传文件:使用 POST 请求发送文件分片(每个分片的大小可以自定义),请求头需要包含 `filename`(文件名)、`chunkIndex`(当前分片索引,从 0 开始)、`totalChunks`(总分片数)三个字段。 3. 断点续传:使用 GET 请求获取已上传文件,请求头需要包含 `filename`(文件名)字段和 `range`(请求的字节范围)字段。如果请求头中没有 `range` 字段,则返回整个文件的内容。如果请求头中有 `range` 字段,则只返回指定字节范围内的文件内容。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值