'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 };