Skip to content

Commit

Permalink
refactor download method
Browse files Browse the repository at this point in the history
  • Loading branch information
hugomontero committed Jan 28, 2025
1 parent a6ae2d7 commit 8e89478
Showing 1 changed file with 64 additions and 66 deletions.
130 changes: 64 additions & 66 deletions src/lib/download-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,89 +77,87 @@ class DownloadManager {
}


// eslint-disable-next-line max-statements
async _downloadFile(url, outputFileName, options = {}) {
const { maxRetries = 5, timeout = 10000, waitTime = 5000 } = options;
const progressFilePath = path.join(this.downloadDir, `${outputFileName}.progress`);
const finalFilePath = path.join(this.downloadDir, outputFileName);
const progressBar = this.ui.createProgressBar();
let attempt = 0;

while (attempt < maxRetries) {
try {
let downloadedBytes = 0;
if (fs.existsSync(progressFilePath)) {
downloadedBytes = fs.statSync(progressFilePath).size;
this.ui.write(`Resuming download file: ${outputFileName}`);
}

const headers = downloadedBytes > 0 ? { Range: `bytes=${downloadedBytes}-` } : {};
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);

const response = await fetch(url, { headers, signal: controller.signal });
clearTimeout(timeoutId); // Clear timeout when fetch completes successfully

if (!response.ok && response.status !== 206) {
throw new Error(`Unexpected response status: ${response.status}`);
}

const totalBytes = parseInt(response.headers.get('content-length') || '0', 10) + downloadedBytes;
if (progressBar && totalBytes) {
progressBar.start(totalBytes, downloadedBytes, { description: `Downloading ${outputFileName} ...` });
}

const writer = fs.createWriteStream(progressFilePath, { flags: 'a' });

await new Promise((resolve, reject) => {
let streamTimeout = setTimeout(() => {
controller.abort(); // Abort the stream if no data is received
reject(new Error('Stream timeout'));
}, timeout);

response.body.on('data', (chunk) => {
clearTimeout(streamTimeout); // Reset timeout on data receipt
streamTimeout = setTimeout(() => {
controller.abort();
reject(new Error('Stream timeout'));
}, timeout);

downloadedBytes += chunk.length;
if (progressBar) {
progressBar.increment(chunk.length);
}
});

response.body.pipe(writer);
response.body.on('error', (err) => {
clearTimeout(streamTimeout);
reject(err);
});

writer.on('finish', () => {
clearTimeout(streamTimeout);
resolve();
});
});

// Rename progress file to final file
fs.renameSync(progressFilePath, finalFilePath);
this.ui.write(`Download completed: ${finalFilePath}`);
return finalFilePath;
return await this._attemptDownload(url, outputFileName, progressFilePath, finalFilePath, timeout);
} catch (error) {
attempt++;
if (attempt >= maxRetries) {
throw new Error(`Failed to download file after ${maxRetries} attempts: ${error.message}`);
}
this.ui.write(`Retrying download for ${outputFileName} after waiting for ${waitTime}ms...`);
await delay(waitTime);
} finally {
if (progressBar) {
progressBar.stop();
}
}
}
}

async _attemptDownload(url, outputFileName, progressFilePath, finalFilePath, timeout) {
const progressBar = this.ui.createProgressBar();
let downloadedBytes = 0;
if (fs.existsSync(progressFilePath)) {
downloadedBytes = fs.statSync(progressFilePath).size;
this.ui.write(`Resuming download file: ${outputFileName}`);
}
const headers = downloadedBytes > 0 ? { Range: `bytes=${downloadedBytes}-` } : {};
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, { headers, signal: controller.signal });
clearTimeout(timeoutId);
if (!response.ok && response.status !== 206) {
throw new Error(`Unexpected response status: ${response.status}`);
}
const totalBytes = parseInt(response.headers.get('content-length') || '0', 10) + downloadedBytes;
if (progressBar && totalBytes) {
progressBar.start(totalBytes, downloadedBytes, { description: `Downloading ${outputFileName} ...` });
}
await this._streamToFile(response.body, progressFilePath, progressBar, downloadedBytes, timeout, controller);
fs.renameSync(progressFilePath, finalFilePath);
this.ui.write(`Download completed: ${finalFilePath}`);
return finalFilePath;
} finally {
if (progressBar) {
progressBar.stop();
}
}
}

async _streamToFile(stream, filePath, progressBar, downloadedBytes, timeout, controller) {
const writer = fs.createWriteStream(filePath, { flags: 'a' });
return new Promise((resolve, reject) => {
let streamTimeout = setTimeout(() => {
controller.abort();
reject(new Error('Stream timeout'));
}, timeout);
stream.on('data', (chunk) => {
clearTimeout(streamTimeout);
streamTimeout = setTimeout(() => {
controller.abort();
reject(new Error('Stream timeout'));
}, timeout);
downloadedBytes += chunk.length;
if (progressBar) {
progressBar.increment(chunk.length);
}
});
stream.pipe(writer);
stream.on('error', (err) => {
clearTimeout(streamTimeout);
reject(err);
});
writer.on('finish', () => {
clearTimeout(streamTimeout);
resolve();
});
});
}


async _getCachedFile(fileName, expectedChecksum) {
const cachedFilePath = path.join(this.downloadDir, fileName);
if (fs.existsSync(cachedFilePath)) {
Expand Down

0 comments on commit 8e89478

Please sign in to comment.