Skip to content
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
122 changes: 64 additions & 58 deletions src/lib/pushers/asset-pusher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function extractErrorMessage(error: any): string {
// Check for direct axios response data first (our direct axios calls)
if (error?.response?.data) {
const data = error.response.data;
if (typeof data === 'string') return data;
if (typeof data === "string") return data;
if (data.exceptionMessage) return data.exceptionMessage; // Agility API format
if (data.message) return data.message;
if (data.error) return data.error;
Expand All @@ -27,7 +27,7 @@ function extractErrorMessage(error: any): string {
// Check for SDK-wrapped axios response data
if (error?.innerError?.response?.data) {
const data = error.innerError.response.data;
if (typeof data === 'string') return data;
if (typeof data === "string") return data;
if (data.exceptionMessage) return data.exceptionMessage;
if (data.message) return data.message;
if (data.error) return data.error;
Expand All @@ -41,7 +41,7 @@ function extractErrorMessage(error: any): string {
export async function pushAssets(
sourceData: mgmtApi.Media[], // TODO: Type these
targetData: mgmtApi.Media[], // TODO: Type these
onProgress?: (processed: number, total: number, status?: "success" | "error") => void
onProgress?: (processed: number, total: number, status?: "success" | "error") => void,
): Promise<{ status: "success" | "error"; successful: number; failed: number; skipped: number }> {
// Extract data from sourceData - unified parameter pattern
const assets: mgmtApi.Media[] = sourceData || [];
Expand Down Expand Up @@ -93,36 +93,44 @@ export async function pushAssets(
// root level folder needs to be "/", otherwise the variable is OK to use.
let folderPath = containerFolderPath === "." ? "/" : containerFolderPath;


// Use simplified change detection pattern
const existingMapping = referenceMapper.getAssetMapping(media, "source");

// Also check if asset already exists in target by originKey (path+filename)
const targetAssetByOriginKey = targetData.find(t => t.originKey === media.originKey);
const targetAssetByOriginKey = targetData.find((t) => t.originKey === media.originKey);

// Debug logging for asset matching (verbose mode)
if (state.verbose && !existingMapping && !targetAssetByOriginKey) {
// Show first few target originKeys for comparison
const sampleTargetKeys = targetData.slice(0, 3).map(t => t.originKey);
const sampleTargetKeys = targetData.slice(0, 3).map((t) => t.originKey);
}

// If no mapping but asset exists by originKey in target, create mapping and skip
if (!existingMapping && targetAssetByOriginKey) {
referenceMapper.addMapping(media, targetAssetByOriginKey);
logger.asset.skipped(media, "already exists in target by path", targetGuid[0]);
skipped++;
continue;
}

const shouldCreate = existingMapping === null && !targetAssetByOriginKey;

// get the target asset, check if the source and targets need updates
const targetAsset: mgmtApi.Media = targetData.find(targetAsset => targetAsset.mediaID === existingMapping?.targetMediaID) || null;
const isTargetSafe = existingMapping !== null && referenceMapper.hasTargetChanged(targetAsset);
const targetAsset: mgmtApi.Media =
targetData.find((targetAsset) => targetAsset.mediaID === existingMapping?.targetMediaID) || null;
const hasTargetChanges = existingMapping !== null && referenceMapper.hasTargetChanged(targetAsset);
const hasSourceChanges = existingMapping !== null && referenceMapper.hasSourceChanged(media);
const shouldUpdate = existingMapping !== null && isTargetSafe && hasSourceChanges;
const shouldSkip = existingMapping !== null && !isTargetSafe && !hasSourceChanges;

const isConflict = existingMapping !== null && hasTargetChanges && hasSourceChanges;
const isOverwrite = state.overwrite;
const shouldSkip = existingMapping !== null && !hasTargetChanges && !hasSourceChanges;
// Update if:
// We have existing mapping
// AND We are not skipping because target AND source have no changes
// AND Either IF
// We have no conflict
// OR we are in overwrite mode (force update even if conflict)
const shouldUpdate = existingMapping !== null && !shouldSkip && (!isConflict || isOverwrite);

if (shouldCreate) {
// Asset needs to be created (doesn't exist in target)
Expand All @@ -134,7 +142,7 @@ export async function pushAssets(
sourceGuid[0],
targetGuid[0],
referenceMapper,
logger
logger,
);
referenceMapper.addMapping(media, createdAsset);
successful++;
Expand All @@ -148,7 +156,7 @@ export async function pushAssets(
sourceGuid[0],
targetGuid[0],
referenceMapper,
logger
logger,
);
referenceMapper.addMapping(media, updatedAsset);
successful++;
Expand All @@ -160,8 +168,7 @@ export async function pushAssets(
} catch (error: any) {
const errorMsg = extractErrorMessage(error);
logger.asset.error(media, errorMsg, targetGuid[0]);



failed++;
currentStatus = "error";
overallStatus = "error";
Expand All @@ -175,7 +182,7 @@ export async function pushAssets(
}

console.log(
ansiColors.yellow(`Processed ${successful}/${totalAssets} assets (${failed} failed, ${skipped} skipped)`)
ansiColors.yellow(`Processed ${successful}/${totalAssets} assets (${failed} failed, ${skipped} skipped)`),
);
return { status: overallStatus, successful, failed, skipped };
}
Expand All @@ -192,53 +199,52 @@ async function createAsset(
sourceGuid: string,
targetGuid: string,
referenceMapper: AssetMapper,
logger: Logs
logger: Logs,
): Promise<mgmtApi.Media> {
// Handle gallery if present
let targetMediaGroupingID = await resolveGalleryMapping(media, apiClient, sourceGuid, targetGuid);

const fs = require('fs');
const pathModule = require('path');
const fs = require("fs");
const pathModule = require("path");

// Resolve to absolute path from workspace root
const resolvedPath = pathModule.resolve(process.cwd(), absoluteLocalFilePath);

// Check file exists and has content
if (!fs.existsSync(resolvedPath)) {
throw new Error(`Local asset file not found: ${resolvedPath}`);
}

const fileStats = fs.statSync(resolvedPath);
if (fileStats.size === 0) {
throw new Error(`Local asset file is empty (0 bytes): ${resolvedPath}`);
}


// Build form data with file stream using resolved absolute path
const form = new FormData();
const fileStream = fs.createReadStream(resolvedPath);
form.append("files", fileStream, media.fileName);

// Make direct axios call with form-data headers (SDK bug workaround)
// The SDK's executePost doesn't include form.getHeaders() which is required for multipart uploads
const axios = require('axios');
const axios = require("axios");

// Get the base URL from the API client's options
const baseUrl = (apiClient as any)._options?.baseUrl || determineBaseUrl(targetGuid);
const token = (apiClient as any)._options?.token;

const apiPath = `asset/upload?folderPath=${encodeURIComponent(folderPath)}&groupingID=${targetMediaGroupingID}`;
const url = `${baseUrl}/api/v1/instance/${targetGuid}/${apiPath}`;

const response = await axios.post(url, form, {
headers: {
...form.getHeaders(), // Critical: include multipart boundary
'Authorization': `Bearer ${token}`,
'Cache-Control': 'no-cache'
Authorization: `Bearer ${token}`,
"Cache-Control": "no-cache",
},
maxContentLength: Infinity,
maxBodyLength: Infinity,
httpsAgent: state.local ? new (require('https').Agent)({ rejectUnauthorized: false }) : undefined
httpsAgent: state.local ? new (require("https").Agent)({ rejectUnauthorized: false }) : undefined,
});

const uploadedMediaArray = response.data as mgmtApi.Media[];
Expand All @@ -258,13 +264,13 @@ async function createAsset(
* Determine base URL for a GUID (fallback if not available from SDK)
*/
function determineBaseUrl(guid: string): string {
const separator = guid.split('-');
if (separator[1] === 'd') return "https://mgmt-dev.aglty.io";
if (separator[1] === 'u') return "https://mgmt.aglty.io";
if (separator[1] === 'us2') return "https://mgmt-usa2.aglty.io";
if (separator[1] === 'c') return "https://mgmt-ca.aglty.io";
if (separator[1] === 'e') return "https://mgmt-eu.aglty.io";
if (separator[1] === 'a') return "https://mgmt-aus.aglty.io";
const separator = guid.split("-");
if (separator[1] === "d") return "https://mgmt-dev.aglty.io";
if (separator[1] === "u") return "https://mgmt.aglty.io";
if (separator[1] === "us2") return "https://mgmt-usa2.aglty.io";
if (separator[1] === "c") return "https://mgmt-ca.aglty.io";
if (separator[1] === "e") return "https://mgmt-eu.aglty.io";
if (separator[1] === "a") return "https://mgmt-aus.aglty.io";
return "https://mgmt.aglty.io";
}

Expand All @@ -280,58 +286,58 @@ async function updateAsset(
sourceGuid: string,
targetGuid: string,
referenceMapper: AssetMapper,
logger: Logs
logger: Logs,
): Promise<mgmtApi.Media> {
// Handle gallery if present
let targetMediaGroupingID = await resolveGalleryMapping(media, apiClient, sourceGuid, targetGuid);
const fs = require('fs');
const pathModule = require('path');

const fs = require("fs");
const pathModule = require("path");

// Resolve to absolute path from workspace root
const resolvedPath = pathModule.resolve(process.cwd(), absoluteLocalFilePath);

// Check file exists and has content
if (!fs.existsSync(resolvedPath)) {
throw new Error(`Local asset file not found: ${resolvedPath}`);
}

const fileStats = fs.statSync(resolvedPath);
if (fileStats.size === 0) {
throw new Error(`Local asset file is empty (0 bytes): ${resolvedPath}`);
}

// Build form data with file stream using resolved absolute path
const form = new FormData();
const fileStream = fs.createReadStream(resolvedPath);
form.append("files", fileStream, media.fileName);

// Make direct axios call with form-data headers (SDK bug workaround)
const axios = require('axios');
const axios = require("axios");

const baseUrl = (apiClient as any)._options?.baseUrl || determineBaseUrl(targetGuid);
const token = (apiClient as any)._options?.token;

const apiPath = `asset/upload?folderPath=${encodeURIComponent(folderPath)}&groupingID=${targetMediaGroupingID}`;
const url = `${baseUrl}/api/v1/instance/${targetGuid}/${apiPath}`;

const response = await axios.post(url, form, {
headers: {
...form.getHeaders(),
'Authorization': `Bearer ${token}`,
'Cache-Control': 'no-cache'
Authorization: `Bearer ${token}`,
"Cache-Control": "no-cache",
},
maxContentLength: Infinity,
maxBodyLength: Infinity,
httpsAgent: state.local ? new (require('https').Agent)({ rejectUnauthorized: false }) : undefined
httpsAgent: state.local ? new (require("https").Agent)({ rejectUnauthorized: false }) : undefined,
});

const uploadedMediaArray = response.data as mgmtApi.Media[];

if (!uploadedMediaArray || uploadedMediaArray.length === 0) {
throw new Error(`API did not return uploaded media details for ${media.fileName}`);
}

const uploadedMedia = uploadedMediaArray[0];

logger.asset.uploaded(media, "uploaded", targetGuid);
Expand All @@ -348,7 +354,7 @@ async function resolveGalleryMapping(
apiClient: mgmtApi.ApiClient,

sourceGuid: string,
targetGuid: string
targetGuid: string,
// referenceMapper: AssetMapper,
): Promise<number> {
let targetMediaGroupingID = -1;
Expand All @@ -375,7 +381,7 @@ async function resolveGalleryMapping(
} catch (error: any) {
// Gallery doesn't exist - this is normal, asset will upload without gallery
console.log(
`[Asset] Gallery ${media.mediaGroupingName} not found - asset will upload without gallery association`
`[Asset] Gallery ${media.mediaGroupingName} not found - asset will upload without gallery association`,
);
}
}
Expand Down
12 changes: 7 additions & 5 deletions src/lib/pushers/container-pusher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,9 @@ export async function pushContainers(
const hasTargetChanges = containerMapper.hasTargetChanged(targetContainer);
const hasSourceChanges = containerMapper.hasSourceChanged(sourceContainer);
shouldUpdate = !hasTargetChanges && hasSourceChanges;
shouldSkip =
(existingMapping !== null && hasTargetChanges && !hasSourceChanges) ||
(existingMapping !== null && !hasSourceChanges && !hasTargetChanges);
shouldSkip = !hasSourceChanges && !hasTargetChanges;

if (overwrite) {
if (overwrite && hasTargetChanges) {
shouldUpdate = true;
shouldSkip = false;
}
Expand All @@ -110,7 +108,11 @@ export async function pushContainers(
// Container exists and is up to date - skip
logger.container.skipped(sourceContainer, "up to date, skipping", targetGuid[0]);
skipped++;
} else if (shouldUpdate) {
}else if (hasTargetChanges && !overwrite) {
// Container exists and is up to date - skip
logger.container.error(sourceContainer, "Conflict detected, use --overwrite to force changes", targetGuid[0]);
skipped++;
}else if (shouldUpdate) {

// Container exists but needs updating
const updateResult = await updateExistingContainer(
Expand Down
28 changes: 0 additions & 28 deletions src/lib/pushers/content-pusher/content-batch-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,6 @@ export class ContentBatchProcessor {
`[${progress}%] Bulk batch ${batchNumber}/${contentBatches.length}: Processing ${contentBatch.length} ${batchType} content items (ETA: ${etaMinutes}m)...`,
);

// if (onProgress) {
// onProgress(batchNumber, contentBatches.length, processedSoFar, contentItems.length, "processing");
// }

try {
// Prepare content payloads for bulk upload

Expand Down Expand Up @@ -191,17 +187,6 @@ export class ContentBatchProcessor {
// Don't fail the entire batch due to callback errors
}
}

// if (onProgress) {
// onProgress(
// batchNumber,
// contentBatches.length,
// processedSoFar + contentBatch.length,
// contentItems.length,
// "success"
// );
// }

// Add small delay between batches to prevent API throttling
if (i < contentBatches.length - 1) {
await new Promise((resolve) => setTimeout(resolve, 100));
Expand All @@ -218,21 +203,9 @@ export class ContentBatchProcessor {

totalFailureCount += failedBatchItems.length;
allFailedItems.push(...failedBatchItems);

// if (onProgress) {
// onProgress(
// batchNumber,
// contentBatches.length,
// processedSoFar + contentBatch.length,
// contentItems.length,
// "error"
// );
// }
}
}

// console.log(`🎯 Content batch processing complete: ${totalSuccessCount} success, ${totalFailureCount} failed`);

// Filter final publishableIds to only include items Published (state === 2) in source
const publishableItems = allSuccessfulItems.filter((item) => {
const sourceState = item.originalContent?.properties?.state;
Expand Down Expand Up @@ -364,7 +337,6 @@ export class ContentBatchProcessor {
existingContentID = existingTargetContentItem ? existingTargetContentItem.contentID : -1;
}


if (!existingMapping && !existingTargetContentItem) {
//see if this content item has been mapped in another locale
existingContentID = await findContentInOtherLocale({
Expand Down
Loading
Loading