From f11a8df64c6e297df364608f1242207edfe5d654 Mon Sep 17 00:00:00 2001 From: Hugo Montero Date: Thu, 30 Jan 2025 08:32:32 -0600 Subject: [PATCH] implement cache limit validation --- settings.js | 3 +- src/lib/download-manager.js | 63 ++++++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/settings.js b/settings.js index e2f7d039b..ab1aeb04e 100644 --- a/settings.js +++ b/settings.js @@ -41,7 +41,8 @@ let settings = { 'tinker': true }, - tachyonMeta: 'https://tachyon-ci.particle.io/meta' + tachyonMeta: 'https://tachyon-ci.particle.io/meta', + tachyonCacheLimit: 10 }; function envValue(varName, defaultValue) { diff --git a/src/lib/download-manager.js b/src/lib/download-manager.js index f31866c5c..1c9641e1d 100644 --- a/src/lib/download-manager.js +++ b/src/lib/download-manager.js @@ -59,6 +59,7 @@ class DownloadManager { this.ui.write(`Using cached file: ${cachedFile}`); return cachedFile; } + await this._validateCacheLimit({ url, currentFileName: outputFileName }); const filePath = await this._downloadFile(url, outputFileName, options); // Validate checksum after download completes // Validate checksum after download completes @@ -96,6 +97,53 @@ class DownloadManager { } } + async _validateCacheLimit({ url, maxCacheSize = settings.tachyonCacheLimit, currentFileName }) { + const fileHeaders = await fetch(url, { method: 'HEAD' }); + + const contentLength = parseInt(fileHeaders.headers.get('content-length') || '0', 10); + // get the size of the download directory + const downloadDirStats = await this.getDownloadedFilesStats(currentFileName); + const totalSize = (downloadDirStats.totalSize + contentLength) / (1024 * 1024 * 1024); // convert to GB + if (totalSize > maxCacheSize && downloadDirStats.fileStats.length > 0) { + // prompt to delete files + const question = { + type: 'confirm', + name: 'cleanCache', + message: 'Downloaded files exceed the cache limit. Do you want to clean up the cache?', + default: true + }; + const answer = await this.ui.prompt(question); + if (answer.cleanCache) { + await this._freeCacheSpace(downloadDirStats); + } else { + // remove files until the cache limit is satisfied + this.ui.write('Cache cleanup skipped. Remove files manually to free up space.'); + } + } + return; + } + + async getDownloadedFilesStats(currentFile) { + const files = await fs.readdir(this.downloadDir); + // filter out the current file in progress + const filteredFiles = currentFile ? files.filter(file => file !== currentFile && file !== `${currentFile}.progress`) : files; + // sort files by date modified + const sortedFileStats = filteredFiles.sort((a, b) => { + const aStats = fs.statSync(path.join(this.downloadDir, a)); + const bStats = fs.statSync(path.join(this.downloadDir, b)); + return aStats.mtime - bStats.mtime; + }); + const fileStats = await Promise.all(sortedFileStats.map(async (file) => { + const filePath = path.join(this.downloadDir, file); + const stats = await fs.stat(filePath); + return { filePath, size: stats.size }; + })); + return { + totalSize: fileStats.reduce((sum, file) => sum + file.size, 0), + fileStats + }; + } + async _attemptDownload(url, outputFileName, progressFilePath, finalFilePath, timeout) { const progressBar = this.ui.createProgressBar(); let downloadedBytes = 0; @@ -118,7 +166,6 @@ class DownloadManager { } 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) { @@ -219,6 +266,20 @@ class DownloadManager { throw error; } } + + async _freeCacheSpace(downloadDirStats) { + const { fileStats, totalSize } = downloadDirStats; + let deletedSize = 0; + for (const file of fileStats) { + const { filePath, size } = file; + await fs.remove(filePath); + this.ui.write(`Removed file: ${file.filePath}`); + deletedSize += size; + if ((totalSize - deletedSize) <= settings.tachyonCacheLimit) { + return; + } + } + } } module.exports = DownloadManager;