feat: include uploads folder in network sync (all three subfolders)

Daily sync and Sync Now both pull database + uploads in one operation.
PC1 streams uploads/ as a zip via GET /network-backup-files (archiver).
PC2 clears cloud-storage, patients, and patient-documents then extracts
the fresh copy before resolving. Timeout extended to 5 min for large files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ff
2026-06-09 10:10:46 -04:00
parent f5f3768108
commit a8ec1a21c0
4 changed files with 111 additions and 7 deletions

View File

@@ -4,8 +4,11 @@ import https from "https";
import { URL } from "url";
import path from "path";
import fs from "fs";
import os from "os";
import { prisma } from "@repo/db/client";
const UPLOADS_DIR = path.resolve(process.cwd(), "uploads");
const MIGRATIONS_DIR = path.resolve(__dirname, "../../../../packages/db/prisma/migrations");
async function applyMissingMigrations() {
@@ -115,3 +118,74 @@ export function runNetworkSync(sourceUrl: string, apiKey: string): Promise<void>
});
});
}
export function runNetworkFilesSync(sourceUrl: string, apiKey: string): Promise<void> {
return new Promise((resolve, reject) => {
let targetUrl: URL;
try {
targetUrl = new URL("/api/database-management/network-backup-files", sourceUrl);
} catch {
return reject(new Error(`Invalid source URL: ${sourceUrl}`));
}
const client = targetUrl.protocol === "https:" ? https : http;
const tmpZip = path.join(os.tmpdir(), `network_files_${Date.now()}.zip`);
const tmpFile = fs.createWriteStream(tmpZip);
const req = client.get(
targetUrl.href,
{ headers: { "x-network-backup-key": apiKey } },
(res) => {
if (res.statusCode !== 200) {
res.resume();
tmpFile.close(() => { try { fs.unlinkSync(tmpZip); } catch {} });
return reject(new Error(`Source returned HTTP ${res.statusCode}: ${res.statusMessage}`));
}
res.pipe(tmpFile);
tmpFile.on("finish", () => {
tmpFile.close(() => {
// Clear all three upload subfolders, then extract fresh copy
for (const sub of ["cloud-storage", "patients", "patient-documents"]) {
const subDir = path.join(UPLOADS_DIR, sub);
if (fs.existsSync(subDir)) {
fs.rmSync(subDir, { recursive: true, force: true });
}
fs.mkdirSync(subDir, { recursive: true });
}
const unzip = spawn("unzip", ["-o", tmpZip, "-d", UPLOADS_DIR]);
let stderr = "";
unzip.stderr.on("data", (d) => (stderr += d.toString()));
unzip.on("error", (err) => {
try { fs.unlinkSync(tmpZip); } catch {}
reject(new Error(`Failed to extract uploads zip: ${err.message}`));
});
unzip.on("close", (code) => {
try { fs.unlinkSync(tmpZip); } catch {}
if (code !== 0) {
return reject(new Error(`unzip exited ${code}: ${stderr}`));
}
resolve();
});
});
});
tmpFile.on("error", (err) => {
try { fs.unlinkSync(tmpZip); } catch {}
reject(new Error(`Failed to write temp zip: ${err.message}`));
});
}
);
req.on("error", (err) => {
try { fs.unlinkSync(tmpZip); } catch {}
reject(new Error(`Network request failed: ${err.message}`));
});
req.setTimeout(300_000, () => {
req.destroy();
reject(new Error("File sync request timed out after 5 minutes"));
});
});
}