服务端-node
- 导入所需的Node.js模块,包括Express.js用于创建Web应用程序、body-parser用于解析HTTP请求体、path用于处理文件路径、fse用于文件操作、multiparty用于处理多部分表单数据。
- 创建Express.js应用程序的实例。
const express = require('express');
const bodyParser = require('body-parser');
const path = require('path');
const fse = require('fs-extra');
const multiparty = require('multiparty');
const app = express();
- 定义一些配置项,包括应用程序的端口号(可以从环境变量中获取,默认为3000)、切片文件存储目录的路径(
ALL_CHUNKS_PATH)以及上传完成文件存储的路径(UPLOAD_FILE_PATH)。
const PORT = process.env.PORT || 3000;
const ALL_CHUNKS_PATH = path.resolve(__dirname, 'chunks');
const UPLOAD_FILE_PATH = path.resolve(__dirname, 'public/files');
- 配置Express.js中间件。
express.static 中间件用于提供静态文件服务,将/public目录中的文件公开供客户端访问。
bodyParser 中间件用于解析请求体数据,允许你访问请求体中的数据。
app.use(express.static(__dirname + '/public'));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
- 启动Express.js应用程序,监听3000端口,并在成功启动时输出日志。
app.listen(3000, () => console.log('3000 端口启动成功'));
app.post('/upload', (req, res) => {
const multipartyForm = new multiparty.Form();
multipartyForm.parse(req, async (err, fields, files) => {
if (err) {
console.error('文件切片上传失败:', err);
return res.status(500).json({ code: 0, message: '文件切片上传失败' });
}
const [file] = files.file;
debugger
const { fileName: [fileName], chunkName: [chunkName] } = fields;
const chunksPath = path.resolve(ALL_CHUNKS_PATH, fileName);
if (!fse.existsSync(chunksPath)) {
fse.mkdirSync(chunksPath);
}
try {
await fse.move(file.path, `${chunksPath}/${chunkName}`);
res.status(200).json({ code: 1, message: '切片上传成功' });
} catch (error) {
console.error('文件切片移动失败:', error);
res.status(500).json({ code: 0, message: '文件切片上传失败' });
}
});
});
app.post('/merge', async (req, res) => {
const { chunkSize, fileName } = req.body;
const uploadedFile = path.resolve(UPLOAD_FILE_PATH, fileName);
const chunksPath = path.resolve(ALL_CHUNKS_PATH, fileName);
try {
const chunksName = await fse.readdir(chunksPath);
chunksName.sort((a, b) => (a - 0) - (b - 0));
for (let index = 0; index < chunksName.length; index++) {
const chunkPath = path.resolve(chunksPath, chunksName[index]);
const readChunk = fse.createReadStream(chunkPath);
const writeChunk = fse.createWriteStream(uploadedFile, {
start: index * chunkSize,
end: (index + 1) * chunkSize,
});
await new Promise((resolve) => {
readChunk.pipe(writeChunk);
readChunk.on('end', () => {
fse.unlinkSync(chunkPath);
resolve();
});
});
}
fse.rmdirSync(chunksPath);
res.status(200).json({ code: 1, message: '文件上传成功' });
} catch (error) {
console.error('切片合并失败:', error);
res.status(500).json({ code: 0, message: '切片合并失败' });
}
});
app.post('/verify', async (req, res) => {
const { fileName } = req.body;
const filePath = path.resolve(UPLOAD_FILE_PATH, fileName);
const chunksPath = path.resolve(ALL_CHUNKS_PATH, fileName);
if (fse.existsSync(filePath)) {
res.status(200).json({ code: 1, message: '文件已存在,不需要重新上传' });
} else if (fse.existsSync(chunksPath)) {
const uploaded = await fse.readdir(chunksPath);
res.status(200).json({ code: 0, message: '文件有部分上传数据', uploaded });
} else {
res.status(200).json({ code: 0, message: '文件未上传过' });
}
});
前端
<div>
<input type="file" id="chooseFile" />
<button id="uploadFile">上传</button>
</div>
<div style="margin: 20px 0">
<progress value="0" id="progress"></progress>
<span id="message"></span>
</div>
<div>
<button id="stopUpload">暂停</button>
<button id="keepUpload">续传</button>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
axios.defaults.baseURL = "http://localhost:3000";
</script>
<script>
const chooseFile = document.getElementById("chooseFile");
const uploadFile = document.getElementById("uploadFile");
const progress = document.getElementById("progress");
const message = document.getElementById("message");
const stopUpload = document.getElementById("stopUpload");
const keepUpload = document.getElementById("keepUpload");
let checkedFile = null;
chooseFile.addEventListener("change", function (e) {
const [file] = e.target.files;
if (file) {
checkedFile = file;
}
});
const chunkSize = 5 * 1024 * 1024;
let chunksList = [];
const uploadChunks = async () => {
for (let i = 0; i < chunksList.length; i++) {
const formData = new FormData();
formData.append("file", chunksList[i]);
formData.append("fileName", checkedFile.name);
formData.append("chunkName", i);
try {
await axios({
url: "/upload",
data: formData,
method: "POST",
});
progress.value++;
} catch (error) {
console.error("切片上传失败: " + error.message);
break;
}
}
mergeChunks();
};
const mergeChunks = async () => {
try {
const { data } = await axios({
url: "/merge",
method: "POST",
data: {
chunkSize,
fileName: checkedFile.name,
},
});
if (data.code) {
message.innerText = "文件上传成功";
}
} catch (error) {
console.error("切片合并失败: " + error.message);
message.innerText = "文件上传失败";
}
};
uploadFile.addEventListener("click", async function () {
if (!checkedFile) {
alert("请选择文件");
return;
}
const res = await verifyFile();
if (res.code) {
message.innerText = "上传成功(秒传)";
progress.value = progress.max;
return;
}
chunksList = createChunks();
uploadChunks();
});
const verifyFile = async () => {
try {
const { data } = await axios({
url: "/verify",
method: "POST",
data: {
fileName: checkedFile.name,
},
});
return data;
} catch (error) {
console.error("文件验证失败: " + error.message);
return { code: 0 };
}
};
const CancelToken = axios.CancelToken;
let source = CancelToken.source();
stopUpload.addEventListener("click", function () {
source.cancel("终止上传!");
source = CancelToken.source();
message.innerText = "暂停上传";
});
keepUpload.addEventListener("click", async function () {
const res = await verifyFile();
if (!res.code) {
uploadChunks();
}
});
const createChunks = () => {
let start = 0;
const chunks = [];
while (start < checkedFile.size) {
const chunkItem = checkedFile.slice(start, start + chunkSize);
chunks.push(chunkItem);
start += chunkSize;
}
return chunks;
};
</script>