Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { Command } from '@contentstack/cli-command';
import { flags, FlagInput, cliux, log } from '@contentstack/cli-utilities';
import { ImportRecoveryManager } from '../../../utils';

export default class ImportRecoveryCommand extends Command {
static description = 'Analyze and recover from failed import operations';

static examples: string[] = [
'csdx cm:stacks:import-recovery --data-dir <path>',
'csdx cm:stacks:import-recovery --data-dir <path> --clean',
'csdx cm:stacks:import-recovery --data-dir <path> --report'
];

static flags: FlagInput = {
'data-dir': flags.string({
char: 'd',
description: 'The path to the directory containing the import data and state files',
required: true,
}),
clean: flags.boolean({
description: 'Clean the import state to start fresh (creates backup)',
default: false,
}),
report: flags.boolean({
description: 'Generate a detailed recovery report',
default: false,
}),
'output-file': flags.string({
description: 'Save the recovery report to a file',
}),
};

static usage: string = 'cm:stacks:import-recovery --data-dir <value> [--clean] [--report] [--output-file <value>]';

async run(): Promise<void> {
try {
const { flags } = await this.parse(ImportRecoveryCommand);

const recoveryManager = ImportRecoveryManager.create(flags['data-dir']);

if (flags.clean) {
await this.cleanImportState(recoveryManager);
return;
}

if (flags.report) {
await this.generateReport(recoveryManager, flags['output-file']);
return;
}

// Default: analyze and provide recommendations
await this.analyzeAndRecommend(recoveryManager);

} catch (error) {
log.error(`Recovery command failed: ${error}`);
cliux.print(`Error: ${error}`, { color: 'red' });
}
}

private async analyzeAndRecommend(recoveryManager: ImportRecoveryManager): Promise<void> {
cliux.print('\n🔍 Analyzing import state...', { color: 'blue' });

const info = recoveryManager.analyzeImportState();
const recommendation = recoveryManager.getRecoveryRecommendation(info);

// Display state information
cliux.print('\n📊 Import State Summary:', { color: 'cyan' });
cliux.print(` State File: ${info.stateFileExists ? '✅ Found' : '❌ Not Found'}`);
cliux.print(` Assets Processed: ${info.mappingCounts.assets}`);
cliux.print(` Folders Processed: ${info.mappingCounts.folders}`);
cliux.print(` URL Mappings: ${info.mappingCounts.urls}`);

if (info.lastUpdated) {
const lastUpdated = new Date(info.lastUpdated);
cliux.print(` Last Updated: ${lastUpdated.toLocaleString()}`);
}

if (info.estimatedProgress) {
cliux.print(` Estimated Progress: ~${info.estimatedProgress}%`);
}

// Display recommendation
cliux.print(`\n💡 Recommendation: ${recommendation.action.toUpperCase()}`, {
color: recommendation.action === 'resume' ? 'green' :
recommendation.action === 'restart' ? 'yellow' : 'red'
});
cliux.print(` ${recommendation.reason}`);

if (recommendation.commands && recommendation.commands.length > 0) {
cliux.print('\n📋 Commands:', { color: 'cyan' });
recommendation.commands.forEach(cmd => {
if (cmd.startsWith('#')) {
cliux.print(` ${cmd}`, { color: 'gray' });
} else if (cmd.trim() === '') {
cliux.print('');
} else {
cliux.print(` ${cmd}`, { color: 'white' });
}
});
}

if (recommendation.warnings && recommendation.warnings.length > 0) {
cliux.print('\n⚠️ Warnings:', { color: 'yellow' });
recommendation.warnings.forEach(warning => {
cliux.print(` ${warning}`);
});
}

cliux.print('\n💡 Tip: Use --report flag for a detailed analysis or --clean to start fresh\n');
}

private async cleanImportState(recoveryManager: ImportRecoveryManager): Promise<void> {
cliux.print('\n🧹 Cleaning import state...', { color: 'yellow' });

const info = recoveryManager.analyzeImportState();

if (!info.stateFileExists) {
cliux.print('✅ No import state found. Nothing to clean.', { color: 'green' });
return;
}

if (info.mappingCounts.assets > 0 || info.mappingCounts.folders > 0) {
cliux.print(`⚠️ Warning: This will remove progress for ${info.mappingCounts.assets} assets and ${info.mappingCounts.folders} folders.`, { color: 'yellow' });

const confirm = await cliux.confirm('Are you sure you want to clean the import state? (y/N)');
if (!confirm) {
cliux.print('❌ Operation cancelled.', { color: 'red' });
return;
}
}

const success = recoveryManager.cleanImportState();

if (success) {
cliux.print('✅ Import state cleaned successfully. You can now start a fresh import.', { color: 'green' });
cliux.print('💡 A backup of the previous state has been created.', { color: 'blue' });
} else {
cliux.print('❌ Failed to clean import state. Check the logs for details.', { color: 'red' });
}
}

private async generateReport(recoveryManager: ImportRecoveryManager, outputFile?: string): Promise<void> {
cliux.print('\n📄 Generating recovery report...', { color: 'blue' });

const report = recoveryManager.generateRecoveryReport();

if (outputFile) {
try {
const fs = require('fs');
fs.writeFileSync(outputFile, report);
cliux.print(`✅ Recovery report saved to: ${outputFile}`, { color: 'green' });
} catch (error) {
cliux.print(`❌ Failed to save report to file: ${error}`, { color: 'red' });
cliux.print('\n📄 Recovery Report:', { color: 'cyan' });
cliux.print(report);
}
} else {
cliux.print('\n📄 Recovery Report:', { color: 'cyan' });
cliux.print(report);
}
}
}
14 changes: 14 additions & 0 deletions packages/contentstack-import/src/commands/cm/stacks/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,20 @@ export default class ImportCommand extends Command {
description: 'Skips entry publishing during the import process',
default: false,
}),
'force-backup': flags.boolean({
description: 'Forces backup creation even for large datasets that would normally skip backup for memory optimization.',
default: false,
}),
'disable-memory-optimization': flags.boolean({
description: 'Disables memory optimization features and uses legacy processing (not recommended for large datasets).',
default: false,
}),
'memory-threshold': flags.integer({
description: 'Memory threshold in MB for triggering garbage collection (default: 768MB for large datasets).',
}),
'asset-concurrency': flags.integer({
description: 'Number of concurrent asset uploads (default: 10).',
}),
};

static usage: string =
Expand Down
14 changes: 12 additions & 2 deletions packages/contentstack-import/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,23 @@ const config: DefaultConfig = {
assetBatchLimit: 1,
fileName: 'assets.json',
importSameStructure: true,
uploadAssetsConcurrency: 2,
uploadAssetsConcurrency: 10, // Increased from 2 to 10 based on customer success
displayExecutionTime: false,
importFoldersConcurrency: 1,
importFoldersConcurrency: 5, // Increased from 1 to 5 for better performance
includeVersionedAssets: false,
host: 'https://api.contentstack.io',
folderValidKeys: ['name', 'parent_uid'],
validKeys: ['title', 'parent_uid', 'description', 'tags'],
// New memory management configuration
enableMemoryMonitoring: true, // Enable memory monitoring by default
memoryThresholdMB: 768, // Memory pressure threshold for large datasets
enableIncrementalPersistence: true, // Enable incremental state saving
maxRetries: 5, // Retry logic for failed uploads
retryDelay: 2000, // Delay between retries (ms)
enableRateLimiting: true, // Enable rate limiting
rateLimitDelay: 200, // Delay between API calls (ms)
backupSkipThresholdGB: 1, // Skip backup for datasets larger than 1GB
queueClearInterval: 100, // Clear completed queue items every N items
},
'assets-old': {
dirName: 'assets',
Expand Down
Loading
Loading