import Client from "ssh2-sftp-client"; import { readFileSync } from "node:fs"; import process from "node:process"; import { Worker, isMainThread, workerData } from "node:worker_threads"; type Config = { host: string; port: number; username: string; password: string; }; type Profiles = Record; type ConfigFile = { profiles: Profiles; }; const CONFIG_PATH = new URL("./config.json", import.meta.url); function loadConfig(configArg?: string): Config { let configFile: ConfigFile; try { configFile = JSON.parse(readFileSync(CONFIG_PATH, "utf-8")) as ConfigFile; } catch (error) { throw new Error( `Could not read config file at ${CONFIG_PATH.href}. Copy config.example.json to config.json and fill in your credentials.`, ); } const profiles = configFile.profiles ?? {}; const profileName = configArg ?? "default"; const config = profiles[profileName]; if (!config) { throw new Error( `Unknown profile "${profileName}". Available profiles: ${Object.keys(profiles).join(", ")}`, ); } if (!config.host || !config.username || !config.password) { throw new Error( `Profile "${profileName}" is missing host, username, or password in config.json.`, ); } return { ...config, port: Number(config.port) || 22 }; } class SftpService { private readonly config: Config; constructor(config: Config) { this.config = config; } async uploadFile(sourcePath: string, remotePath: string) { const client = new Client(); try { await client.connect(this.config); await client.put(sourcePath, remotePath); } catch (error) { if (error instanceof Error) { console.error(error.message); } } finally { await client.end(); } } async deleteFile(remotePath: string) { const client = new Client(); try { await client.connect(this.config); await client.delete(remotePath); } catch (error) { if (error instanceof Error) { console.error(error.message); } } finally { await client.end(); } } } function runWorker(runIndex: number, configArg?: string): Promise { return new Promise((resolve, reject) => { const worker = new Worker(new URL(import.meta.url), { workerData: { runIndex, configArg }, }); worker.on("exit", (code) => { if (code !== 0) { reject(new Error(`Worker ${runIndex} exited with code ${code}`)); } else { resolve(); } }); worker.on("error", (err) => reject(err)); }); } async function executeIteration(runIndex: number, configArg?: string) { const sftp = new SftpService(loadConfig(configArg)); const remotePath = `./${runIndex}-test.txt`; const sourcePath = "./test.txt"; console.log(`[Worker ${runIndex}] Starting run number ${runIndex}`); console.log(`[Worker ${runIndex}] Uploading file...`); await sftp.uploadFile(sourcePath, remotePath); console.log(`[Worker ${runIndex}] File uploaded`); console.log(`[Worker ${runIndex}] Deleting file...`); await sftp.deleteFile(remotePath); console.log(`[Worker ${runIndex}] File deleted`); } async function main() { if (isMainThread) { const configArg = process.argv[2]; const TOTAL_RUNS = 6; console.log(`[Main] Spawning ${TOTAL_RUNS} parallel workers...`); const workerPromises: Promise[] = []; for (let i = 0; i < TOTAL_RUNS; i++) { workerPromises.push(runWorker(i, configArg)); } await Promise.all(workerPromises); console.log("[Main] All workers completed execution successfully."); } else { const { runIndex, configArg } = workerData; await executeIteration(runIndex, configArg); } } await main();