Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- "use strict";
- /**
- * Synchronization.
- *
- * @module
- */
- var _ = require("lodash");
- var fp = require("lodash/fp");
- var FtpClient = require("ftp");
- var GitClient = require("nodegit");
- var mkdirp = require("mkdirp");
- var spawn = require("cross-spawn");
- var CONF = require("./config");
- var logger = require("./logger");
- var Pool = require("./pool");
- var ftp; // ftp client instance
- /**
- * Synchronizes `FTP` folder with `GIT` repository.
- *
- * It makes following:
- * - Clones or connects to existing `GIT` repo;
- * - Pulls updates from remote `GIT` repo;
- * - Collects files on `GIT` folder;
- * - Collects files on `FTP` folder;
- * - If file is present in `GIT` folder and absent in `FTP` folder, it is
- * removed from `GIT` folder;
- * - If file is present in `FTP` folder and absent in `GIT` folder, it is
- * downloaded from `FTP` to `GIT` folder;
- * - If file is present in `FTP` and `GIT` folders, it is downloaded from
- * `FTP` to `GIT` folder only if last modified date of file in `FTP` is
- * later that in `GIT` folder;
- * - Pulls updates from remote `GIT` repo;
- * - If there are changes in `GIT` folder, they are committed and pushed
- * to remote `GIT` repo;
- * If options are not specified, they are requested from config.
- *
- * @function
- * @arg {object} [opts] - Options.
- * @arg {number} [opts.numberOfThreads] - Number of threads to download
- * FTP files.
- * @arg {object} [opts.ftp] - FTP options.
- * @arg {string} [opts.ftp.folder] - Path to folder on FTP, which will be
- * uploaded to GIT repo.
- * @arg {string} [opts.ftp.host] - FTP host.
- * @arg {number} [opts.ftp.port] - FTP port.
- * @arg {string} [opts.ftp.username] - FTP user name.
- * @arg {string} [opts.ftp.password] - FTP user password.
- * @arg {object} [opts.git] - GIT options.
- * @arg {string} [opts.git.root] - Path to GIT root folder.
- * @arg {string} [opts.git.folder] - Path to folder in GIT, where FTP files
- * will be downloaded.
- * @arg {string} [opts.git.url] - URL of GIT repository.
- * @arg {string} [opts.git.committerName] - Name of GIT committer.
- * @arg {string} [opts.git.committerEmail] - E-mail of GIT committer.
- * @arg {string} [opts.git.publicKey] - Path to GIT associated public key.
- * @arg {string} [opts.git.privateKey] - Path to GIT associated private key.
- */
- var sync = module.exports = opts => {
- opts = opts || {};
- opts = fp.merge(CONF, opts);
- var gitAuth = {
- callbacks: {
- credentials: (url, userName) => {
- return GitClient.Cred.sshKeyNew(opts.git.commiterName,
- opts.git.publicKey,
- opts.git.privateKey);
- },
- },
- };
- console.log("Synchronization is started.")
- ftp = new FtpClient();
- ftp.on("ready", async () => {
- var gitFiles = collectGitFiles(opts.git.root, opts.git.folder);
- var ftpFiles = await collectFtpFiles(opts.ftp.folder);
- var filesToAdd, filesToRemove;
- [ filesToAdd, filesToRemove ] = getFilesToUpdate(ftpFiles, gitFiles);
- await downloadFiles(filesToAdd,
- ftpFiles,
- opts.ftp.folder,
- opts.git.folder,
- opts.numberOfThreads);
- removeFiles(filesToRemove, opts.git.folder);
- ftp.end();
- });
- ftp.on("greeting", msg => {
- logger.info("FTP connection established:", msg);
- });
- ftp.on("error", err => {
- logger.error("FTP error detected:", err);
- });
- ftp.on("end", async () => {
- await pushRepoChanges();
- console.log("Synchronization is finished.")
- });
- getRepo(opts.git.root,
- opts.git.url,
- opts.git.commiterName,
- opts.git.publicKey,
- opts.git.privateKey,
- () => {
- var options = { host: opts.ftp.host,
- port: opts.ftp.port,
- user: opts.ftp.username,
- password: opts.ftp.password };
- ftp.connect(options);
- });
- };
- /**
- * Collects ftp files.
- *
- * @function
- * @async
- * @arg {string} remoteRoot - Root folder of FTP server to scan.
- * @return {object} - Dictionary of FTP files data.
- */
- var collectFtpFiles = async remoteRoot => {
- var files = {};
- var folders = [""];
- logger.info("Collecting files on FTP server...");
- console.log("Collecting files on FTP server...");
- var collect = folder => {
- return new Promise((resolve, reject) => {
- var remoteFolder = path.resolve(remoteRoot, folder);
- ftp.cwd(remoteFolder, () => {
- ftp.pwd((err, cwd) => {
- if (err) {
- reject(err);
- return;
- };
- ftp.list(cwd, (err, files) => {
- if (err) {
- reject(err);
- return;
- };
- for (var file of files) {
- var filePath = file.name;
- if (folder) {
- filePath = folder + path.sep + filePath;
- };
- if (file.type === "d") {
- folders.push(filePath);
- } else {
- files[filePath] = { path: filePath,
- size: file.size,
- date: file.date };
- logger.debug("Collect FTP file", filePath);
- process.stdout.write('.');
- };
- };
- resolve();
- });
- });
- });
- });
- };
- while (folders.length) {
- await collect(folders.shift());
- };
- logger.info("FTP collection is finished.");
- console.log("FTP collection is finished.");
- return files;
- };
- /**
- * Collects GIT files.
- *
- * @function
- * @arg {string} gitRoot - Root of GIT.
- * @arg {string} localRoot - Folder of GIT to scan.
- * @return {object} - Dictionary of GIT files data.
- */
- var collectGitFiles = (gitRoot, localRoot) => {
- var files = {};
- var folders = [""];
- logger.info("Collecting files on GIT repository...")
- console.log("Collecting files on GIT repository...")
- while (folders.length) {
- var folder = folders.shift();
- var localFolder = path.resolve(localRoot, folder);
- var fileNames = fs.readdirSync(localFolder);
- for (var fileName of fileNames) {
- var filePath = fileName;
- if (folder) {
- filePath = folder + path.sep + filePath;
- };
- var fullPath = path.resolve(localRoot, filePath);
- var fileStat = fs.statSync(fullPath);
- if (fileStat.isDirectory()) {
- folders.push(filePath);
- } else {
- files[filePath] = { path: filePath,
- size: fileStat.size,
- date: _gitLastModified(gitRoot, fullPath) };
- logger.debug("Collect GIT file", filePath);
- process.stdout.write('.');
- };
- };
- };
- logger.info("GIT collection is finished.");
- console.log("GIT collection is finished.");
- return files;
- };
- /**
- * Gets files, which should be updated in GIT repository.
- *
- * @function
- * @arg {object} ftpFiles - FTP collected files data.
- * @arg {object} gitFiles - GIT collected files data.
- * @return {Array[]} - List with list of files to add
- * and with list of files to delete.
- */
- var getFilesToUpdate = (ftpFiles, gitFiles) => {
- var ftpList = Object.keys(ftpFiles);
- var gitList = Object.keys(gitFiles);
- var filesToDelete = _.difference(gitList, ftpList);
- var filesToAdd = [];
- for (var fileName of ftpList) {
- var ftpFile = ftpFiles[fileName];
- var gitFile = gitFiles[fileName];
- if (!gitFile || (ftpFile.date > gitFile.date)) {
- filesToAdd.push(fileName);
- };
- };
- return [ filesToAdd, filesToDelete ];
- };
- /**
- * Downloads FTP files.
- *
- * @function
- * @async
- * @arg {string[]} filePaths - List of ftp file pathes to download.
- * @arg {object} ftpFiles - FTP collected files data.
- * @arg {string} remoteRoot - Path to root folder on FTP server.
- * @arg {string} localRoot - Path to root folder in GIT repository.
- * @arg {number} numberOfThreads - Number of threads for downloading.
- * @arg {number} [attemptions=10] - Number of attemptions to download file.
- */
- var downloadFiles = async (filePaths,
- ftpFiles,
- remoteRoot,
- localRoot,
- numberOfThreads,
- attemptions) => {
- attemptions = attemptions || 10;
- var fileFails = {};
- var pool = new Pool(numberOfThreads);
- logger.info("FTP files downloading...");
- console.log("FTP files downloading...");
- var _download = filePath => {
- return () => {
- logger.debug(`Downloading ${filePath} ...`);
- return ftpDownload(filePath).then(() => {
- var idx = filePaths.indexOf(filePath);
- filePaths.splice(idx, 1);
- logger.debug(`Downloaded ${filePath}`);
- process.stdout.write('.');
- }).catch(err => {
- logger.error(`Fail to download ${filePath}`, err);
- fileFails[filePath] = (fileFails[filePath] || 0) + 1;
- if (fileFails[filePath] < attemptions) {
- filePaths.push(filePath);
- var idx = filePaths.indexOf(filePath);
- filePaths.splice(idx, 1);
- logger.debug(`Reschedule to download ${filePath}`);
- pool.add(ftpFiles[filePath].size, _download(filePath));
- };
- });
- };
- };
- for (var filePath of filePaths) {
- pool.add(ftpFiles[filePath].size, _download(filePath));
- };
- await new Promise(resolve => {
- var timerId = setInterval(() => {
- if (!filePaths.length) {
- clearInterval(timerId);
- logger.info("FTP files are downloaded.");
- console.log("FTP files are downloaded.");
- resolve();
- };
- }, 1000);
- });
- };
- /**
- * Downloads FTP file.
- *
- * @function
- * @arg {string} filePath - Path of file to download from FTP.
- * @arg {string} remoteRoot - Path to root folder on FTP server.
- * @arg {string} localRoot - Path to root folder in GIT repository.
- * @return {Promise}
- */
- var ftpDownload = (filePath, remoteRoot, localRoot) => {
- var remotePath = path.resolve(remoteRoot, filePath)
- var localPath = path.resolve(localRoot, filePath);
- mkdirp.sync(path.dirname(localPath));
- return new Promise((resolve, reject) => {
- ftp.get(remotePath, (err, remoteStream) => {
- if (err) {
- reject(err);
- return;
- };
- if (!remoteStream) {
- reject("No FTP stream");
- return;
- };
- var localStream = fs.createWriteStream(localPath);
- remoteStream.pipe(localStream);
- var _streamError = err => {
- if (fs.existsSync(localPath)) {
- fs.unlinkSync(localPath);
- };
- reject(err);
- };
- localStream
- .on("finish", () => localStream.close(resolve))
- .on("error", _streamError);
- remoteStream.on("error", _streamError);
- });
- });
- };
- /**
- * Removes files on GIT repository.
- *
- * @function
- * @arg {string[]} filesList - List of file paths to remove.
- * @arg {string} gitFolder - GIT repository folder, where files will be
- * removed from.
- */
- var removeFiles = (filesList, gitFolder) => {
- logger.info("Removing FTP-deleted files from GIT...")
- console.log("Removing FTP-deleted files from GIT...")
- for (var filePath of filesList) {
- var fullPath = path.resolve(gitFolder, filePath);
- if (fs.existsSync(fullPath)) {
- fs.unlinkSync(fullPath);
- logger.debug("Remove from GIT", fullPath);
- process.stdout.write(".");
- } else {
- logger.warn("GIT doesn't contain", fullPath);
- };
- };
- logger.info("GIT cleaning is finished.");
- console.log("GIT cleaning is finished.");
- };
- /**
- * Gets or clones GIT repository.
- *
- * @function
- * @async
- * @arg {string} gitRoot - Path to local GIT root folder.
- * @arg {string} gitUrl - URL of GIT repository.
- * @arg {object} gitAuth - GIT auth options.
- * @arg {function} cb - Callback.
- * @return {Promise}
- */
- var getRepo = (gitRoot, gitUrl, gitAuth, cb) => {
- try {
- return GitClient.Repository.open(gitRoot);
- } catch {
- logger.warn("Can't open GIT repository.");
- };
- logger.info("Cloning GIT repository...");
- console.log("Cloning GIT repository...");
- var repo;
- return GitClient.Clone
- .clone(gitUrl, gitRoot, { fetchOpts: gitAuth })
- .then(repository => {
- repo = repository;
- logger.info("GIT repository is cloned.");
- console.log("GIT repository is cloned.");
- return cb();
- })
- .then(() => repo);
- };
- /**
- * Pushes GIT changes to remote repository, if there are changes.
- *
- * @function
- * @async
- * @arg {string} gitRoot - Path to local GIT root folder.
- * @arg {string} gitUrl - URL of GIT repository.
- * @arg {object} gitAuth - GIT auth options.
- * @arg {string} committerName - Name of GIT committer.
- * @arg {string} committerEmail - E-mail of GIT committer.
- * @arg {function} cb - Callback.
- * @return {Promise}
- */
- var pushRepo = async (gitRoot, gitAuth, committerName, committerEmail) => {
- var changedFiles = await GitClient.Repository
- .open(gitRoot)
- .then(repo => repo.getStatus());
- if (_.isEmpty(changedFiles)) {
- logger.info("Nothing to commit.");
- return;
- };
- logger.info("Uploading changes to remote GIT repository...");
- console.log("Uploading changes to remote GIT repository...");
- var repo = await nodegit.Repository.open(gitRoot);
- var index = await repo.refreshIndex();
- var filePaths = (await repo.getStatus()).map(file => file.path());
- await index.addAll(filePaths);
- await index.write();
- var oldCommit = await index.writeTree();
- var head = await GitClient.Reference.nameToId(repo, "HEAD");
- var parent = await repo.getCommit(head);
- var commitId = await repo.createCommit("HEAD", author, committerName, commitMessage, oldCommit, [parent]);
- var remote = await repo.getRemote("origin");
- await remote.push([`refs/heads/${originBranch}:refs/heads/${originBranch}`], gitAuth);
- logger.info("Changes are saved in remote GIT.");
- console.log("Changes are saved in remote GIT.");
- };
- /**
- * Helper to get last modified date of file in GIT.
- *
- * @function
- * @ignore
- * @arg {string} gitRoot - Root folder of GIT.
- * @arg {string} filePath - Path of file in GIT.
- * @return {Date} - Last modified date of file in GIT.
- */
- var _gitLastModified = (gitRoot, filePath) => {
- var proc = spawn.sync("git", [ "-C", gitRoot,
- "log",
- "--max-count", 1,
- "--pretty=format:%aI",
- filePath ]);
- var stderr = proc.stderr.toString();
- if (stderr) {
- throw new Error(stderr);
- };
- var stdout = proc.stdout.toString();
- var errMsg = `Can't retrieve git modified date of ${filePath}`;
- if (!stdout) {
- throw new Error(errMsg);
- };
- var date = new Date(stdout);
- if (date.toString().toLowerCase() === 'invalid date') {
- throw new Error(errMsg);
- };
- return date;
- };
- /**
- * Helper to exit on error.
- *
- * @function
- * @arg {Error} err - Programm error.
- */
- var _processError = err => {
- logger.error("UNEXPECTED ERROR", err);
- console.log(err);
- process.exit(1);
- };
- process.on("uncaughtException", _processError);
- process.on("unhandledRejection", _processError);
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement