import fs from "fs"; import path from "path"; import os from "os"; import util from "util"; import crypto from "crypto"; import { exec } from "child_process"; import { SyncFoldersSyncFnParams } from "../types"; import grabDirNames from "./grab-dir-names"; import { fldFileToStr } from "./grab-folders-files-string-paths"; import delay from "./delay"; const execPromise = util.promisify(exec); const LOCK_STALE_MS = 5 * 60 * 1000; const LOCK_RETRY_MS = 100; function lockPathFor(dirPath: string) { const hash = crypto .createHash("sha1") .update(path.resolve(dirPath)) .digest("hex") .slice(0, 16); return path.join(os.tmpdir(), `turbosync-${hash}.lock`); } async function acquireLock(lockPath: string) { while (true) { try { const fd = fs.openSync(lockPath, "wx"); fs.writeFileSync(fd, String(process.pid)); fs.closeSync(fd); return; } catch (error: any) { if (error.code !== "EEXIST") throw error; try { const { mtimeMs } = fs.statSync(lockPath); if (Date.now() - mtimeMs > LOCK_STALE_MS) { fs.unlinkSync(lockPath); continue; } } catch { continue; } await delay(LOCK_RETRY_MS); } } } async function acquireLocks(lockPaths: string[]) { for (const lockPath of lockPaths) { await acquireLock(lockPath); } } function releaseLocks(lockPaths: string[]) { for (const lockPath of lockPaths) { try { fs.unlinkSync(lockPath); } catch {} } } export default async function sync({ options, dirs, dirPath, isFiles, firstRun, }: SyncFoldersSyncFnParams) { const dstDirs = dirs.filter((dr) => { if (typeof dr == "string") return dr !== dirPath; if (dr?.path) return dr.path !== dirPath; return false; }); const { ignoreFileName } = grabDirNames(); const rsyncIgnoreFile = path.join(dirPath, ignoreFileName); const rsyncTrailingSlash = isFiles ? "" : "/"; const allCommandsArr: string[][] = []; for (let j = 0; j < dstDirs.length; j++) { const dstDr = dstDirs[j]; let cmdArray = ["rsync", firstRun ? "-az" : "-azu", "--inplace"]; if (options?.delete) { cmdArray.push("--delete"); } if (options?.include?.[0]) { options.include.forEach((incl) => { cmdArray.push(`--include='${incl}'`); }); } if (fs.existsSync(rsyncIgnoreFile)) { cmdArray.push(`--exclude-from=${rsyncIgnoreFile}`); } if (options?.exclude?.[0]) { options.exclude.forEach((excl) => { cmdArray.push(`--exclude='${excl}'`); }); } if (typeof dstDr == "string") { if (!fs.existsSync(dstDr)) continue; if (dirPath === dstDr) { console.log( `You can't sync the same paths. Please check your configuration and resolve duplicate paths` ); process.exit(6); } cmdArray.push( path.normalize(dirPath) + rsyncTrailingSlash, path.normalize(dstDr) + rsyncTrailingSlash ); } else if (dstDr.path) { if (!dstDr.host && !fs.existsSync(dstDr.path)) continue; if (dirPath === dstDr.path) { console.log( `You can't sync the same paths. Please check your configuration and resolve duplicate paths` ); process.exit(6); } if (dstDr.host && dstDr.ssh_key && dstDr.user) { cmdArray.push("-e", `'ssh -i ${dstDr.ssh_key}'`); cmdArray.push( path.normalize(dirPath) + rsyncTrailingSlash, `${dstDr.user}@${dstDr.host}:${dstDr.path}${rsyncTrailingSlash}` ); } else { cmdArray.push( path.normalize(dirPath), path.normalize(dstDr.path) ); } } allCommandsArr.push(cmdArray); } const lockPaths = [dirPath, ...dstDirs.map((dr) => fldFileToStr(dr))] .filter((pth): pth is string => Boolean(pth)) .map((pth) => lockPathFor(pth)) .sort(); await acquireLocks(lockPaths); try { await Promise.all( allCommandsArr.map((cmdArr) => { return execPromise(cmdArr.join(" ")); }) ); } finally { releaseLocks(lockPaths); } console.log(`${dirPath} Folder Sync Complete. Exiting ...`); }