import { execFile } from 'child_process';
import { promisify } from 'util';
import path from 'path';
import { appConfig } from '../../core/config/config.js';
import { logger } from '../../core/logger.js';
const execFileAsync = promisify(execFile);
/**
* CDN Uploader — uploads processed images to a CDN server via system `scp`.
*/
export class CDNUploader {
/**
* Check if SCP upload is configured (all 4 env vars present)
*/
isConfigured(): boolean {
return appConfig.isSCPConfigured();
}
/**
* Upload a single file to the CDN.
* @param localPath - Local file path to upload
* @param contentType - Content type directory (post, story, pin)
* @returns Remote path on the CDN server
*/
async upload(localPath: string, contentType: 'post' | 'story' | 'pin'): Promise<string> {
const scpConfig = appConfig.getSCPConfig();
if (!scpConfig) {
throw new Error(
'SCP not configured. Set SCP_HOST, SCP_USER, SCP_KEY_PATH, SCP_REMOTE_BASE_PATH in .env'
);
}
const { host, user, keyPath, remoteBasePath } = scpConfig;
const resolvedKeyPath = keyPath.startsWith('~')
? path.join(process.env.HOME || '', keyPath.slice(1))
: keyPath;
const filename = path.basename(localPath);
const remoteDir = path.posix.join(remoteBasePath, contentType);
const remotePath = path.posix.join(remoteDir, filename);
const remoteTarget = `${user}@${host}:${remotePath}`;
// Create remote directory if needed
logger.debug('Ensuring remote directory exists', { remoteDir });
try {
await execFileAsync('ssh', [
'-i', resolvedKeyPath,
'-o', 'StrictHostKeyChecking=accept-new',
`${user}@${host}`,
`mkdir -p ${remoteDir}`,
]);
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to create remote directory ${remoteDir}: ${msg}`);
}
// Upload via scp
logger.info('Uploading to CDN', { localPath: filename, remote: remoteTarget });
try {
await execFileAsync('scp', [
'-i', resolvedKeyPath,
'-o', 'StrictHostKeyChecking=accept-new',
localPath,
remoteTarget,
]);
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
throw new Error(`SCP upload failed for ${filename}: ${msg}`);
}
logger.info('Upload complete', { remotePath });
return remotePath;
}
/**
* Upload multiple files sequentially (to avoid SSH connection limits).
* @param files - Array of { localPath, contentType }
* @returns Array of remote paths
*/
async uploadBatch(
files: Array<{ localPath: string; contentType: 'post' | 'story' | 'pin' }>
): Promise<string[]> {
const remotePaths: string[] = [];
for (const file of files) {
const remotePath = await this.upload(file.localPath, file.contentType);
remotePaths.push(remotePath);
}
return remotePaths;
}
}
export const cdnUploader = new CDNUploader();