Skip to content
Open
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
2 changes: 2 additions & 0 deletions .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ fileignoreconfig:
checksum: 2df43be75a96e1a3a00dd256628e191909aa9d7f192b672b56cb3772e074958c
- filename: pnpm-lock.yaml
checksum: eae269c31146f807a80978acb68b83b761742b6d8e82116b6af48dcc0156f085
- filename: packages/contentstack-asset-management/test/unit/utils/cs-assets-api-adapter.test.ts
checksum: f3620fb4441b7ced00ea85831d21f6c6b8e446a0cfe36b5bfcaaaf70788b09cc
version: ""
32 changes: 25 additions & 7 deletions packages/contentstack-asset-management/src/export/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ const ASSET_META_KEYS = ['uid', 'url', 'filename', 'file_name', 'parent_uid'];

type AssetRecord = { uid?: string; _uid?: string; url?: string; filename?: string; file_name?: string };

/** Per-space export counts surfaced to the summary (assets = downloaded binaries; folders = entities). */
export type SpaceExportCounts = { assets: number; folders: number };
/**
* Per-space export counts surfaced to the summary (assets = downloaded binaries; folders = entities;
* failedAssets = metadata records missing after permanent page failures + binary download failures).
*/
export type SpaceExportCounts = { assets: number; folders: number; failedAssets: number };

export default class ExportAssets extends CSAssetsExportAdapter {
constructor(apiConfig: CSAssetsAPIConfig, exportContext: ExportContext) {
Expand Down Expand Up @@ -53,11 +56,22 @@ export default class ExportAssets extends CSAssetsExportAdapter {
};

log.debug(`Fetching folders and streaming assets for space ${workspace.space_uid}`, this.exportContext.context);
const [folders] = await Promise.all([
const [folders, streamResult] = await Promise.all([
this.getWorkspaceFolders(workspace.space_uid, workspace.uid, this.apiPageSize, this.apiFetchConcurrency),
this.streamWorkspaceAssets(workspace.space_uid, workspace.uid, onPage, this.apiPageSize, this.apiFetchConcurrency),
]);

// Permanently failed metadata pages (or mid-export drift) — these records never reached
// assets.json, so the download loop can't see them. Count them as failures in the summary;
// they are recoverable via a re-export or a targeted query-export, so don't abort the run.
if (streamResult.missing > 0) {
log.error(
`${streamResult.missing} asset metadata record(s) could not be fetched for space ${workspace.space_uid} — ` +
'they are missing from this export. Re-export (or use query-export) to recover them.',
this.exportContext.context,
);
}

if (fsWriter) fsWriter.completeFile(true);
else await this.writeEmptyChunkedJson(assetsDir, 'assets.json');
log.debug(`Wrote chunked assets metadata (${totalStreamed} item(s)) under ${assetsDir}`, this.exportContext.context);
Expand All @@ -78,17 +92,21 @@ export default class ExportAssets extends CSAssetsExportAdapter {
this.tick(true, `metadata: ${workspace.space_uid} (${totalStreamed})`, null);

log.debug(`Starting binary downloads for space ${workspace.space_uid}`, this.exportContext.context);
const assetsDownloaded = await this.downloadWorkspaceAssets(assetsDir, workspace.space_uid, downloadableCount);
const downloads = await this.downloadWorkspaceAssets(assetsDir, workspace.space_uid, downloadableCount);

const folderCount = getArrayFromResponse(folders, 'folders').length;
return { assets: assetsDownloaded, folders: folderCount };
return { assets: downloads.ok, folders: folderCount, failedAssets: streamResult.missing + downloads.fail };
}

/**
* Download asset binaries by reading the just-written chunked `assets.json` back from disk
* (one chunk at a time), so we never re-materialize the whole asset list in memory.
*/
private async downloadWorkspaceAssets(assetsDir: string, spaceUid: string, expectedDownloads: number): Promise<number> {
private async downloadWorkspaceAssets(
assetsDir: string,
spaceUid: string,
expectedDownloads: number,
): Promise<{ ok: number; fail: number }> {
const filesDir = pResolve(assetsDir, 'files');
await mkdir(filesDir, { recursive: true });

Expand Down Expand Up @@ -202,7 +220,7 @@ export default class ExportAssets extends CSAssetsExportAdapter {
this.exportContext.context,
);

return downloadOk;
return { ok: downloadOk, fail: downloadFail };
}

}
16 changes: 13 additions & 3 deletions packages/contentstack-asset-management/src/export/spaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export type AssetExportCounts = {
folders: number;
assetTypes: number;
fields: number;
/** Assets missing from the export: permanently failed metadata pages + failed binary downloads. */
failedAssets: number;
};

/**
Expand Down Expand Up @@ -53,7 +55,7 @@ export class ExportSpaces {

if (!linkedWorkspaces.length) {
log.debug('No linked workspaces to export', context);
return { assets: 0, folders: 0, assetTypes: 0, fields: 0 };
return { assets: 0, folders: 0, assetTypes: 0, fields: 0, failedAssets: 0 };
}

log.debug('Starting Contentstack Assets export process...', context);
Expand Down Expand Up @@ -105,6 +107,7 @@ export class ExportSpaces {
// Real entity counts accumulated for the summary (Bug 3).
let assetsTotal = 0;
let foldersTotal = 0;
let failedAssetsTotal = 0;
let assetTypesCount = 0;
let fieldsCount = 0;
try {
Expand Down Expand Up @@ -140,6 +143,7 @@ export class ExportSpaces {
const spaceCounts = await exportWorkspace.start(ws, spaceDir, branchName || 'main', spaceProcess);
assetsTotal += spaceCounts.assets;
foldersTotal += spaceCounts.folders;
failedAssetsTotal += spaceCounts.failedAssets;
progress.completeProcess(spaceProcess, true);
log.debug(`Exported workspace structure for space ${ws.space_uid}`, context);
} catch (err) {
Expand All @@ -157,14 +161,20 @@ export class ExportSpaces {
}

log.info(
anySpaceFailed
anySpaceFailed || failedAssetsTotal > 0
? 'Contentstack Assets export completed with errors in one or more spaces'
: 'Contentstack Assets export completed successfully',
context,
);
log.debug('Contentstack Assets export completed', context);

return { assets: assetsTotal, folders: foldersTotal, assetTypes: assetTypesCount, fields: fieldsCount };
return {
assets: assetsTotal,
folders: foldersTotal,
assetTypes: assetTypesCount,
fields: fieldsCount,
failedAssets: failedAssetsTotal,
};
} catch (err) {
if (!bootstrapFailed) {
// Mark any spaces that hadn't been processed as failed so the multibar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,21 @@ export type SearchAssetsParams = {
limit?: number;
};

/** Response shape of GET /api/bff/spaces/{space_uid}/assets/count. */
export type AssetCountResponse = {
count: number;
};

/**
* Result of streaming a workspace's assets. `missing` is the gap between the authoritative
* count-API total and the records actually streamed (permanently failed pages and/or items
* changed mid-export) — surfaced as failures in the export summary rather than aborting the run.
*/
export type StreamWorkspaceAssetsResult = {
streamed: number;
missing: number;
};

/** Response shape from POST /api/search for assets. */
export type SearchAssetsResponse = {
count?: number;
Expand All @@ -174,14 +189,15 @@ export interface ICSAssetsAdapter {
listSpaces(pageSize?: number, fetchConcurrency?: number): Promise<SpacesListResponse>;
getSpace(spaceUid: string): Promise<SpaceResponse>;
getWorkspaceFields(spaceUid: string, pageSize?: number, fetchConcurrency?: number): Promise<FieldsResponse>;
getWorkspaceAssets(spaceUid: string, workspaceUid?: string, pageSize?: number, fetchConcurrency?: number): Promise<unknown>;
getAssetsCount(spaceUid: string, workspaceUid?: string): Promise<number>;
getFoldersCount(spaceUid: string, workspaceUid?: string): Promise<number>;
streamWorkspaceAssets(
spaceUid: string,
workspaceUid: string | undefined,
onPage: (items: unknown[]) => void | Promise<void>,
pageSize?: number,
fetchConcurrency?: number,
): Promise<number>;
): Promise<StreamWorkspaceAssetsResult>;
getWorkspaceFolders(spaceUid: string, workspaceUid?: string, pageSize?: number, fetchConcurrency?: number): Promise<unknown>;
getWorkspaceAssetTypes(spaceUid: string, pageSize?: number, fetchConcurrency?: number): Promise<AssetTypesResponse>;
searchAssets(params: SearchAssetsParams): Promise<SearchAssetsResponse>;
Expand Down
Loading
Loading