Files
turbo-sync/utils/sync.ts
T
2026-08-10 11:07:39 +01:00

172 lines
4.7 KiB
TypeScript

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 ...`);
}