首页 > 编程语言 >短视频源码,大文件切片上传的实现逻辑

短视频源码,大文件切片上传的实现逻辑

时间:2024-08-31 09:37:01浏览次数:4  
标签:文件 const 切片 源码 let file 上传

短视频源码,大文件切片上传的实现逻辑

逻辑梗概

将大文件分割成多个文件块
逐个上传文件块
服务端将文件块顺序合并成完整文件

优势分析

减轻服务器压力:如果一次性上传大文件,服务器的存储和网络带宽压力都会非常大,而通过切片,可以将这些压力分散到多个小文件中,减轻服务器的压力。
断点续传、错误重试:因为大文件被肢解了,如果因为一些原因中断、错误了,已经上传的部分就不用再重新上传了,只需要把后续的传上就好了。

前端部分

1.1 切文件(前端)
1.2 判定切片是否完成上传完成(前端)

客户端记录切片的上传状态,只需要上传未成功的切片

1.3 断点、错误续传(前端)

客户端上传文件时,记录已上传的切片位置
下次上传时,根据记录的位置,继续上传

后端部分
1.1 收切片、存切片

将相关切片保存在目标文件夹

1.2 合并切片

服务端根据切片的顺序,将切片合并成完整文件

1.3 文件是否存在校验

服务端根据文件Hash值、文件名,校验该文件是否已经上传

代码实现

1、搭建基础项目
服务器(基于express)

const express = require('express')
const app = express()
app.listen(3000, () => {
    console.log('服务已运行:http://localhost:3000');
})

 

前端

基础页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        input{
            display: block;
            margin: 10px 0;
        }
    </style>
</head>
<body>
    <input type="file" id="file">
    <input type="button" id="upload" value="上传">
    <input type="button" id="continue" value="继续上传">
</body>
</html>

 

引入资源

<script type="module" src="./spark-md5.js"></script>
<script type="module" src="./operate.js"></script>
operate.js

// 获取文件域
const fileEle = document.querySelector("#file");
const uploadButton = document.querySelector("#upload");
const continueButton = document.querySelector("#continue");
uploadButton.addEventListener("click", async () => {
    console.log("点击了上传按钮")
})
continueButton.addEventListener('click', async () => {
    console.log("点击了继续上传按钮")
})

 

3、静态资源托管(server)

app.use(express.static('static'))

 

4、上传接口
搭建上传接口(server)

使用body-parser中间价解析请求体

// 导入中间件
const bodyParser = require('body-parser')
// 使用中间件
// 处理URL编码格式的数据
app.use(bodyParser.urlencoded({ extended: false })); 
// 处理JSON格式的数据
app.use(bodyParser.json()); 

 

上传接口

app.post('/upload', (req, res) => {
    res.send({
        msg: '上传成功',
        success: true
    })
})

 

测试接口(前端)

// 单个文件上传
const uploadHandler = async (file) => {
    fetch('http://localhost:3000/upload', {
        method: "POST",
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            fileName: '大文件',
        }),
    })
}
uploadButton.addEventListener("click", async (e) => {
    uploadHandler()
})

 

5、文件上传接口存储文件(server)

使用multer中间件处理上传文件

设置uploadFiles文件夹为文件存储路径

const multer = require('multer')
const storage = multer.diskStorage({
    destination: function (req, file, cb) {
        cb(null, './uploadFiles');
    },
});
const upload = multer({
    storage
})

app.post('/upload', upload.single('file'), (req, res) => {
    
})

 

测试

// 单个文件上传
const uploadHandler = async (file) => {
    let fd = new FormData();
    fd.append('file', file);
    fetch('http://localhost:3000/upload', {
        method: "POST",
        body: fd
    })
}
uploadButton.addEventListener("click", async () => {
    let file = fileEle.files[0];
    uploadHandler(file)
})

 

6、文件切片

注意

假设切片大小为1M
保存切片顺序(为了合成大文件时正确性)
上传状态(为了断点续传、前端显示进度条)

// 使用单独常量保存预设切片大小 1MB
const chunkSize = 1024 * 1024 * 1; 
// 文件切片
const createChunks = (file) => {
    // 接受一个文件对象,要把这个文件对象切片,返回一个切片数组
    const chunks = [];
    // 文件大小.slice(开始位置,结束位置)
    let start = 0;
    let index = 0;
    while (start < file.size) {
        let curChunk = file.slice(start, start + chunkSize);
        chunks.push({
            file: curChunk,
            uploaded: false,
            chunkIndex: index,
        });
        index++;
        start += chunkSize;
    }
    return chunks;
}

 

测试文件切片函数

// 存储当前文件所有切片
let chunks = [];
uploadButton.addEventListener("click", async () => {
    let file = fileEle.files[0];
    chunks = createChunks(file);
    console.log(chunks);
})

 

注意:将来要把这些切片全部都上传到服务器,并且最后需要把这些切片合并成一个文件,且要做出文件秒传功能,需要保留当前文件的hash值和文件名,以辨别文件和合并文件。

在页面中引入spark-md5.js

<script type="module" src="./spark-md5.js"></script>

 

获取文件Hash值

const getHash = (file) => {
    return new Promise((resolve) => {
        const fileReader = new FileReader();
        fileReader.readAsArrayBuffer(file);
        fileReader.onload = function (e) {
            let fileMd5 = SparkMD5.ArrayBuffer.hash(e.target.result);
            resolve(fileMd5);
        }
    });
}

 

把文件的hash值保存在切片信息中

// 文件hash值
let fileHash = "";
// 文件名
let fileName = "";
// 创建切片数组
const createChunks = (file) => {
    // 接受一个文件对象,要把这个文件对象切片,返回一个切片数组
    const chunks = [];
    // 文件大小.slice(开始位置,结束位置)
    let start = 0;
    let index = 0;
    while (start < file.size) {
        let curChunk = file.slice(start, start + chunkSize);
        chunks.push({
            file: curChunk,
            uploaded: false,
            fileHash: fileHash,
            chunkIndex: index,
        });
        index++;
        start += chunkSize;
    }
    return chunks;
}
// 上传执行函数
const uploadFile = async(file) => {
    // 设置文件名
    fileName = file.name;
    // 获取文件hash值
    fileHash = await getHash(file);
    chunks = createChunks(file);
    console.log(chunks);
}

 

7、上传逻辑修改
前端部分

单个文件上传函数修改:

插入文件名、文件Hash值、切片索引
上传成功之后修改状态标识(可用于断点续传、上传进度回显)

// 单个文件上传
const uploadHandler = (chunk) => {
    return new Promise(async (resolve, reject) => {
        try {
            let fd = new FormData();
            fd.append('file', chunk.file);
            fd.append('fileHash', chunk.fileHash);
            fd.append('chunkIndex', chunk.chunkIndex);
            let result = await fetch('http://localhost:3000/upload', {
                method: 'POST',
                body: fd
            }).then(res => res.json());
            chunk.uploaded = true;
            resolve(result)
        } catch (err) {
            reject(err)
        }
    })
}

 

批量上传切片

限制并发数量(减轻服务器压力)

// 批量上传切片
const uploadChunks = (chunks, maxRequest = 6) => {
    return new Promise((resolve, reject) => {
        if (chunks.length == 0) {
            resolve([]);
        }
        let requestSliceArr = []
        let start = 0;
        while (start < chunks.length) {
            requestSliceArr.push(chunks.slice(start, start + maxRequest))
            start += maxRequest;
        }
        let index = 0;
        let requestReaults = [];
        let requestErrReaults = [];

        const request = async () => {
            if (index > requestSliceArr.length - 1) {
                resolve(requestReaults)
                return;
            }
            let sliceChunks = requestSliceArr[index];
            Promise.all(
                sliceChunks.map(chunk => uploadHandler(chunk))
            ).then((res) => {
                requestReaults.push(...(Array.isArray(res) ? res : []))
                index++;
                request()
            }).catch((err) => {
                requestErrReaults.push(...(Array.isArray(err) ? err : []))
                reject(requestErrReaults)
            })
        }
        request()
    })
}

 

抽离上传操作

// 文件上传
const uploadFile = async (file) => {
    // 设置文件名
    fileName = file.name;
    // 获取文件hash值
    fileHash = await getHash(file);
    // 获取切片
    chunks = createChunks(file);
    try {
        await uploadChunks(chunks)
    } catch (err) {
        return {
            mag: "文件上传错误",
            success: false
        }
    }
}

 

后端部分

修改上传接口,增加功能

使用一个文件Hash值同名的文件夹保存所有切片
这里使用了node内置模块path处理路径
使用fs-extra第三方模块处理文件操作

const path = require('path')
const fse = require('fs-extra')
app.post('/upload', upload.single('file'), (req, res) => {
    const { fileHash, chunkIndex } = req.body;
    // 上传文件临时目录文件夹
    let tempFileDir = path.resolve('uploadFiles', fileHash);
    // 如果当前文件的临时文件夹不存在,则创建该文件夹
    if (!fse.pathExistsSync(tempFileDir)) {
        fse.mkdirSync(tempFileDir)
    }
    // 如果无临时文件夹或不存在该切片,则将用户上传的切片移到临时文件夹里
    // 如果有临时文件夹并存在该切片,则删除用户上传的切片(因为用不到了)
    // 目标切片位置
    const tempChunkPath = path.resolve(tempFileDir, chunkIndex);
    // 当前切片位置(multer默认保存的位置)
    let currentChunkPath = path.resolve(req.file.path);
    if (!fse.existsSync(tempChunkPath)) {
        fse.moveSync(currentChunkPath, tempChunkPath)
    } else {
        fse.removeSync(currentChunkPath)
    }
    res.send({
        msg: '上传成功',
        success: true
    })
})

 

8、合并文件
编写合并接口(server)

合并成的文件名为 文件哈希值.文件扩展名

所以需要传入文件Hash值、文件名

app.get('/merge', async (req, res) => {
    const { fileHash, fileName } = req.query;
    res.send({
        msg: `Hash:${fileHash},文件名:${fileName}`,
        success: true
    });
})

 

请求合并接口(前端)

封装合并请求函数

// 合并分片请求
const mergeRequest = (fileHash, fileName) => {
    return fetch(`http://localhost:3000/merge?fileHash=${fileHash}&fileName=${fileName}`, {
        method: "GET",
    }).then(res => res.json());
};

 

在切片上传完成后,调用合并接口

// 文件上传
const uploadFile = async (file) => {
    // 设置文件名
    fileName = file.name;
    // 获取文件hash值
    fileHash = await getHash(file);
    // 获取切片
    chunks = createChunks(file);
    try {
        await uploadChunks(chunks)
        await mergeRequest(fileHash, fileName)
    } catch (err) {
        return {
            mag: "文件上传错误",
            success: false
        }
    }
}

 

合并接口逻辑

1、根据文件Hash值,找到所有切片

app.get('/merge', async (req, res) => {
    const { fileHash, fileName } = req.query;
    // 最终合并的文件路径
    const filePath = path.resolve('uploadFiles', fileHash + path.extname(fileName));
    // 临时文件夹路径
    let tempFileDir = path.resolve('uploadFiles', fileHash);
    // 读取临时文件夹,获取所有切片
    const chunkPaths = fse.readdirSync(tempFileDir);
    console.log('chunkPaths:', chunkPaths);
    res.send({
        msg: "合并成功",
        success: true
    });
})

 

合并接口逻辑

2、遍历获取所有切片路径数组,根据路径找到切片,合并成一个文件,删除原有文件夹

app.get('/merge', async (req, res) => {
    const { fileHash, fileName } = req.query;
    // 最终合并的文件路径
    const filePath = path.resolve('uploadFiles', fileHash + path.extname(fileName));
    // 临时文件夹路径
    let tempFileDir = path.resolve('uploadFiles', fileHash);

    // 读取临时文件夹,获取所有切片
    const chunkPaths = fse.readdirSync(tempFileDir);

    console.log('chunkPaths:', chunkPaths);

    // 将切片追加到文件中
    let mergeTasks = [];
    for (let index = 0; index < chunkPaths.length; index++) {
        mergeTasks.push(new Promise((resolve) => {
            // 当前遍历的切片路径
            const chunkPath = path.resolve(tempFileDir, index + '');
            // 将当前遍历的切片切片追加到文件中
            fse.appendFileSync(filePath, fse.readFileSync(chunkPath));
            // 删除当前遍历的切片
            fse.unlinkSync(chunkPath);
            resolve();
        }))
    }
    await Promise.all(mergeTasks);
    // 等待所有切片追加到文件后,删除临时文件夹
    fse.removeSync(tempFileDir);
    res.send({
        msg: "合并成功",
        success: true
    });
})

 

10、断点续传

封装continueUpload方法

在continueUpload方法中,只上传 uploaded 为false的切片
修改后此功能对用户来说即是黑盒,用户只需要重复调用continueUpload方法即可

// 文件上传
const continueUpload = async (file) => {
    if(chunks.length == 0 || !fileHash || !fileName){
        return;
    }
    try {
        await uploadChunks(chunks.filter(chunk => !chunk.uploaded))
        await mergeRequest(fileHash, fileName)
    } catch (err) {
        return {
            mag: "文件上传错误",
            success: false
        }
    }
}

 

以上就是短视频源码,大文件切片上传的实现逻辑, 更多内容欢迎关注之后的文章

标签:文件,const,切片,源码,let,file,上传
From: https://www.cnblogs.com/yunbaomengnan/p/18389885

相关文章