From 21d6ba573334b81031ac522899d4e702cf2e90d0 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Wed, 16 Oct 2024 05:44:48 +0100 Subject: [PATCH] First Commit --- .gitignore | 179 ++++++++++++++++++++++++++++++++++++++ README.md | 57 ++++++++++++ index.js | 90 +++++++++++++++++++ lib/sync.js | 216 ++++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 17 ++++ package.json | 18 ++++ tsconfig.json | 27 ++++++ types.js | 28 ++++++ 8 files changed, 632 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 index.js create mode 100644 lib/sync.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 tsconfig.json create mode 100644 types.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79a9388 --- /dev/null +++ b/.gitignore @@ -0,0 +1,179 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + +/test +/lib-node +/dump diff --git a/README.md b/README.md new file mode 100644 index 0000000..cee10d3 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# Turbo Sync + +A no-nonsense file/folder synchronization application + +## Requirements + +Turbo sync requires **node js** and **rsync** + +## Installation + +```bash +npm install --registry="https://git.tben.me/api/packages/Moduletrace/npm/" -g turbo-sync +``` + +## Usage + +```bash +turbo-sync ./turbosync.config.json +``` + +### Config File + +The config file is a json file that contains all the information needed to run turbo-sync. Example: + +```json +[ + { + "title": "Sync Files", + "files": [ + "/home/user/file1.txt", + "/home/user/file2.txt", + { + "path": "/home/user/file3", + "user": "root", + "host": "5.34.75.236", + "ssh_key": "/home/user/.ssh/key" + } + ] + }, + { + "title": "Sync Folders", + "options": { + "delete": true + }, + "folders": [ + "/home/user/folder1", + "/home/user/folder2", + { + "path": "/home/user/folder3", + "user": "user", + "host": "5.39.67.76", + "ssh_key": "/home/user/.ssh/key" + } + ] + } +] +``` diff --git a/index.js b/index.js new file mode 100644 index 0000000..c68fa8b --- /dev/null +++ b/index.js @@ -0,0 +1,90 @@ +#! /usr/bin/env node +// @ts-check + +const fs = require("fs"); +const path = require("path"); +const { execSync, spawn, ChildProcess } = require("child_process"); + +/** @type {string[]} */ +let dirs = []; + +console.log("Running Folder Sync ..."); + +const defaultConfigFilePath = path.resolve( + process.cwd(), + "turbosync.config.json" +); + +const confFileProvidedPath = process.argv[process.argv.length - 1]; +const confFileComputedPath = + typeof confFileProvidedPath == "string" && + confFileProvidedPath.endsWith(".json") + ? path.resolve(process.cwd(), confFileProvidedPath) + : null; + +if (!fs.existsSync(defaultConfigFilePath) && !confFileComputedPath) { + console.log( + "Please Provide the path to a config file or add a config file named `turbosync.config.json` to the path you're running this program" + ); + + process.exit(); +} + +if ( + !defaultConfigFilePath && + confFileComputedPath && + !fs.existsSync(confFileComputedPath) +) { + console.log("Config File does not exist"); + process.exit(); +} + +// /** @type {ChildProcess[]} */ +// const childProcesses = []; + +try { + const configJSON = fs.existsSync(defaultConfigFilePath) + ? fs.readFileSync(defaultConfigFilePath, "utf8") + : confFileComputedPath + ? fs.readFileSync(confFileComputedPath, "utf8") + : null; + + if (!configJSON) + throw new Error( + "Config JSON could not be resolved. Please check your files." + ); + + /** @type {TurboSyncConfigArray} */ + const configArray = JSON.parse(configJSON); + + for (let i = 0; i < configArray.length; i++) { + const config = configArray[i]; + console.log(`Syncing \`${config.title} ...\``); + + const childProcess = spawn( + "node", + [ + path.resolve(__dirname, "./lib/sync.js"), + `${JSON.stringify(config)}`, + ], + { + stdio: "inherit", + detached: false, + } + ); + } +} catch (error) { + console.log(`Process Error =>`, error.message); + process.exit(); +} + +setInterval(() => { + console.log("Turbo Sync Running ..."); +}, 60000); + +// process.on("exit", () => { +// for (let i = 0; i < childProcesses.length; i++) { +// const childProcess = childProcesses[i]; +// childProcess.kill("SIGTERM"); +// } +// }); diff --git a/lib/sync.js b/lib/sync.js new file mode 100644 index 0000000..c4deaca --- /dev/null +++ b/lib/sync.js @@ -0,0 +1,216 @@ +#! /usr/bin/env node + +// @ts-check + +const fs = require("fs"); +const path = require("path"); +const { execSync, spawn } = require("child_process"); + +const confFileProvidedJSON = process.argv[process.argv.length - 1]; + +try { + /** @type {TurboSyncConfigObject} */ + const configFileObject = JSON.parse(confFileProvidedJSON); + + console.log(`Running '${configFileObject.title}' ...`); + + if ( + Array.isArray(configFileObject.files) && + Array.isArray(configFileObject.folders) + ) { + throw new Error("Choose wither `files` or `folders`. Not both"); + } + + const files = configFileObject?.files; + const firstFile = files?.[0]; + const folders = configFileObject?.folders; + const firstFolder = folders?.[0]; + + const options = configFileObject.options; + + if (firstFile && files) { + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const filePath = + typeof file == "string" ? file : file?.path ? file.path : null; + const interval = typeof file == "object" ? file.interval : null; + if (!filePath) continue; + + if (typeof file == "string" && !fs.existsSync(filePath)) + throw new Error("File Doesn't exist"); + if (typeof file == "string" && !fs.statSync(filePath).isFile()) { + throw new Error(`'${filePath}' is not a File!`); + } + + if (typeof file == "object" && file.host) { + // TODO Handle SSH + } else if (typeof file == "string") { + fs.watchFile( + filePath, + { + interval: interval || 500, + }, + (curr, prev) => { + let cmdArray = ["rsync", "-avh"]; + + if (options?.delete) { + cmdArray.push("--delete"); + } + + if (options?.exclude?.[0]) { + options.exclude.forEach((excl) => { + cmdArray.push(`--exclude '${excl}'`); + }); + } + + const destFiles = files.filter((fl) => { + if (typeof fl == "string") return fl !== filePath; + if (fl?.path) return fl.path !== filePath; + return false; + }); + + for (let j = 0; j < destFiles.length; j++) { + const dstFl = destFiles[j]; + if (typeof dstFl == "string") { + if (!fs.existsSync(dstFl)) continue; + cmdArray.push(filePath, dstFl); + const cmd = cmdArray.join(" "); + execSync(cmd, { + stdio: "inherit", + }); + } else if (dstFl.path) { + if (!dstFl.host && !fs.existsSync(dstFl.path)) + continue; + + if (dstFl.host && dstFl.ssh_key && dstFl.user) { + cmdArray.push( + "-e", + `'ssh -i ${dstFl.ssh_key}'` + ); + cmdArray.push( + filePath, + `${dstFl.user}@${dstFl.host}:${dstFl.path}` + ); + const cmd = cmdArray.join(" "); + execSync(cmd, { + stdio: "inherit", + }); + } else { + cmdArray.push(filePath, dstFl.path); + const cmd = cmdArray.join(" "); + execSync(cmd, { + stdio: "inherit", + }); + } + } + } + + process.exit(1); + } + ); + } + } + } else if (firstFolder && folders?.[0]) { + const dirs = folders; + + for (let i = 0; i < dirs.length; i++) { + const dir = dirs[i]; + + if (!dir) continue; + + const dirPath = typeof dir == "string" ? dir : dir.path; + + if (typeof dir == "string") { + } + + if (typeof dir == "string" && !fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { + recursive: true, + }); + } + + if (typeof dir == "string") { + fs.watch(dirPath, { recursive: true }, (evt, fileName) => { + let cmdArray = ["rsync", "-avh"]; + + if (options?.delete) { + cmdArray.push("--delete"); + } + + if (options?.exclude?.[0]) { + options.exclude.forEach((excl) => { + cmdArray.push(`--exclude '${excl}'`); + }); + } + + const dstDirs = dirs.filter((dr) => { + if (typeof dr == "string") return dr !== dirPath; + if (dr?.path) return dr.path !== dirPath; + return false; + }); + + for (let j = 0; j < dstDirs.length; j++) { + const dstDr = dstDirs[j]; + if (typeof dstDr == "string") { + if (!fs.existsSync(dstDr)) continue; + cmdArray.push( + path.normalize(dirPath) + "/", + path.normalize(dstDr) + "/" + ); + const cmd = cmdArray.join(" "); + execSync(cmd, { + stdio: "inherit", + }); + } else if (dstDr.path) { + if (!dstDr.host && !fs.existsSync(dstDr.path)) + continue; + + if (dstDr.host && dstDr.ssh_key && dstDr.user) { + cmdArray.push( + "-e", + `'ssh -i ${dstDr.ssh_key}'` + ); + cmdArray.push( + path.normalize(dirPath) + "/", + `${dstDr.user}@${dstDr.host}:${dstDr.path}/` + ); + const cmd = cmdArray.join(" "); + execSync(cmd, { + stdio: "inherit", + }); + } else { + cmdArray.push( + path.normalize(dirPath), + path.normalize(dstDr.path) + ); + const cmd = cmdArray.join(" "); + execSync(cmd, { + stdio: "inherit", + }); + } + } + } + + process.exit(1); + }); + } + } + } +} catch (error) { + console.log(error); + process.exit(); +} + +process.on("exit", (code) => { + if (code == 1) { + const args = process.argv; + const cmd = args.shift(); + if (cmd) { + spawn(cmd, args, { + stdio: "inherit", + }); + } + } else { + process.exit(0); + } +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..cdfdab4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,17 @@ +{ + "name": "turbo-sync", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "turbo-sync", + "version": "1.0.0", + "license": "ISC", + "bin": { + "turbo-sync": "index.js", + "turbosync": "index.js" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fc74551 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "turbo-sync", + "version": "1.0.0", + "module": "index.js", + "scripts": { + "start": "node index.ts", + "build": "tsc", + "dev": "node index.js --watch" + }, + "bin": { + "turbo-sync": "./index.js", + "turbosync": "./index.js" + }, + "description": "To install dependencies:", + "main": "index.js", + "author": "Benjamin Toby", + "license": "ISC" +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ec3f79f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2015", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "emitDeclarationOnly": true, + + "noUnusedLocals": false, + "noUnusedParameters": false, + "declaration": true, + "outDir": "dump/dist", + "noPropertyAccessFromIndexSignature": false + }, + "include": ["index.ts", "index.d.ts", "**/*.ts"], + "exclude": ["dump"] +} diff --git a/types.js b/types.js new file mode 100644 index 0000000..4750ad5 --- /dev/null +++ b/types.js @@ -0,0 +1,28 @@ +// @ts-check + +/** + * @typedef {TurboSyncConfigObject[]} TurboSyncConfigArray + */ + +/** + * @typedef {object} TurboSyncConfigObject + * @property {string} [title] + * @property {string[] | TurboSyncFileObject[]} [files] + * @property {string[] | TurboSyncFileObject[]} [folders] + * @property {TurboSyncOptions} [options] + */ + +/** + * @typedef {object} TurboSyncFileObject + * @property {string} path + * @property {string} [host] + * @property {string} [user] + * @property {string} [ssh_key] + * @property {number} [interval] + */ + +/** + * @typedef {object} TurboSyncOptions + * @property {boolean} [delete] - Should files removed be deleted in all destinations? + * @property {string[]} [exclude] - Patterns that should be ignored. Eg "*.log" + */