大文件切片上传
- 拿到文件对象
- 使用线程切割文件
- 拿到切片数组循环上传
- 上传完毕后通知后端合并文件
后端服务
import cors from "cors"; import multer from "multer"; import express from "express"; import fs from "node:fs"; import path from "node:path";
const storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, path.resolve(__dirname, "./test/uploads/")); }, filename: function (req, file, cb) { cb(null, Date.now() + "-" + req.body.fileName); }, });
const upload = multer({ storage: storage }); const app = express(); app.use(cors()); app.use(express.json());
app.post("/upload", upload.single("chunk"), (req, res) => { res.send({ ok: true }); });
app.post("/merge", async (req, res) => { const uploadDir = path.join(__dirname, "./test/uploads/"); let dirs = fs.readdirSync(uploadDir); dirs.sort((a, b) => a.localeCompare(b, "zh-CN")); const video = path.join( __dirname, "./test/video", `${req.query.fileName}.mp4` ); dirs.forEach((item) => { const data = fs.readFileSync(path.join(uploadDir, item)); fs.appendFileSync(video, data); fs.unlinkSync(path.join(uploadDir, item)); }); res.send({ ok: true }); }); app.listen(8080, () => { console.log("server is running at http://localhost:8080"); });
|
前端实现
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>大文件分片</title> </head> <body> <input type="file" class="input" /> <script src="./main.js" type="module"></script> </body> </html>
|
main.js
import { cutFile } from "./cutFile.js";
const fileDom = document.querySelector(".input");
fileDom.onchange = async (e) => { const file = e.target.files; const chunks = await cutFile(file[0]); const list = []; chunks.forEach((chunk) => { const formData = new FormData(); formData.append("hash", chunk.spark); formData.append("index", chunk.index); formData.append("chunk", chunk.blob); list.push( fetch("http://localhost:8080/upload", { method: "POST", body: formData, }) ); }); Promise.all(list).then(() => { fetch("http://127.0.0.1:8080/merge?fileName=李世民", { method: "POST", }); }); };
|
cutFile.js
const FILE_SIZE = 1024 * 1024 * 5; const CUP_NUM = navigator.hardwareConcurrency || 4; export function cutFile(file) { return new Promise((resolve) => { const fileCount = Math.ceil(file.size / FILE_SIZE); const taskNum = Math.ceil(fileCount / CUP_NUM); let doneNum = 0; const result = []; for (let i = 0, len = CUP_NUM; i < len; i++) { const start = i * taskNum; let end = (i + 1) * taskNum; if (end > fileCount) end = fileCount; const worker = new Worker("./worker.js", { type: "module", }); worker.postMessage({ file, fileCount, start, end, }); worker.onmessage = (e) => { for (let i = start, len = end; i < len; i++) { result[i] = e.data[i - start]; } worker.terminate(); doneNum++; doneNum === CUP_NUM ? resolve(result) : ""; }; } }); }
|
worker.js
import { createChunk } from "./createChunk.js"; onmessage = async (e) => { const { file, fileCount, start, end } = e.data; const proms = []; for (let i = start, len = end; i < len; i++) { const chunk = await createChunk(file, i, fileCount); proms.push(chunk); } const chunks = await Promise.all(proms); postMessage(chunks); };
|
createChunk.js
由于我使用spark-md5来进行唯一标识的时候,浏览器网络控制台无法返回期望信息,也不给我报错,单纯引入这个库都没使用都会返回无法加载。可是我在vue又能用?希望有大佬解答下。所以我使用随机数来模拟唯一标识。
export async function createChunk(file, index, chunkSize) { return new Promise((resolve) => { const start = index * chunkSize; const end = start + chunkSize; const spark = Math.random() * 1000; const blob = file.slice(start, end); const reader = new FileReader(); reader.onload = (e) => { resolve({ start, end, index, blob, spark, }); }; reader.readAsArrayBuffer(blob); }); }
|