最近一直想写个大型文件续传的逻辑代码块,今天就把它贡献出来吧。代码不做讲解,有兴趣的自己体会。
path: src\pages\tempContent\displayCounter\index.vue
<!--
* Copyright ©
* #
* @author: zw
* @date: 2021-12-07
-->
<template>
<input type="file" ref="uploadRef" @change="total = 0" />
<el-button type="primary" @click="uploadFile" :disabled="total > 0 ? true : false" :loading="computedFileSize" >
上传
</el-button>
<el-button @click="pauseBykeep">{{ loading ? "继续" : "暂停" }}</el-button>
<div class="">
<div class="divSpan">
{{ computedFileSize ? "计算文件大小" : `上传进度:${total}%` }}
</div>
<el-progress :percentage="total" :status="total < 100 ? 'exception' : 'success'" />
</div>
</template>
<script lang="js">
import { defineComponent, reactive, toRefs } from 'vue'
import axios from "axios";
import SparkMD5 from "spark-md5";
import { useInstance } from "@/common";
import { SIZE, fileParse, chunkList, formData } from "./utils";
export default defineComponent({
name: 'displayCounter',
setup(props, {emit, slots}) {
const { $message: message } = useInstance();
const state = reactive({
uploadRef: null,
total: 0,
computedFileSize: false,
loading: false,
partList: [],
hash: '',
isErr: false,
abort: false
});
const uploadFile = () => {
state.computedFileSize = true;
createChunkFile(state.uploadRef.files[0]);
}
const createChunkFile = async (file) => {
if (!file) return;
const buffer = await fileParse(file);
const spark = new SparkMD5.ArrayBuffer();
spark.append(buffer);
const hash = spark.end();
state.partList = chunkList(hash, file);
state.hash = hash;
getLoadingFiles(hash);
};
const getLoadingFiles = async (hash) => {
const { data: { data, code }} = await axios({url: "http://localhost:8888/loadingUpload", method: 'GET', params: { hash }});
if (code !== 0) return;
if (data.length === 0) {
uploadFn();
} else {
const progress = parseInt(Math.ceil(100 / state.partList.length));
state.total = data.length * progress;
uploadFn(data);
}
};
const uploadFn = async (list) => {
function request(formData) {
return axios.post("http://localhost:8888/upload", formData, { headers: { "Content-Type": "multipart/form-data" } });
}
const uploadList = state.partList.map(item => formData(item, request));
state.computedFileSize = false;
const index = list ? list.length : 0;
uploadSend(uploadList, index);
};
const pauseBykeep = () => {
if (!state.uploadRef.files[0]) return;
if (state.loading) {
state.loading = false;
state.abort = false;
uploadFn();
} else {
state.loading = true;
state.abort = true;
}
};
const uploadSend = async (uploadList, index) => {
if (state.abort) return;
if (index >= uploadList.length) return uploadComplete();
try {
const progress = parseInt(Math.ceil(100 / state.partList.length));
const { data: { data, code } } = await uploadList[index]();
if (code === 2) state.isErr = true;
if (code === 0) {
const status = state.total + progress;
state.partList.shift(1);
state.total = status >= 100 ? 100 : status;
}
} catch (error) {
state.loading = true;
state.abort = true;
}
if (state.isErr) {
return message.error("当前文件已上传");
}
index++;
uploadSend(uploadList, index);
};
const uploadComplete = async () => {
const {data: { data, code }} = await axios({url: "http://localhost:8888/merge", method: 'GET', params: { hash: state.hash } });
if (code !== 0) return;
message.success("上传成功");
};
return {
uploadFile,
pauseBykeep,
...toRefs(state)
}
}
});
</script>
<style lang="css" scoped>
.el-progress {
width: 400px;
}
</style>
utils的代码提取块。
path: src\pages\tempContent\displayCounter\utils.js
export const SIZE = 10 * 1024 * 1024;
export function fileParse(file){
return new Promise(resolve => {
const fileRead = new FileReader();
fileRead.readAsArrayBuffer(file);
fileRead.onload = (evt) => resolve(evt.target.result);
});
}
export function suffixName(name) {
return /\.([0-9a-zA-Z]+)$/i.exec(name)[1];
}
export function chunkList(hash, file) {
const partList = [];
const count = Math.ceil(file.size / SIZE);
const partSize = file.size / count;
const suffix = suffixName(file.name);
let i = 0, cur = 0;
while (i < count) {
const item = { chunk: file.slice(cur, cur + partSize), filename: `${hash}_${i}.${suffix}` };
cur += partSize;
partList.push(item);
i++;
}
return partList;
}
export function formData(item, callbeck) {
const formData = new FormData();
formData.append("chunk", item.chunk);
formData.append("filename", item.filename);
return callbeck.bind(null, formData);
}
common代码提取块。
path: src\common\index.js
import { getCurrentInstance } from 'vue'
function useInstance (target) {
const Instance = getCurrentInstance();
const Properties = Instance.appContext.config.globalProperties;
return target ? Properties[target] : Properties
}
function create_uuid () {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
return (c === 'x' ? (Math.random() * 16 | 0) : ('r&0x3' | '0x8')).toString(16)
})
}
function loop(m, c) {
return m.find(e => e.id === c ? e : loop(e.children, c));
}
export {
useInstance,
create_uuid,
loop,
}
node 服务模块的代码。文件用的模块自己按照习惯去安装配置就行。
path: server\server.js
const express = require("express");
const fs = require("fs");
const { join, resolve } = require("path");
const multiparty = require("multiparty");
const cors = require("cors");
const PORT = 8888;
const uploadDir = `${__dirname}/upload`;
const app = express();
app.use(express.static("./"));
app.use(cors());
app.listen(PORT, () => console.log(`服务端已启动,端口号:${PORT}`));
app.use(express.urlencoded({ extended: false, limit: "2048mb" }));
function isDirSync(currentPath) {
try {
return fs.statSync(currentPath).isDirectory();
} catch (e) {
if (e.code === 'ENOENT') return false;
throw e;
}
}
function handleMultiparty(req, res, temp) {
return new Promise((resolve, reject) => {
const options = { maxFieldsSize: 200 * 1024 * 1024 };
!temp ? (options.uploadDir = uploadDir) : null;
const form = new multiparty.Form(options);
form.parse(req, (err, fields, files) => {
if (err) return reject(err);
resolve({ fields, files });
});
});
}
function findSync(startPath) {
const result = [];
function finder(_path) {
const files = fs.readdirSync(_path);
files.forEach(item => {
const _findPath = join(_path, item);
const stats = fs.statSync(_findPath);
if (stats.isFile()) result.push(item.split(".")[0]);
});
}
finder(startPath);
return result;
}
function findLoadingSync(startPath, hash) {
const result = [];
function finder(_path, isChild) {
if (!isDirSync(join(__dirname, 'upload'))) {
fs.mkdirSync(join(__dirname, 'upload'));
}
const _files = fs.readdirSync(_path);
_files.forEach(item => {
const _findPath = join(_path, item);
const stats = fs.statSync(_findPath);
if (stats.isDirectory() && hash == item) finder(_findPath, item);
if (stats.isFile() && isChild == hash) result.push(item);
});
}
finder(startPath);
return result;
}
app.get("/loadingUpload", (req, res) => {
const { hash } = req.query;
const _hasUpList = findLoadingSync(uploadDir, hash);
res.send({ code: 0, data: _hasUpList });
});
app.post("/upload", async (req, res) => {
const { fields, files } = await handleMultiparty(req, res, true);
const [chunk] = files.chunk;
const [filename] = fields.filename;
const _hash = /([0-9a-zA-Z]+)_\d+/.exec(filename)[1];
let _path = `${uploadDir}/${_hash}`;
const _hasUpList = findSync(uploadDir);
if (_hasUpList.indexOf(_hash) > -1) {
return res.send({ code: 2, msg: "当前文件已经上传!" });
}
!fs.existsSync(_path) ? fs.mkdirSync(_path) : null;
_path = `${_path}/${filename}`;
fs.access(_path, async (err) => {
if (!err) {
return res.send({ code: 0, path: _path.replace(__dirname, `http://127.0.0.1:${PORT}`) });
};
const readStream = fs.createReadStream(chunk.path);
const writeStream = fs.createWriteStream(_path);
readStream.pipe(writeStream);
readStream.on("end", () => {
fs.unlinkSync(chunk.path);
res.send({ code: 0, path: _path.replace(__dirname, `http://127.0.0.1:${PORT}`) });
});
});
});
app.get("/merge", (req, res) => {
const { hash } = req.query;
const _path = `${uploadDir}/${hash}`;
const fileList = fs.readdirSync(_path);
let suffix;
fileList.sort((a, b) => /_(\d+)/.exec(a)[1] - /_(\d+)/.exec(b)[1]);
fileList.forEach(item => {
!suffix ? (suffix = /\.([0-9a-zA-Z]+)$/.exec(item)[1]) : null;
fs.appendFileSync( `${uploadDir}/${hash}.${suffix}`, fs.readFileSync(`${_path}/${item}`) );
fs.unlinkSync(`${_path}/${item}`);
});
fs.rmdirSync(_path);
res.send({ code: 0, path: `http://127.0.0.1:${PORT}/upload/${hash}.${suffix}` });
});
app.use((req, res) => {
res.status(404);
res.send("NOT FOUND!");
});
vue2版本 el-upload
<!--
* Copyright ©
* #
* @author: zw
* @date: 2022-09-13
-->
<template>
<el-row>
<el-col>
<el-form class="ml-10" :model="query" inline="inline" label-position="left" size="small">
<el-col :span="1.5" class="fr">
<el-form-item class="mb-10 mt-10">
<el-button type="success" icon="el-icon-upload" @click="upload.visible = true">文件上传</el-button>
</el-form-item>
</el-col>
</el-form>
</el-col>
<el-dialog title="上传升级包" :visible.sync="upload.visible" append-to-body width="35%">
<el-upload align="center" ref="upload" :limit="1" :multiple="false" :accept="upload.accept" action="" name="file" :auto-upload="false" :before-remove="file => $confirm(`确定移除 ${file.name}?`)" :on-success="onSuccess" :on-error="onError" :http-request="customUpload" drag>
<i class="el-icon-upload" />
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">仅支持上传升级包文件</div>
<br>
<br>
<div class="el-upload__tip" style="color:blue" slot="tip">提示:仅允许导入<span style="color:red">“{{upload.accept}}”</span>格式文件!</div>
<div class="el-upload__tip" slot="tip">
<el-progress v-if="upload.total > 0" class="w--30%" :percentage="upload.total" :status="upload.total < 100 ? 'exception' : 'success'" />
</div>
</el-upload>
<div slot="footer">
<el-button type="primary" @click="$refs.upload.submit()" size="mini">确 定</el-button>
<el-button @click="upload.visible = false" size="mini">取 消</el-button>
</div>
</el-dialog>
</el-row>
</template>
<script>
import axios from "axios";
import SparkMD5 from "spark-md5";
import { fileParse, chunkList, formData } from "@/utils";
const BASE_URL = "http://localhost:8888"
export default {
name: 'upload',
data() {
return {
query: {},
upload: {
visible: false,
uploadCtx: null,
total: 0,
computedFileSize: false,
loading: false,
partList: [],
hash: '',
isErr: false,
abort: false
}
};
},
mounted() { },
methods: {
onSuccess(response = {}) {
this.$message.success(response.msg);
},
onError(response = {}) {
this.$message.error(response.msg);
},
pauseBykeep() {
if (!this.upload.uploadCtx.file) return;
if (this.upload.loading) {
this.upload.loading = false;
this.upload.abort = false;
this.uploadFn();
} else {
this.upload.loading = true;
this.upload.abort = true;
}
},
customUpload(ctx) {
console.log(ctx);
this.upload.uploadCtx = ctx;
this.upload.computedFileSize = true;
this.createChunkFile(ctx.file);
},
async createChunkFile(file) {
if (!file) return;
const buffer = await fileParse(file);
const spark = new SparkMD5.ArrayBuffer();
spark.append(buffer);
const hash = spark.end();
this.upload.partList = chunkList(hash, file);
this.upload.hash = hash;
this.getLoadingFiles(hash);
},
async getLoadingFiles(hash) {
const { data: { data, code } } = await axios({ url: `${BASE_URL}/loadingUpload`, method: 'GET', params: { hash } });
if (code !== 0) return;
if (data.length === 0) {
this.uploadFn();
} else {
const progress = parseInt(Math.ceil(100 / this.upload.partList.length));
this.upload.total = data.length * progress;
this.uploadFn(data);
}
},
uploadFn(list) {
const request = (formData) => axios.post(`${BASE_URL}/upload`, formData, { headers: { "Content-Type": "multipart/form-data" } });
const uploadList = this.upload.partList.map(item => formData(item, request));
this.upload.computedFileSize = false;
const index = list ? list.length : 0;
this.uploadSend(uploadList, index);
},
async uploadSend(uploadList, index) {
if (this.upload.abort) return;
if (index >= uploadList.length) return this.uploadComplete();
try {
const progress = parseInt(Math.ceil(100 / this.upload.partList.length));
const { data: { code } } = await uploadList[index]();
if (code === 2) this.upload.isErr = true;
if (code === 0) {
const status = this.upload.total + progress;
this.upload.partList.shift(1);
this.upload.total = status >= 100 ? 100 : status;
}
} catch (error) {
console.error(error);
this.upload.loading = true;
this.upload.abort = true;
this.$refs.upload.onError({ msg: error.message });
}
if (this.upload.isErr) {
return this.$refs.upload.onError({ msg: "当前文件已上传" });
}
index++;
this.uploadSend(uploadList, index);
},
async uploadComplete() {
const { data: { code } } = await axios({ url: `${BASE_URL}/merge`, method: 'GET', params: { hash: this.upload.hash } });
if (code !== 0) return;
this.$refs.upload.onSuccess({ msg: "上传成功" });
}
}
}
</script>
<style lang='css' scoped>
</style>