Skip to content

Commit

Permalink
wip: add download manager allow resume downloads
Browse files Browse the repository at this point in the history
  • Loading branch information
hugomontero committed Jan 22, 2025
1 parent 7bf69e9 commit 3f5d0fc
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 0 deletions.
103 changes: 103 additions & 0 deletions src/lib/download-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
const { ensureFolder } = require('../../settings');
const fs = require('fs-extra');
const path = require('path');
const https = require('https');

class DownloadManager {
/**
* @param {Object} channel
* @param {string} channel.name - The name of the channel
* @param {string} channel.url - The URL of the channel
*/
constructor(channel) {
const particleDir = ensureFolder();
if (!channel) {
throw new Error('Channel is required');
}
this.channel = channel;
this._baseDir = path.join(particleDir, 'channels', this.channel.name);
this._tempDir = path.join(this._baseDir, 'tmp');
this._downloadDir = path.join(this._baseDir, 'downloads');
this._ensureWorkDir();

}

get tempDir() {
return this._tempDir;
}

get downloadDir() {
return this._downloadDir;
}

_ensureWorkDir() {
try {
// Create the temp directory if it doesn't exist
fs.mkdirSync(this._tempDir, { recursive: true });
// Create the download directory if it doesn't exist
fs.mkdirSync(this._downloadDir, { recursive: true });
} catch (error) {
console.error(`Error creating directories for channel "${this.channel.name}":`, error.message);
throw error;
}
}

async download(fileUrl, outputFileName) {
const tempFilePath = path.join(this._tempDir, outputFileName);
const finalFilePath = path.join(this._downloadDir, outputFileName);

try {
let downloadedBytes = 0;
if (fs.existsSync(tempFilePath)) {
downloadedBytes = fs.statSync(tempFilePath).size;
console.log(`Resuming download from byte ${downloadedBytes}`);
}

const options = new URL(fileUrl);
options.headers = { Range: `bytes=${downloadedBytes}-` };

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

await new Promise((resolve, reject) => {
https.get(options, (response) => {
if (response.statusCode !== 200 && response.statusCode !== 206) {
return reject(new Error(`Unexpected response status: ${response.statusCode}`));
}

response.pipe(writer);

writer.on('finish', resolve);
writer.on('error', reject);
});
});

// Move temp file to final location
fs.renameSync(tempFilePath, finalFilePath);
console.log(`Download completed: ${finalFilePath}`);
} catch (error) {
console.error(`Error downloading file from ${fileUrl}:`, error.message);
throw error;
}
}

async cleanup({ fileName, cleanTemp = true, cleanDownload = true } = {}) {
try {
if (fileName) {
await fs.remove(path.join(this._downloadDir, fileName));
} else {
if (cleanTemp) {
await fs.remove(this._tempDir);
}
if (cleanDownload) {
await fs.remove(this._downloadDir);
}
}
} catch (error) {
console.error(`Error cleaning up temp directory for channel "${this.channel.name}":`, error.message);
throw error;
}
}

}

module.exports = DownloadManager;
90 changes: 90 additions & 0 deletions src/lib/download-manager.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
const { expect, sinon } = require('../../test/setup');
const DownloadManager = require('./download-manager');
const fs = require('fs-extra');
const path = require('path');
const { PATH_TMP_DIR } = require('../../test/lib/env');

describe('DownloadManager', () => {
const originalEnv = process.env;

beforeEach(() => {
process.env = {
...originalEnv,
home: PATH_TMP_DIR,
};
});

afterEach(async () => {
sinon.restore();
process.env = originalEnv;
await fs.remove(path.join(PATH_TMP_DIR, '.particle/channels/test'));
});

describe('initialize', () => {
it('creates a download manager', () => {
const channel = {
name: 'test',
url: 'http://example.com'
};
const downloadManager = new DownloadManager(channel);
expect(downloadManager.channel).to.eql(channel);
expect(downloadManager.tempDir).to.eql(path.join(PATH_TMP_DIR, '.particle/channels/test/tmp'));
expect(downloadManager.downloadDir).to.eql(path.join(PATH_TMP_DIR,'.particle/channels/test/downloads'));
expect(fs.existsSync(downloadManager.tempDir)).to.be.true;
expect(fs.existsSync(downloadManager.downloadDir)).to.be.true;
});

it('throws an error if channel is not provided', () => {
expect(() => new DownloadManager()).to.throw('Channel is required');
});
});

describe('cleanup', () => {
it('removes the download directory', async () => {
const channel = {
name: 'test',
url: 'http://example.com'
};
const downloadManager = new DownloadManager(channel);
await downloadManager.cleanup();
expect(fs.existsSync(downloadManager.tempDir)).to.be.false;
expect(fs.existsSync(downloadManager.downloadDir)).to.be.false;
});

it('removes a specific file from the download directory', async () => {
const channel = {
name: 'test',
url: 'http://example.com'
};
const downloadManager = new DownloadManager(channel);
const testFile = path.join(downloadManager.downloadDir, 'test.bin');
await fs.ensureFile(testFile);
expect(fs.existsSync(testFile)).to.be.true;
await downloadManager.cleanup({ fileName: 'test.bin' });
expect(fs.existsSync(testFile)).to.be.false;
});

it('removes only the temp directory when specified', async () => {
const channel = {
name: 'test',
url: 'http://example.com'
};
const downloadManager = new DownloadManager(channel);
await downloadManager.cleanup({ cleanTemp: true, cleanDownload: false });
expect(fs.existsSync(downloadManager.tempDir)).to.be.false;
expect(fs.existsSync(downloadManager.downloadDir)).to.be.true;
});

it('removes only the download directory when specified', async () => {
const channel = {
name: 'test',
url: 'http://example.com'
};
const downloadManager = new DownloadManager(channel);
await downloadManager.cleanup({ cleanTemp: false, cleanDownload: true });
expect(fs.existsSync(downloadManager.tempDir)).to.be.true;
expect(fs.existsSync(downloadManager.downloadDir)).to.be.false;
});
});

});
16 changes: 16 additions & 0 deletions src/lib/tachyon-downloader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const DownloadManager = require('./download-manager');
const { channels } = require('../../settings');
async function downloadBinary ({ channelName, version, board, region, variant }) {
const channel = channels[channelName] || channels.production;
const downloadManager = new DownloadManager(channel);
const downloadDir = downloadManager.downloadDir;
console.log(`Downloading ${region} binary for ${board} from ${channel.url}`);
console.log(`Version: ${version}`);
console.log(`Destination: ${downloadDir}`);
console.log(`Variant: ${variant}`);
}


module.exports = {
downloadBinary
};

0 comments on commit 3f5d0fc

Please sign in to comment.