This commit is contained in:
proddy
2026-09-04 16:30:58 +02:00
parent 6291bdfa5d
commit 0a50a9b1fa
20 changed files with 171 additions and 121 deletions
@@ -1,5 +1,6 @@
// Simulate EventSource for log messages in EMS-ESP standalone mode
// Server-Sent Events (SSE)
// EMS-ESP standalone mode to simulate
// a REST server for file uploads
// an EventSource (Server-Sent Events) for log messages in EMS-ESP standalone mode
const ONE_SECOND_MS = 1000;
// padding function
@@ -21,11 +22,113 @@ const formatDate = (date) => {
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
};
const MOCK_REST_ORIGIN = 'http://localhost:3080';
const UPLOAD_SIMULATED_KB_PER_SEC = 256;
const UPLOAD_SLICE_BYTES = 16 * 1024;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const readRequestThrottled = (req) =>
new Promise((resolve, reject) => {
const chunks = [];
let ended = false;
let settled = false;
const settle = (settler, value) => {
if (settled) {
return;
}
settled = true;
settler(value);
};
req.on('error', (error) => settle(reject, error));
req.on('end', () => {
ended = true;
void step();
});
const step = async () => {
if (settled) {
return;
}
try {
const chunk = req.read(UPLOAD_SLICE_BYTES);
if (chunk) {
chunks.push(chunk);
await sleep((chunk.length / (UPLOAD_SIMULATED_KB_PER_SEC * 1024)) * 1000);
void step();
return;
}
if (ended || req.readableEnded) {
settle(resolve, Buffer.concat(chunks));
return;
}
req.once('readable', () => {
void step();
});
} catch (error) {
settle(reject, error);
}
};
void step();
});
const forwardThrottledUpload = async (req, res) => {
const startedAt = Date.now();
try {
const body = await readRequestThrottled(req);
console.log(
`Upload received: ${body.length} bytes in ${((Date.now() - startedAt) / 1000).toFixed(1)}s`
);
const headers = {};
if (req.headers['content-type']) {
headers['content-type'] = req.headers['content-type'];
}
if (req.headers.authorization) {
headers.authorization = req.headers.authorization;
}
headers['content-length'] = String(body.length);
const bunRes = await fetch(`${MOCK_REST_ORIGIN}${req.url}`, {
method: 'POST',
headers,
body
});
const buf = Buffer.from(await bunRes.arrayBuffer());
res.statusCode = bunRes.status;
bunRes.headers.forEach((value, key) => {
if (key !== 'transfer-encoding' && key !== 'content-encoding') {
res.setHeader(key, value);
}
});
res.end(buf);
} catch (error) {
if (error?.code === 'ECONNRESET' || req.destroyed) {
console.log('Upload cancelled by the client');
} else {
console.error('Throttled upload proxy failed', error);
}
if (!res.headersSent) {
res.statusCode = 502;
}
if (!res.writableEnded) {
res.end();
}
}
};
export default () => {
return {
name: 'vite:mockserver',
name: 'vite:mockServer',
configureServer: async (server) => {
server.middlewares.use(async (req, res, next) => {
if (req.url.startsWith('/rest/uploadFile') && req.method === 'POST') {
await forwardThrottledUpload(req, res);
return;
}
// Handle Server-Sent Events (SSE) for log streaming
if (req.url.startsWith('/es/log')) {
// Set SSE headers
+1 -49
View File
@@ -242,15 +242,6 @@ let countHardwarePoll = 0; // for during an upload
let pendingFirmwareMd5 = false; // set by a .md5 upload, cleared by the next .bin
const VALID_UPLOAD_EXTENSIONS = new Set(['bin', 'json', 'md5']);
const UPLOAD_PROGRESS_LOG_STEP = 10; // log every 10%
// A real device takes a while to accept a firmware image, so uploads are read at
// this rate instead of completing instantly. That keeps the WebUI progress bar on
// screen long enough to see. Lower it to watch the progress bar for longer.
const UPLOAD_SIMULATED_KB_PER_SEC = 2048;
const UPLOAD_SLICE_BYTES = 64 * 1024; // granularity of the simulated transfer
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
function parseMd5Digest(text: string): string | null {
const token = text.trim().split(/\s+/)[0];
@@ -260,45 +251,6 @@ function parseMd5Digest(text: string): string | null {
return null;
}
// Read the multipart body in slices at UPLOAD_SIMULATED_KB_PER_SEC, logging progress
// as it goes, then hand the buffered bytes back as FormData so they parse normally.
async function readUploadSlowly(request: Request): Promise<FormData> {
const contentType = request.headers.get('content-type') || '';
const expected = Number(request.headers.get('content-length') || '0');
const reader = request.body?.getReader();
const chunks: Uint8Array[] = [];
let received = 0;
let loggedPercentage = 0;
while (reader) {
const { done, value } = await reader.read();
if (done) {
break;
}
chunks.push(value);
// A fast local client arrives in very few chunks, so pace each one slice by
// slice to keep the reported progress gradual instead of jumping to 100%
for (let offset = 0; offset < value.length; offset += UPLOAD_SLICE_BYTES) {
const sliceLength = Math.min(UPLOAD_SLICE_BYTES, value.length - offset);
await sleep((sliceLength / (UPLOAD_SIMULATED_KB_PER_SEC * 1024)) * 1000);
received += sliceLength;
if (expected > 0) {
const percentage = Math.round((received / expected) * 100);
if (percentage >= loggedPercentage + UPLOAD_PROGRESS_LOG_STEP) {
loggedPercentage = percentage;
console.log(`Upload progress: ${percentage}%`);
}
}
}
}
return new Response(new Blob(chunks as BlobPart[]), {
headers: { 'content-type': contentType }
}).formData();
}
// DeviceTypes
const enum ScheduleFlag {
SCHEDULE_SUN = 1,
@@ -4702,7 +4654,7 @@ router
// SYSTEM and SETTINGS
router
.post(EMSESP_UPLOAD_FILE_ENDPOINT, async (request: Request) => {
const formData = await readUploadSlowly(request);
const formData = await request.formData();
const uploaded = formData.get('file');
if (!uploaded || typeof uploaded === 'string') {
return status(400);