From 8868ef1c1af2b3048e50398fd7b3b8ca6cf1d45e Mon Sep 17 00:00:00 2001 From: Michal Kochel Date: Wed, 9 Mar 2016 18:22:23 +0100 Subject: [PATCH] Code refactoring. --- README.md | 33 +- aemsync.js | 553 ------------------------------- bin/aemsync | 2 +- index.js | 81 +++-- package.json | 3 +- src/handlers/bundle-handler.js | 2 + src/handlers/content-handler.js | 14 +- src/log.js | 34 +- src/package.js | 57 ++-- src/pusher.js | 44 +-- src/sender.js | 29 +- src/watcher.js | 83 ++++- src/watchers/chokidar-watcher.js | 135 -------- src/watchers/node-watcher.js | 31 -- src/zip.js | 162 +++++---- 15 files changed, 318 insertions(+), 945 deletions(-) delete mode 100644 aemsync.js delete mode 100644 src/watchers/chokidar-watcher.js delete mode 100644 src/watchers/node-watcher.js diff --git a/README.md b/README.md index 519234f..45f0c35 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ aemsync -t targets -w path_to_watch -t: Comma separated list of target hosts; default is http://admin:admin@localhost:4502. -w: Folder to watch; default is current. -i: Update interval; default is 300ms. --f: Anymatch filter; any file matching the pattern will be skipped. +-e: Anymatch exclude filter; any file matching the pattern will be skipped. -d: Enable debug mode. ``` @@ -38,26 +38,31 @@ aemsync -t http://admin:admin@localhost:4502,http://admin:admin@localhost:4503 - JavaScript ```JavaScript -const aemsync = require('aemsync'); +const aemsync = require('aemsync') -var sync = new aemsync.Sync(); // Synchronisation object. -var workingDir = "~/workspace/my_project"~; -var targets = [ +let workingDir = "~/workspace/my_project" +let targets = [ "http://admin:admin@localhost:4502", - "http://admin:admin@localhost:4503"]; -var userFilter = "*.orig"; // Skip merge files. -var syncerInterval = 300; - -new aemsync.Watcher(workingDir, userFilter, sync, function() { - new aemsync.Pusher(targets, syncerInterval, sync); -}); + "http://admin:admin@localhost:4503" +] +let userFilter = "*.orig" // Skip merge files. +let syncerInterval = 300 + +new aemsync.Watcher(workingDir, userFilter, () => { + new aemsync.Pusher(targets, syncerInterval, (err, host) => { + if (err) { + return console.log(`Error when pushing package to ${host}.`, err) + } + console.log(`Package pushed to ${host}.`) + }) +}) ``` ### Description -When run, the Watcher scans for `jcr_root/*` folders within the `path_to_watch` (dot-prefixed and "target" folders are omitted). This may take a while depending on the size. After the scan is done, file system changes inside those folders are detected and deployed to AEM instance(s) as a package. +The Watcher uses Node's `fs.watch()` function to watch over directory changes recursively. For Windows and OSX the `recursive` option is used, which significantly improves the performance. Any changes inside `jcr_root/*` folders are detected and deployed to AEM instance(s) as a package. -Update interval is the time the Pusher waits for file changes changes before the package is created. In case of multiple file changes (e.g. switching between code branches), creating a new package per file should be avoided and instead, all changes should be pushed in one go. Lowering the value decreases the delay for a single file change but may increase the delay for multiple file changes. If you are unsure, please leave the default value. +Update interval is the time the Pusher waits for file changes before the package is created. In case of multiple file changes (e.g. switching between code branches), creating a new package per file should be avoided and instead, all changes should be pushed in one go. Lowering the value decreases the delay for a single file change but may increase the delay for multiple file changes. If you are unsure, please leave the default value. ### Known issues diff --git a/aemsync.js b/aemsync.js deleted file mode 100644 index 5cdf950..0000000 --- a/aemsync.js +++ /dev/null @@ -1,553 +0,0 @@ -/*jslint node: true*/ -"use strict"; - -// ----------------------------------------------------------------------------- -// VARIABLES -// ----------------------------------------------------------------------------- - -// Built-in packages -const os = require("os"); -const path = require("path"); -const parseUrl = require("url").parse; -const StringDecoder = require("string_decoder").StringDecoder; - -// NPM packages -const fs = require("graceful-fs"); -const minimist = require("minimist"); -const archiver = require("archiver"); // TODO: consider using zip-stream for less dependencies. -const FormData = require("form-data"); -const chokidar = require("chokidar"); -const util = require("util"); -require('colors'); - -// Constants -const MSG_HELP = "Usage: aemsync -t targets (defult is 'http://admin:admin@localhost:4502) -w path_to_watch (default is current)\nWebsite: https://github.com/gavoja/aemsync\n"; -const MSG_INIT = "Working directory: %s\nTarget(s): %s\nUpdate interval: %s\nFilter: %s\n"; -const MSG_EXIT = "\nGracefully shutting down from SIGINT (Ctrl-C)..."; -const MSG_INST = "Deploying to [%s]: %s"; -const FILTER_WRAPPER = '\n' + -'%s\n' + -''; -const FILTER = '\n' + -' '; -const FILTER_ZIP_PATH = "META-INF/vault/filter.xml"; -const NT_FOLDER = __dirname + "/data/nt_folder/.content.xml"; -const ZIP_NAME = "/aemsync.zip"; -const RE_SPECIAL = /(.*?)\/(_jcr_content|[^\/]+\.dir|\.content\.xml).*/; -const RE_STATUS = /code="([0-9]+)">(.*) 0) { - --sync.lock; - } - if (sync.lock === 0) { - console.log("\nAwaiting file changes..."); - } -} - -// ----------------------------------------------------------------------------- -// ZIP HANDLER -// ----------------------------------------------------------------------------- - -/** Creates zip archive. */ -class Zip { - constructor() { - this.path = DEBUG_MODE ? __dirname + ZIP_NAME : os.tmpdir() + ZIP_NAME; - this.zip = archiver("zip"); - - debug("Creating archive: " + this.path); - this.output = fs.createWriteStream(this.path); - this.zip.pipe(this.output); - } - - addLocalFile(localPath, zipPath) { - debug(" Zipping: " + zipPath); - this.zip.append(fs.createReadStream(localPath), { - name: zipPath - }); - } - - addFile(content, zipPath) { - debug(" Zipping: " + zipPath); - this.zip.append(content, { - name: zipPath - }); - } - - save(onSave) { - var that = this; - this.output.on("close", function () { - onSave(that.path); - }); - this.zip.finalize(); // Trigers the above. - }; -} - -// ----------------------------------------------------------------------------- -// SYNC -// ----------------------------------------------------------------------------- - -class Sync { - constructor() { - this.queue = []; - this.lock = 0; - } -} - -// ----------------------------------------------------------------------------- -// PUSHER -// ----------------------------------------------------------------------------- - -/** Pushes changes to AEM. */ -function Pusher(targets, interval, sync) { - - /** Submits the package manager form. */ - var sendForm = function (zipPath) { - debug("Posting..."); - for (var i = 0; i < targets.length; ++i) { - sendFormToTarget(zipPath, targets[i]); - } - }; - - var sendFormToTarget = function (zipPath, target) { - var params = parseUrl(target); - var auth = new Buffer(params.auth).toString("base64"); - var timestamp = Date.now(); - - var options = {}; - options.path = PACKAGE_MANAGER_URL; - options.port = params.port; - options.host = params.hostname; - options.headers = { - "Authorization": "Basic " + auth - }; - - var form = new FormData(); - form.append("file", fs.createReadStream(zipPath)); - form.append("force", "true"); - form.append("install", "true"); - // releaseLock(sync); - // return; - form.submit(options, function (err, res) { - onSubmit(err, res, zipPath, target, timestamp); - }); - }; - - /** Package install submit callback */ - var onSubmit = function (err, res, zipPath, target, timestamp) { - var host = target.substring(target.indexOf("@") + 1); - if (!res) { - console.log(util.format(MSG_INST, host.magenta, err.code.red)); - // Do not retry on server error. Servler is likely to be down. - releaseLock(sync); - return; - } - - var decoder = new StringDecoder("utf8"); - var output = "\nOutput from " + host + ":"; - res.on("data", function (chunk) { - - // Get message and remove new line. - var textChunk = decoder.write(chunk); - textChunk = textChunk.substring(0, textChunk.length - 1); - output += "\n" + textChunk; - - // Parse message. - var match = RE_STATUS.exec(textChunk); - if (match === null || match.length !== 3) { - return; - } - - var code = match[1]; - var msg = match[2]; - - debug(output); - - if (code === "200") { - var delta = Date.now() - timestamp; - var time = new Date().toISOString(); - var msg = util.format("completed in %sms at %s", delta, time); - console.log(util.format(MSG_INST, host.magenta, msg.green)); - } else { // Error. - console.log(util.format(MSG_INST, host.magenta, msg.red)); - } - - releaseLock(sync); - }); - }; - - /** Creates a package. */ - var createPackage = function () { - var zip = new Zip(); - var path = __dirname + "/data/package_content"; - var fileList = walkSync(path); - - fileList.forEach(function (subItem) { - if (fs.statSync(subItem).isFile()) { - zip.addLocalFile(subItem, subItem.substr(path.length + 1)); - } - }); - - return { - zip: zip, - filters: "" - }; - }; - - /** Installs a package. */ - var installPackage = function (pack) { - // Add filters. - // TODO: Add support for rep:policy nodes. - pack.filters = util.format(FILTER_WRAPPER, pack.filters); - pack.zip.addFile(new Buffer(pack.filters), FILTER_ZIP_PATH); - - debug("\nPackage filters:\n" + pack.filters + "\n"); - - // TODO: Make in-memory zip perhaps? - pack.zip.save(sendForm); - }; - - /** Adds item to package. */ - var addItemInPackage = function (pack, item) { - console.log("ADD: " + item.substring(item.indexOf("jcr_root")).yellow); - var filterPath = getFilterPath(item); - var dirName = path.dirname(filterPath); - pack.filters += util.format(FILTER, filterPath); - - // Add file. - if (fs.lstatSync(item).isFile()) { - pack.zip.addLocalFile(item, getZipPath(item)); - return; - } - - // Add files in directory. - walkSync(item, function (localPath) { - // Ignore dot-prefixed files and directories except ".content.xml". - var baseName = path.basename(localPath); - if (baseName.indexOf(".") === 0 && baseName !== ".content.xml") { - debug(" Skipped: " + getZipPath(localPath)); - return true; - } - - return false; - }).forEach(function (subItem) { - // Add files - if (fs.lstatSync(subItem).isFile()) { - pack.zip.addLocalFile(subItem, getZipPath(subItem)); - return; - } - - // Add NT_FOLDER if needed. - var contentXml = subItem + "/.content.xml"; - var hasContentXml = fs.existsSync(contentXml); - var hasContentFolder = subItem.indexOf('/_jcr_content') !== -1; - if (!hasContentFolder && !hasContentXml) { - pack.zip.addLocalFile(NT_FOLDER, getZipPath(contentXml)); - debug(" Added as nt:folder.") - } - }); - }; - - /** Deletes item in package. */ - var deleteItemInPackage = function (pack, item) { - console.log("DEL: " + item.substring(item.indexOf("jcr_root")).yellow); - - var filterPath = getFilterPath(item); - pack.filters += util.format(FILTER, filterPath); - }; - - /** Processes queue items; duplicates and descendants are removed. */ - var processQueueItem = function (item, dict) { - - // If item is special, use the parent. - item = item.replace(RE_SPECIAL, '$1'); - console.log(item); - - // Make sure only parent items are processed. - for (var dictItem in dict) { - // Skip item if ancestor was already added to dict. - if (item.indexOf(dictItem + "/") === 0) { - item = null; - break; - } - - // Remove item if item is ancestor. - if (dictItem.indexOf(item + "/") === 0) { - delete dict[dictItem]; - } - } - - // Add to dictionary. - if (item) { - dict[item] = true; - } - }; - - /** Processes queue. */ - this.processQueue = function () { - var i, item, dict = {}; - - // Wait for the previous package to install. - // Otherwise an error may occur if two concurrent packages try to make - // changes to the same node. - if (sync.lock > 0) { - return; - } - - // Dequeue items (dictionary takes care of duplicates). - while ((i = sync.queue.pop())) { - processQueueItem(i, dict); - } - - // Skip if no items. - if (Object.keys(dict).length === 0) { - return; - } - - sync.lock = targets.length; - - var pack = createPackage(); - for (item in dict) { - if (fs.existsSync(item)) { - addItemInPackage(pack, item); - } else { - deleteItemInPackage(pack, item); - } - } - installPackage(pack); - }; - - // captureSigint(); - setInterval(this.processQueue, interval); -} - -// ----------------------------------------------------------------------------- -// WATCHER -// ----------------------------------------------------------------------------- - -//** Watches for file system changes. */ -class Watcher { - constructor(workingDir, userFilter, sync, callback) { - this.sync = sync; - - var that = this; - fs.exists(workingDir, function (exists) { - if (!exists) { - console.error("Invalid path: " + workingDir); - return; - } - - that.processWorkingDir(workingDir, userFilter, callback) - }); - } - - processWorkingDir(workingDir, userFilter, callback) { - // Get paths to watch. - console.log("Scanning for 'jcr_root/*' folders ..."); - var pathsToWatch = this.getPathsToWatch(workingDir); - if (pathsToWatch.length === 0) { - console.log("No 'jcr_root/*' folders found."); - return; - } - - // Get ignored. - var ignored = [this.skipHiddenFilter]; - if (userFilter) { - ignored.push(userFilter); - } - - this.startChokidar(pathsToWatch, userFilter, ignored, callback); - } - - // Ignore all dot-prefixed folders and files except ".content.xml". - skipHiddenFilter(localPath) { - var baseName = path.basename(localPath); - if (baseName.indexOf(".") === 0 && baseName !== ".content.xml") { - return true; - } - - return false; - } - - startChokidar(pathsToWatch, userFilter, ignored, callback) { - // Start watcher. - var watcher = chokidar.watch(pathsToWatch, { - ignored: ignored, - persistent: true - }); - - // When scan is complete. - var that = this; - watcher.on("ready", function () { - console.log(util.format("Found %s 'jcr_root/*' folder(s).'", - pathsToWatch.length)); - - // Just to print the message. - releaseLock(that.sync); - - // Detect all changes. - watcher.on("all", function (eventName, localPath) { - localPath = cleanPath(localPath); - debug("Change detected: " + localPath); - that.sync.queue.push(localPath); - }); - - // Fire callback. - callback(); - }); - } - - /** Get paths to watch. - * By ignoring the lookup of certain folders (e.g. dot-prefixed or - * "target"), we speed up chokidar's initial scan, as the paths are - * narrowed down to "jcr_root/*" only. - * It is set to work one level below "jcr_root" intentionally in order - * to prevent accidental removal of first level nodes such as "libs". - */ - getPathsToWatch(workingDir) { - return walkSync(workingDir, function (localPath, stats) { - // Skip non-directories. - if (stats.isDirectory() === false) { - return true; - } - - // Skip dot-prefixed directories. - if (localPath.indexOf("\/.") !== -1) { - return true; - } - - // Skip target directories outside "jcr_root". - var i = localPath.indexOf("\/jcr_root"); - var j = localPath.indexOf("\/target\/"); - if (i !== -1 && j !== -1 && j < i) { - return true; - } - - // Skip directories two levels inside "jcr_root". - var parentParentDir = path.basename( - path.dirname(path.dirname(localPath))); - if (i !== -1 && parentParentDir === "jcr_root") { - return true; - } - }).filter(function (localPath) { - // Remove found items that are not "jcr_root/*". - if (localPath.match(RE_WATCH_PATH)) { - debug(" " + localPath); - return true; - } - }); - } -} - -// ----------------------------------------------------------------------------- -// MAIN -// ----------------------------------------------------------------------------- - -function main() { - var args = minimist(process.argv.slice(2)); - - // Show help. - if (args.h) { - console.log(MSG_HELP); - return; - } - - // Set debug mode. - DEBUG_MODE = args.d; - - // Get configuration. - var targets = args.t ? args.t : DEFAULT_TARGET; - var workingDir = args.w ? cleanPath(args.w) : - cleanPath(DEFAULT_WORKING_DIR); - var syncerInterval = args.i ? args.i : DEFAULT_SYNCER_INTERVAL; - var userFilter = args.f ? args.f : ""; - - // Show info. - console.log(util.format(MSG_INIT, workingDir.yellow, targets.yellow, - (syncerInterval + "ms").yellow, userFilter.yellow)); - - // Create synchronisation object. - var sync = new Sync(); - - // Start the watcher. - new Watcher(workingDir, userFilter, sync, function() { - // Start the syncer. - new Pusher(targets.split(","), syncerInterval, sync); - }); -} - -if (require.main === module) { - main(); -} - -exports.Sync = Sync; -exports.Pusher = Pusher; -exports.Watcher = Watcher; diff --git a/bin/aemsync b/bin/aemsync index 0d27495..0d47c94 100755 --- a/bin/aemsync +++ b/bin/aemsync @@ -1,2 +1,2 @@ #!/usr/bin/env node -require("../index"); +require('../index') diff --git a/index.js b/index.js index dd6404f..918bbc5 100644 --- a/index.js +++ b/index.js @@ -1,57 +1,66 @@ -const minimist = require('minimist'); -const path = require('path'); -const fs = require('graceful-fs'); -const log = require('./src/log.js'); -const chalk = require('chalk'); -const Watcher = require('./src/watcher.js').Watcher; -const Pusher = require('./src/pusher.js').Pusher; +const minimist = require('minimist') +const path = require('path') +const fs = require('graceful-fs') +const log = require('./src/log.js') +const chalk = require('chalk') +const Watcher = require('./src/watcher.js').Watcher +const Pusher = require('./src/pusher.js').Pusher -const MSG_HELP = `Usage: aemsync -t targets (defult is http://admin:admin@localhost:4502) -w path_to_watch (default is current) -Website: https://github.com/gavoja/aemsync`; +const MSG_HELP = `Usage: aemsync [OPTIONS] + +Options: + -t targets Defult is http://admin:admin@localhost:4502 + -w path_to_watch Default is current + -e exclude_filter Anymach exclude filter; disabled by default + -i sync_interval Update interval; default is 300ms + -d Enable debug mode + -h Displays this screen + +Website: https://github.com/gavoja/aemsync` -function main() { - var args = minimist(process.argv.slice(2)); +function main () { + var args = minimist(process.argv.slice(2)) // Show help. if (args.h) { - console.log(MSG_HELP); - return; + console.log(MSG_HELP) + return } // Get other args. - log.isDebug = args.d; - var workingDir = path.resolve(args.w ? args.w : '.'); + log.isDebug = args.d + var workingDir = path.resolve(args.w ? args.w : '.') if (!fs.existsSync(workingDir)) { - log.info('Invalid path:', chalk.yellow(workingDir)); - return; + log.info('Invalid path:', chalk.yellow(workingDir)) + return } - var targets = args.t ? args.t : 'http://admin:admin@localhost:4502'; - var pushInterval = args.i ? args.i : 300; - var userFilter = args.f ? args.f : ''; + var targets = args.t ? args.t : 'http://admin:admin@localhost:4502' + var pushInterval = args.i ? args.i : 300 + var exclude = args.e ? args.e : '' - log.info(` - Working dir: ${chalk.yellow(workingDir)} - Targets: ${chalk.yellow(targets)} - Interval: ${chalk.yellow(pushInterval)} - Filter: ${chalk.yellow(userFilter)} - `); + log.info(` + Working dir: ${chalk.yellow(workingDir)} + Targets: ${chalk.yellow(targets)} + Interval: ${chalk.yellow(pushInterval)} + Exclude: ${chalk.yellow(exclude)} + `) - log.info('Awaiting changes...'); + log.info('Awaiting changes...') - var pusher = new Pusher(targets.split(','), pushInterval); - var watcher = new Watcher(); + var pusher = new Pusher(targets.split(','), pushInterval) + var watcher = new Watcher() - pusher.start(); - watcher.watch(workingDir, null, (localPath) => { - pusher.addItem(localPath); - }); + pusher.start() + watcher.watch(workingDir, exclude, (localPath) => { + pusher.addItem(localPath) + }) } if (require.main === module) { - main(); + main() } -module.exports.Watcher = Watcher; -module.exports.Pusher = Pusher; +module.exports.Watcher = Watcher +module.exports.Pusher = Pusher diff --git a/package.json b/package.json index 08eb398..a9bd231 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aemsync", - "version": "1.0.0", + "version": "1.0.0-beta.1", "description": "Adobe AEM Synchronization Tool", "author": "Michal Kochel ", "keywords": [ @@ -19,6 +19,7 @@ }, "main": "index.js", "dependencies": { + "anymatch": "^1.3.0", "archiver": "~0.12.0", "chalk": "^1.1.1", "chokidar": "~1.0.3", diff --git a/src/handlers/bundle-handler.js b/src/handlers/bundle-handler.js index 7e91341..9076141 100644 --- a/src/handlers/bundle-handler.js +++ b/src/handlers/bundle-handler.js @@ -3,3 +3,5 @@ class BundleHandler { // TODO: Implement. } + +module.exports.BundleHandler = BundleHandler diff --git a/src/handlers/content-handler.js b/src/handlers/content-handler.js index 62b11ef..884b4fc 100644 --- a/src/handlers/content-handler.js +++ b/src/handlers/content-handler.js @@ -1,17 +1,16 @@ -'use strict'; +'use strict' const fs = require('graceful-fs') const path = require('path') -const chalk = require('chalk') -const log = require('../log.js') const RE_DOT = /^.*\/\..*$/ const RE_CONTENT_PATH = /^.*\/jcr_root(\/[^\/]+){2,}$/ const RE_SPECIAL = /^.*\/(_jcr_content|[^\/]+\.dir|\.content\.xml).*$/ const RE_ZIP_PATH = /^.*[\/\\](jcr_root[\/\\].*)$/ +const RE_TARGET_PATH = /^.*\/target\/(.*\/)?jcr_root\/.*$/ class ContentHandler { - process(items, localPath) { + process (items, localPath) { let cleanPath = localPath.replace(/\\/g, '/') // Ignore dot-prefixed files and directories except ".content.xml". @@ -24,6 +23,11 @@ class ContentHandler { return } + // Skip paths on 'target' + if (cleanPath.match(RE_TARGET_PATH)) { + return + } + // Use parent if item is 'special'. if (cleanPath.match(RE_SPECIAL)) { return this.process(items, path.dirname(localPath)) @@ -33,7 +37,7 @@ class ContentHandler { action: fs.existsSync(localPath) ? 'ADD' : 'DEL', localPath: localPath, zipPath: localPath.replace(RE_ZIP_PATH, '$1') - }); + }) } } diff --git a/src/log.js b/src/log.js index 4546a82..b071b57 100644 --- a/src/log.js +++ b/src/log.js @@ -1,26 +1,24 @@ -'use strict'; +'use strict' const Console = require('console').Console const chalk = require('chalk') -const util = require('util') -let c = null; +let c = null -function Log() { +function Log () { c = c || new Console(process.stdout, process.stderr) - let args = Array.prototype.slice.call(arguments) let prefix = '' c.isDebug = false - c.format = function(args, color) { + c.format = function (args, color) { args = Array.apply(null, args) prefix && args.unshift(prefix.slice(0, -1)) - args = args.map(function(arg) { + args = args.map(function (arg) { if (typeof arg === 'string') { // Handle prefix. - arg = arg.replace(/\n/g, `\n${prefix}`) + arg = arg.replace(/\n/g, '\n' + prefix) // Handle color. arg = color ? color(arg) : arg @@ -32,27 +30,27 @@ function Log() { return args } - c.debug = function() { + c.debug = function () { if (this.isDebug) { this.log.apply(this, this.format(arguments, chalk.gray)) - } + } } - c.error = function() { + c.error = function () { this.log.apply(this, this.format(arguments, chalk.red)) - }; + } - c.info = function() { + c.info = function () { this.log.apply(this, this.format(arguments)) - }; + } - c.group = function() { + c.group = function () { prefix += ' ' - }; + } - c.groupEnd = function() { + c.groupEnd = function () { prefix = prefix.slice(0, -2) - }; + } return c } diff --git a/src/package.js b/src/package.js index 28495db..f1e60b8 100644 --- a/src/package.js +++ b/src/package.js @@ -10,31 +10,30 @@ const DATA_PATH = path.resolve(__dirname, '..', 'data') const PACKAGE_CONTENT_PATH = path.join(DATA_PATH, 'package_content') const NT_FOLDER_PATH = path.join(DATA_PATH, 'nt_folder', '.content.xml') -const FILTER_ZIP_PATH = "META-INF/vault/filter.xml" -const FILTER_WRAPPER = ` -%s +const FILTER_ZIP_PATH = 'META-INF/vault/filter.xml' +const FILTER_WRAPPER = ` +%s ` -const FILTER = ` +const FILTER = ` ` -const FILTER_CHILDREN = ` - - - - +const FILTER_CHILDREN = ` + + + + ` - class Package { - constructor() { + constructor () { this.items = [] this.path = [] } - update(localPath, zipPath, action) { + update (localPath, zipPath, action) { // Check if item or its parent is already in the package. for (let i = 0; i < this.items.length; ++i) { if (localPath.indexOf(this.items[i].localPath) === 0) { - return; + return } } @@ -44,12 +43,12 @@ class Package { localPath: localPath, zipPath: zipPath !== null ? zipPath : this.getZipPath(localPath), filterPath: this.getFilterPath(zipPath) - }); + }) } - save(callback) { - if (this.items.length == 0) { - callback(null) + save (callback) { + if (this.items.length === 0) { + callback(null) } // Create archive and add default package content. @@ -70,7 +69,7 @@ class Package { if (item.action === 'ADD') { let dirName = path.dirname(item.filterPath) filters += util.format(FILTER_CHILDREN, dirName, dirName, - item.filterPath, item.filterPath) + item.filterPath, item.filterPath) } else { filters += util.format(FILTER, item.filterPath) } @@ -91,7 +90,7 @@ class Package { if (stat.isDirectory()) { let cb = (localPath, zipPath) => { that.onItemAdd(archive, localPath, zipPath) - }; + } archive.addLocalDirectory(item.localPath, item.zipPath, cb) } @@ -99,13 +98,13 @@ class Package { // Wrap filters filters = util.format(FILTER_WRAPPER, filters) - archive.addFile(new Buffer(filters), FILTER_ZIP_PATH) + archive.addFile(new Buffer(filters), FILTER_ZIP_PATH) log.debug(filters) archive.save(callback) } /** Additional handling of directories added recursively. */ - onItemAdd(archive, localPath, zipPath) { + onItemAdd (archive, localPath, zipPath) { if (!fs.lstatSync(localPath).isDirectory()) { return } @@ -123,20 +122,20 @@ class Package { } /** Replaces backslashes with slashes. */ - cleanPath(localPath) { - return path.resolve(localPath).replace(/\\/g, '/') + cleanPath (localPath) { + return path.resolve(localPath).replace(/\\/g, '/') } /** Gets a zip path from a local path. */ - getZipPath(localPath) { - return this.cleanPath(localPath).replace(/.*\/(jcr_root\/.*)/, '$1') + getZipPath (localPath) { + return this.cleanPath(localPath).replace(/.*\/(jcr_root\/.*)/, '$1') } /** Gets a filter path from a local path. */ - getFilterPath(localPath) { - return this.cleanPath(localPath) - .replace(/(.*jcr_root)|(\.xml$)|(\.dir)/g, '') - .replace(/\/_([^\/]*)_([^\/]*)$/g, '\/$1:$2') + getFilterPath (localPath) { + return this.cleanPath(localPath) + .replace(/(.*jcr_root)|(\.xml$)|(\.dir)/g, '') + .replace(/\/_([^\/]*)_([^\/]*)$/g, '\/$1:$2') } } diff --git a/src/pusher.js b/src/pusher.js index 5c0816b..2467d41 100644 --- a/src/pusher.js +++ b/src/pusher.js @@ -8,38 +8,38 @@ const log = require('./log.js') /** Pushes changes to AEM. */ class Pusher { - constructor(targets, interval) { - this.lock = 0; + constructor (targets, interval, onPushEnd) { + this.lock = 0 this.queue = [] this.targets = targets this.interval = interval || 300 this.handlers = [new ContentHandler()] - this.sender = new Sender(targets) + this.onPushEnd = onPushEnd || function () {} } - start() { + start () { setInterval(() => { this.processQueue() }, this.interval) } - addItem(localPath) { + addItem (localPath) { this.queue.push(localPath) } /** Processes queue. */ - processQueue() { - // Wait for the previous package to install. - // Otherwise an error may occur if two concurrent packages try to make - // changes to the same node. - if (this.lock > 0) { - return; - } + processQueue () { + // Wait for the previous package to install. + // Otherwise an error may occur if two concurrent packages try to make + // changes to the same node. + if (this.lock > 0) { + return + } // Get unique list of local paths. - let dict = {}; - while(this.queue.length > 0) { + let dict = {} + while (this.queue.length > 0) { dict[this.queue.pop()] = true } let localPaths = Object.keys(dict) @@ -69,21 +69,23 @@ class Pusher { this.lock = this.targets.length pack.save((packagePath) => { this.onSend(packagePath) - }); - } + }) + } - onSend(packagePath) { + onSend (packagePath) { this.sender.send(packagePath, (err, host, delta, time) => { + let prefix = `Deploying to [${chalk.yellow(host)}] in ${delta} ms at ${time}` + if (err) { - log.info(`Deploying to [${chalk.yellow(host)}]: ${chalk.red(err)}`) + log.info(`${prefix}: ${chalk.red(err)}`) } else { - log.info(`Deploying to [${chalk.yellow(host)}]:`, chalk.green('OK')) + log.info(`${prefix}: ${chalk.green('OK')}`) } - log.info(`Completed in ${delta} ms at ${time}`) + this.onPushEnd(err, host) log.groupEnd() this.lock -= 1 - }); + }) } } diff --git a/src/sender.js b/src/sender.js index 75689c9..e6356c9 100644 --- a/src/sender.js +++ b/src/sender.js @@ -1,28 +1,28 @@ -'use strict'; +'use strict' -const fs = require('graceful-fs'); -const parseUrl = require('url').parse; -const FormData = require('form-data'); -const StringDecoder = require("string_decoder").StringDecoder; -const log = require('./log'); +const fs = require('graceful-fs') +const parseUrl = require('url').parse +const FormData = require('form-data') +const StringDecoder = require('string_decoder').StringDecoder +const log = require('./log') -const PACKAGE_MANAGER_URL = "/crx/packmgr/service.jsp"; -const RE_STATUS = /code="([0-9]+)">(.*)(.*) { + if (err) { + return log.debug(err.toString()) + } + + // Skip if not a directory. + if (!stats.isDirectory()) { + return + } + + this.watchFolder(parent, false, exclude, callback) + log.debug(`Watching over: ${parent}`) + + // Iterate over list of children. + fs.readdir(parent, (err, children) => { + if (err) { + return log.debug(err.toString()) + } + + children.forEach((child) => { + if (child.startsWith('.')) { + return + } + + child = path.resolve(parent, child) + this.watchFolderFallback(child, exclude, callback) + }) + }) + }) + } + + watchFolder (workingDir, recursive, exclude, callback) { + let options = { persistent: true, recursive: recursive } + + fs.watch(workingDir, options, (event, fileName) => { + if (!fileName) { + log.debug('Error while watching.') + return + } + + let localPath = path.join(workingDir, fileName) + log.debug('Changed:', localPath) + + fs.stat(localPath, (err, stats) => { + if (err) { + return + } + + if (event === 'change' && stats && stats.isDirectory()) { + return + } + + // Skip excluded. + if (exclude && anymatch(exclude, localPath)) { + return + } + + callback(localPath) + }) + }) + } } module.exports.Watcher = Watcher diff --git a/src/watchers/chokidar-watcher.js b/src/watchers/chokidar-watcher.js deleted file mode 100644 index 5ce25b1..0000000 --- a/src/watchers/chokidar-watcher.js +++ /dev/null @@ -1,135 +0,0 @@ -'use strict'; - -const path = require('path'); -const fs = require('graceful-fs'); -const chokidar = require('chokidar'); -const util = require('util'); -const Helper = require('./helper.js').Helper; -const WatcherDefaultHandler = require('./handlers/watcher-default.js'); - -const RE_WATCH_PATH = /^.*\/jcr_root\/[^\/]*$/; - -//** Watches for file system changes. */ -class Watcher { - constructor(sync, log) { - this.sync = sync; - this.log = log; - this.helper = new Helper(); - } - - watch(workingDir, userFilter, callback) { - var that = this; - fs.exists(workingDir, function (exists) { - if (!exists) { - that.log.error('Invalid path: ', workingDir); - return; - } - - that.processWorkingDir(workingDir, userFilter, callback) - }); - } - - processWorkingDir(workingDir, userFilter, callback) { - // Get paths to watch. - this.log.info('Scanning for package folders ...'); - var pathsToWatch = this.getPathsToWatch(workingDir); - if (pathsToWatch.length === 0) { - this.log.info('No package folders found.'); - return; - } - - // Get ignored. - var ignored = [this.skipHiddenFilter]; - if (userFilter) { - ignored.push(userFilter); - } - - this.startChokidar(pathsToWatch, userFilter, ignored, callback); - } - - // Ignore all dot-prefixed folders and files except '.content.xml'. - skipHiddenFilter(localPath) { - var baseName = path.basename(localPath); - if (baseName.indexOf('.') === 0 && baseName !== '.content.xml') { - return true; - } - - return false; - } - - startChokidar(pathsToWatch, userFilter, ignored, callback) { - // Start watcher. - var watcher = chokidar.watch(pathsToWatch, { - ignored: ignored, - persistent: true - }); - - // When scan is complete. - var that = this; - watcher.on('ready', function () { - that.log.info(util.format('Found %s package folder(s).', - pathsToWatch.length)); - - // Just to print the message. - that.helper.releaseLock(that.sync); - - // Detect all changes. - watcher.on('all', function (eventName, localPath) { - localPath = that.helper.cleanPath(localPath); - that.log.debug('Change detected: ' + localPath); - that.sync.queue.push(localPath); - }); - - // Fire callback. - callback(); - }); - } - - /** Get paths to watch. - * By ignoring the lookup of certain folders (e.g. dot-prefixed or - * 'target'), we speed up chokidar's initial scan, as the paths are - * narrowed down to 'jcr_root/*' only. - * It is set to work one level below 'jcr_root' intentionally in order - * to prevent accidental removal of first level nodes such as 'libs'. - */ - getPathsToWatch(workingDir) { - var that = this; - - this.log.group(); - return this.helper.walkSync(workingDir, function (localPath, stats) { - // Skip non-directories. - if (stats.isDirectory() === false) { - return true; - } - - // Skip dot-prefixed directories. - if (localPath.indexOf('\/.') !== -1) { - return true; - } - - // Skip target directories outside 'jcr_root'. - var i = localPath.indexOf('\/jcr_root'); - var j = localPath.indexOf('\/target\/'); - if (i !== -1 && j !== -1 && j < i) { - return true; - } - - // Skip directories two levels inside 'jcr_root'. - var parentParentDir = path.basename( - path.dirname(path.dirname(localPath))); - if (i !== -1 && parentParentDir === 'jcr_root') { - return true; - } - }).filter(function (localPath) { - // Remove found items that are not 'jcr_root/*'. - if (localPath.match(RE_WATCH_PATH)) { - that.log.debug(localPath); - return true; - } - }); - - this.log.groupEnd(); - } -} - -module.exports.Watcher = Watcher; diff --git a/src/watchers/node-watcher.js b/src/watchers/node-watcher.js deleted file mode 100644 index 9216c4a..0000000 --- a/src/watchers/node-watcher.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -const fs = require('graceful-fs'); -const path = require('path'); -const log = require('../log.js'); - -class NodeWatcher { - watch(workingDir, userFilter, callback) { - var options = { persistent: true, recursive: true }; - fs.watch(workingDir, options, (event, fileName) => { - - if (!fileName) { - log.debug('Error while watching.') - return; - } - - var localPath = path.join(workingDir, fileName); - log.debug('Changed:', localPath); - - fs.stat(localPath, (err, stats) => { - if (event === 'change' && stats && stats.isDirectory()) { - return; - } - - callback(localPath); - }); - }); - } -} - -module.exports.NodeWatcher = NodeWatcher; diff --git a/src/zip.js b/src/zip.js index abe9441..077563e 100644 --- a/src/zip.js +++ b/src/zip.js @@ -8,92 +8,90 @@ const log = require('./log.js') const DEFAULT_ZIP_NAME = 'aemsync.zip' class Zip { - constructor(zipPath) { - // TODO: path.join(os.tmpdir(), DEFAULT_ZIP_NAME); - this.path = path.join(__dirname, '..', DEFAULT_ZIP_NAME) - this.zip = archiver('zip') - - log.debug('Creating archive:', this.path) - this.output = fs.createWriteStream(this.path) - this.zip.pipe(this.output) - } - - addLocalFile(localPath, zipPath) { - // Normalize slashes. - zipPath = zipPath.replace(/\\/g, '/') - - // Only files can be zipped. - if (!fs.statSync(localPath).isFile()) { + constructor (zipPath) { + // TODO: path.join(os.tmpdir(), DEFAULT_ZIP_NAME) + this.path = path.join(__dirname, '..', DEFAULT_ZIP_NAME) + this.zip = archiver('zip') + + log.debug('Creating archive:', this.path) + this.output = fs.createWriteStream(this.path) + this.zip.pipe(this.output) + } + + addLocalFile (localPath, zipPath) { + // Normalize slashes. + zipPath = zipPath.replace(/\\/g, '/') + + // Only files can be zipped. + if (!fs.statSync(localPath).isFile()) { return } + log.debug('Zipping:', zipPath) + this.zip.append(fs.createReadStream(localPath), { + name: zipPath + }) + } - log.debug('Zipping:', zipPath); - this.zip.append(fs.createReadStream(localPath), { - name: zipPath - }); - } - - addLocalDirectory(localPath, zipPath, callback) { - if (!fs.statSync(localPath).isDirectory()) { - return - } - - // Ensure slash. - zipPath = zipPath.endsWith('/') ? zipPath : `${zipPath}/` - - let items = this.walkSync(localPath) - for (let i = 0; i < items.length; ++i) { - let subLocalPath = items[i] - let subZipPath = zipPath + subLocalPath.substr(localPath.length + 1) - this.addLocalFile(subLocalPath, subZipPath) - callback && callback(subLocalPath, subZipPath) - } - } - - addFile(content, zipPath) { - log.debug('Zipping:', zipPath) - this.zip.append(content, { - name: zipPath - }); - } - - /** Recursively walks over directory. */ - walkSync(localPath) { - localPath = path.resolve(localPath) - - let results = [] - let stats = fs.statSync(localPath) - - // Add current item. - results.push(localPath) - - // No need for recursion if not a directory. - if (!stats.isDirectory()) { - return results - } - - // Iterate over list of children. - let that = this - let children = fs.readdirSync(localPath) - - for (let i = 0; i < children.length; ++i) { - let child = path.resolve(localPath, children[i]) - results = results.concat(this.walkSync(child)) - } - - return results - } - - save(callback) { - let that = this - - this.output.on('close', () => { - callback(that.path) - }); - - this.zip.finalize() // Trigers the above. - } + addLocalDirectory (localPath, zipPath, callback) { + if (!fs.statSync(localPath).isDirectory()) { + return + } + + // Ensure slash. + zipPath = zipPath.endsWith('/') ? zipPath : `${zipPath}/` + + let items = this.walkSync(localPath) + for (let i = 0; i < items.length; ++i) { + let subLocalPath = items[i] + let subZipPath = zipPath + subLocalPath.substr(localPath.length + 1) + this.addLocalFile(subLocalPath, subZipPath) + callback && callback(subLocalPath, subZipPath) + } + } + + addFile (content, zipPath) { + log.debug('Zipping:', zipPath) + this.zip.append(content, { + name: zipPath + }) + } + + /** Recursively walks over directory. */ + walkSync (localPath) { + localPath = path.resolve(localPath) + + let results = [] + let stats = fs.statSync(localPath) + + // Add current item. + results.push(localPath) + + // No need for recursion if not a directory. + if (!stats.isDirectory()) { + return results + } + + // Iterate over list of children. + let children = fs.readdirSync(localPath) + + for (let i = 0; i < children.length; ++i) { + let child = path.resolve(localPath, children[i]) + results = results.concat(this.walkSync(child)) + } + + return results + } + + save (callback) { + let that = this + + this.output.on('close', () => { + callback(that.path) + }) + + this.zip.finalize() // Trigers the above. + } } module.exports.Zip = Zip