import path from 'path';
import fs from 'fs/promises';
import { sharpProcessor } from './sharp-processor.js';
import { cdnUploader } from '../../services/cdn/cdn-uploader.js';
import { cdnUrlBuilder } from '../../utils/cdn/cdn-url-builder.js';
import { appConfig } from '../../core/config/config.js';
import { isUrl } from '../../utils/helpers/shared-helpers.js';
import { logger } from '../../core/logger.js';
type ContentType = 'post' | 'story' | 'pin';
export interface ImagePipelineInput {
imageInput: string;
contentTypes: ContentType[];
}
export interface ImagePipelineResult {
cdnUrls: Partial<Record<ContentType, string>>;
skipped: boolean;
}
/**
* Image Pipeline — orchestrates sharp processing, CDN upload, and URL building.
*
* - If imageInput is a URL → skip processing, build CDN URLs directly.
* - If imageInput is a local filename → resolve from IMAGES_SOURCE_DIR (default: images/), process, upload, return CDN URLs.
*/
export class ImagePipeline {
/**
* Process an image input and return CDN URLs for each requested content type.
*/
async process(input: ImagePipelineInput): Promise<ImagePipelineResult> {
const { imageInput, contentTypes } = input;
if (contentTypes.length === 0) {
return { cdnUrls: {}, skipped: true };
}
// If input is already a URL, skip processing and build CDN URLs directly
if (isUrl(imageInput)) {
logger.debug('Image is a URL, skipping local processing', { imageInput });
const cdnUrls: Partial<Record<ContentType, string>> = {};
const filename = path.basename(new URL(imageInput).pathname);
for (const ct of contentTypes) {
cdnUrls[ct] = cdnUrlBuilder.isConfigured()
? cdnUrlBuilder.buildURLForPublishing(filename, ct)
: imageInput;
}
return { cdnUrls, skipped: true };
}
// Resolve local file path (bare filename → images source dir from config)
const sourcePath = imageInput.includes(path.sep) || imageInput.includes('/')
? imageInput
: path.join(appConfig.getImagesSourceDir(), imageInput);
// Check file exists
try {
await fs.access(sourcePath);
} catch {
throw new Error(`Image not found: ${sourcePath}`);
}
// Check SCP is configured for local images
if (!cdnUploader.isConfigured()) {
throw new Error(
'SCP not configured. Set SCP_HOST, SCP_USER, SCP_KEY_PATH, SCP_REMOTE_BASE_PATH in .env'
);
}
if (!cdnUrlBuilder.isConfigured()) {
throw new Error('CDN_BASE_URL must be configured when processing local images');
}
// Process image for each content type
const processed = await sharpProcessor.processImage(sourcePath, contentTypes);
// Upload to CDN
const uploadFiles = processed.map((p) => ({
localPath: p.outputPath,
contentType: p.contentType,
}));
await cdnUploader.uploadBatch(uploadFiles);
// Build CDN URLs from the processed filenames (always .jpg)
const cdnUrls: Partial<Record<ContentType, string>> = {};
for (const p of processed) {
const jpgFilename = path.basename(p.outputPath);
cdnUrls[p.contentType] = cdnUrlBuilder.buildURLForPublishing(jpgFilename, p.contentType);
}
logger.info('Image pipeline complete', { cdnUrls });
return { cdnUrls, skipped: false };
}
}
export const imagePipeline = new ImagePipeline();