UNPKG

git-ripper

Version:

CLI tool that lets you download specific folders from GitHub repositories without cloning the entire repo.

186 lines (159 loc) 6.47 kB
import fs from 'fs'; import path from 'path'; import archiver from 'archiver'; import chalk from 'chalk'; /** * Validates the output path for an archive file * @param {string} outputPath - Path where the archive should be saved * @returns {boolean} - True if the path is valid, throws an error otherwise * @throws {Error} - If the output path is invalid */ const validateArchivePath = (outputPath) => { // Check if path is provided if (!outputPath) { throw new Error('Output path is required'); } // Check if the output directory exists or can be created const outputDir = path.dirname(outputPath); try { if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } // Check if the directory is writable fs.accessSync(outputDir, fs.constants.W_OK); // Check if file already exists and is writable if (fs.existsSync(outputPath)) { fs.accessSync(outputPath, fs.constants.W_OK); // File exists and is writable, so we'll overwrite it console.warn(chalk.yellow(`Warning: File ${outputPath} already exists and will be overwritten`)); } return true; } catch (error) { if (error.code === 'EACCES') { throw new Error(`Permission denied: Cannot write to ${outputPath}`); } throw new Error(`Invalid output path: ${error.message}`); } }; /** * Creates an archive (zip or tar) from a directory * * @param {string} sourceDir - Source directory to archive * @param {string} outputPath - Path where the archive should be saved * @param {object} options - Archive options * @param {string} options.format - Archive format ('zip' or 'tar') * @param {number} options.compressionLevel - Compression level (0-9, default: 6) * @returns {Promise<string>} - Path to the created archive */ export const createArchive = (sourceDir, outputPath, options = {}) => { return new Promise((resolve, reject) => { try { const { format = 'zip', compressionLevel = 6 } = options; // Validate source directory if (!fs.existsSync(sourceDir)) { return reject(new Error(`Source directory does not exist: ${sourceDir}`)); } const stats = fs.statSync(sourceDir); if (!stats.isDirectory()) { return reject(new Error(`Source path is not a directory: ${sourceDir}`)); } // Validate output path validateArchivePath(outputPath); // Ensure the output directory exists const outputDir = path.dirname(outputPath); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } // Create output stream const output = fs.createWriteStream(outputPath); let archive; // Create the appropriate archive type if (format === 'zip') { archive = archiver('zip', { zlib: { level: compressionLevel } }); } else if (format === 'tar') { archive = archiver('tar'); // Use gzip compression for tar if compressionLevel > 0 if (compressionLevel > 0) { archive = archiver('tar', { gzip: true, gzipOptions: { level: compressionLevel } }); } } else { return reject(new Error(`Unsupported archive format: ${format}`)); } // Listen for archive events output.on('close', () => { const size = archive.pointer(); console.log(chalk.green(`✓ Archive created: ${outputPath} (${(size / 1024 / 1024).toFixed(2)} MB)`)); resolve(outputPath); }); archive.on('error', (err) => { reject(err); }); archive.on('warning', (err) => { if (err.code === 'ENOENT') { console.warn(chalk.yellow(`Warning: ${err.message}`)); } else { reject(err); } }); // Pipe archive data to the output file archive.pipe(output); // Add the directory contents to the archive archive.directory(sourceDir, false); // Finalize the archive archive.finalize(); } catch (error) { reject(error); } }); }; /** * Downloads folder contents and creates an archive * * @param {object} repoInfo - Repository information object * @param {string} outputDir - Directory where files should be downloaded * @param {string} archiveFormat - Archive format ('zip' or 'tar') * @param {string} archiveName - Custom name for the archive file * @param {number} compressionLevel - Compression level (0-9) * @returns {Promise<string>} - Path to the created archive */ export const downloadAndArchive = async (repoInfo, outputDir, archiveFormat = 'zip', archiveName = null, compressionLevel = 6) => { const { downloadFolder } = await import('./downloader.js'); console.log(chalk.cyan(`Downloading folder and preparing to create ${archiveFormat.toUpperCase()} archive...`)); // Create a temporary directory for the download const tempDir = path.join(outputDir, `.temp-${Date.now()}`); fs.mkdirSync(tempDir, { recursive: true }); try { // Download the folder contents await downloadFolder(repoInfo, tempDir); // Determine archive filename let archiveFileName = archiveName; if (!archiveFileName) { const { owner, repo, folderPath } = repoInfo; const folderName = folderPath ? folderPath.split('/').pop() : repo; archiveFileName = `${folderName || repo}-${owner}`; } // Add extension if not present if (!archiveFileName.endsWith(`.${archiveFormat}`)) { archiveFileName += `.${archiveFormat}`; } const archivePath = path.join(outputDir, archiveFileName); // Create the archive console.log(chalk.cyan(`Creating ${archiveFormat.toUpperCase()} archive...`)); await createArchive(tempDir, archivePath, { format: archiveFormat, compressionLevel }); return archivePath; } catch (error) { throw new Error(`Failed to create archive: ${error.message}`); } finally { // Clean up temporary directory try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch (err) { console.warn(chalk.yellow(`Warning: Failed to clean up temporary directory: ${err.message}`)); } } };