111 lines
3.4 KiB
JavaScript
111 lines
3.4 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
|
|
const MAX_RETRIES = 2;
|
|
const RETRY_DELAY_MS = 1000;
|
|
const PROGRESS_INTERVAL_MS = 250;
|
|
|
|
const sleep = (milliseconds) => new Promise((resolve) => {
|
|
setTimeout(resolve, milliseconds);
|
|
});
|
|
|
|
const copyWithProgress = async ({ source, destination, jobLog, updateProgress, label }) => {
|
|
const sourceSize = fs.statSync(source).size;
|
|
const sourceSizeMb = sourceSize / 1024 / 1024;
|
|
let lastError;
|
|
|
|
const report = (progress, message) => {
|
|
if (typeof updateProgress === 'function') {
|
|
updateProgress({ progress, jobLog: message });
|
|
}
|
|
if (typeof jobLog === 'function') jobLog(`${message}\n`);
|
|
};
|
|
|
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt += 1) {
|
|
if (attempt > 0) {
|
|
report(0, `[${label}] Retrying copy (${attempt}/${MAX_RETRIES})...`);
|
|
await sleep(RETRY_DELAY_MS);
|
|
}
|
|
|
|
try {
|
|
if (fs.existsSync(destination)) fs.unlinkSync(destination);
|
|
|
|
const startedAt = Date.now();
|
|
let lastReportedPercent = -1;
|
|
let previousBytes = 0;
|
|
let previousAt = startedAt;
|
|
let copying = true;
|
|
|
|
const progressTimer = setInterval(() => {
|
|
if (!copying || !fs.existsSync(destination)) return;
|
|
let copiedBytes;
|
|
try {
|
|
copiedBytes = fs.statSync(destination).size;
|
|
} catch (error) {
|
|
if (error.code === 'ENOENT') return;
|
|
return;
|
|
}
|
|
const percent = sourceSize === 0
|
|
? 99
|
|
: Math.min(Math.floor((copiedBytes / sourceSize) * 100), 99);
|
|
if (percent <= lastReportedPercent) return;
|
|
|
|
const now = Date.now();
|
|
const intervalSeconds = Math.max((now - previousAt) / 1000, 0.001);
|
|
const intervalSpeed = (copiedBytes - previousBytes) / 1024 / 1024 / intervalSeconds;
|
|
report(
|
|
percent,
|
|
`[${label}] Progress: ${percent}% `
|
|
+ `(${(copiedBytes / 1024 / 1024).toFixed(1)}/${sourceSizeMb.toFixed(1)} MB, `
|
|
+ `${intervalSpeed.toFixed(1)} MB/s)`,
|
|
);
|
|
lastReportedPercent = percent;
|
|
previousBytes = copiedBytes;
|
|
previousAt = now;
|
|
}, PROGRESS_INTERVAL_MS);
|
|
|
|
try {
|
|
await fs.promises.copyFile(source, destination);
|
|
} finally {
|
|
copying = false;
|
|
clearInterval(progressTimer);
|
|
}
|
|
|
|
const destinationSize = fs.statSync(destination).size;
|
|
if (destinationSize !== sourceSize) {
|
|
throw new Error(
|
|
`Copy size mismatch: expected ${sourceSize} bytes, got ${destinationSize}`,
|
|
);
|
|
}
|
|
|
|
const elapsedSeconds = Math.max((Date.now() - startedAt) / 1000, 0.001);
|
|
const averageSpeed = sourceSize / 1024 / 1024 / elapsedSeconds;
|
|
report(
|
|
100,
|
|
`[${label}] Done: ${sourceSizeMb.toFixed(1)} MB copied in `
|
|
+ `${elapsedSeconds.toFixed(1)}s (${averageSpeed.toFixed(1)} MB/s average)`,
|
|
);
|
|
return;
|
|
} catch (error) {
|
|
lastError = error;
|
|
try {
|
|
if (fs.existsSync(destination)) fs.unlinkSync(destination);
|
|
} catch (cleanupError) {
|
|
lastError = new Error(
|
|
`${error.message}; could not remove partial copy: ${cleanupError.message}`,
|
|
);
|
|
}
|
|
if (attempt < MAX_RETRIES && typeof jobLog === 'function') {
|
|
jobLog(`[${label}] Copy attempt ${attempt + 1} failed: ${lastError.message}\n`);
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new Error(
|
|
`[${label}] Copy failed after ${MAX_RETRIES + 1} attempts: ${lastError.message}`,
|
|
);
|
|
};
|
|
|
|
module.exports = { copyWithProgress };
|