import { spawn, ChildProcess } from "child_process"; import path from "path"; import fs from "fs"; import { readRcloneConfig, RCLONE_USER, RCLONE_PASS } from "./rcloneConfigService"; const APP_ROOT = process.cwd(); const SYNC_FOLDERS = ["backups", "chat-history", "uploads"]; let serverProcess: ChildProcess | null = null; export function checkRcloneInstalled(): Promise { return new Promise((resolve) => { const proc = spawn("rclone", ["version"]); proc.on("error", () => resolve(false)); proc.on("close", (code) => resolve(code === 0)); }); } function obscurePassword(password: string): Promise { return new Promise((resolve, reject) => { const proc = spawn("rclone", ["obscure", password]); let stdout = ""; let stderr = ""; proc.stdout.on("data", (d) => (stdout += d.toString())); proc.stderr.on("data", (d) => (stderr += d.toString())); proc.on("error", (err) => reject(new Error(`Failed to run rclone obscure: ${err.message}`))); proc.on("close", (code) => { if (code !== 0) return reject(new Error(`rclone obscure failed: ${stderr}`)); resolve(stdout.trim()); }); }); } export function isServerRunning(): boolean { return serverProcess !== null && serverProcess.exitCode === null; } // Source PC: rclone serve webdav ./backups --addr :8080 --user MyDentalApp --pass SuperSecret!@2026 export async function startWebDavServer(): Promise { if (isServerRunning()) return; const installed = await checkRcloneInstalled(); if (!installed) { throw new Error("rclone is not installed. Run: curl https://rclone.org/install.sh | sudo bash"); } const config = readRcloneConfig(); const args = [ "serve", "webdav", APP_ROOT, "--addr", `:${config.serverPort}`, "--user", RCLONE_USER, "--pass", RCLONE_PASS, "--read-only", ]; serverProcess = spawn("rclone", args, { stdio: "pipe" }); serverProcess.stdout?.on("data", (d) => console.log(`[rclone-server] ${d.toString().trim()}`)); serverProcess.stderr?.on("data", (d) => console.log(`[rclone-server] ${d.toString().trim()}`)); serverProcess.on("error", (err) => { console.error("[rclone-server] Failed to start:", err.message); serverProcess = null; }); serverProcess.on("close", (code) => { console.log(`[rclone-server] Exited with code ${code}`); serverProcess = null; }); await new Promise((resolve) => setTimeout(resolve, 1000)); if (!isServerRunning()) { throw new Error("rclone WebDAV server failed to start"); } console.log(`[rclone-server] WebDAV server started on port ${config.serverPort}`); } export function stopWebDavServer(): void { if (serverProcess && serverProcess.exitCode === null) { serverProcess.kill("SIGTERM"); serverProcess = null; console.log("[rclone-server] WebDAV server stopped"); } } // Sync a single folder from the source PC via WebDAV function syncFolder(folder: string, webdavUrl: string, obscuredPass: string): Promise { const localDir = path.join(APP_ROOT, folder); if (!fs.existsSync(localDir)) { fs.mkdirSync(localDir, { recursive: true }); } const args = [ "sync", `:webdav:/${folder}`, localDir, "--webdav-url", webdavUrl, "--webdav-user", RCLONE_USER, "--webdav-pass", obscuredPass, "-v", ]; return new Promise((resolve, reject) => { const proc = spawn("rclone", args); let stderr = ""; proc.stdout.on("data", (d) => console.log(`[rclone-pull] ${d.toString().trim()}`)); proc.stderr.on("data", (d) => { const line = d.toString().trim(); stderr += line + "\n"; console.log(`[rclone-pull] ${line}`); }); proc.on("error", (err) => reject(new Error(`Failed to start rclone: ${err.message}`))); proc.on("close", (code) => { if (code !== 0) return reject(new Error(`rclone sync ${folder} failed (exit ${code}): ${stderr}`)); resolve(); }); }); } // Receiver PC: pull backups, chat-history, and uploads from source export async function runRclonePull(): Promise { const config = readRcloneConfig(); if (!config.sourceIp || !config.sourcePort) { throw new Error("Rclone receiver configuration is incomplete — source IP and port are required"); } const installed = await checkRcloneInstalled(); if (!installed) { throw new Error("rclone is not installed. Run: curl https://rclone.org/install.sh | sudo bash"); } const obscuredPass = await obscurePassword(RCLONE_PASS); const webdavUrl = `http://${config.sourceIp}:${config.sourcePort}`; for (const folder of SYNC_FOLDERS) { console.log(`[rclone-pull] Syncing ${folder}...`); await syncFolder(folder, webdavUrl, obscuredPass); } } export async function autoStartServer(): Promise { const config = readRcloneConfig(); if (config.serverEnabled) { try { await startWebDavServer(); } catch (err) { console.error("[rclone-server] Auto-start failed:", err); } } }