Skip to content

Large number of assets batch support #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 11, 2025
Merged
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
1 change: 0 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ jobs:
token: ${{ secrets.NPM_TOKEN }}
package: ./package.json
access: public
tag: beta
- name: Create Release
if: ${{ steps.publish-plugin.conclusion == 'success' }}
id: create_release
Expand Down
2 changes: 2 additions & 0 deletions .talismanrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
fileignoreconfig:
- filename: .env-example
checksum: 591f1e672d4df287107092b8fd37c27913e09225c6ced55293e1d459b1119d05
- filename: src/core/query-executor.ts
checksum: 53f90091c9877c79f43bf0e3dcc26281f0c6b2073d63380803cda1481712a5ae
version: '1.0'
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@contentstack/cli-cm-export-query",
"description": "Contentstack CLI plugin to export content from stack",
"version": "1.0.0-beta.2",
"version": "1.0.0-beta.3",
"author": "Contentstack",
"bugs": "https://github.com/contentstack/cli/issues",
"dependencies": {
Expand Down
Empty file removed snyk_output.log
Empty file.
125 changes: 117 additions & 8 deletions src/core/query-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,23 +286,132 @@ export class QueryExporter {
log(this.exportQueryConfig, 'Starting export of referenced assets...', 'info');

try {
const assetsDir = path.join(
sanitizePath(this.exportQueryConfig.exportDir),
sanitizePath(this.exportQueryConfig.branchName || ''),
'assets',
);

const metadataFilePath = path.join(assetsDir, 'metadata.json');
const assetFilePath = path.join(assetsDir, 'assets.json');

// Define temp file paths
const tempMetadataFilePath = path.join(assetsDir, 'metadata_temp.json');
const tempAssetFilePath = path.join(assetsDir, 'assets_temp.json');

const assetHandler = new AssetReferenceHandler(this.exportQueryConfig);

// Extract referenced asset UIDs from all entries
const assetUIDs = assetHandler.extractReferencedAssets();

if (assetUIDs.length > 0) {
log(this.exportQueryConfig, `Exporting ${assetUIDs.length} referenced assets...`, 'info');
log(this.exportQueryConfig, `Found ${assetUIDs.length} referenced assets to export`, 'info');

const query = {
modules: {
assets: {
uid: { $in: assetUIDs },
// Define batch size - can be configurable through exportQueryConfig
const batchSize = this.exportQueryConfig.assetBatchSize || 100;

if (assetUIDs.length <= batchSize) {
const query = {
modules: {
assets: {
uid: { $in: assetUIDs },
},
},
},
};
};

await this.moduleExporter.exportModule('assets', { query });
}

// if asset size is bigger than batch size, then we need to export in batches
// Calculate number of batches
const totalBatches = Math.ceil(assetUIDs.length / batchSize);
log(this.exportQueryConfig, `Processing assets in ${totalBatches} batches of ${batchSize}`, 'info');

// Process assets in batches
for (let i = 0; i < totalBatches; i++) {
const start = i * batchSize;
const end = Math.min(start + batchSize, assetUIDs.length);
const batchAssetUIDs = assetUIDs.slice(start, end);

log(
this.exportQueryConfig,
`Exporting batch ${i + 1}/${totalBatches} (${batchAssetUIDs.length} assets)...`,
'info',
);

const query = {
modules: {
assets: {
uid: { $in: batchAssetUIDs },
},
},
};

await this.moduleExporter.exportModule('assets', { query });

// Read the current batch's metadata.json and assets.json files
const currentMetadata: any = fsUtil.readFile(sanitizePath(metadataFilePath));
const currentAssets: any = fsUtil.readFile(sanitizePath(assetFilePath));

// Check if this is the first batch
if (i === 0) {
// For first batch, initialize temp files with current content
fsUtil.writeFile(sanitizePath(tempMetadataFilePath), currentMetadata);
fsUtil.writeFile(sanitizePath(tempAssetFilePath), currentAssets);
log(this.exportQueryConfig, `Initialized temporary files with first batch data`, 'info');
} else {
// For subsequent batches, append to temp files with incremented keys

// Handle metadata (which contains arrays of asset info)
const tempMetadata: any = fsUtil.readFile(sanitizePath(tempMetadataFilePath)) || {};

// Merge metadata by combining arrays
if (currentMetadata) {
Object.keys(currentMetadata).forEach((key: string) => {
if (!tempMetadata[key]) {
tempMetadata[key] = currentMetadata[key];
}
});
}

// Write updated metadata back to temp file
fsUtil.writeFile(sanitizePath(tempMetadataFilePath), tempMetadata);

// Handle assets (which is an object with numeric keys)
const tempAssets: any = fsUtil.readFile(sanitizePath(tempAssetFilePath)) || {};
let nextIndex = Object.keys(tempAssets).length + 1;

// Add current assets with incremented keys
Object.values(currentAssets).forEach((value: any) => {
tempAssets[nextIndex.toString()] = value;
nextIndex++;
});

fsUtil.writeFile(sanitizePath(tempAssetFilePath), tempAssets);

log(this.exportQueryConfig, `Updated temporary files with batch ${i + 1} data`, 'info');
}

// Optional: Add delay between batches to avoid rate limiting
if (i < totalBatches - 1 && this.exportQueryConfig.batchDelayMs) {
await new Promise((resolve) => setTimeout(resolve, this.exportQueryConfig.batchDelayMs));
}
}

// After all batches are processed, copy temp files back to original files
const finalMetadata = fsUtil.readFile(sanitizePath(tempMetadataFilePath));
const finalAssets = fsUtil.readFile(sanitizePath(tempAssetFilePath));

fsUtil.writeFile(sanitizePath(metadataFilePath), finalMetadata);
fsUtil.writeFile(sanitizePath(assetFilePath), finalAssets);

log(this.exportQueryConfig, `Final data written back to original files`, 'info');

// Clean up temp files
fsUtil.removeFile(sanitizePath(tempMetadataFilePath));
fsUtil.removeFile(sanitizePath(tempAssetFilePath));

await this.moduleExporter.exportModule('assets', { query });
log(this.exportQueryConfig, `Temporary files cleaned up`, 'info');
log(this.exportQueryConfig, 'Referenced assets exported successfully', 'success');
} else {
log(this.exportQueryConfig, 'No referenced assets found in entries', 'info');
Expand Down
3 changes: 3 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,9 @@ export interface QueryExportConfig extends DefaultConfig {
logsPath: string;
dataPath: string;
exportDelayMs?: number;
batchDelayMs?: number;
assetBatchSize?: number;
assetBatchDelayMs?: number;
}

export interface QueryMetadata {
Expand Down
5 changes: 5 additions & 0 deletions src/utils/config-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export async function setupQueryExportConfig(flags: any): Promise<QueryExportCon
externalConfigPath: path.join(__dirname, '../config/export-config.json'),
};

// override the external config path if the user provides a config file
if (flags.config) {
exportQueryConfig.externalConfigPath = sanitizePath(flags['config']);
}

// Handle authentication
if (flags.alias) {
const { token, apiKey } = configHandler.get(`tokens.${flags.alias}`) || {};
Expand Down