Section uploading
The front end
- Slice the file using the blob.prototype. slice method
- Use the spark-MD5 library to compute the hash of files. (The spark-MD5 library cannot directly compute the entire file, but needs to compute fragments. For details, see the example spark-md5-example on its official website.
- Create a Web worker to compute hash and reduce page lag.
- Upload each fragment file to the server with hash
- When all files are uploaded, a merge request is sent with the file name and fragment size
The service side
Create a folder based on the received hash and store the fragmented files in the folder. After receiving the merge request, read the fragmented files and merge them. Delete the folder that stores the fragmented files and their contents
A pass way of thinking
The client
- When uploading a file, it computes the hash and sends the hash and file name to the server
- The server returns the status of whether the file exists
- If yes, the client displays a message indicating that the file is uploaded successfully. Otherwise, the file is uploaded
The service side
- The server sets a hash= file mapping table
- Search the table based on the hash sent by the client
- Returns an existing state
Web worker on pit
The self.importScript function can only import files with absolute paths, but the documentation says it can use relative paths, but has not been tested many times. Therefore, save the spark.md5.js file in the public folder.
rendering
The client
upload.js
import React, { useState, useEffect, useMemo } from "react";
import request from ".. /utils/request";
import styled from "styled-components";
import hashWorker from ".. /utils/hash-worker";
import WorkerBuilder from ".. /utils/worker-build";
const chunkSize = 1 * 1024 * 1024;
const UpLoadFile = function () {
const [sourceFile, setSourceFile] = useState(null);
const [chunksData, setChunksData] = useState([])
const [myWorker, setMyWorker] = useState(null)
const [fileHash, setFileHash] = useState("")
const [hashPercentage, setHashPercentage] = useState(0)
const handleFileChange = (e) = > {
const { files } = e.target;
if (files.length === 0) return;
// Save the source file
setSourceFile(files[0]);
// File fragmentation
splitFile(files[0])}// Send the merge request
const mergeRequest = () = > {
request({
url: "http://localhost:3001/merge".method: "post".headers: {
"content-type": "application/json"
},
data: JSON.stringify({
filename: sourceFile.name,
// For the server to merge files
size: chunkSize
})
})
}
// Upload fragments
const uploadChunks = async (chunksData) => {
const formDataList = chunksData.map(({ chunk, hash }) = > {
const formData = new FormData()
formData.append("chunk", chunk);
formData.append("hash", hash);
formData.append("filename", sourceFile.name);
return { formData };
})
const requestList = formDataList.map(({ formData }, index) = > {
return request({
url: "http://localhost:3001/upload".data: formData,
onprogress: e= > {
let list = [...chunksData];
list[index].progress = parseInt(String((e.loaded / e.total) * 100));
setChunksData(list)
}
})
})
// Upload the file
await Promise.all(requestList);
// Delay sending merge request
setTimeout(() = > {
mergeRequest();
}, 500);
}
// Computes the file hash
const calculateHash = (chunkList) = > {
return new Promise(resolve= > {
const w = new WorkerBuilder(hashWorker)
w.postMessage({ chunkList: chunkList })
w.onmessage = e= > {
const { percentage, hash } = e.data;
setHashPercentage(percentage);
if (hash) {
// When the hash is complete, execute resolve
resolve(hash)
}
}
setMyWorker(w)
})
}
// Upload the file
const handleUpload = async (e) => {
if(! sourceFile) { alert("Please select the file first")
return;
}
// Split the file
const chunklist = splitFile(sourceFile);
/ / calculate the hash
const hash = await calculateHash(chunklist)
console.log("hash=======", hash)
setFileHash(hash)
const { shouldUpload } = await verfileIsExist(sourceFile.name, hash);
if(! shouldUpload) { alert("File already exists, no need to upload again");
return;
}
const chunksData = chunklist.map(({ chunk }, index) = > ({
chunk: chunk,
hash: hash + "-" + index,
progress: 0
}))
// Save fragmented data
setChunksData(chunksData)
// Start uploading fragments
uploadChunks(chunksData)
}
// Split the file
const splitFile = (file, size = chunkSize) = > {
const fileChunkList = [];
let curChunkIndex = 0;
while (curChunkIndex <= file.size) {
const chunk = file.slice(curChunkIndex, curChunkIndex + size);
fileChunkList.push({ chunk: chunk, })
curChunkIndex += size;
}
return fileChunkList;
}
// second pass: verifies whether the file exists on the server
const verfileIsExist = async (fileName, fileHash) => {
const { data } = await request({
url: "http://localhost:3001/verFileIsExist".headers: {
"content-type": "application/json"
},
data: JSON.stringify({
fileName: fileName,
fileHash: fileHash
})
})
return JSON.parse(data);
}
return (
<div>
<input type="file" onChange={handleFileChange} /><br />
<button onClick={handleUpload}>upload</button>
<ProgressBox chunkList={chunksData} />
</div>)}const BlockWraper = styled.div`
width: ${({ size }) => size + "px"};
height: ${({ size }) => size + "px"};
text-align: center;
font-size: 12px;
line-height: ${({ size }) => size + "px"};
border: 1px solid #ccc;
position: relative;
float: left;
&:before {
content: "${({ chunkIndex }) => chunkIndex}";
position: absolute;
width: 100%;
height: 10px;
left: 0;
top: 0;
font-size: 12px;
text-align: left;
line-height: initial;
color: #000
}
&:after {
content: "";
position: absolute;
width: 100%;
height: ${({ progress }) => progress + "%"};
background-color: pink;
left: 0;
top: 0;
z-index: -1;
}
`
const ChunksProgress = styled.div` *zoom: 1; &:after { content: ""; display: block; clear: both; } `
const Label = styled.h3` `
const ProgressWraper = styled.div` `
const Block = ({ progress, size, chunkIndex }) = > {
return (<BlockWraper size={size} chunkIndex={chunkIndex} progress={progress}>
{progress}%
</BlockWraper>)}const ProgressBox = ({ chunkList = [], size = 40 }) = > {
const sumProgress = useMemo(() = > {
if (chunkList.length === 0) return 0
return chunkList.reduce((pre, cur, sum) = > pre + cur.progress / 100.0) * 100 / (chunkList.length)
}, [chunkList])
return (
<ProgressWraper>
<Label>The file is divided into {chunklist. length} segments, and the upload progress of each segment is as follows:</Label>
<ChunksProgress>
{chunkList.map(({ progress }, index) => (
<Block key={index} size={size} chunkIndex={index} progress={progress} />
))}
</ChunksProgress>
<Label>Total progress: {sumProgress. ToFixed (2)} %</Label>
</ProgressWraper >
)
}
export default UpLoadFile;
Copy the code
hash-worker.js
const hashWorker = () = > {
self.importScripts("http://localhost:3000/spark-md5.min.js")
self.onmessage = (e) = > {
const { chunkList } = e.data;
const spark = new self.SparkMD5.ArrayBuffer();
let percentage = 0;
let count = 0;
const loadNext = index= > {
const reader = new FileReader();
reader.readAsArrayBuffer(chunkList[index].chunk);
reader.onload = event= > {
count++;
spark.append(event.target.result);
if (count === chunkList.length) {
self.postMessage({
percentage: 100.hash: spark.end()
})
self.close();
} else {
percentage += (100 / chunkList.length)
self.postMessage({
percentage
})
loadNext(count)
}
}
}
loadNext(count)
}
}
export default hashWorker
Copy the code
worker-build.js
export default class WorkerBuilder extends Worker {
constructor(worker) {
const code = worker.toString();
const blob = new Blob([` (${code}`) ()]);
return newWorker(URL.createObjectURL(blob)); }}Copy the code
request.js
const request = ({
url,
method = "post",
data,
headers = {},
onprogress
}) = > {
return new Promise(resolve= > {
const xhr = new XMLHttpRequest();
xhr.open(method, url);
Object.keys(headers).forEach(key= >
xhr.setRequestHeader(key, headers[key])
);
xhr.upload.onprogress = onprogress
xhr.send(data);
xhr.onload = e= > {
resolve({
data: e.target.response
});
};
});
}
export default request;
Copy the code
The service side
import express from 'express'
import path from "path";
import fse from "fs-extra";
import multiparty from "multiparty";
import bodyParser from "body-parser"
let app = express()
const __dirname = path.resolve(path.dirname(' '));
const upload_files_dir = path.resolve(__dirname, "./filelist")
const jsonParser = bodyParser.json({ extended: false });
app.use(function (req, res, next) {
res.setHeader("Access-Control-Allow-Origin"."*");
res.setHeader("Access-Control-Allow-Headers"."*");
next()
})
app.post('/verFileIsExist', jsonParser, async (req, res) => {
const { fileName, fileHash } = req.body;
const filePath = path.resolve(upload_files_dir, fileName);
if (fse.existsSync(filePath)) {
res.send({
code: 200.shouldUpload: false})}else {
res.send({
code: 200.shouldUpload: true
})
}
})
app.post('/upload'.async (req, res) => {
const multipart = new multiparty.Form();
multipart.parse(req, async (err, fields, files) => {
if (err) return;
const [chunk] = files.chunk;
const [hash] = fields.hash;
const [filename] = fields.filename;
// Add "_dir" to prevent file name and folder name conflicts during merge
const chunkDir = path.resolve(upload_files_dir, filename + "_dir");
if(! fse.existsSync(chunkDir)) {await fse.mkdirs(chunkDir);
}
await fse.move(chunk.path, `${chunkDir}/${hash}`);
})
res.status(200).send("received file chunk")})const pipeStream = (path, writeStream) = >
new Promise(resolve= > {
const readStream = fse.createReadStream(path);
readStream.on("end".() = > {
fse.unlinkSync(path);
resolve();
});
readStream.pipe(writeStream);
});
// merge slices
const mergeFileChunk = async (filePath, filename, size) => {
const chunkDir = path.resolve(upload_files_dir, filename + "_dir");
const chunkPaths = await fse.readdir(chunkDir);
// Sort by slice subscript
// Otherwise, the order of obtaining the directory directly may be out of order
chunkPaths.sort((a, b) = > a.split("-") [1] - b.split("-") [1]);
console.log("Specify a location to create a writable stream", filePath);
await Promise.all(
chunkPaths.map((chunkPath, index) = >
pipeStream(
path.resolve(chunkDir, chunkPath),
// Create a writable stream at the specified location
fse.createWriteStream(filePath, {
start: index * size,
end: (index + 1) * size
})
)
)
);
fse.rmdirSync(chunkDir); // Delete the directory where the slices are saved after merging
};
app.post('/merge', jsonParser, async (req, res) => {
const { filename, size } = req.body;
const filePath = path.resolve(upload_files_dir, filename);
await mergeFileChunk(filePath, filename, size);
res.send({
code: 200.message: "success"
});
})
app.listen(3001.() = > {
console.log('listen:3001')})Copy the code