0
点赞
收藏
分享

微信扫一扫

Express(一):文件上传 - FormData、Base64、断点续传(文件切片)

后来的六六 2022-01-30 阅读 27

准备

安装及运行

$ npx express-generator --view=ejs

$ npm install

$ DEBUG=upload-app:* npm start 或者 pm2 start bin/www

$ npm install multiparty

$ npm i spark-md5    

$ npm install mime-types

客户端Layout

<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="UTF-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title><%= title %></title>

	<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
	<link rel="stylesheet" href="/css/style.css">

	<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
	<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
	<script src="https://unpkg.com/element-ui/lib/index.js"></script>
	<script src="/js/spark-md5.min.js"></script>
</head>

<body>
	<div class="container">
		<section class="item"><%- include('upload_01'); -%></section>
		<section class="item"><%- include('upload_02'); -%></section>
		<section class="item"><%- include('upload_03'); -%></section>
	</div>
</body>

</html>

多文件上传 【FORM-DATA】

客户端

<div id="upload_01">
    <h3>多文件上传 【FORM-DATA】</h3>
    <el-row :gutter="10" type="flex" class="content">
        <el-col :span="12">
            <el-form :model="form" label-position="top">
                <el-form-item label="是否允许服务器自动操作上传:">
                    <el-switch v-model="form.auto" :active-value="1" :inactive-value="0"></el-switch>
                </el-form-item>
                <el-form-item label="文件上传:">
                    <el-upload ref="upload" action="#" :on-change="handleFileChange" :auto-upload="false"
                        :show-file-list="false" multiple>
                        <el-button slot="trigger" size="mini" type="primary">选择文件</el-button>
                        <el-button size="mini" type="primary" @click="uploadFile" :disabled="fileList.length === 0">开始上传
                        </el-button>
                    </el-upload>
                </el-form-item>
                <el-form-item label="注:">
                    <span>pm2 start xxx --watch (--watch下上传大文件会报错,可去掉--watch解决)</span>
                </el-form-item>
                <el-form-item label="进度:">
                    <el-progress :text-inside="true" :stroke-width="24" :percentage="value" status="success">
                    </el-progress>
                </el-form-item>
            </el-form>
        </el-col>
        <el-col :span="12">
            <div class="request-res">
                <pre>{{result}}</pre>
            </div>
        </el-col>
    </el-row>
</div>

<script>
    new Vue({
        el: '#upload_01',
        data: {
            form: {
                auto: 1,
            },
            fileList: [],
            value: 0,
            result: null,
        },
        methods: {
            handleFileChange(file, fileList) {
                this.fileList = fileList;
            },
            uploadFile() {
                let formData = new FormData();
                this.fileList.forEach(info => formData.append('file', info.raw));
                formData.append('fileNum', this.fileList.length);
                axios({
                    url: '/upload_01',
                    method: 'post',
                    data: formData,
                    params: this.form,
                    onUploadProgress: (pe) => {
                        if (pe.lengthComputable) this.value = Number((pe.loaded / pe.total * 100).toFixed(2));
                    }
                }).then(res => {
                    this.result = res.data;
                }).catch(err => {
                    this.result = { err };
                }).finally(_ => {
                    this.$refs['upload'].clearFiles();
                    this.fileList = [];
                })
            }
        }
    })
</script>

服务端

/** FormData文件上传 */
var multiparty = require('multiparty');
var path = require('path');
var fs = require('fs');
var express = require('express');
var router = express.Router();

/**
 * 文件上传
 * @param {Object} req      客户端传入参数
 * @param {Boolean} auto    是否让multiparty自动完成上传
 */
const multipartyUploadFile = (req, auto) => {
    let config = {};
    if (auto === true) config.uploadDir = `${path.join(__dirname, '../')}upload`;
    return new Promise((resolve, reject) => {
        let form = new multiparty.Form(config);
        form.parse(req, (err, fields, files) => {
            if (err) return reject(err);
            if (!auto) {
                let fileList = files.file;
                for (let i = 0; i < fileList.length; i++) {
                    const file = fileList[i];
                    // 剪切并重命名
                    fs.renameSync(file.path, `${path.join(__dirname, '../')}upload/${file.originalFilename}`);
                    file.realPath = `${path.join(__dirname, '../')}upload/${file.originalFilename}`;
                }
            }
            return resolve({ fields, files });
        });
    })
}

router.post('/', (req, res, next) => {
    const auto = !!+req.query['auto'];
    multipartyUploadFile(req, auto)
        .then(value => {
            res.status(200).json(value);
        }).catch(reason => {
            res.status(500).json(reason);
        })
});

module.exports = router;

多文件上传 【BASE64】,利用SparkMD5判断文件是否存在

客户端

<div id="upload_02">
    <h3>多文件上传 【BASE64】,利用SparkMD5判断文件是否存在</h3>
    <el-row :gutter="10" type="flex" class="content">
        <el-col :span="12">
            <el-form label-position="top">
                <el-form-item label="文件上传:">
                    <el-upload ref="upload" action="#" :on-change="handleFileChange" :auto-upload="false"
                        :show-file-list="false" multiple>
                        <el-button slot="trigger" size="mini" type="primary">选择文件</el-button>
                        <el-button size="mini" type="primary" @click="uploadFile" :disabled="fileList.length === 0">开始上传
                        </el-button>
                    </el-upload>
                </el-form-item>
                <el-form-item label="进度:">
                    <el-progress :text-inside="true" :stroke-width="24" :percentage="value" status="success">
                    </el-progress>
                </el-form-item>
            </el-form>
        </el-col>
        <el-col :span="12">
            <div class="request-res">
                <pre>{{result}}</pre>
            </div>
        </el-col>
    </el-row>
</div>

<script>
    new Vue({
        el: '#upload_02',
        data: {
            fileList: [],
            value: 0,
            result: null,
        },
        methods: {
            handleFileChange(file, fileList) {
                this.fileList = fileList;
            },
            uploadFile() {
                Promise.all(this.fileList.map(async info => await this.fileToBase64(info.raw))).then(base64s => {
                    axios({
                        url: '/upload_02',
                        method: 'post',
                        data: base64s,
                        onUploadProgress: (pe) => {
                            if (pe.lengthComputable) this.value = Number((pe.loaded / pe.total * 100).toFixed(2));
                        }
                    }).then(res => {
                        this.result = res.data;
                    }).catch(err => {
                        this.result = { err };
                    }).finally(_ => {
                        this.$refs['upload'].clearFiles();
                        this.fileList = [];
                    })
                })
            },
            // 文件转Base64
            fileToBase64(file) {
                return new Promise((resolve, rejecct) => {
                    // 实例化文件读取对象
                    var reader = new FileReader();
                    // 将文件读取为DataURL,也就是base64编码
                    reader.readAsDataURL(file);
                    // 文件读取成功完成时触发
                    reader.onload = (e) => {
                        resolve({
                            name: file.name,
                            base64: e.target.result // base64编码
                        });
                    }
                });
            }
        }
    })
</script>

服务端

/** Base64文件上传 */
var SparkMD5 = require('spark-md5');
const mime = require('mime-types');
var fs = require('fs');
var path = require('path');
var express = require('express');
var router = express.Router();

router.post('/', (req, res, next) => {
    let result = [];
    spark = new SparkMD5.ArrayBuffer();
    for (let index = 0; index < req.body.length; index++) {
        const item = req.body[index];
        const base64s = item.base64.split(',');
        // 扩展名
        const extension = mime.extension(base64s[0].replace(/^data:|;base64$/g, ''));
        // 文件
        const file = Buffer.from(base64s[1], 'base64');
        // 文件名
        spark.append(file);
        const realName = spark.end();
        // 存储路径
        const savePath = `${path.join(__dirname, '../')}upload/${realName}.${extension}`;
        // 检测文件是否不存
        const isExists = fs.existsSync(savePath);
        // 检测文件不存在将写入文件
        if (!isExists) fs.writeFileSync(savePath, file);
        result.push({ name: item.name, realName, extension, path: savePath, msg: isExists ? '文件已存在!' : '文件创建成功!' });
    }
    res.status(200).json(result);
});

module.exports = router;

单文件上传 【断点续传】

客户端

<div id="upload_03">
    <h3>单文件上传 【断点续传】</h3>
    <el-row :gutter="10" type="flex" class="content">
        <el-col :span="12">
            <el-form label-position="top">
                <el-form-item label="文件上传:">
                    <el-upload ref="upload" action="#" :http-request="uploadFile" :show-file-list="false">
                        <el-button slot="trigger" size="mini" type="primary">上传文件</el-button>
                    </el-upload>
                </el-form-item>
                <el-form-item label="进度:">
                    <el-progress :text-inside="true" :stroke-width="24" :percentage="value" status="success">
                    </el-progress>
                </el-form-item>
            </el-form>
        </el-col>
        <el-col :span="12">
            <div class="request-res">
                <pre>{{result}}</pre>
            </div>
        </el-col>
    </el-row>
</div>

<script>
    new Vue({
        el: '#upload_03',
        data: {
            value: 0,
            result: null,
        },
        methods: {
            async uploadFile({ file }) {
                this.result = [];
                let chunks = await this.getFileChunks(file, Math.pow(1024, 2));
                console.log(chunks);
                await chunks.forEach(chunk => {
                    let formData = new FormData();
                    formData.append('file', chunk.file);
                    axios({
                        url: '/upload_03',
                        method: 'post',
                        data: formData,
                        params: {
                            hash: chunk.hash,
                            name: chunk.name,
                            type: chunk.type,
                        }
                    }).then(async res => {
                        this.value = Number(((chunk.name + 1) / chunks.length * 100).toFixed(2));
                        this.result.push(res.data);
                    }).catch(err => {
                        this.result = { err };
                    })
                });

                let { hash, type } = await this.getFileInfo(file);
                axios({
                    url: '/upload_03/merge',
                    method: 'get',
                    params: { hash, type }
                }).then(result => {
                    this.value = 100;
                    this.result = [result.data, ...this.result];
                })
            },
            getFileInfo(file) {
                return new Promise((resolve, reject) => {
                    let fileReader = new FileReader();
                    fileReader.readAsArrayBuffer(file);
                    fileReader.onload = (e) => {
                        let buffer = e.target.result;
                        let spark = new SparkMD5.ArrayBuffer();
                        spark.append(buffer);
                        let hash = spark.end();
                        resolve({ hash, type: file.type });
                    }
                })
            },
            /**
             * 文件切片
             * file 文件
             * size 单个切片大小
             */
            async getFileChunks(file, size) {
                let { hash, type } = await this.getFileInfo(file);
                let chunks = [];
                let count = Math.ceil(file.size / size);
                let index = 0;
                while (index < count) {
                    chunks.push({ file: file.slice(index * size, (index + 1) * size), hash, name: index, type });
                    index++;
                }
                return chunks;
            }
        }
    })
</script>

服务端

/** 断点续传 */
var multiparty = require('multiparty');
var mime = require('mime-types');
var path = require('path');
var fs = require('fs');
var express = require('express');
var router = express.Router();

/**
 * 文件上传
 * @param {Object} req      客户端传入参数
 */
const multipartyUploadFile = (req) => {
    let { hash, name } = req.query;
    return new Promise((resolve, reject) => {
        let form = new multiparty.Form({});
        form.parse(req, (err, fields, files) => {
            if (err) return reject(err);
            let file = files.file[0];
            let dir = `${path.join(__dirname, '../')}upload/${hash}`;
            if (!fs.existsSync(dir)) fs.mkdirSync(dir);
            let savePath = `${dir}/${name}`;
            fs.renameSync(file.path, savePath);
            file.realPath = savePath;
            return resolve({ fields, files });
        });
    })
}

router.post('/', (req, res, next) => {
    let { hash, name, type } = req.query;
    let dir = `${path.join(__dirname, '../')}upload/${hash}`;
    // 文件是否已经上传过
    let realPath = `${dir}.${mime.extension(type)}`;
    if (fs.existsSync(realPath)) {
        res.status(200).json({ realPath, msg: '文件已存在,无需上传!' });
        return
    }
    // 切片路径 判断切片是否上传过
    let chunkPath = `${dir}/${name}`;
    if (fs.existsSync(chunkPath)) {
        res.status(200).json({ chunkPath, msg: '切片已存在,跳过此切片' });
    } else {
        multipartyUploadFile(req)
            .then(value => {
                res.status(200).json(value);
            }).catch(reason => {
                res.status(500).json(reason);
            })
    }
});

// 合并文件
router.get('/merge', (req, res, next) => {
    let { hash, type } = req.query;
    let dir = `${path.join(__dirname, '../')}upload/${hash}`;
    // 文件是否已经上传过
    let realPath = `${dir}.${mime.extension(type)}`;
    if (fs.existsSync(realPath)) {
        res.status(200).json({ realPath, msg: '文件已存在,无需合并!' });
        return
    }
    let fileList = fs.readdirSync(dir);
    fileList.sort((a, b) => a - b).forEach(item => {
        fs.appendFileSync(`${dir}.${mime.extension(type)}`, fs.readFileSync(`${dir}/${item}`));
        fs.unlinkSync(`${dir}/${item}`);
    })
    fs.rmdirSync(dir);
    res.status(200).json({ path: `${dir}.${mime.extension(type)}`, msg: '合并成功!' });
});

module.exports = router;
举报

相关推荐

0 条评论