-
Notifications
You must be signed in to change notification settings - Fork 92
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
wip: add download manager allow resume downloads
- Loading branch information
1 parent
7bf69e9
commit 3f5d0fc
Showing
3 changed files
with
209 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}); | ||
}); | ||
|
||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}; |