feat: integrate rclone WebDAV backup for PC-to-PC file sync

Source PC serves backups/ folder via rclone WebDAV server (auto-starts with app).
Receiver PC pulls backups on schedule using rclone sync.
Network Backup UI now has two tabs: Rclone and API Key.
Rclone installed automatically via postinstall script.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ff
2026-06-24 23:29:36 -04:00
co-authored by Claude Sonnet 4.6
parent 24e66bfaf9
commit 5e881c9ff7
8 changed files with 723 additions and 148 deletions
@@ -15,6 +15,8 @@ import {
writeSyncConfig,
} from "../services/networkSyncConfigService";
import { runNetworkSync, runNetworkFilesSync } from "../services/networkSyncService";
import { readRcloneConfig, writeRcloneConfig } from "../services/rcloneConfigService";
import { checkRcloneInstalled, isServerRunning, startWebDavServer, stopWebDavServer, runRclonePull } from "../services/rcloneService";
const UPLOADS_DIR = path.join(process.cwd(), "uploads");
@@ -605,4 +607,63 @@ router.post("/network-sync-now", async (req: Request, res: Response): Promise<an
}
});
// ==============================
// Rclone Backup
// ==============================
router.get("/rclone-config", (req, res) => {
if (!req.user?.id) return res.status(401).json({ error: "Unauthorized" });
res.json(readRcloneConfig());
});
router.put("/rclone-config", (req: Request, res: Response): any => {
if (!req.user?.id) return res.status(401).json({ error: "Unauthorized" });
const { serverEnabled, serverPort, receiverEnabled, receiverSyncHour, sourceIp, sourcePort } = req.body;
const patch: any = {};
if (typeof serverEnabled === "boolean") patch.serverEnabled = serverEnabled;
if (typeof serverPort === "number") patch.serverPort = serverPort;
if (typeof receiverEnabled === "boolean") patch.receiverEnabled = receiverEnabled;
if (typeof receiverSyncHour === "number") patch.receiverSyncHour = receiverSyncHour;
if (typeof sourceIp === "string") patch.sourceIp = sourceIp.trim();
if (typeof sourcePort === "number") patch.sourcePort = sourcePort;
const updated = writeRcloneConfig(patch);
res.json(updated);
});
router.get("/rclone-status", async (req, res) => {
if (!req.user?.id) return res.status(401).json({ error: "Unauthorized" });
const installed = await checkRcloneInstalled();
const serverRunning = isServerRunning();
res.json({ installed, serverRunning });
});
router.post("/rclone-server/start", async (req: Request, res: Response): Promise<any> => {
if (!req.user?.id) return res.status(401).json({ error: "Unauthorized" });
try {
await startWebDavServer();
res.json({ success: true, running: true });
} catch (err: any) {
res.status(500).json({ error: "Failed to start rclone server", details: err.message });
}
});
router.post("/rclone-server/stop", (req: Request, res: Response): any => {
if (!req.user?.id) return res.status(401).json({ error: "Unauthorized" });
stopWebDavServer();
res.json({ success: true, running: false });
});
router.post("/rclone-pull-now", async (req: Request, res: Response): Promise<any> => {
if (!req.user?.id) return res.status(401).json({ error: "Unauthorized" });
try {
await runRclonePull();
writeRcloneConfig({ lastSyncAt: new Date().toISOString(), lastSyncStatus: "success", lastSyncError: null });
res.json({ success: true, syncedAt: new Date() });
} catch (err: any) {
writeRcloneConfig({ lastSyncAt: new Date().toISOString(), lastSyncStatus: "failed", lastSyncError: err.message });
res.status(500).json({ error: "Rclone pull failed", details: err.message });
}
});
export default router;