文件流式上传—不落盘方案
无哈希校验版本
后端
app.js
import express from 'express';
import cors from 'cors';
import { handleUpload } from './uploader.js';
const app = express();
const port = 3000;
app.use(cors());
// 上传路由
app.post('/upload', (req, res) => {
handleUpload(req, res, {
host: '192.168.1.8',
port: 22,
username: 'root',
password: '******',
});
});
app.listen(port, () => {
console.log(`Server listening on http://localhost:${port}`);
});
uploader.js
// uploader.js
import formidable from 'formidable';
import { Client } from 'ssh2';
// 封装上传逻辑
export function handleUpload(req, res, sshConfig) {
// SSE头
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
const conn = new Client();
conn.on('ready', () => {
conn.sftp((err, sftp) => {
if (err) {
sendSSE(res, 'error', { error: err.message });
return res.end();
}
// formidable 配置
const form = formidable({
maxFileSize: 200 * 1024 * 1024 * 1024, // 200GB
fileWriteStreamHandler: (file) => {
const remotePath = `/var/lib/vz/dump/${file.originalFilename}`;
const writeStream = sftp.createWriteStream(remotePath);
let uploaded = 0;
file.on('data', (chunk) => {
uploaded += chunk.length;
const percent = ((uploaded / file.size) * 100).toFixed(2);
sendSSE(res, 'progress', {
filename: file.originalFilename,
percent,
});
});
return writeStream;
},
});
// 解析上传
form.parse(req, (err, fields, files) => {
if (err) {
sendSSE(res, 'error', { error: err.message });
} else {
sendSSE(res, 'complete', {
message: 'Upload complete',
fields,
});
}
return res.end();
});
});
}).connect(sshConfig);
}
// 通用 SSE 发送函数
function sendSSE(res, event, data) {
res.write(`event: ${event}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
}
前端
test.html
<input type="file" id="fileInput" />
<button onclick="uploadFile()">上传</button>
<br>
<progress id="progressBar" value="0" max="100" style="width: 300px;"></progress>
<span id="percentText">0%</span>
<script>
function uploadFile() {
const file = document.getElementById('fileInput').files[0];
if (!file) return alert('请选择文件');
const formData = new FormData();
formData.append('file', file);
const xhr = new XMLHttpRequest();
xhr.open('POST', 'http://localhost:3000/upload');
// 上传进度事件
xhr.upload.onprogress = function (event) {
if (event.lengthComputable) {
const percent = ((event.loaded / event.total) * 100).toFixed(2);
document.getElementById('progressBar').value = percent;
document.getElementById('percentText').innerText = percent + '%';
}
};
xhr.onload = function () {
if (xhr.status === 200) {
alert('上传完成');
} else {
alert('上传失败: ' + xhr.status);
}
};
xhr.onerror = function () {
alert('上传出错');
};
xhr.send(formData);
}
</script>
测试用例
curl --location --request POST 'http://localhost:3000/upload' \
--form 'file=@"C:\\Users\\kk\\Desktop\\vzdump-qemu-1007-2024_07_29-17_30_25.vma.zst"'
哈希校验版本
哈希值可选,推荐填写
后端
app.js
import express from 'express';
import cors from 'cors';
import { uploadAndVerify } from './uploader.js';
const app = express();
const port = 3000;
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 上传并校验文件接口
app.post('/upload-file', uploadAndVerify);
app.listen(port, () => {
console.log(`Server listening on http://localhost:${port}`);
});
uploader.js
import formidable from 'formidable';
import { Client } from 'ssh2';
export function uploadAndVerify(req, res) {
const sshConfig = {
host: '192.168.1.8',
port: 22,
username: 'root',
password: '******', // 替换成真实密码
};
const conn = new Client();
conn.on('ready', () => {
conn.sftp((err, sftp) => {
if (err) return res.status(500).json({ error: err.message });
const form = formidable({
maxFileSize: 200 * 1024 * 1024 * 1024, // 200GB
fileWriteStreamHandler: (file) => {
const remotePath = `/var/lib/vz/dump/${file.originalFilename}`;
const writeStream = sftp.createWriteStream(remotePath);
writeStream.on('error', (err) => console.error('SFTP 写入失败', err));
file.on('data', (chunk) => {
const percent = file.size ? ((file.bytesReceived / file.size) * 100).toFixed(2) : 0;
console.log(`${file.originalFilename} 上传进度: ${percent}%`);
});
return writeStream;
},
});
// 文件解析完成后触发远程 SHA256 校验
form.parse(req, (err, fields, files) => {
if (err) return res.status(500).json({ error: err.message });
const uploadedFile = Array.isArray(files.file) ? files.file[0] : files.file;
const remotePath = `/var/lib/vz/dump/${uploadedFile.originalFilename}`;
// 兼容 fields.sha256 可能是数组或 undefined
const expectedHash = (Array.isArray(fields.sha256) ? fields.sha256[0] : fields.sha256 || '').toString().trim().toLowerCase();
// 延迟 1 秒,确保大文件落盘完成
setTimeout(() => {
const cmd = `sha256sum ${remotePath} | awk '{print $1}'`;
conn.exec(cmd, (err, stream) => {
if (err) return res.status(500).json({ error: err.message });
let output = '';
stream.on('data', (data) => { output += data.toString(); });
stream.on('close', () => {
const realHash = output.trim().toLowerCase();
if (!expectedHash) {
res.json({ message: '上传完成,但未提供 SHA256,未校验' });
} else if (realHash === expectedHash) {
res.json({ message: '上传完成,校验成功' });
} else {
res.status(400).json({ message: '上传完成,但校验失败' });
}
conn.end();
});
});
}, 1000);
});
});
})
.on('error', (err) => res.status(500).json({ error: 'SSH连接失败: ' + err.message }))
.connect(sshConfig);
}
前端
test.html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>文件上传并校验</title>
</head>
<body>
<input type="file" id="fileInput" />
<br>
SHA256: <input type="text" id="sha256Input" style="width: 400px" />
<br>
<button onclick="uploadFile()">上传并校验</button>
<br>
<progress id="progressBar" value="0" max="100" style="width: 300px"></progress>
<span id="percentText">0%</span>
<script>
function uploadFile() {
const file = document.getElementById('fileInput').files[0];
const sha256 = document.getElementById('sha256Input').value.trim();
if (!file) return alert('请选择文件');
const formData = new FormData();
formData.append('file', file);
formData.append('sha256', sha256);
const xhr = new XMLHttpRequest();
xhr.open('POST', 'http://localhost:3000/upload-file');
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
const percent = ((e.loaded / e.total) * 100).toFixed(2);
document.getElementById('progressBar').value = percent;
document.getElementById('percentText').innerText = percent + '%';
}
};
xhr.onload = () => {
if (xhr.status === 200) {
const resp = JSON.parse(xhr.responseText);
alert(resp.message);
} else {
alert('上传失败或校验失败: ' + xhr.status);
}
};
xhr.onerror = () => alert('上传出错');
xhr.send(formData);
}
</script>
</body>
</html>
测试用例
curl --location --request POST 'http://localhost:3000/upload-file' \
--form 'file=@"C:\\Users\\kk\\Desktop\\vzdump-qemu-1007-2024_07_29-17_30_25.vma.zst"' \
--form 'sha256="f6d6087be20858c78c61a183b752416ec29a35267c297f229b1524cead992803"'