基于Node/Electron环境实现Axios断点续传文件下载
Implementing Pause/Resume for File Downloads in Node/Electron
Great work getting the progress tracking up and running! Adding pause/resume functionality relies on HTTP Range requests (to fetch only the remaining bytes) and careful management of your download stream and request state. Let's adapt your existing code to support these features.
Key Concepts to Understand
- HTTP Range Headers: When resuming, we tell the server to send only the bytes we haven't downloaded yet using
Range: bytes=X-whereXis the number of bytes already received. - AbortController: Used to cancel the ongoing HTTP request when pausing (prevents unnecessary data from being downloaded).
- File Append Mode: Instead of overwriting the file each time, we'll open it in append mode (
flags: 'a') to add new bytes to the existing file. - State Tracking: We need to keep track of downloaded bytes, the active stream, and the abort controller to manage pause/resume actions.
Modified Code with Pause/Resume Support
Here's an updated version of your code with all the required features. I've added comments to explain each new part:
const Fs = require('fs'); const Path = require('path'); const Axios = require('axios'); // Track download state let startTime, endTime; let receivedBytes = 0; let writer = null; let abortController = null; let downloadUrl = ''; const outputPath = Path.resolve(__dirname, 'images', 'axios.jpg'); function startTimer() { startTime = new Date(); } function endTimer() { endTime = new Date(); const timeDiff = (endTime - startTime) / 1000; console.log(`Download completed in ${timeDiff} seconds`); } // Check if a partial download exists (for resume on app restart) function getExistingFileSize() { try { return Fs.statSync(outputPath).size; } catch (err) { // File doesn't exist yet return 0; } } async function downloadFile(url) { downloadUrl = url; // Load existing downloaded bytes if file exists receivedBytes = getExistingFileSize(); startTimer(); console.log(receivedBytes > 0 ? 'Resuming download…' : 'Starting download…'); abortController = new AbortController(); const signal = abortController.signal; try { const { data, headers } = await Axios({ url, method: 'GET', responseType: 'stream', signal, // Send Range header if we're resuming headers: receivedBytes > 0 ? { Range: `bytes=${receivedBytes}-` } : {} }); const totalLength = receivedBytes + parseInt(headers['content-length']); // Open file in append mode (create if it doesn't exist) writer = Fs.createWriteStream(outputPath, { flags: 'a' }); data.pipe(writer); data.on('data', (chunk) => { receivedBytes += chunk.length; showProgress(receivedBytes, totalLength); }); data.on('end', () => { endTimer(); console.log('Download finished!'); // Reset state after completion resetState(); }); data.on('error', (error) => { if (error.name === 'AbortError') { console.log('Download paused'); } else { console.log('Download error:', error); resetState(); } }); writer.on('error', (error) => { console.log('File write error:', error); abortController.abort(); resetState(); }); } catch (err) { if (err.name !== 'AbortError') { console.log('Request failed:', err); resetState(); } } } function pauseDownload() { if (abortController) { abortController.abort(); if (writer) { writer.close(); // Close the file stream to prevent corruption } } } function resumeDownload() { if (downloadUrl) { downloadFile(downloadUrl); } } function showProgress(received, total) { const percentage = ((received / total) * 100).toFixed(2); console.log(`${percentage}% | ${received} bytes out of ${total} bytes`); } function resetState() { abortController = null; writer = null; } // Example usage: downloadFile('https://unsplash.com/photos/AaEQmoufHLk/download?force=true'); // To pause after 5 seconds (for testing): // setTimeout(pauseDownload, 5000); // To resume after another 5 seconds: // setTimeout(resumeDownload, 10000);
How It Works
- State Tracking: We maintain variables for
receivedBytes,abortController, andwriterto manage the current download state. - Resume on Startup: The
getExistingFileSizefunction checks if a partial file exists, so we can pick up where we left off even if the app restarts. - Pause: Calling
pauseDownload()usesAbortControllerto cancel the HTTP request and closes the file stream to prevent corruption. - Resume:
resumeDownload()re-callsdownloadFile, which sends a Range header to fetch only the remaining bytes and appends them to the existing file. - Error Handling: We differentiate between intentional aborts (pauses) and other errors to keep the state clean.
Notes for Electron
- If you're building a UI, you can bind
pauseDownload()andresumeDownload()to button click events directly. - For better persistence across app restarts, save the
receivedBytesvalue to a config file (e.g., usingelectron-store) so you can resume downloads even after the app is closed and reopened. - Verify the server supports Range requests first (most modern servers do, but you can check with a
HEADrequest to confirm theAccept-Rangesheader is present).
内容的提问来源于stack exchange,提问作者Cristian Muscalu
相关产品推荐
相关产品推荐

