Advertisement
Guest User

Untitled

a guest
Oct 14th, 2017
128
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 16.13 KB | None | 0 0
  1. "use strict";
  2. /**
  3. * Synchronization.
  4. *
  5. * @module
  6. */
  7.  
  8. var _ = require("lodash");
  9. var fp = require("lodash/fp");
  10. var FtpClient = require("ftp");
  11. var GitClient = require("nodegit");
  12. var mkdirp = require("mkdirp");
  13. var spawn = require("cross-spawn");
  14.  
  15. var CONF = require("./config");
  16. var logger = require("./logger");
  17. var Pool = require("./pool");
  18.  
  19. var ftp; // ftp client instance
  20. /**
  21. * Synchronizes `FTP` folder with `GIT` repository.
  22. *
  23. * It makes following:
  24. * - Clones or connects to existing `GIT` repo;
  25. * - Pulls updates from remote `GIT` repo;
  26. * - Collects files on `GIT` folder;
  27. * - Collects files on `FTP` folder;
  28. * - If file is present in `GIT` folder and absent in `FTP` folder, it is
  29. * removed from `GIT` folder;
  30. * - If file is present in `FTP` folder and absent in `GIT` folder, it is
  31. * downloaded from `FTP` to `GIT` folder;
  32. * - If file is present in `FTP` and `GIT` folders, it is downloaded from
  33. * `FTP` to `GIT` folder only if last modified date of file in `FTP` is
  34. * later that in `GIT` folder;
  35. * - Pulls updates from remote `GIT` repo;
  36. * - If there are changes in `GIT` folder, they are committed and pushed
  37. * to remote `GIT` repo;
  38. * If options are not specified, they are requested from config.
  39. *
  40. * @function
  41. * @arg {object} [opts] - Options.
  42. * @arg {number} [opts.numberOfThreads] - Number of threads to download
  43. * FTP files.
  44. * @arg {object} [opts.ftp] - FTP options.
  45. * @arg {string} [opts.ftp.folder] - Path to folder on FTP, which will be
  46. * uploaded to GIT repo.
  47. * @arg {string} [opts.ftp.host] - FTP host.
  48. * @arg {number} [opts.ftp.port] - FTP port.
  49. * @arg {string} [opts.ftp.username] - FTP user name.
  50. * @arg {string} [opts.ftp.password] - FTP user password.
  51. * @arg {object} [opts.git] - GIT options.
  52. * @arg {string} [opts.git.root] - Path to GIT root folder.
  53. * @arg {string} [opts.git.folder] - Path to folder in GIT, where FTP files
  54. * will be downloaded.
  55. * @arg {string} [opts.git.url] - URL of GIT repository.
  56. * @arg {string} [opts.git.committerName] - Name of GIT committer.
  57. * @arg {string} [opts.git.committerEmail] - E-mail of GIT committer.
  58. * @arg {string} [opts.git.publicKey] - Path to GIT associated public key.
  59. * @arg {string} [opts.git.privateKey] - Path to GIT associated private key.
  60. */
  61. var sync = module.exports = opts => {
  62.  
  63. opts = opts || {};
  64. opts = fp.merge(CONF, opts);
  65.  
  66. var gitAuth = {
  67. callbacks: {
  68. credentials: (url, userName) => {
  69. return GitClient.Cred.sshKeyNew(opts.git.commiterName,
  70. opts.git.publicKey,
  71. opts.git.privateKey);
  72. },
  73. },
  74. };
  75.  
  76. console.log("Synchronization is started.")
  77. ftp = new FtpClient();
  78.  
  79. ftp.on("ready", async () => {
  80. var gitFiles = collectGitFiles(opts.git.root, opts.git.folder);
  81. var ftpFiles = await collectFtpFiles(opts.ftp.folder);
  82.  
  83. var filesToAdd, filesToRemove;
  84. [ filesToAdd, filesToRemove ] = getFilesToUpdate(ftpFiles, gitFiles);
  85.  
  86. await downloadFiles(filesToAdd,
  87. ftpFiles,
  88. opts.ftp.folder,
  89. opts.git.folder,
  90. opts.numberOfThreads);
  91. removeFiles(filesToRemove, opts.git.folder);
  92.  
  93. ftp.end();
  94. });
  95.  
  96. ftp.on("greeting", msg => {
  97. logger.info("FTP connection established:", msg);
  98. });
  99.  
  100. ftp.on("error", err => {
  101. logger.error("FTP error detected:", err);
  102. });
  103.  
  104. ftp.on("end", async () => {
  105. await pushRepoChanges();
  106. console.log("Synchronization is finished.")
  107. });
  108.  
  109. getRepo(opts.git.root,
  110. opts.git.url,
  111. opts.git.commiterName,
  112. opts.git.publicKey,
  113. opts.git.privateKey,
  114. () => {
  115. var options = { host: opts.ftp.host,
  116. port: opts.ftp.port,
  117. user: opts.ftp.username,
  118. password: opts.ftp.password };
  119. ftp.connect(options);
  120. });
  121. };
  122. /**
  123. * Collects ftp files.
  124. *
  125. * @function
  126. * @async
  127. * @arg {string} remoteRoot - Root folder of FTP server to scan.
  128. * @return {object} - Dictionary of FTP files data.
  129. */
  130. var collectFtpFiles = async remoteRoot => {
  131. var files = {};
  132. var folders = [""];
  133.  
  134. logger.info("Collecting files on FTP server...");
  135. console.log("Collecting files on FTP server...");
  136.  
  137. var collect = folder => {
  138.  
  139. return new Promise((resolve, reject) => {
  140. var remoteFolder = path.resolve(remoteRoot, folder);
  141.  
  142. ftp.cwd(remoteFolder, () => {
  143. ftp.pwd((err, cwd) => {
  144. if (err) {
  145. reject(err);
  146. return;
  147. };
  148.  
  149. ftp.list(cwd, (err, files) => {
  150. if (err) {
  151. reject(err);
  152. return;
  153. };
  154.  
  155. for (var file of files) {
  156. var filePath = file.name;
  157.  
  158. if (folder) {
  159. filePath = folder + path.sep + filePath;
  160. };
  161.  
  162. if (file.type === "d") {
  163. folders.push(filePath);
  164. } else {
  165. files[filePath] = { path: filePath,
  166. size: file.size,
  167. date: file.date };
  168.  
  169. logger.debug("Collect FTP file", filePath);
  170. process.stdout.write('.');
  171. };
  172. };
  173. resolve();
  174. });
  175. });
  176. });
  177. });
  178. };
  179.  
  180. while (folders.length) {
  181. await collect(folders.shift());
  182. };
  183.  
  184. logger.info("FTP collection is finished.");
  185. console.log("FTP collection is finished.");
  186. return files;
  187. };
  188. /**
  189. * Collects GIT files.
  190. *
  191. * @function
  192. * @arg {string} gitRoot - Root of GIT.
  193. * @arg {string} localRoot - Folder of GIT to scan.
  194. * @return {object} - Dictionary of GIT files data.
  195. */
  196. var collectGitFiles = (gitRoot, localRoot) => {
  197. var files = {};
  198. var folders = [""];
  199.  
  200. logger.info("Collecting files on GIT repository...")
  201. console.log("Collecting files on GIT repository...")
  202.  
  203. while (folders.length) {
  204. var folder = folders.shift();
  205. var localFolder = path.resolve(localRoot, folder);
  206.  
  207. var fileNames = fs.readdirSync(localFolder);
  208. for (var fileName of fileNames) {
  209.  
  210. var filePath = fileName;
  211. if (folder) {
  212. filePath = folder + path.sep + filePath;
  213. };
  214.  
  215. var fullPath = path.resolve(localRoot, filePath);
  216. var fileStat = fs.statSync(fullPath);
  217.  
  218. if (fileStat.isDirectory()) {
  219. folders.push(filePath);
  220.  
  221. } else {
  222. files[filePath] = { path: filePath,
  223. size: fileStat.size,
  224. date: _gitLastModified(gitRoot, fullPath) };
  225.  
  226. logger.debug("Collect GIT file", filePath);
  227. process.stdout.write('.');
  228. };
  229. };
  230. };
  231.  
  232. logger.info("GIT collection is finished.");
  233. console.log("GIT collection is finished.");
  234. return files;
  235. };
  236. /**
  237. * Gets files, which should be updated in GIT repository.
  238. *
  239. * @function
  240. * @arg {object} ftpFiles - FTP collected files data.
  241. * @arg {object} gitFiles - GIT collected files data.
  242. * @return {Array[]} - List with list of files to add
  243. * and with list of files to delete.
  244. */
  245. var getFilesToUpdate = (ftpFiles, gitFiles) => {
  246. var ftpList = Object.keys(ftpFiles);
  247. var gitList = Object.keys(gitFiles);
  248. var filesToDelete = _.difference(gitList, ftpList);
  249.  
  250. var filesToAdd = [];
  251. for (var fileName of ftpList) {
  252.  
  253. var ftpFile = ftpFiles[fileName];
  254. var gitFile = gitFiles[fileName];
  255.  
  256. if (!gitFile || (ftpFile.date > gitFile.date)) {
  257. filesToAdd.push(fileName);
  258. };
  259. };
  260.  
  261. return [ filesToAdd, filesToDelete ];
  262. };
  263. /**
  264. * Downloads FTP files.
  265. *
  266. * @function
  267. * @async
  268. * @arg {string[]} filePaths - List of ftp file pathes to download.
  269. * @arg {object} ftpFiles - FTP collected files data.
  270. * @arg {string} remoteRoot - Path to root folder on FTP server.
  271. * @arg {string} localRoot - Path to root folder in GIT repository.
  272. * @arg {number} numberOfThreads - Number of threads for downloading.
  273. * @arg {number} [attemptions=10] - Number of attemptions to download file.
  274. */
  275. var downloadFiles = async (filePaths,
  276. ftpFiles,
  277. remoteRoot,
  278. localRoot,
  279. numberOfThreads,
  280. attemptions) => {
  281. attemptions = attemptions || 10;
  282. var fileFails = {};
  283. var pool = new Pool(numberOfThreads);
  284.  
  285. logger.info("FTP files downloading...");
  286. console.log("FTP files downloading...");
  287.  
  288. var _download = filePath => {
  289.  
  290. return () => {
  291. logger.debug(`Downloading ${filePath} ...`);
  292.  
  293. return ftpDownload(filePath).then(() => {
  294.  
  295. var idx = filePaths.indexOf(filePath);
  296. filePaths.splice(idx, 1);
  297. logger.debug(`Downloaded ${filePath}`);
  298. process.stdout.write('.');
  299.  
  300. }).catch(err => {
  301.  
  302. logger.error(`Fail to download ${filePath}`, err);
  303. fileFails[filePath] = (fileFails[filePath] || 0) + 1;
  304.  
  305. if (fileFails[filePath] < attemptions) {
  306.  
  307. filePaths.push(filePath);
  308. var idx = filePaths.indexOf(filePath);
  309. filePaths.splice(idx, 1);
  310.  
  311. logger.debug(`Reschedule to download ${filePath}`);
  312. pool.add(ftpFiles[filePath].size, _download(filePath));
  313. };
  314. });
  315. };
  316. };
  317.  
  318. for (var filePath of filePaths) {
  319. pool.add(ftpFiles[filePath].size, _download(filePath));
  320. };
  321.  
  322. await new Promise(resolve => {
  323. var timerId = setInterval(() => {
  324.  
  325. if (!filePaths.length) {
  326. clearInterval(timerId);
  327. logger.info("FTP files are downloaded.");
  328. console.log("FTP files are downloaded.");
  329. resolve();
  330. };
  331. }, 1000);
  332. });
  333. };
  334. /**
  335. * Downloads FTP file.
  336. *
  337. * @function
  338. * @arg {string} filePath - Path of file to download from FTP.
  339. * @arg {string} remoteRoot - Path to root folder on FTP server.
  340. * @arg {string} localRoot - Path to root folder in GIT repository.
  341. * @return {Promise}
  342. */
  343. var ftpDownload = (filePath, remoteRoot, localRoot) => {
  344. var remotePath = path.resolve(remoteRoot, filePath)
  345. var localPath = path.resolve(localRoot, filePath);
  346. mkdirp.sync(path.dirname(localPath));
  347.  
  348. return new Promise((resolve, reject) => {
  349.  
  350. ftp.get(remotePath, (err, remoteStream) => {
  351.  
  352. if (err) {
  353. reject(err);
  354. return;
  355. };
  356. if (!remoteStream) {
  357. reject("No FTP stream");
  358. return;
  359. };
  360.  
  361. var localStream = fs.createWriteStream(localPath);
  362. remoteStream.pipe(localStream);
  363.  
  364. var _streamError = err => {
  365. if (fs.existsSync(localPath)) {
  366. fs.unlinkSync(localPath);
  367. };
  368. reject(err);
  369. };
  370.  
  371. localStream
  372. .on("finish", () => localStream.close(resolve))
  373. .on("error", _streamError);
  374. remoteStream.on("error", _streamError);
  375. });
  376. });
  377. };
  378. /**
  379. * Removes files on GIT repository.
  380. *
  381. * @function
  382. * @arg {string[]} filesList - List of file paths to remove.
  383. * @arg {string} gitFolder - GIT repository folder, where files will be
  384. * removed from.
  385. */
  386. var removeFiles = (filesList, gitFolder) => {
  387. logger.info("Removing FTP-deleted files from GIT...")
  388. console.log("Removing FTP-deleted files from GIT...")
  389.  
  390. for (var filePath of filesList) {
  391. var fullPath = path.resolve(gitFolder, filePath);
  392. if (fs.existsSync(fullPath)) {
  393. fs.unlinkSync(fullPath);
  394. logger.debug("Remove from GIT", fullPath);
  395. process.stdout.write(".");
  396. } else {
  397. logger.warn("GIT doesn't contain", fullPath);
  398. };
  399. };
  400. logger.info("GIT cleaning is finished.");
  401. console.log("GIT cleaning is finished.");
  402. };
  403. /**
  404. * Gets or clones GIT repository.
  405. *
  406. * @function
  407. * @async
  408. * @arg {string} gitRoot - Path to local GIT root folder.
  409. * @arg {string} gitUrl - URL of GIT repository.
  410. * @arg {object} gitAuth - GIT auth options.
  411. * @arg {function} cb - Callback.
  412. * @return {Promise}
  413. */
  414. var getRepo = (gitRoot, gitUrl, gitAuth, cb) => {
  415.  
  416. try {
  417. return GitClient.Repository.open(gitRoot);
  418. } catch {
  419. logger.warn("Can't open GIT repository.");
  420. };
  421.  
  422. logger.info("Cloning GIT repository...");
  423. console.log("Cloning GIT repository...");
  424.  
  425. var repo;
  426. return GitClient.Clone
  427. .clone(gitUrl, gitRoot, { fetchOpts: gitAuth })
  428. .then(repository => {
  429. repo = repository;
  430. logger.info("GIT repository is cloned.");
  431. console.log("GIT repository is cloned.");
  432. return cb();
  433. })
  434. .then(() => repo);
  435. };
  436. /**
  437. * Pushes GIT changes to remote repository, if there are changes.
  438. *
  439. * @function
  440. * @async
  441. * @arg {string} gitRoot - Path to local GIT root folder.
  442. * @arg {string} gitUrl - URL of GIT repository.
  443. * @arg {object} gitAuth - GIT auth options.
  444. * @arg {string} committerName - Name of GIT committer.
  445. * @arg {string} committerEmail - E-mail of GIT committer.
  446. * @arg {function} cb - Callback.
  447. * @return {Promise}
  448. */
  449. var pushRepo = async (gitRoot, gitAuth, committerName, committerEmail) => {
  450.  
  451. var changedFiles = await GitClient.Repository
  452. .open(gitRoot)
  453. .then(repo => repo.getStatus());
  454.  
  455. if (_.isEmpty(changedFiles)) {
  456. logger.info("Nothing to commit.");
  457. return;
  458. };
  459.  
  460. logger.info("Uploading changes to remote GIT repository...");
  461. console.log("Uploading changes to remote GIT repository...");
  462.  
  463. var repo = await nodegit.Repository.open(gitRoot);
  464. var index = await repo.refreshIndex();
  465. var filePaths = (await repo.getStatus()).map(file => file.path());
  466. await index.addAll(filePaths);
  467. await index.write();
  468. var oldCommit = await index.writeTree();
  469. var head = await GitClient.Reference.nameToId(repo, "HEAD");
  470. var parent = await repo.getCommit(head);
  471. var commitId = await repo.createCommit("HEAD", author, committerName, commitMessage, oldCommit, [parent]);
  472. var remote = await repo.getRemote("origin");
  473.  
  474. await remote.push([`refs/heads/${originBranch}:refs/heads/${originBranch}`], gitAuth);
  475.  
  476. logger.info("Changes are saved in remote GIT.");
  477. console.log("Changes are saved in remote GIT.");
  478. };
  479. /**
  480. * Helper to get last modified date of file in GIT.
  481. *
  482. * @function
  483. * @ignore
  484. * @arg {string} gitRoot - Root folder of GIT.
  485. * @arg {string} filePath - Path of file in GIT.
  486. * @return {Date} - Last modified date of file in GIT.
  487. */
  488. var _gitLastModified = (gitRoot, filePath) => {
  489. var proc = spawn.sync("git", [ "-C", gitRoot,
  490. "log",
  491. "--max-count", 1,
  492. "--pretty=format:%aI",
  493. filePath ]);
  494. var stderr = proc.stderr.toString();
  495. if (stderr) {
  496. throw new Error(stderr);
  497. };
  498. var stdout = proc.stdout.toString();
  499. var errMsg = `Can't retrieve git modified date of ${filePath}`;
  500. if (!stdout) {
  501. throw new Error(errMsg);
  502. };
  503. var date = new Date(stdout);
  504. if (date.toString().toLowerCase() === 'invalid date') {
  505. throw new Error(errMsg);
  506. };
  507. return date;
  508. };
  509. /**
  510. * Helper to exit on error.
  511. *
  512. * @function
  513. * @arg {Error} err - Programm error.
  514. */
  515. var _processError = err => {
  516. logger.error("UNEXPECTED ERROR", err);
  517. console.log(err);
  518. process.exit(1);
  519. };
  520.  
  521. process.on("uncaughtException", _processError);
  522. process.on("unhandledRejection", _processError);
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement