Skip to content

Commit

Permalink
add checksum validation
Browse files Browse the repository at this point in the history
  • Loading branch information
hugomontero committed Jan 24, 2025
1 parent c45601b commit c6bfb1c
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 5 deletions.
34 changes: 29 additions & 5 deletions src/lib/download-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const fs = require('fs-extra');
const path = require('path');
const fetch = require('node-fetch');
const UI = require('./ui');
const crypto = require('crypto');

class DownloadManager {
/**
Expand Down Expand Up @@ -45,10 +46,11 @@ class DownloadManager {
}
}

async _downloadFile(fileUrl, outputFileName) {
async _downloadFile(fileUrl, outputFileName, expectedChecksum) {
const tempFilePath = path.join(this._tempDir, outputFileName);
const finalFilePath = path.join(this._downloadDir, outputFileName);
const baseUrl = this.channel.url;
const progressBar = this.ui.createProgressBar();
const url = `${baseUrl}/${fileUrl}`;
// TODO (hmontero): Implement cache for downloaded files
const cachedFile = await this.getCachedFile(outputFileName);
Expand All @@ -61,7 +63,7 @@ class DownloadManager {
let downloadedBytes = 0;
if (fs.existsSync(tempFilePath)) {
downloadedBytes = fs.statSync(tempFilePath).size;
this.ui.write(`Resuming download from byte ${downloadedBytes}`);
this.ui.write(`Resuming download file: ${outputFileName}`);
}

const headers = downloadedBytes > 0 ? { Range: `bytes=${downloadedBytes}-` } : {};
Expand All @@ -70,20 +72,42 @@ class DownloadManager {
if (!response.ok && response.status !== 206) {
throw new Error(`Unexpected response status: ${response.status}`);
}

const totalBytes = parseInt(response.headers.get('content-length') || '0', 10);
if (progressBar && totalBytes) {
progressBar.start(totalBytes, downloadedBytes, { description: `Downloading ${outputFileName} ...` });
}
const writer = fs.createWriteStream(tempFilePath, { flags: 'a' });
await new Promise((resolve, reject) => {
const hash = crypto.createHash('sha256');
response.body.on('data', (chunk) => {
downloadedBytes += chunk.length;
hash.update(chunk);
if (progressBar) {
progressBar.increment(chunk.length);
}
});
response.body.pipe(writer);
response.body.on('error', reject);
writer.on('finish', resolve);
writer.on('finish', async () => {
const fileChecksum = hash.digest('hex');
if (expectedChecksum && fileChecksum !== expectedChecksum) {
// if we don't remove the file, the next time we try to download it, it will be resumed
await fs.remove(tempFilePath);
return reject(new Error(`Checksum validation failed for ${outputFileName}`));
}
resolve();
});
});

// Move temp file to final location
fs.renameSync(tempFilePath, finalFilePath);
this.ui.write(`Download completed: ${finalFilePath}`);
} catch (error) {
this.ui.error(`Error downloading file from ${url}: ${error.message}`);
throw error;
} finally {
if (progressBar) {
progressBar.stop();
}
}
}

Expand Down
39 changes: 39 additions & 0 deletions src/lib/download-manager.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ describe('DownloadManager', () => {
ui = sinon.createStubInstance(UI, {
write: sinon.stub(),
error: sinon.stub(),
createProgressBar: sinon.stub(),
});
downloadManager = new DownloadManager(channel, ui);
});
Expand Down Expand Up @@ -176,6 +177,44 @@ describe('DownloadManager', () => {
expect(error.message).to.include('Unexpected response status: 500');
}
});

it('throws an error if checksum does not match', async () => {
const fileUrl = 'file.txt';
const outputFileName = 'file.txt';
const fileContent = 'This is a test file.';

// Mock the HTTP response
nock(channel.url)
.get(`/${fileUrl}`)
.reply(200, fileContent);

try {
await downloadManager._downloadFile(fileUrl, outputFileName, 'invalidchecksum');
throw new Error('Expected method to throw.');
} catch (error) {
expect(error.message).to.include('Checksum validation failed for file.txt');
const tempPath = path.join(downloadManager.tempDir, outputFileName);
expect(fs.existsSync(tempPath)).to.be.false;
}
});
it('validates checksum and save the file', async () => {
const fileUrl = 'file.txt';
const outputFileName = 'file.txt';
const fileContent = 'This is a test file.';
const checksum = 'f29bc64a9d3732b4b9035125fdb3285f5b6455778edca72414671e0ca3b2e0de';

// Mock the HTTP response
nock(channel.url)
.get(`/${fileUrl}`)
.reply(200, fileContent);

await downloadManager._downloadFile(fileUrl, outputFileName, checksum);

const finalFilePath = path.join(downloadManager.downloadDir, outputFileName);
expect(fs.existsSync(finalFilePath)).to.be.true;
const content = fs.readFileSync(finalFilePath, 'utf8');
expect(content).to.equal(fileContent);
});
});

});

0 comments on commit c6bfb1c

Please sign in to comment.