前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >文件分片上传设计

文件分片上传设计

原创
作者头像
shigen
发布2023-11-08 00:07:01
4990
发布2023-11-08 00:07:01
举报
文章被收录于专栏:shigen的学习笔记

shigen日更文章的博客写手,擅长Java、python、vue、shell等编程语言和各种应用程序、脚本的开发。记录成长,分享认知,留住感动。

现在是接近凌晨了,突然有伙伴给我提到了文件分片上传的事情,我一想,这个我熟悉呀。因为在若干月前,我想亲手写了这部分的代码,还给自己整理出了飞书文档。对,一看文件,原来是遥远的2023年6月20日

shigen的飞书文档截图
shigen的飞书文档截图

其实说分片上传,原理很简单,就是前端分片、上传,后端的解析合并。其实半句话就可以讲清楚,但是代码实现起来要花很大的功夫。

今天的代码案例shigen选取的是node.js作为后端服务写的文件上传。

我们先来看一下实现的效果:

选择文件之后出现一批调用上传的接口
选择文件之后出现一批调用上传的接口
后端产生的文件夹
后端产生的文件夹
查看到的小姐姐视频
查看到的小姐姐视频
控制台输出的部分日志
控制台输出的部分日志

整体的传输效果很快,会在文件夹里存储分片,在所有的分片上传完毕之后,整合成一个文件。我可以直接的打开和预览。

那代码怎么设计的呢?这是个核心的问题。一起来和shigen看看吧。

代码设计

前端

文件名为index.html

代码语言:html
复制
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>file-upload</title>
</head>

<body>
    <input type="file" onchange="selFile(event)" />
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.4/axios.min.js"></script>

    <script>
        // default size: 0.5MB
        function createThunk(file, size = 1024 * 1024 * 0.5) {
            const res = [];
            let cur = 0
            while (cur < file.size) {
                res.push({
                    tempFile: file.slice(cur, cur + size),
                });
                cur += size;
            }
            return res;
        }

        function selFile(event) {
            const file = event.currentTarget.files[0];
            const fileList = createThunk(file);
            console.log(file);
            // console.log(fileList);
            // 发送请求, uuid作为文件名
            // const uuid = crypto.randomUUID(); // Uncaught TypeError: crypto.randomUUID is not a function
            const uuid = file.name;
            const uploadList = fileList.map((item, index) => {
                const formData = new FormData();
                // formData includes chunk,name, filename
                formData.append('chunk', item.tempFile);
                formData.append('name', uuid + "_" + index);
                formData.append('filename', uuid);
                return axios.post('/upload_file_thunk', formData);
            });
            // after all files are uploaded
            Promise.all(uploadList).then((res) => {
                console.log('upload success');
                axios.post('/upload_thunk_end', {
                    filename: uuid,
                    extname: file.name.split('.').slice(-1)[0],
                }).then((res) => {
                    console.log(res.data);
                });
            });
        }    
    </script>
</body>

</html>

前端部分的代码分析如下:

  1. 异步的网络请求-上传文件选取的是axios作为工具,很符合promise风格,写起来也丝滑友好;
  2. 采用了输入框的失去焦点事件,失去焦点即上传文件。文件根据规定的大小0.5MB分块,用UUID+文件分片序号作为新的文件标识,异步的调用分片上传文件的接口
  3. 当所有的分片上传完毕之后,调用合并文件的接口,实现文件的合并。

是不是顿时感觉so easy了。我们再来看看后端的代码。

后端

文件名为:app.js

代码语言:javascript
复制
const express = require('express');
const multiparty = require('multiparty');
const fs = require('fs');
const path = require('path');

const app = express();

app.use(express.json());
app.use('/', express.static('./public'));

app.post('/upload_file_thunk', (req, res) => {
    const form = new multiparty.Form();
    form.parse(req, (err, fields, files) => {
        if (err) {
            res.json({
                code: 0,
                data: {},
            });
        } else {
            // save chunk files
            console.log(fields);
            fs.mkdirSync('./public/uploads/thunk/' + fields['filename'][0], {
                recursive: true
            });
            // move
            console.log('files', files);
            fs.renameSync(files['chunk'][0].path, './public/uploads/thunk/' + fields['filename'][0] + '/' + fields['name'][0]);
            res.json({
                code: 1,
                data: '分片上传成功',
            });
        }
    });
});

/**
 * 文件合并
 * @param {*} sourceFiles 源文件
 * @param {*} targetFile  目标文件
 */
function thunkStreamMerge(sourceFiles, targetFile) {
    const thunkFilesDir = sourceFiles;
    const list = fs.readdirSync(thunkFilesDir); // 读取目录中的文件

    const fileList = list
        .sort((a, b) => a.split('_')[1] * 1 - b.split('_')[1] * 1)
        .map((name) => ({
            name,
            filePath: path.resolve(thunkFilesDir, name),
        }));
    const fileWriteStream = fs.createWriteStream(targetFile);
    thunkStreamMergeProgress(fileList, fileWriteStream, sourceFiles);
}

/**
 * 合并每一个切片
 * @param {*} fileList        文件数据
 * @param {*} fileWriteStream 最终的写入结果
 * @param {*} sourceFiles     文件路径
 */
function thunkStreamMergeProgress(fileList, fileWriteStream, sourceFiles) {
    if (!fileList.length) {
        // thunkStreamMergeProgress(fileList)
        fileWriteStream.end('完成了');
        // 删除临时目录
        // if (sourceFiles)
        //     fs.rmdirSync(sourceFiles, { recursive: true, force: true });
        return;
    }
    const data = fileList.shift(); // 取第一个数据
    const { filePath: chunkFilePath } = data;
    const currentReadStream = fs.createReadStream(chunkFilePath); // 读取文件
    // 把结果往最终的生成文件上进行拼接
    currentReadStream.pipe(fileWriteStream, { end: false });
    currentReadStream.on('end', () => {
        // console.log(chunkFilePath);
        // 拼接完之后进入下一次循环
        thunkStreamMergeProgress(fileList, fileWriteStream, sourceFiles);
    });
}

// 合并切片
app.post('/upload_thunk_end', (req, res) => {
    const fileName = req.body.filename;
    const extName = req.body.extname;
    const targetFile = './public/uploads/' + fileName + '.' + extName;
    thunkStreamMerge('./public/uploads/thunk/' + fileName, targetFile);
    res.json({
        code: 1,
        data: targetFile,
    });
});


function getLocalIP() {
    const os = require('os');
    //获取本机ip
    var interfaces = os.networkInterfaces();
    for (var devName in interfaces) {
        var iface = interfaces[devName];
        for (var i = 0; i < iface.length; i++) {
            var alias = iface[i];
            if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
                return alias.address;
            }
        }
    }
}


port = 9000;

const ip = getLocalIP();
console.log('ip', ip);
app.listen(port, () => console.log(`server running on ${getLocalIP()}:${port}......`));

这个代码就有点多了,115行,但是都是对应着后端的操作,并提供http服务。

shigen从分析每一个接口开始:

  1. /:主要是代理到public文件夹下,展示index.html,即我们上边的代码;
  2. upload_file_thunk:主要就是上传分片,并把分片从系统的某个空间转移到我们约定的目录之下
  3. upload_thunk_end: 主要就是合并我所有的分片了。它会调用我上边定义的方法,递归的拼接文件
  4. 最后的getLocalIP是我调用锡荣的工具类实现获得局域网下我的电脑IP地址,实现内网的相互访问和文件共享。岂不是很nice、smart!

那我启动起来就是一个命令即可:

代码语言:shell
复制
node app.js

浏览器访问输出的IP+端口即可。

后记

最近突然有了一种偏见,这些设计完全都是没用的。因为仙子云服务这么成熟的了,对象存储这么成熟了,谁还成天研究这些东西。我们以腾讯云的对象存储COS为例子,我们看看腾讯云COS操作文档

COS分片上传
COS分片上传

作为云服务提供厂商,它已经帮我们想好了遇到的各种情况,甚至把相应的API设计好了。我们再去想破头实现,显得是那么的无意义。因为在云时代,我们更关注的是效率的提升和业务的增长。作为云服务厂商,它给我们提供了广大的平台,我们只需要拿来即用即可。

也希望每个企业,无论是国企、还是小公司、外包,拥抱云时代,别再花心思自研一些虚无的东西。业务的增长才是硬实力。


以上就是今天分享的全部内容了,觉得不错的话,记得点赞 在看 关注支持一下哈,您的鼓励和支持将是shigen坚持日更的动力。同时,shigen在多个平台都有文章的同步,也可以同步的浏览和订阅:

平台

账号

链接

CSDN

shigen01

知乎

gen-2019

掘金

shigen01

腾讯云开发者社区

shigen

微信公众平台

shigen

公众号名:shigen

shigen一起,每天不一样!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 代码设计
    • 前端
      • 后端
      • 后记
      相关产品与服务
      对象存储
      对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档