准备
安装及运行
$ 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>
服务端
var multiparty = require('multiparty');
var path = require('path');
var fs = require('fs');
var express = require('express');
var router = express.Router();
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 = [];
})
})
},
fileToBase64(file) {
return new Promise((resolve, rejecct) => {
var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (e) => {
resolve({
name: file.name,
base64: e.target.result
});
}
});
}
}
})
</script>
服务端
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 });
}
})
},
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();
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;