Skip to content

Instantly share code, notes, and snippets.

@manzke
Created December 9, 2025 10:29
Show Gist options
  • Select an option

  • Save manzke/8765077618bcbd9bcd5c0b62453cd487 to your computer and use it in GitHub Desktop.

Select an option

Save manzke/8765077618bcbd9bcd5c0b62453cd487 to your computer and use it in GitHub Desktop.
Get a report of files in your nexus / maven repository as well as a clean up script to purge certain versions
#!/usr/bin/env node
/**
* Nexus Repository Analyzer
* Analyzes Nexus Maven repositories and generates a CSV report of artifacts by size
*/
const https = require('https');
const http = require('http');
const fs = require('fs');
const { URL } = require('url');
// Generate timestamped filename
function getTimestampedFilename(baseFilename) {
const timestamp = new Date().toISOString()
.replace(/:/g, '-') // Replace colons with hyphens
.replace(/\..+/, '') // Remove milliseconds
.replace('T', '_'); // Replace T with underscore
// If baseFilename has an extension, insert timestamp before it
const lastDot = baseFilename.lastIndexOf('.');
if (lastDot > 0) {
return baseFilename.substring(0, lastDot) + '_' + timestamp + baseFilename.substring(lastDot);
}
return baseFilename + '_' + timestamp;
}
// Configuration - Update these values
const config = {
nexusUrl: process.env.NEXUS_URL || 'https://nexus.local.io',
username: process.env.NEXUS_USERNAME || 'USER_NAME',
password: process.env.NEXUS_PASSWORD || 'SECRET_PASSWORD',
repository: process.env.NEXUS_REPO || '', // Leave empty to scan all repos
ignoreRepos: process.env.IGNORE_REPOS ? process.env.IGNORE_REPOS.split(',') : ['public'], // Comma-separated list of repos to ignore
sortBySize: process.env.SORT_BY_SIZE !== 'false', // Set to 'false' to disable sorting
outputFile: process.env.OUTPUT_FILE || getTimestampedFilename('nexus-artifacts.csv'),
pageSize: 1000, // Number of items per API request
};
// Helper function to make HTTP requests
function makeRequest(url, auth) {
return new Promise((resolve, reject) => {
const urlObj = new URL(url);
const isHttps = urlObj.protocol === 'https:';
const client = isHttps ? https : http;
const options = {
hostname: urlObj.hostname,
port: urlObj.port || (isHttps ? 443 : 80),
path: urlObj.pathname + urlObj.search,
method: 'GET',
headers: {
'Authorization': 'Basic ' + Buffer.from(`${auth.username}:${auth.password}`).toString('base64'),
'Accept': 'application/json'
}
};
const req = client.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
resolve(JSON.parse(data));
} catch (e) {
reject(new Error(`Failed to parse JSON: ${e.message}`));
}
} else {
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
}
});
});
req.on('error', reject);
req.end();
});
}
// Check if repository should be ignored
function shouldIgnoreRepo(repoName) {
return config.ignoreRepos.some(pattern => {
// Support wildcards and regex-like patterns
const regexPattern = pattern.replace(/\*/g, '.*');
const regex = new RegExp(`^${regexPattern}$`, 'i');
return regex.test(repoName);
});
}
// Get list of repositories
async function getRepositories() {
console.log('Fetching repository list...');
const url = `${config.nexusUrl}/service/rest/v1/repositories`;
const repos = await makeRequest(url, config);
// Filter for Maven repositories and apply ignore patterns
const filtered = repos.filter(repo =>
repo.format === 'maven2' &&
(config.repository === '' || repo.name === config.repository) &&
!shouldIgnoreRepo(repo.name)
);
if (config.ignoreRepos.length > 0) {
const ignored = repos.filter(repo => shouldIgnoreRepo(repo.name));
if (ignored.length > 0) {
console.log(`Ignoring ${ignored.length} repositories: ${ignored.map(r => r.name).join(', ')}`);
}
}
return filtered;
}
// Get all components from a repository with pagination
async function getComponents(repositoryName) {
console.log(`Fetching components from repository: ${repositoryName}...`);
const components = [];
let continuationToken = null;
do {
const url = `${config.nexusUrl}/service/rest/v1/components?repository=${repositoryName}` +
(continuationToken ? `&continuationToken=${continuationToken}` : '');
const response = await makeRequest(url, config);
if (response.items) {
components.push(...response.items);
console.log(` Retrieved ${components.length} components so far...`);
}
continuationToken = response.continuationToken;
} while (continuationToken);
return components;
}
// Get detailed asset information for a component
async function getAssets(componentId) {
const url = `${config.nexusUrl}/service/rest/v1/assets?repository=${config.repository}`;
const response = await makeRequest(url, config);
return response.items.filter(asset => asset.componentId === componentId);
}
// Format bytes to human-readable format
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Generate artifact URL
function generateArtifactUrl(repositoryName, group, name, version) {
// Convert group to path (replace dots with slashes)
const groupPath = group ? group.replace(/\./g, '/') : '';
// Construct the repository browser URL
return `${config.nexusUrl}/#browse/browse:${repositoryName}`;
}
// Escape CSV field
function escapeCsvField(field) {
if (field === null || field === undefined) return '';
const str = String(field);
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
return `"${str.replace(/"/g, '""')}"`;
}
return str;
}
// Main function
async function analyze() {
const startTime = new Date();
console.log('=== Nexus Repository Analyzer ===\n');
console.log(`Run Date: ${startTime.toISOString()}`);
console.log(`Nexus URL: ${config.nexusUrl}`);
console.log(`Output file: ${config.outputFile}`);
console.log(`Sort by size: ${config.sortBySize}`);
console.log(`Ignore repos: ${config.ignoreRepos.join(', ')}\n`);
try {
const repositories = await getRepositories();
console.log(`Found ${repositories.length} Maven repositories to analyze\n`);
if (repositories.length === 0) {
console.log('No repositories found. Check your configuration and credentials.');
process.exit(1);
}
const allArtifacts = [];
// Process each repository
for (const repo of repositories) {
console.log(`\n--- Processing repository: ${repo.name} ---`);
const components = await getComponents(repo.name);
// Extract artifact information
for (const component of components) {
// Calculate total size of all assets in this component
let totalSize = 0;
if (component.assets) {
component.assets.forEach(asset => {
totalSize += asset.fileSize || 0;
});
}
allArtifacts.push({
repository: repo.name,
group: component.group || '',
name: component.name || '',
version: component.version || '',
format: component.format || '',
assetCount: component.assets ? component.assets.length : 0,
sizeBytes: totalSize,
sizeFormatted: formatBytes(totalSize),
lastModified: component.assets && component.assets.length > 0
? component.assets[0].lastModified
: '',
url: generateArtifactUrl(repo.name, component.group, component.name, component.version)
});
}
console.log(` Total components in ${repo.name}: ${components.length}`);
}
// Sort by size (descending) if enabled
if (config.sortBySize) {
console.log('\nSorting artifacts by size...');
allArtifacts.sort((a, b) => b.sizeBytes - a.sizeBytes);
}
// Calculate total size
const totalSize = allArtifacts.reduce((sum, artifact) => sum + artifact.sizeBytes, 0);
const runDate = new Date().toISOString();
// Write CSV file
console.log(`\n--- Writing results to ${config.outputFile} ---`);
// Add metadata header as comment
let csvContent = `# Nexus Repository Analysis\n`;
csvContent += `# Generated: ${runDate}\n`;
csvContent += `# Nexus URL: ${config.nexusUrl}\n`;
csvContent += `# Total Artifacts: ${allArtifacts.length}\n`;
csvContent += `# Total Size: ${formatBytes(totalSize)}\n`;
csvContent += `# Sorted by Size: ${config.sortBySize}\n`;
csvContent += `#\n`;
const csvHeaders = [
'Repository',
'Group',
'Artifact',
'Version',
'Format',
'Asset Count',
'Size (Bytes)',
'Size (Formatted)',
'Last Modified',
'URL'
];
csvContent += csvHeaders.join(',') + '\n';
for (const artifact of allArtifacts) {
const row = [
escapeCsvField(artifact.repository),
escapeCsvField(artifact.group),
escapeCsvField(artifact.name),
escapeCsvField(artifact.version),
escapeCsvField(artifact.format),
escapeCsvField(artifact.assetCount),
escapeCsvField(artifact.sizeBytes),
escapeCsvField(artifact.sizeFormatted),
escapeCsvField(artifact.lastModified),
escapeCsvField(artifact.url)
];
csvContent += row.join(',') + '\n';
}
fs.writeFileSync(config.outputFile, csvContent);
// Print summary
console.log('\n=== Summary ===');
console.log(`Total artifacts: ${allArtifacts.length}`);
console.log(`Total size: ${formatBytes(totalSize)} (${totalSize} bytes)`);
console.log(`\nTop 10 largest artifacts:`);
allArtifacts.slice(0, 10).forEach((artifact, index) => {
console.log(`${index + 1}. ${artifact.group}:${artifact.name}:${artifact.version} - ${artifact.sizeFormatted}`);
});
console.log(`\n✓ CSV report saved to: ${config.outputFile}`);
} catch (error) {
console.error('Error:', error.message);
if (error.message.includes('401')) {
console.error('\nAuthentication failed. Please check your username and password.');
} else if (error.message.includes('ECONNREFUSED')) {
console.error('\nCould not connect to Nexus. Please check the URL.');
}
process.exit(1);
}
}
// Run the analyzer
analyze();
#!/usr/bin/env node
/**
* Nexus Repository Cleanup Script
* Deletes artifacts from Nexus Maven repositories based on repository, component, and version criteria
*
* DANGER: This script permanently deletes artifacts. Use with caution!
*/
const https = require('https');
const http = require('http');
const fs = require('fs');
const readline = require('readline');
const { URL } = require('url');
// Configuration
const config = {
nexusUrl: process.env.NEXUS_URL || 'https://nexus.local.io',
username: process.env.NEXUS_USERNAME || 'USER_NAME',
password: process.env.NEXUS_PASSWORD || 'SECRET_PASSWORD',
repository: process.env.NEXUS_REPO || '', // REQUIRED
component: process.env.COMPONENT || '', // Optional - if empty, applies to all components
version: process.env.VERSION || '', // REQUIRED
dryRun: process.env.DRY_RUN !== 'false', // Default to dry-run mode for safety
autoConfirm: process.env.AUTO_CONFIRM === 'true', // Skip confirmation prompts
useCache: process.env.USE_CACHE === 'true', // Use cached data if available
cacheFile: process.env.CACHE_FILE || '.nexus-cache.json', // Cache file location
maxCacheAge: parseInt(process.env.MAX_CACHE_AGE || '3600', 10), // Cache validity in seconds (default 1 hour)
};
// Helper function to make HTTP requests
function makeRequest(url, auth, method = 'GET') {
return new Promise((resolve, reject) => {
const urlObj = new URL(url);
const isHttps = urlObj.protocol === 'https:';
const client = isHttps ? https : http;
const options = {
hostname: urlObj.hostname,
port: urlObj.port || (isHttps ? 443 : 80),
path: urlObj.pathname + urlObj.search,
method: method,
headers: {
'Authorization': 'Basic ' + Buffer.from(`${auth.username}:${auth.password}`).toString('base64'),
'Accept': 'application/json'
}
};
const req = client.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
if (data) {
resolve(JSON.parse(data));
} else {
resolve({ status: 'success', statusCode: res.statusCode });
}
} catch (e) {
// For DELETE requests, empty response is OK
if (method === 'DELETE') {
resolve({ status: 'deleted', statusCode: res.statusCode });
} else {
reject(new Error(`Failed to parse JSON: ${e.message}`));
}
}
} else {
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
}
});
});
req.on('error', reject);
req.end();
});
}
// Save cache to file
function saveCache(repositoryName, components) {
const cache = {
repository: repositoryName,
nexusUrl: config.nexusUrl,
timestamp: new Date().toISOString(),
componentCount: components.length,
components: components
};
try {
fs.writeFileSync(config.cacheFile, JSON.stringify(cache, null, 2));
console.log(`✓ Cached ${components.length} components to ${config.cacheFile}`);
} catch (error) {
console.warn(`Warning: Failed to save cache: ${error.message}`);
}
}
// Load cache from file
function loadCache(repositoryName) {
try {
if (!fs.existsSync(config.cacheFile)) {
return null;
}
const cacheData = fs.readFileSync(config.cacheFile, 'utf8');
const cache = JSON.parse(cacheData);
// Validate cache
if (cache.repository !== repositoryName) {
console.log(`Cache is for different repository (${cache.repository} vs ${repositoryName}), fetching fresh data...`);
return null;
}
if (cache.nexusUrl !== config.nexusUrl) {
console.log(`Cache is for different Nexus URL, fetching fresh data...`);
return null;
}
// Check cache age
const cacheAge = (new Date() - new Date(cache.timestamp)) / 1000;
if (cacheAge > config.maxCacheAge) {
const ageMinutes = Math.floor(cacheAge / 60);
console.log(`Cache is too old (${ageMinutes} minutes), fetching fresh data...`);
return null;
}
const cacheMinutes = Math.floor(cacheAge / 60);
const cacheSeconds = Math.floor(cacheAge % 60);
console.log(`✓ Using cached data (${cache.componentCount} components, ${cacheMinutes}m ${cacheSeconds}s old)`);
return cache.components;
} catch (error) {
console.warn(`Warning: Failed to load cache: ${error.message}`);
return null;
}
}
// Get all components from a repository with pagination
async function getComponents(repositoryName) {
// Try to use cache if enabled
if (config.useCache) {
const cachedComponents = loadCache(repositoryName);
if (cachedComponents) {
return cachedComponents;
}
}
console.log(`Fetching components from repository: ${repositoryName}...`);
const components = [];
let continuationToken = null;
do {
const url = `${config.nexusUrl}/service/rest/v1/components?repository=${repositoryName}` +
(continuationToken ? `&continuationToken=${continuationToken}` : '');
const response = await makeRequest(url, config);
if (response.items) {
components.push(...response.items);
console.log(` Retrieved ${components.length} components so far...`);
}
continuationToken = response.continuationToken;
} while (continuationToken);
// Save to cache if enabled
if (config.useCache) {
saveCache(repositoryName, components);
}
return components;
}
// Delete a component by ID
async function deleteComponent(componentId) {
const url = `${config.nexusUrl}/service/rest/v1/components/${componentId}`;
return await makeRequest(url, config, 'DELETE');
}
// Format bytes to human-readable format
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Check if version matches the pattern (supports wildcards)
function versionMatches(componentVersion, versionPattern) {
// Convert wildcard pattern to regex
const regexPattern = versionPattern
.replace(/\./g, '\\.') // Escape dots
.replace(/\*/g, '.*'); // Convert * to .*
const regex = new RegExp(`^${regexPattern}$`, 'i');
return regex.test(componentVersion);
}
// Check if component name matches the pattern (supports wildcards)
function componentMatches(componentName, componentPattern) {
if (!componentPattern) return true; // If no pattern specified, match all
const regexPattern = componentPattern
.replace(/\./g, '\\.') // Escape dots
.replace(/\*/g, '.*'); // Convert * to .*
const regex = new RegExp(`^${regexPattern}$`, 'i');
return regex.test(componentName);
}
// Ask for user confirmation
function askConfirmation(question) {
return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question(question + ' (yes/no): ', (answer) => {
rl.close();
resolve(answer.toLowerCase() === 'yes' || answer.toLowerCase() === 'y');
});
});
}
// Validate configuration
function validateConfig() {
const errors = [];
if (!config.repository) {
errors.push('NEXUS_REPO is required (repository name)');
}
if (!config.version) {
errors.push('VERSION is required (version pattern to delete)');
}
if (errors.length > 0) {
console.error('\n❌ Configuration errors:');
errors.forEach(err => console.error(` - ${err}`));
console.error('\nUsage:');
console.error(' NEXUS_REPO=repo-name VERSION=1.0.0 node nexus-cleanup.js');
console.error(' NEXUS_REPO=repo-name COMPONENT=my-artifact VERSION=1.* node nexus-cleanup.js');
console.error('\nEnvironment variables:');
console.error(' NEXUS_REPO (required) - Repository name');
console.error(' VERSION (required) - Version pattern (supports wildcards: 1.*, *-SNAPSHOT)');
console.error(' COMPONENT (optional) - Component name pattern (supports wildcards)');
console.error(' DRY_RUN (default: true) - Set to "false" to actually delete');
console.error(' AUTO_CONFIRM (default: false) - Set to "true" to skip confirmations');
console.error(' USE_CACHE (default: false) - Set to "true" to use cached repository data');
console.error(' CACHE_FILE (default: .nexus-cache.json) - Cache file location');
console.error(' MAX_CACHE_AGE (default: 3600) - Cache validity in seconds');
return false;
}
return true;
}
// Main function
async function cleanup() {
console.log('=== Nexus Repository Cleanup ===\n');
// Validate configuration
if (!validateConfig()) {
process.exit(1);
}
console.log(`Nexus URL: ${config.nexusUrl}`);
console.log(`Repository: ${config.repository}`);
console.log(`Component: ${config.component || '* (all components)'}`);
console.log(`Version: ${config.version}`);
console.log(`Dry Run: ${config.dryRun ? 'YES (no actual deletion)' : 'NO (WILL DELETE!)'}`);
console.log(`Use Cache: ${config.useCache ? 'YES' : 'NO'}${config.useCache ? ` (${config.cacheFile}, max age: ${config.maxCacheAge}s)` : ''}\n`);
if (config.dryRun) {
console.log('🔒 Running in DRY-RUN mode. No artifacts will be deleted.');
console.log(' Set DRY_RUN=false to actually delete artifacts.\n');
} else {
console.log('⚠️ WARNING: DRY-RUN is DISABLED. Artifacts WILL BE DELETED!\n');
}
try {
// Get all components from the repository
const components = await getComponents(config.repository);
console.log(`\nTotal components in repository: ${components.length}`);
// Filter components that match our criteria
const matchingComponents = components.filter(component => {
const versionMatch = versionMatches(component.version, config.version);
const nameMatch = componentMatches(component.name, config.component);
return versionMatch && nameMatch;
});
console.log(`\nFound ${matchingComponents.length} components matching criteria:\n`);
if (matchingComponents.length === 0) {
console.log('No components to delete. Exiting.');
return;
}
// Calculate total size
let totalSize = 0;
const componentList = [];
matchingComponents.forEach((component, index) => {
let componentSize = 0;
if (component.assets) {
component.assets.forEach(asset => {
componentSize += asset.fileSize || 0;
});
}
totalSize += componentSize;
componentList.push({
index: index + 1,
id: component.id,
group: component.group || '',
name: component.name || '',
version: component.version || '',
size: componentSize,
sizeFormatted: formatBytes(componentSize),
assetCount: component.assets ? component.assets.length : 0
});
});
// Display components to be deleted
console.log('Components to be deleted:');
console.log('─'.repeat(100));
componentList.forEach(comp => {
console.log(`${comp.index}. ${comp.group}:${comp.name}:${comp.version}`);
console.log(` Size: ${comp.sizeFormatted} (${comp.assetCount} files)`);
});
console.log('─'.repeat(100));
console.log(`\nTotal size to be freed: ${formatBytes(totalSize)} (${totalSize} bytes)`);
console.log(`Total components to delete: ${componentList.length}\n`);
// Ask for confirmation unless auto-confirm is enabled
if (!config.autoConfirm) {
if (config.dryRun) {
console.log('This is a DRY-RUN. The above components would be deleted if DRY_RUN=false.\n');
const proceed = await askConfirmation('Do you want to see the deletion process (no actual deletion)?');
if (!proceed) {
console.log('Cancelled.');
return;
}
} else {
console.log('⚠️ THIS WILL PERMANENTLY DELETE THE ABOVE COMPONENTS! ⚠️\n');
const proceed = await askConfirmation('Are you absolutely sure you want to delete these components?');
if (!proceed) {
console.log('Cancelled.');
return;
}
}
}
// Delete components
console.log(`\n${config.dryRun ? 'Simulating' : 'Starting'} deletion...\n`);
let deletedCount = 0;
let failedCount = 0;
const deletedLog = [];
for (const comp of componentList) {
const action = config.dryRun ? '[DRY-RUN]' : '[DELETING]';
console.log(`${action} ${comp.group}:${comp.name}:${comp.version} (${comp.sizeFormatted})`);
if (!config.dryRun) {
try {
await deleteComponent(comp.id);
deletedCount++;
deletedLog.push({
group: comp.group,
name: comp.name,
version: comp.version,
size: comp.size,
status: 'deleted'
});
console.log(` ✓ Deleted successfully`);
} catch (error) {
failedCount++;
deletedLog.push({
group: comp.group,
name: comp.name,
version: comp.version,
size: comp.size,
status: 'failed',
error: error.message
});
console.log(` ✗ Failed: ${error.message}`);
}
} else {
deletedCount++;
console.log(` ✓ Would be deleted`);
}
}
// Summary
console.log('\n=== Summary ===');
if (config.dryRun) {
console.log(`Would delete: ${deletedCount} components`);
console.log(`Would free: ${formatBytes(totalSize)}`);
console.log('\n💡 To actually delete these components, run with: DRY_RUN=false');
} else {
console.log(`Successfully deleted: ${deletedCount} components`);
console.log(`Failed: ${failedCount} components`);
console.log(`Space freed: ${formatBytes(totalSize)}`);
// Write deletion log
const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '').replace('T', '_');
const logFile = `deletion-log_${timestamp}.json`;
const fs = require('fs');
fs.writeFileSync(logFile, JSON.stringify({
timestamp: new Date().toISOString(),
repository: config.repository,
component: config.component,
version: config.version,
totalDeleted: deletedCount,
totalFailed: failedCount,
spaceFreed: totalSize,
components: deletedLog
}, null, 2));
console.log(`\nDeletion log saved to: ${logFile}`);
}
} catch (error) {
console.error('Error:', error.message);
if (error.message.includes('401')) {
console.error('\nAuthentication failed. Please check your username and password.');
} else if (error.message.includes('ECONNREFUSED')) {
console.error('\nCould not connect to Nexus. Please check the URL.');
}
process.exit(1);
}
}
// Run the cleanup
cleanup();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment