diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..91efde9 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Test", + "program": "${workspaceFolder}\\test\\test.js", + "args": ["-v"] + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index e316f86..4aa59a5 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ aemsync ======= -AEM (Adobe CQ) Synchronization Tool. +The code and content synchronization for Sling / AEM (Adobe Experience Manager). ### Synopsis -The tool pushes code changes to AEM instance(s) upon a file change. +The tool pushes content to AEM instance(s) upon a file change. * There is no vault dependency. * It can push to multiple instances at the same time (e.g. author and publish). -* IDE/editor agnostic. +* IDE / editor agnostic. * Works on Windows, Linux and Mac. ### Installation @@ -21,88 +21,117 @@ npm install aemsync -g ### Usage +Simply run `aemsync` on your project path, make a change to any of your files or directories and watch the magic happen. + +### Advanced usage + Commandline ``` Usage: aemsync [OPTIONS] Options: - -t Defult is http://admin:admin@localhost:4502 - -w Default is current - -p Path to push directly; used instead of above, - no watching takes place - -e Micromatch exclude filter; disabled by default - -i Update interval; default is 300ms - -u Package manager path; default is - /crx/packmgr/service.jsp - -d Enable debug mode - -h Displays this screen -``` -``` -aemsync -t http://admin:admin@localhost:4502,http://admin:admin@localhost:4503 -w ~/workspace/my_project + -t URL to AEM instance; multiple can be set. + Default: ${defaults.targets} + -w Watch over folder. + Default: CWD + -p Push specific file or folder. + -e Extended glob filter; multiple can be set. + Default: + **/jcr_root/* + **/@(.git|.svn|.hg|target) + **/@(.git|.svn|.hg|target)/** + -i Update interval. + Default: ${defaults.interval} ms + -u Package manager path. + Default: ${defaults.packmgrPath} + -d Enable debug mode. + -h Display this screen. + +Examples: + Magic: + > aemsync + Custom targets: + > aemsync -t http://admin:admin@localhost:4502 -t http://admin:admin@localhost:4503 -w ~/workspace/my_project + Custom exclude rules: + > aemsync -e **/@(.git) -e **/@(.git)/** -e '**/*.orig' + Just push, don't watch: + > aemsync -p /foo/bar/my-workspace/jcr_content/apps/my-app/components/my-component ``` JavaScript (full watch example): ```JavaScript -// Import aemsync. const aemsync = require('aemsync') -// Set up the environment. const workingDir = '~/workspace/my_project' + +// Arguments below are optional. const targets = [ 'http://admin:admin@localhost:4502', 'http://admin:admin@localhost:4503' ] -const exclude = '**/*.orig' // Skip merge files. -const interval = 300 +const exclude = ['**/*.orig'] // Skip merge files. const packmgrUrl = '/foo/crx/packmgr/service.jsp' -const onPushEnd = (err, host) => { +const interval = 300 +const onPushEnd = (err, target, log) => { + // Called for each of the targets. if (err) { - return console.log(`Error when pushing package to ${host}.`, err) + console.log(`Error when pushing package to ${target}.`, err.message) + } else { + console.log(`Package pushed to ${target}. Response log:\n${target.log}`) } - console.log(`Package pushed to ${host}.`) } -// Will watch for changes on workingDir and push them. -aemsync({workingDir, targets, exclude, interval, packmgrUrl, onPushEnd}) +// Will watch for changes on workingDir and push them upon a file change. +aemsync(workingDir, { targets, exclude, interval, packmgrUrl, onPushEnd }) ``` JavaScript (direct push example): ```JavaScript -// Import aemsync. -const aemsync = require('aemsync') +const { push } = require('aemsync') + +const pathToPush = '~/foo/bar/my-workspace/jcr_content/apps/my-app/components/my-component' -// Set up the environment. -const path = '~/foo/bar/my-workspace/jcr_content/apps/my-app/components/my-component' +// Arguments below are optional. const targets = [ 'http://admin:admin@localhost:4502', 'http://admin:admin@localhost:4503' ] -const onPushEnd = (err, host) => { +const onPushEnd = (err, target, log) => { + // Called for each of the targets. if (err) { - return console.log(`Error when pushing package to ${host}.`, err) + console.log(`Error when pushing package to ${target}.`, err.message) + } else { + console.log(`Package pushed to ${target}. Response log:\n${target.log}`) } - console.log(`Package pushed to ${host}.`) } // Will push the path to AEM. -aemsync.push({path, targets, onPushEnd}) +// To use await, the call must be made inside an async function. +// The result is a Promise so it can also be resolved with .then(). +await push(pathToPush, { targets, onPushEnd }) ``` ### Description 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. Rules: + +Any changes inside `jcr_root` folders are detected and deployed to AEM instance(s) as a package. By default, there is an exclude filter in palce: * Changes to first level directories under `jcr_root` are ingored. This is to avoid accidentally removing `apps`, `libs` or any other first level node in AEM. -* The following are ignored by default: `.svn`, `.git`, `.hg`. +* Any paths containing `.svn`, `.git`, `.hg` or `target` are ignored. +* The exclude filter can be overriden. Do note that this will remove the above rules completely and if required, they must be added manually. 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. -Note that some of the file changes will result in pushing the entire parent folder: -* Ading, removing or renaming files or directories. -* Changing `.content.xml`. -* Changing any file or directory inside `nt:unstructured` subtree. In this case the first non `nt:unstructured` ancestor will be pushed. This behaviour ensures proper handling of self-contained unstructured blocks of nodes such as dialogs that are distributed across multiple files (see [issue 19](https://github.com/gavoja/aemsync/issues/19)). +### Caveats + +1. Packages are installed using package manager service (`/crx/packmgr/service.jsp`), which takes some time to initialize after AEM startup. If the push happens before, the Sling Post Servlet will take over causing the `/crx/packmgr/service.jsp/file` node to be added to the repository. +2. Changing any XML file will cause the parent folder to be pushed. Given the many special cases around XML files, the handlig is left to the package manager. -### Known issues +### Backward incompatible changes since version 4 -Packages are installed using package manager service (`/crx/packmgr/service.jsp`), which takes some time to initialize after AEM startup. If the push happens before, the Sling Post Servlet will take over causing the `/crx/packmgr/service.jsp/file` node to be added to the repository. +1. Multiple targes are now specified with multiple `-t` options rather than a comma separated string. +2. The same goes for the exclude filter (`-e`). +3. Exclude filter supports extended globbing only. Setting exclude filter with `-e` option overrides the default. +4. JavaScript API functions have a different signature. This is to spearate mandatory and optional arguments. +5. The `push()` function returns Promise and can be resolved with `await`. diff --git a/data/nt_folder/.content.xml b/data/nt-folder/.content.xml similarity index 100% rename from data/nt_folder/.content.xml rename to data/nt-folder/.content.xml diff --git a/data/nt_file/.content.xml b/data/nt_file/.content.xml deleted file mode 100644 index 2203a99..0000000 --- a/data/nt_file/.content.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/data/package_content/META-INF/vault/config.xml b/data/package-content/META-INF/vault/config.xml similarity index 100% rename from data/package_content/META-INF/vault/config.xml rename to data/package-content/META-INF/vault/config.xml diff --git a/data/package_content/META-INF/vault/definition/.content.xml b/data/package-content/META-INF/vault/definition/.content.xml similarity index 100% rename from data/package_content/META-INF/vault/definition/.content.xml rename to data/package-content/META-INF/vault/definition/.content.xml diff --git a/data/package_content/META-INF/vault/nodetypes.cnd b/data/package-content/META-INF/vault/nodetypes.cnd similarity index 100% rename from data/package_content/META-INF/vault/nodetypes.cnd rename to data/package-content/META-INF/vault/nodetypes.cnd diff --git a/data/package_content/META-INF/vault/properties.xml b/data/package-content/META-INF/vault/properties.xml similarity index 100% rename from data/package_content/META-INF/vault/properties.xml rename to data/package-content/META-INF/vault/properties.xml diff --git a/data/package_content/jcr_root/aemsync.txt b/data/package-content/jcr_root/aemsync.txt similarity index 100% rename from data/package_content/jcr_root/aemsync.txt rename to data/package-content/jcr_root/aemsync.txt diff --git a/index.js b/index.js index 5fc86d6..0afa582 100644 --- a/index.js +++ b/index.js @@ -2,48 +2,53 @@ const minimist = require('minimist') const path = require('path') -const fs = require('graceful-fs') +const fs = require('fs') +const watch = require('simple-watcher') +const defaults = require('./src/defaults') const log = require('./src/log') -const chalk = require('chalk') -const Watcher = require('./src/watcher') const Pipeline = require('./src/pipeline') +const { version } = require('./package.json') const MSG_HELP = ` +The code and content synchronization for Sling / AEM; version ${version}. + Usage: aemsync [OPTIONS] Options: - -t Defult is http://admin:admin@localhost:4502 - -w Default is current - -p Path to push directly; used instead of above, - no watching takes place - -e Micromatch exclude filter; disabled by default - -i Update interval; default is 300ms - -u Package manager path; default is - /crx/packmgr/service.jsp - -d Enable debug mode - -h Displays this screen + -t URL to AEM instance; multiple can be set. + Default: ${defaults.targets} + -w Watch over folder. + Default: CWD + -p Push specific file or folder. + -e Extended glob filter; multiple can be set. + Default: + **/jcr_root/* + **/@(.git|.svn|.hg|target) + **/@(.git|.svn|.hg|target)/** + -i Update interval. + Default: ${defaults.interval} ms + -u Package manager path. + Default: ${defaults.packmgrPath} + -d Enable debug mode. + -h Display this screen. Website: https://github.com/gavoja/aemsync ` -function aemsync (args) { - const pipeline = new Pipeline(args) - const watcher = new Watcher() +function aemsync (workingDir, { targets, interval, exclude, packmgrPath, onPushEnd }) { + const pipeline = new Pipeline({ targets, interval, exclude, packmgrPath, onPushEnd }) pipeline.start() - - args.callback = (localPath) => { + watch(workingDir, localPath => { pipeline.enqueue(localPath) - } - - watcher.watch(args) + }) } -function push (args) { - const pipeline = new Pipeline(args) - pipeline.push(args.pathToPush) +async function push (pathToPush, { targets, exclude, packmgrPath }) { + const pipeline = new Pipeline({ targets, exclude, packmgrPath }) + return pipeline.push(pathToPush) } function main () { @@ -54,44 +59,51 @@ function main () { return console.log(MSG_HELP) } - // Get other args. - log.isDebug = args.d - const workingDir = path.resolve(args.w || '.') - const targets = (args.t || 'http://admin:admin@localhost:4502').split(',') - const interval = args.i || 300 - const exclude = args.e || '' - const packmgrPath = args.u + // Print additional debug information. + args.d && log.enableDebug() + // Get the args. + const pathToPush = args.p ? path.resolve(args.p) : null + const workingDir = path.resolve(args.w || defaults.workingDir) + const targets = args.t ? (typeof args.t === 'string' ? [args.t] : args.t) : defaults.targets + const exclude = args.e ? (typeof args.e === 'string' ? [args.e] : args.e) : defaults.exclude + const interval = args.i || defaults.interval + const packmgrPath = args.u || defaults.packmgrPath + + // // Just the push. - if (args.p) { - let pathToPush = path.resolve(args.p) - if (!fs.existsSync(pathToPush)) { - return log.info('Invalid path:', chalk.yellow(workingDir)) - } + // - return push({pathToPush, targets}) + if (pathToPush) { + // Path to push does not have to exist. + // Non-existing path can be used for deletion. + return push(pathToPush, { targets }) } + // + // Watch mode. + // + if (!fs.existsSync(workingDir)) { - return log.info('Invalid path:', chalk.yellow(workingDir)) + return log.info('Invalid path:', log.gray(workingDir)) } // Start aemsync - log.info(` - Working dir: ${chalk.yellow(workingDir)} - Targets: ${chalk.yellow(targets)} - Interval: ${chalk.yellow(interval)} - Exclude: ${chalk.yellow(exclude)} + log.info(`aemsync version ${version} + + Watch over: ${log.gray(workingDir)} + Targets: ${log.gray(targets.map((t, ii) => ii === 0 ? t : ''.padStart(16, ' ') + t).join('\n'))} + Exclude: ${log.gray(exclude.map((x, ii) => ii === 0 ? x : ''.padStart(16, ' ') + x).join('\n'))} + Interval: ${log.gray(interval)} `) - aemsync({workingDir, targets, interval, exclude, packmgrPath}) + aemsync(workingDir, { targets, interval, exclude, packmgrPath }) } if (require.main === module) { main() } -aemsync.Watcher = Watcher aemsync.Pipeline = Pipeline aemsync.main = main aemsync.push = push diff --git a/package-lock.json b/package-lock.json index f1f26c4..599bbee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,301 +1,58 @@ { "name": "aemsync", - "version": "3.0.2", + "version": "4.0.0-rc1", "lockfileVersion": 1, "requires": true, "dependencies": { - "ansi-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz", - "integrity": "sha1-xQYbbg74qBd15Q9dZhUb9r83EQc=" + "adm-zip": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.13.tgz", + "integrity": "sha512-fERNJX8sOXfel6qCBCMPvZLzENBEhZTzKqg6vrOW5pvoEaQuJhRU4ndTAh6lHOxn1I6jnz2NHra56ZODM751uw==" }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "archiver": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-2.1.1.tgz", - "integrity": "sha1-/2YrSnggFJSj7lRNOjP+dJZQnrw=", - "requires": { - "archiver-utils": "^1.3.0", - "async": "^2.0.0", - "buffer-crc32": "^0.2.1", - "glob": "^7.0.0", - "lodash": "^4.8.0", - "readable-stream": "^2.0.0", - "tar-stream": "^1.5.0", - "zip-stream": "^1.2.0" - } - }, - "archiver-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-1.3.0.tgz", - "integrity": "sha1-5QtMCccL89aA4y/xt5lOn52JUXQ=", - "requires": { - "glob": "^7.0.0", - "graceful-fs": "^4.1.0", - "lazystream": "^1.0.0", - "lodash": "^4.8.0", - "normalize-path": "^2.0.0", - "readable-stream": "^2.0.0" - } - }, - "async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", - "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", - "requires": { - "lodash": "^4.17.10" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base64-js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", - "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" - }, - "bl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", - "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", - "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.0.tgz", - "integrity": "sha512-nUJyfChH7PMJy75eRDCCKtszSEFokUNXC1hNVSe+o+VdcgvDPLs20k3v8UXI8ruRYAJiYtyRea8mYyqPxoHWDw==", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "requires": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" - }, - "buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "combined-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz", - "integrity": "sha1-ATfmV7qlp1QcV6w3rF/AfXO03B8=", - "requires": { - "delayed-stream": "0.0.5" - } - }, - "compress-commons": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.2.2.tgz", - "integrity": "sha1-UkqfEJA/OoEzibAiXSfEi7dRiQ8=", - "requires": { - "buffer-crc32": "^0.2.1", - "crc32-stream": "^2.0.0", - "normalize-path": "^2.0.0", - "readable-stream": "^2.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", "requires": { - "buffer": "^5.1.0" - } - }, - "crc32-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-2.0.0.tgz", - "integrity": "sha1-483TtN8xaN10494/u8t7KX/pCPQ=", - "requires": { - "crc": "^3.4.4", - "readable-stream": "^2.0.0" + "delayed-stream": "~1.0.0" } }, "delayed-stream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz", - "integrity": "sha1-1LH0OpPoKW3+AmlPRoC8N6MTxz8=" - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "requires": { - "once": "^1.4.0" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "form-data": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-0.2.0.tgz", - "integrity": "sha1-Jvi8JtpkQOKZy9z7aQNcT3em5GY=", - "requires": { - "async": "~0.9.0", - "combined-stream": "~0.0.4", - "mime-types": "~2.0.3" - }, - "dependencies": { - "async": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", - "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" - } - } - }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "ieee754": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", - "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "isarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, - "lazystream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", - "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "requires": { - "readable-stream": "^2.0.5" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" } }, - "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + "globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" }, "mime-db": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz", - "integrity": "sha1-PQxjGA9FjrENMlqqN9fFiuMS6dc=" + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" }, "mime-types": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz", - "integrity": "sha1-MQ4VnbI+B3+Lsit0jav6SVcUCqY=", + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", "requires": { - "mime-db": "~1.12.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" + "mime-db": "1.40.0" } }, "minimist": { @@ -303,126 +60,26 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "node-fetch": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.4.1.tgz", + "integrity": "sha512-P9UbpFK87NyqBZzUuDBDz4f6Yiys8xm8j7ACDbi6usvFm6KItklQUKjeoqTrYS/S1k6I8oaOC2YLLDr/gg26Mw==" }, "simple-watcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/simple-watcher/-/simple-watcher-3.0.0.tgz", - "integrity": "sha1-xFjxoobbRoKC85S+po0MaFHoLcA=" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - }, - "tar-stream": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.1.tgz", - "integrity": "sha512-IFLM5wp3QrJODQFPm6/to3LJZrONdBY/otxcvDIQzu217zKye6yVR3hhi9lAjrC2Z+m/j5oDxMPb1qcd8cIvpA==", - "requires": { - "bl": "^1.0.0", - "buffer-alloc": "^1.1.0", - "end-of-stream": "^1.0.0", - "fs-constants": "^1.0.0", - "readable-stream": "^2.3.0", - "to-buffer": "^1.1.0", - "xtend": "^4.0.0" - } - }, - "to-buffer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/simple-watcher/-/simple-watcher-4.0.0.tgz", + "integrity": "sha512-vdT8721TVJWUXyxsFoLr7mHzGNsLuuloQ0GIePAqcpFeH9+p4CUO4nLg9X0F5uYIF3Afw01qwc3Ob90gzaY42w==" }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "triala": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/triala/-/triala-0.3.0.tgz", + "integrity": "sha512-0NJfAZ/KBm2oumORpbstIxQs4OwDCpjBzeWDs1sla06R5KY0CW6EqKh49BYrxhBSES3aGk3ReUhgGKlQyu0LJw==", + "dev": true }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - }, - "zip-stream": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-1.2.0.tgz", - "integrity": "sha1-qLxF9MG0lpnGuQGYuqyqzbzUugQ=", - "requires": { - "archiver-utils": "^1.3.0", - "compress-commons": "^1.2.0", - "lodash": "^4.8.0", - "readable-stream": "^2.0.0" - } + "xml-to-json-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xml-to-json-stream/-/xml-to-json-stream-1.1.0.tgz", + "integrity": "sha512-JaxUCQOk9+KajLNUV51elxlXI8cmN9rX4LVuhdCUtD7Z6N3lBsaD6l6caqjBPB1rieCZXviaoq4OKwAeup4RhA==" } } } diff --git a/package.json b/package.json index e3c9206..fd4758e 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { "name": "aemsync", - "version": "3.0.2", - "description": "Adobe AEM Synchronization Tool", + "version": "4.0.0-rc1", + "description": "The code and content synchronization for Sling / AEM (Adobe Experience Manager).", "author": "Michal Kochel ", "keywords": [ + "Adobe", "AEM", "CQ", "Sling", @@ -18,14 +19,20 @@ "url": "https://github.com/gavoja/aemsync.git" }, "main": "index.js", + "devDependencies": { + "triala": "^0.3.0" + }, "dependencies": { - "archiver": "^2.1.1", - "chalk": "^1.1.3", - "form-data": "^0.2.0", - "graceful-fs": "^4.1.11", - "minimatch": "^3.0.4", + "adm-zip": "^0.4.13", + "form-data": "^2.3.3", + "globrex": "^0.1.2", "minimist": "^1.2.0", - "simple-watcher": "^3.0.0" + "node-fetch": "^2.4.1", + "simple-watcher": "^4.0.0", + "xml-to-json-stream": "^1.1.0" + }, + "scripts": { + "test": "node test.js -v" }, "bin": { "aemsync": "./bin/aemsync" diff --git a/src/defaults.js b/src/defaults.js new file mode 100644 index 0000000..7aab64b --- /dev/null +++ b/src/defaults.js @@ -0,0 +1,9 @@ +'use strict' + +module.exports = { + workingDir: '.', + exclude: ['**/jcr_root/*', '**/@(.git|.svn|.hg|target)', '**/@(.git|.svn|.hg|target)/**'], + packmgrPath: '/crx/packmgr/service.jsp', + targets: ['http://admin:admin@localhost:4502'], + interval: 300 +} diff --git a/src/handlers/bundle-handler.js b/src/handlers/bundle-handler.js deleted file mode 100644 index 59ebbc2..0000000 --- a/src/handlers/bundle-handler.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict' - -class BundleHandler { - // TODO: Implement. -} - -module.exports = BundleHandler diff --git a/src/handlers/content-handler.js b/src/handlers/content-handler.js deleted file mode 100644 index 9bc06c0..0000000 --- a/src/handlers/content-handler.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict' - -const path = require('path') - -const RE_CONTENT_PATH = /^.*\/jcr_root(\/[^/]+){2,}$/ -const RE_SPECIAL = /^.*\/(_jcr_content|[^/]+\.dir|\.content\.xml).*$/ -const RE_TARGET_PATH = /^.*\/(target|\..*)\/(.*\/)?jcr_root\/.*$/ -const IGNORED = ['.svn', '.hg', '.git'] - -class ContentHandler { - process (localPath) { - let cleanPath = localPath.replace(/\\/g, '/') - // TODO: Simplify path checking. - - // Ignore CVS files. - if (IGNORED.some(i => cleanPath.endsWith(i))) { - return null - } - - // Skip paths on 'target' or dot prefixed folders - if (cleanPath.match(RE_TARGET_PATH)) { - return null - } - - // Process items only under 'jcr_root/*/' - if (!cleanPath.match(RE_CONTENT_PATH)) { - return null - } - - // Use parent if item is 'special'. - if (cleanPath.match(RE_SPECIAL)) { - return this.process(path.dirname(localPath)) - } - - return localPath - } -} - -module.exports = ContentHandler diff --git a/src/log.js b/src/log.js index b071b57..d41f303 100644 --- a/src/log.js +++ b/src/log.js @@ -1,58 +1,59 @@ 'use strict' const Console = require('console').Console -const chalk = require('chalk') -let c = null +class Log extends Console { + static getInstance () { + Log.instance = Log.instance || new Log(process.stdout, process.stderr) + return Log.instance + } + + constructor (stdout, stderr) { + super(stdout, stderr) + this.prefix = '' + } -function Log () { - c = c || new Console(process.stdout, process.stderr) - let prefix = '' + enableDebug () { + this.isDebug = true + } - c.isDebug = false + disableDebug () { + this.isDebug = false + } - c.format = function (args, color) { + _format (args, color) { args = Array.apply(null, args) - prefix && args.unshift(prefix.slice(0, -1)) + this.prefix && args.unshift(this.prefix.slice(0, -1)) - args = args.map(function (arg) { + return args.map(arg => { if (typeof arg === 'string') { - // Handle prefix. - arg = arg.replace(/\n/g, '\n' + prefix) - - // Handle color. - arg = color ? color(arg) : arg + arg = arg.replace(/\n/g, '\n' + this.prefix) // Handle prefix. + arg = color ? color(arg) : arg // Handle color. } return arg }) - - return args } - c.debug = function () { - if (this.isDebug) { - this.log.apply(this, this.format(arguments, chalk.gray)) - } + gray (text) { + return `\x1b[90m${text}\x1b[0m` } - c.error = function () { - this.log.apply(this, this.format(arguments, chalk.red)) + group () { + this.prefix += ' ' } - c.info = function () { - this.log.apply(this, this.format(arguments)) + groupEnd () { + this.prefix = this.prefix.slice(0, -2) } - c.group = function () { - prefix += ' ' + info () { + this.log.apply(this, this._format(arguments)) } - c.groupEnd = function () { - prefix = prefix.slice(0, -2) + debug () { + this.isDebug && super.log.apply(this, this._format(arguments, this.gray)) } - - return c } -module.exports = new Log() +module.exports = Log.getInstance() diff --git a/src/package.js b/src/package.js index ede7046..aa8ca35 100644 --- a/src/package.js +++ b/src/package.js @@ -1,201 +1,239 @@ -'use strict' - -const path = require('path') -const util = require('util') -const fs = require('graceful-fs') -const log = require('./log.js') -const Zip = require('./zip.js') - -const CONTENT_XML = '.content.xml' -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 RE_UNSTRUCTURED = /jcr:primaryType\s*=\s*"nt:unstructured"/g -const RE_CONTENT_PATH = /^.*\/jcr_root(\/[^/]+){2,}$/ -const FILTER_ZIP_PATH = 'META-INF/vault/filter.xml' -const FILTER_WRAPPER = ` -%s -` -const FILTER = ` - ` -const FILTER_CHILDREN = ` - - - - - ` - -class Package { - constructor () { - this.items = [] - this.path = [] - } - - /** Gets item with metadata from local path. */ - getItem (localPath) { - let item = { - localPath: localPath - } - - try { - let stat = fs.statSync(localPath) - item.exists = true - item.isDirectory = stat.isDirectory() - } catch (err) { - item.exists = false - } - - return item - } - - /** Adds local path to package. */ - add (localPath) { - return this.addItem(this.getItem(localPath)) - } - - /** Adds item to package. */ - addItem (item) { - // Handle duplicates. - for (let i = this.items.length - 1; i >= 0; --i) { - let existingItem = this.items[i] - - // Skip if parent already added. - if (item.localPath.startsWith(existingItem.localPath)) { - log.debug(`Already added to package, skipping: ${item.localPath}`) - return - } - - // Force replace or remove child if this one is parent. - if (existingItem.localPath.startsWith(item.localPath)) { - log.debug(`Removing child: ${item.localPath}`) - this.items.splice(i, 1) - } - } - - item.zipPath = item.zipPath || this.getZipPath(item.localPath) - item.filterPath = item.filterPath || this.getFilterPath(item.zipPath) - this.items.push(item) - - return this.handleContentXml(item) - } - - /** Adds all '.content.xml' files on the item's path. */ - handleContentXml (item) { - // Skip if '.content.xml' file. - if (path.basename(item.localPath) === CONTENT_XML) { - return item - } - - // Add all '.content.xml' files going up the path. - let dirPath = path.dirname(item.localPath) - while (this.cleanPath(dirPath).match(RE_CONTENT_PATH)) { - let contentXmlPath = path.join(dirPath, CONTENT_XML) - let contents = this.getFileContents(contentXmlPath) - // Process parent if 'nt:unstructured' found. - if (contents && contents.match(RE_UNSTRUCTURED)) { - return this.addItem(this.getItem(dirPath)) - } - - // Process '.content.xml'. - if (contents) { - this.addItem(this.getItem(contentXmlPath)) - } - - dirPath = path.dirname(dirPath) - } - - return item - } - - /** Saves package. */ - save (callback) { - if (this.items.length === 0) { - callback(null) - } - - // Create archive and add default package content. - let archive = new Zip() - let jcrRoot = path.join(PACKAGE_CONTENT_PATH, 'jcr_root') - let metaInf = path.join(PACKAGE_CONTENT_PATH, 'META-INF') - archive.addLocalDirectory(jcrRoot, 'jcr_root') - archive.addLocalDirectory(metaInf, 'META-INF') - - // Iterate over all items - let filters = '' - this.items.forEach((item) => { - // Update filters (delete). - if (!item.exists) { - filters += util.format(FILTER, item.filterPath) - return - } - - // Update filters (add). - // When adding we need to account for all the sibbling '.content.xml' files. - let dirName = path.dirname(item.filterPath) - filters += util.format(FILTER_CHILDREN, dirName, dirName, item.filterPath, item.filterPath) - - // Add directory to archive. - if (item.isDirectory) { - archive.addLocalDirectory(item.localPath, item.zipPath, (localPath, zipPath) => { - // Add as 'nt:folder' if no '.content.xml'. - this.addNtFolder(archive, localPath, zipPath) - }) - // Add file to archive - } else { - archive.addLocalFile(item.localPath, item.zipPath) - } - }) - - // Wrap filters - filters = util.format(FILTER_WRAPPER, filters) - archive.addFile(Buffer.from(filters), FILTER_ZIP_PATH) - log.debug(filters) - archive.save(callback) - } - - /** Additional handling of directories added recursively. */ - addNtFolder (archive, localPath, zipPath) { - // Add nt:folder if needed. - let contentXml = path.join(localPath, CONTENT_XML) - let hasContentXml = fs.existsSync(contentXml) - let hasContentFolder = localPath.indexOf('_jcr_content') !== -1 - if (!hasContentFolder && !hasContentXml) { - archive.addLocalFile(NT_FOLDER_PATH, this.getZipPath(contentXml)) - log.group() - log.debug('Added as nt:folder.') - log.groupEnd() - } - } - - /** Gets file contents; returns null if does not exist or other error. */ - getFileContents (localPath) { - let contents - try { - contents = fs.readFileSync(localPath, 'utf8') - } catch (err) { - // File likely does not exist. - contents = null - } - - return contents - } - - /** Replaces backslashes with slashes. */ - 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') - } - - /** Gets a filter path from a local path. */ - getFilterPath (localPath) { - return this.cleanPath(localPath) - .replace(/(.*jcr_root)|(\.xml$)|(\.dir)/g, '') - .replace(/\/_([^/]*)_([^/]*)$/g, '/$1:$2') - } -} - -module.exports = Package +'use strict' + +const util = require('util') +const fs = require('fs') +const path = require('path') +const globrex = require('globrex') +const log = require('./log') +const defaults = require('./defaults') +const Zip = require('./zip') + +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 = ` + ` +const FILTER_CHILDREN = ` + + + + + ` + +// https://jackrabbit.apache.org/filevault/vaultfs.html +class Package { + constructor (exclude = defaults.exclude) { + this.zip = new Zip() + this.exclude = exclude || [] + this.entries = [] + } + + // + // Path processing. + // + + add (localPath) { + // Clean path. + localPath = this._cleanPath(localPath) + + // Added path must be inside 'jcr_root' folder. + if (!localPath.includes('jcr_root/')) { + return null + } + + // If the change is to an xml file, the parent folder will be processed. + // It is better to leave the xml file handling to package manager. + if (localPath.endsWith('.xml')) { + return this.add(path.dirname(localPath)) + } + + // Include path. + const entry = this._deduplicateAndAdd(localPath) + if (!entry) { + return null + } + + // If folder, Add missing .content.xml@nt:folder inside. + // This ensures proper handlig when removing inner .content.xml file. + this._addContentXml(localPath) + + // Walk up the tree and add all .content.xml files. + for (let parentPath = path.dirname(localPath); !parentPath.endsWith('jcr_root'); parentPath = path.dirname(parentPath)) { + this._addContentXml(parentPath) + } + + return entry + } + + _addContentXml (localPath) { + try { + if (fs.lstatSync(localPath).isDirectory()) { + const contentXmlPath = path.join(localPath, '.content.xml') + if (fs.existsSync(contentXmlPath)) { + // Include existing .content.xml. + this._deduplicateAndAdd(contentXmlPath) + } else { + // Include missing .content.xml@nt:folder. + // This is needed in case the .content.xml was removed locally. + this._deduplicateAndAdd(contentXmlPath, NT_FOLDER_PATH) + } + } + } catch (err) { + log.debug(err) + } + } + + _deduplicateAndAdd (virtualLocalPath, localPath) { + virtualLocalPath = this._cleanPath(virtualLocalPath) + + // Handle exclusions. + if (this._isExcluded(virtualLocalPath)) { + return null + } + + // Deduplication handling. + const zipPath = this._getZipPath(virtualLocalPath) + for (let i = this.entries.length - 1; i >= 0; --i) { + const existingZipPath = this.entries[i].zipPath + + // Skip if already added. + if (zipPath === existingZipPath) { + return log.debug(`Already added to package, skipping: ${zipPath}`) + } + + // Skip if parent already added (with exception of .content.xml). + if (zipPath.startsWith(existingZipPath) && !zipPath.endsWith('.content.xml')) { + return log.debug(`Parent already added to package, skipping: ${zipPath}`) + } + + // Remove child if path to add is a parent. + if (existingZipPath.startsWith(zipPath)) { + log.debug(`Removing child: ${existingZipPath}`) + this.entries.splice(i, 1) + } + } + + localPath = localPath ? this._cleanPath(localPath) : virtualLocalPath + const entry = this._getEntry(localPath, zipPath) + this.entries.push(entry) + return entry + } + + _isExcluded (localPath) { + for (const globPattern of this.exclude) { + const regex = globrex(globPattern, { globstar: true, extended: true }).regex + if (regex.test(localPath)) { + return true + } + } + + return false + } + + // + // Zip creation. + // + + save (archivePath) { + if (this.entries.length === 0) { + return null + } + + // Create archive and add default package content. + let jcrRoot = path.join(PACKAGE_CONTENT_PATH, 'jcr_root') + let metaInf = path.join(PACKAGE_CONTENT_PATH, 'META-INF') + this.zip.add(jcrRoot, 'jcr_root') + this.zip.add(metaInf, 'META-INF') + + // Add each entry. + const filters = [] + for (let entry of this.entries) { + if (!entry.exists) { + // DELETE + // Only filters need to be updated. + filters.push(util.format(FILTER, entry.filterPath)) + } else { + // ADD + // Filters need to be updated. + const dirName = path.dirname(entry.filterPath) + // if (!entry.localPath.endsWith('.content.xml')) { + filters.push(util.format(FILTER_CHILDREN, dirName, dirName, entry.filterPath, entry.filterPath)) + // } + + // ADD + // File or folder needs to be added to the zip. + this.zip.add(entry.localPath, entry.zipPath) + } + } + + // Add filter file. + const filter = util.format(FILTER_WRAPPER, filters.join('\n')) + this.zip.add(Buffer.from(filter), FILTER_ZIP_PATH) + + // Debug package contents. + log.debug('Package details:') + log.group() + log.debug(JSON.stringify(this.zip.inspect(), null, 2)) + log.groupEnd() + + return this.zip.save(archivePath) + } + + // + // Entry handling. + // + + // Entry format: + // { + // localPath: Path to the local file + // zipPath: Path inside zip + // filterPath: Vault filter path + // isFolder + // exists + // } + _getEntry (localPath, zipPath) { + localPath = this._cleanPath(localPath) + + const entry = { + localPath, + zipPath, + filterPath: this._getFilterPath(zipPath) + } + + try { + const stat = fs.statSync(localPath) + entry.exists = true + entry.isFolder = stat.isDirectory() + } catch (err) { + entry.exists = false + } + + return entry + } + + _cleanPath (localPath) { + return path.resolve(localPath) + .replace(/\\/g, '/') // Replace backlashes with slashes. + .replace(/\/$/, '') // Remove trailing slash. + } + + _getZipPath (localPath) { + return this._cleanPath(localPath) + .replace(/.*\/(jcr_root\/.*)/, '$1') + } + + _getFilterPath (localPath) { + // .content.xml will result in .content entries. + // Although incorrect, it does not matter and makes the handling + // consistent. + return this._cleanPath(localPath) + .replace(/(.*jcr_root)|(\.xml$)|(\.dir)/g, '') + .replace(/\/_([^/]*)_([^/]*)$/g, '/$1:$2') + } +} + +module.exports = Package diff --git a/src/pipeline.js b/src/pipeline.js index 543a47a..cd859ff 100644 --- a/src/pipeline.js +++ b/src/pipeline.js @@ -1,110 +1,128 @@ 'use strict' -const chalk = require('chalk') -const ContentHandler = require('./handlers/content-handler.js') -const Package = require('./package.js') -const Sender = require('./sender.js') -const log = require('./log.js') +const fs = require('fs') +const fetch = require('node-fetch') +const FormData = require('form-data') +const xmlToJson = require('xml-to-json-stream') +const Package = require('./package') +const log = require('./log') +const defaults = require('./defaults') -/** Pushes changes to AEM. */ class Pipeline { - constructor ({targets, interval, packmgrPath, onPushEnd}) { - this.lock = 0 + constructor (opts = {}) { + this.lock = false this.queue = [] - this.targets = targets - this.interval = interval || 300 - this.handlers = [new ContentHandler()] - this.sender = new Sender({targets, packmgrPath}) - this.onPushEnd = onPushEnd || function () {} + this.packmgrPath = opts.packmgrPath || defaults.packmgrPath + this.targets = opts.targets || defaults.target + this.interval = opts.interval || defaults.interval + this.exclude = opts.exclude || defaults.exclude + this.onPushEnd = opts.onPushEnd || function () {} } start () { - setInterval(() => { - this.processQueue() + setInterval(async () => { + await this._processQueue() }, this.interval) } enqueue (localPath) { + log.debug(`Changed: ${localPath}`) this.queue.push(localPath) } - /** Processes queue. */ - processQueue () { + async push (pathToPush) { + this.enqueue(pathToPush) + return this._processQueue() + } + + async _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 + if (this.lock === true || this.queue.length < 1) { + return null } - // Get unique list of local paths. - let dict = {} + // Lock the queue. + this.lock = true + + // Create package. + const pack = new Package(this.exclude) while (this.queue.length > 0) { - dict[this.queue.pop()] = true + const localPath = this.queue.pop() + const item = pack.add(localPath) + item && log.info(item.exists ? '+' : '-', item.zipPath) } - // Get all the items. - let list = [] - Object.keys(dict).forEach(localPath => { - this.handlers.forEach(handler => { - let processedPath = handler.process(localPath) - processedPath && list.push(processedPath) - }) - }) - - // Skip if no items to add to package ... - if (list.length === 0) { - return + // Push package to targets (if any entries detected). + log.group() + const archivePath = pack.save() + if (archivePath) { + for (const target of this.targets) { + const result = await this._post(archivePath, target) + this.onPushEnd(result.err, result.target, result.log) + log.info(log.gray(target + ' >'), log.gray(result.err ? result.err.message : 'OK')) + } } + log.groupEnd() - // .. otherwise, process. - this.process(list, err => { - if (err) { - // Restore the queue if anything goes wrong. - // It will be processed in the next tick. - this.queue = this.queue.concat(list) - } - }) + // Release lock. + this.lock = false + + return pack } - process (list, callback) { - // Finalization function. - let finalize = (err) => { - this.lock = err ? 0 : this.lock - 1 - if (this.lock === 0) { - callback && callback(err) - log.groupEnd() - } - } + async _post (archivePath, target) { + const url = target + this.packmgrPath + const form = new FormData() + form.append('file', fs.createReadStream(archivePath)) + form.append('force', 'true') + form.append('install', 'true') + const result = { target } try { - // Add all paths to the package. - let pack = new Package() - list.forEach(localPath => { - let item = pack.add(localPath) - item && log.info(item.exists ? 'ADD' : 'DEL', chalk.yellow(item.zipPath)) - }) - - // Save the package. - log.group() - this.lock = this.targets.length - pack.save(packagePath => { - // Send the saved package. - this.sender.send(packagePath, (err, host, delta, time) => { - let prefix = `Deploying to [${chalk.yellow(host)}] in ${delta} ms at ${time}` - err ? log.info(`${prefix}: ${chalk.red(err)}`) : log.info(`${prefix}: ${chalk.green('OK')}`) - this.onPushEnd(err, host) - finalize() - }) - }) + const res = await fetch(url, { method: 'POST', body: form }) + + if (res.ok) { + const text = await res.text() + log.debug('Response text:') + log.group() + log.debug(text) + log.groupEnd() + + // Handle errors with AEM response. + try { + const obj = await this._parseXml(text) + result.log = obj.crx.response.data.log + const errorLines = [...new Set(result.log.split('\n').filter(line => line.startsWith('E')))] + + // Errors when installing selected nodes. + if (errorLines.length) { + result.err = new Error('Error installing nodes:\n' + errorLines.join('\n')) + // Error code in status. + } else if (obj.crx.response.status.code !== '200') { + result.err = new Error(obj.crx.response.status.textNode) + } + } catch (err) { + // Unexpected response format. + throw new Error('Unexpected response text format') + } + } else { + // Handle errors with failer request. + result.err = new Error(res.statusText) + } } catch (err) { - log.error(err) - finalize(err) + // Handle unexpeted errors. + result.err = err } + + return result } - push (localPath) { - this.process([localPath]) + _parseXml (xml) { + return new Promise(resolve => { + xmlToJson().xmlToJson(xml, (err, json) => err ? resolve({}) : resolve(json)) + }) } } diff --git a/src/sender.js b/src/sender.js deleted file mode 100644 index 2870c70..0000000 --- a/src/sender.js +++ /dev/null @@ -1,98 +0,0 @@ -'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 PACKMGR_PATH = '/crx/packmgr/service.jsp' -const RE_STATUS = /code="([0-9]+)">(.*) { - this.onSubmit(err, res, zipPath, target, timestamp, callback) - }) - } - - /** Package install submit callback */ - onSubmit (err, res, zipPath, target, timestamp, callback) { - const host = target.substring(target.indexOf('@') + 1) - let errorMessage = 'Invalid response; is the packmgr path valid?' - - // Server error. - if (!res) { - const delta = Date.now() - timestamp - const time = new Date().toISOString() - return callback(err.code, host, delta, time) - } - - const decoder = new StringDecoder('utf8') - const output = [`Output from ${host}:`] - - res.on('data', (chunk) => { - // Get message and remove new line. - let textChunk = decoder.write(chunk) - textChunk = textChunk.replace(/\r/g, '').substring(0, textChunk.length - 1) - output.push(textChunk) - - // Parse message. - const match = RE_STATUS.exec(textChunk) - if (match === null || match.length !== 3) { - return - } - - const code = match[1] - const msg = match[2] - errorMessage = code === '200' ? '' : msg - - log.group() - output.forEach(line => { - log.debug(line) - if (line.startsWith('E ')) { - errorMessage += `\n${line.substr(2)}` - } - }) - - log.groupEnd() - }) - - res.on('end', () => { - let delta = Date.now() - timestamp - let time = new Date().toISOString() - callback(errorMessage, host, delta, time) - }) - } -} - -module.exports = Sender diff --git a/src/watcher.js b/src/watcher.js deleted file mode 100644 index b11ea51..0000000 --- a/src/watcher.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict' - -const log = require('./log.js') -const mm = require('minimatch') -const chalk = require('chalk') -const watcher = require('simple-watcher') - -class Watcher { - watch ({workingDir, exclude, callback}) { - log.info(`Scanning: ${chalk.yellow(workingDir)} ...`) - - watcher(workingDir, (localPath) => { - log.debug('Changed:', localPath) - - // Skip excluded. - if (exclude && mm(localPath, exclude, {dot: true})) { - return - } - - callback(localPath) - }, 0) - - log.info('Awaiting changes ...') - } -} - -module.exports = Watcher diff --git a/src/zip.js b/src/zip.js index 35e3f58..eebea6b 100644 --- a/src/zip.js +++ b/src/zip.js @@ -1,107 +1,109 @@ -'use strict' - -const archiver = require('archiver') // TODO: consider using zip-stream for less dependencies. -const fs = require('graceful-fs') -const path = require('path') -const os = require('os') -const log = require('./log.js') - -const DEFAULT_ZIP_NAME = 'aemsync.zip' - -class Zip { - constructor (zipPath) { - this.path = 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) - } - - isFile (localPath) { - let stat = fs.statSync(localPath) - return stat && stat.isFile() - } - - isDirectory (localPath) { - let stat = fs.statSync(localPath) - return stat && stat.isDirectory() - } - - addLocalFile (localPath, zipPath) { - // Normalize slashes. - zipPath = zipPath.replace(/\\/g, '/') - - // Only files can be zipped. - if (!this.isFile(localPath)) { - return - } - - log.debug('Zipping:', zipPath) - this.zip.append(fs.createReadStream(localPath), { - name: zipPath - }) - } - - addLocalDirectory (localPath, zipPath, callback) { - if (!this.isDirectory(localPath)) { - 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 = [] - - // Add current item. - results.push(localPath) - - // No need for recursion if not a directory. - if (!this.isDirectory(localPath)) { - 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 +'use strict' + +const path = require('path') +const os = require('os') +const fs = require('fs') +const AdmZip = require('adm-zip') + +const DEFAULT_ARCHIVE_PATH = path.join(os.tmpdir(), 'aemsync.zip') + +class Zip { + constructor (archivePath) { + this.zip = archivePath ? new AdmZip(archivePath) : new AdmZip() + } + + // One method to add them all. Clean API is always nice. + add (localPathOrBuffer, zipPath) { + if (typeof localPathOrBuffer === 'string') { + for (const entry of this._getEntriesToAdd(localPathOrBuffer, zipPath)) { + this._addIfNotExists(entry.zipPath, entry.buffer) + } + } else { + this._addIfNotExists(zipPath, localPathOrBuffer) + } + } + + save (archivePath = DEFAULT_ARCHIVE_PATH) { + // TODO: + // Technically, the data could be stored to a buffer instead. + // The package manager however does not play well with buffers. + // Something to think about in future. + this.zip.writeZip(archivePath) + return archivePath + } + + _addIfNotExists (zipPath, buffer) { + if (this.zip.getEntry(zipPath) === null) { + this.zip.addFile(zipPath, buffer) + } + } + + // Using getLocalFolder() and getLocalFile() methods would have been simpler, + // however adm-zip does not handle empty folders properly. + // A top-down walk to identify all items makes the handlinng consistent + // for all the cases. + _getEntriesToAdd (localPath, zipPath) { + const entries = [] // [{ localPath, zipPath, buffer }] + const pipeline = [{ localPath, zipPath }] + + while (pipeline.length) { + const current = pipeline.pop() + if (this._isFolder(current.localPath)) { + // Add folder. + entries.push({ localPath: current.localPath, zipPath: current.zipPath + '/', buffer: Buffer.alloc(0) }) + + // Walk down the tree. + for (const entityName of fs.readdirSync(current.localPath)) { + pipeline.push({ + localPath: current.localPath + '/' + entityName, + zipPath: current.zipPath + '/' + entityName + }) + } + } else { + // Add file. + entries.push({ ...current, buffer: fs.readFileSync(current.localPath) }) + } + } + + return entries + } + + _isFolder (filePath) { + try { + return fs.lstatSync(filePath).isDirectory() + } catch (err) { + return false + } + } + + // + // Archive debugging. + // + + inspect () { + return { entries: this._getEntries(), filter: this._getFilter() } + } + + _getEntries () { + const entries = [] + for (const entry of this.zip.getEntries()) { + if (entry.entryName.endsWith('.content.xml')) { + // Read the resource type. + const re = /jcr:primaryType\s*=\s*"([^"]+)"/g + const content = this.zip.readAsText(entry.entryName) + const type = (re.exec(content) || ['', 'undefined'])[1] + entries.push(entry.entryName + '@' + type) + } else { + entries.push(entry.entryName) + } + } + + return entries + } + + _getFilter () { + return this.zip.readAsText('META-INF/vault/filter.xml').split('\n').map(line => line.trim()) + } +} + +module.exports = Zip diff --git a/test/jcr_root/apps/myapp/component/.content.xml b/test/jcr_root/apps/myapp/component/.content.xml new file mode 100644 index 0000000..e4d23c5 --- /dev/null +++ b/test/jcr_root/apps/myapp/component/.content.xml @@ -0,0 +1,6 @@ + + diff --git a/test/jcr_root/apps/myapp/component/_jcr_content/.content.xml b/test/jcr_root/apps/myapp/component/_jcr_content/.content.xml new file mode 100644 index 0000000..e9e3c47 --- /dev/null +++ b/test/jcr_root/apps/myapp/component/_jcr_content/.content.xml @@ -0,0 +1,4 @@ + + + diff --git a/test/jcr_root/apps/myapp/component/_jcr_content/file-node.xml b/test/jcr_root/apps/myapp/component/_jcr_content/file-node.xml new file mode 100644 index 0000000..4858318 --- /dev/null +++ b/test/jcr_root/apps/myapp/component/_jcr_content/file-node.xml @@ -0,0 +1,4 @@ + + + diff --git a/test/jcr_root/apps/myapp/component/file-node.xml b/test/jcr_root/apps/myapp/component/file-node.xml new file mode 100644 index 0000000..7d7f78d --- /dev/null +++ b/test/jcr_root/apps/myapp/component/file-node.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/test/jcr_root/apps/myapp/component/file.txt b/test/jcr_root/apps/myapp/component/file.txt new file mode 100644 index 0000000..4c33073 --- /dev/null +++ b/test/jcr_root/apps/myapp/component/file.txt @@ -0,0 +1 @@ +file.txt \ No newline at end of file diff --git a/test/jcr_root/apps/myapp/component/file.xml b/test/jcr_root/apps/myapp/component/file.xml new file mode 100644 index 0000000..7914c26 --- /dev/null +++ b/test/jcr_root/apps/myapp/component/file.xml @@ -0,0 +1 @@ +file.xml \ No newline at end of file diff --git a/test/jcr_root/apps/myapp/component/folder-node-nested/.content.xml b/test/jcr_root/apps/myapp/component/folder-node-nested/.content.xml new file mode 100644 index 0000000..4858318 --- /dev/null +++ b/test/jcr_root/apps/myapp/component/folder-node-nested/.content.xml @@ -0,0 +1,4 @@ + + + diff --git a/test/jcr_root/apps/myapp/component/folder-node-nested/foo/.content.xml b/test/jcr_root/apps/myapp/component/folder-node-nested/foo/.content.xml new file mode 100644 index 0000000..4858318 --- /dev/null +++ b/test/jcr_root/apps/myapp/component/folder-node-nested/foo/.content.xml @@ -0,0 +1,4 @@ + + + diff --git a/test/jcr_root/apps/myapp/component/folder-node-nested/foo/bar/.content.xml b/test/jcr_root/apps/myapp/component/folder-node-nested/foo/bar/.content.xml new file mode 100644 index 0000000..4858318 --- /dev/null +++ b/test/jcr_root/apps/myapp/component/folder-node-nested/foo/bar/.content.xml @@ -0,0 +1,4 @@ + + + diff --git a/test/jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/.content.xml b/test/jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/.content.xml new file mode 100644 index 0000000..4858318 --- /dev/null +++ b/test/jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/.content.xml @@ -0,0 +1,4 @@ + + + diff --git a/test/jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/file-node.xml b/test/jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/file-node.xml new file mode 100644 index 0000000..4858318 --- /dev/null +++ b/test/jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/file-node.xml @@ -0,0 +1,4 @@ + + + diff --git a/test/jcr_root/apps/myapp/component/folder-node/.content.xml b/test/jcr_root/apps/myapp/component/folder-node/.content.xml new file mode 100644 index 0000000..4858318 --- /dev/null +++ b/test/jcr_root/apps/myapp/component/folder-node/.content.xml @@ -0,0 +1,4 @@ + + + diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..0376d29 --- /dev/null +++ b/test/test.js @@ -0,0 +1,639 @@ +'use strict' + +const test = require('triala') +const path = require('path') +const aemsync = require('../index') +const assert = require('assert') + +const COMPONENT = path.resolve(__dirname, 'jcr_root/apps/myapp/component') +const TARGET = 'http://admin:admin@localhost:1234' + +test('aemsync', class { + async _before () { + this.pipeline = new aemsync.Pipeline({ targets: [TARGET] }) + } + + async _push (pathToPush) { + const pack = await this.pipeline.push(pathToPush) + return pack.zip.inspect() + } + + async 'onPushEnd failure' () { + const msg = 'Something went wrong' + this.pipeline._post = (archivePath, target) => ({ err: new Error(msg), target }) + + let error = null + this.pipeline.onPushEnd = (err, target, log) => (error = err) + + await this.pipeline.push(COMPONENT) + + // Reset for the rest of the tests. + this.pipeline.onPushEnd = () => {} + this.pipeline._post = (archivePath, target) => ({ target }) + + // Check if error message matches. + assert.strictEqual(error.message, msg) + } + + async 'exclude' () { + const expected = { entries: [], filter: [ '' ] } + + assert.deepStrictEqual(await this._push(path.join('jcr_root')), expected) + assert.deepStrictEqual(await this._push(path.join('jcr_root', 'bar')), expected) + assert.deepStrictEqual(await this._push(path.join('foo', 'jcr_root')), expected) + assert.deepStrictEqual(await this._push(path.join('foo', 'jcr_root', 'bar')), expected) + assert.deepStrictEqual(await this._push(path.join('.svn')), expected) + assert.deepStrictEqual(await this._push(path.join('.hg')), expected) + assert.deepStrictEqual(await this._push(path.join('.git')), expected) + assert.deepStrictEqual(await this._push(path.join('target')), expected) + assert.deepStrictEqual(await this._push(path.join('foo', '.svn')), expected) + assert.deepStrictEqual(await this._push(path.join('foo', '.hg')), expected) + assert.deepStrictEqual(await this._push(path.join('foo', '.git')), expected) + assert.deepStrictEqual(await this._push(path.join('foo', 'target')), expected) + assert.deepStrictEqual(await this._push(path.join('foo', '.svn', 'bar')), expected) + assert.deepStrictEqual(await this._push(path.join('foo', '.hg', 'bar')), expected) + assert.deepStrictEqual(await this._push(path.join('foo', '.git', 'bar')), expected) + assert.deepStrictEqual(await this._push(path.join('foo', 'target', 'bar')), expected) + assert.ok((await this._push(path.join('foo', 'jcr_root', 'bar', 'baz'))).entries.length > 0) + } + + async '+ file.txt' () { + const expected = { + entries: [ + 'jcr_root/', + 'jcr_root/aemsync.txt', + 'jcr_root/apps/.content.xml@nt:folder', + 'jcr_root/apps/myapp/.content.xml@nt:folder', + 'jcr_root/apps/myapp/component/.content.xml@cq:Component', + 'jcr_root/apps/myapp/component/file.txt', + 'META-INF/', + 'META-INF/vault/', + 'META-INF/vault/config.xml', + 'META-INF/vault/definition/', + 'META-INF/vault/definition/.content.xml@vlt:PackageDefinition', + 'META-INF/vault/filter.xml', + 'META-INF/vault/nodetypes.cnd', + 'META-INF/vault/properties.xml' ], + filter: [ + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '' + ] + } + + const result = await this._push(path.join(COMPONENT, 'file.txt')) + assert.deepStrictEqual(result, expected) + } + + async '+ folder' () { + const expected = { + entries: [ + 'jcr_root/', + 'jcr_root/aemsync.txt', + 'jcr_root/apps/.content.xml@nt:folder', + 'jcr_root/apps/myapp/.content.xml@nt:folder', + 'jcr_root/apps/myapp/component/.content.xml@cq:Component', + 'jcr_root/apps/myapp/component/folder/', + 'jcr_root/apps/myapp/component/folder/.content.xml@nt:folder', + 'jcr_root/apps/myapp/component/folder/sub-folder/', + 'META-INF/', + 'META-INF/vault/', + 'META-INF/vault/config.xml', + 'META-INF/vault/definition/', + 'META-INF/vault/definition/.content.xml@vlt:PackageDefinition', + 'META-INF/vault/filter.xml', + 'META-INF/vault/nodetypes.cnd', + 'META-INF/vault/properties.xml' ], + filter: [ + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '' + ] + } + + const result = await this._push(path.join(COMPONENT, 'folder')) + assert.deepStrictEqual(result, expected) + } + + async '+ sub-folder' () { + const expected = { + entries: [ + 'jcr_root/', + 'jcr_root/aemsync.txt', + 'jcr_root/apps/.content.xml@nt:folder', + 'jcr_root/apps/myapp/.content.xml@nt:folder', + 'jcr_root/apps/myapp/component/.content.xml@cq:Component', + 'jcr_root/apps/myapp/component/folder/.content.xml@nt:folder', + 'jcr_root/apps/myapp/component/folder/sub-folder/', + 'jcr_root/apps/myapp/component/folder/sub-folder/.content.xml@nt:folder', + 'META-INF/', + 'META-INF/vault/', + 'META-INF/vault/config.xml', + 'META-INF/vault/definition/', + 'META-INF/vault/definition/.content.xml@vlt:PackageDefinition', + 'META-INF/vault/filter.xml', + 'META-INF/vault/nodetypes.cnd', + 'META-INF/vault/properties.xml' ], + filter: [ + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '' + ] + } + + const result = await this._push(path.join(COMPONENT, 'folder', 'sub-folder')) + assert.deepStrictEqual(result, expected) + } + + async '+ component, + file.xml, + .content.xml, + file-node.xml, - deleted.xml' () { + const expected = { + entries: [ + 'jcr_root/', + 'jcr_root/aemsync.txt', + 'jcr_root/apps/.content.xml@nt:folder', + 'jcr_root/apps/myapp/.content.xml@nt:folder', + 'jcr_root/apps/myapp/component/', + 'jcr_root/apps/myapp/component/.content.xml@cq:Component', + 'jcr_root/apps/myapp/component/_jcr_content/', + 'jcr_root/apps/myapp/component/_jcr_content/.content.xml@cq:PageContent', + 'jcr_root/apps/myapp/component/_jcr_content/file-node.xml', + 'jcr_root/apps/myapp/component/file-node.xml', + 'jcr_root/apps/myapp/component/file.txt', + 'jcr_root/apps/myapp/component/file.xml', + 'jcr_root/apps/myapp/component/folder-node-nested/', + 'jcr_root/apps/myapp/component/folder-node-nested/.content.xml@nt:unstructured', + 'jcr_root/apps/myapp/component/folder-node-nested/foo/', + 'jcr_root/apps/myapp/component/folder-node-nested/foo/.content.xml@nt:unstructured', + 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/', + 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/.content.xml@nt:unstructured', + 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/', + 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/.content.xml@nt:unstructured', + 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/file-node.xml', + 'jcr_root/apps/myapp/component/folder-node/', + 'jcr_root/apps/myapp/component/folder-node/.content.xml@nt:unstructured', + 'jcr_root/apps/myapp/component/folder/', + 'jcr_root/apps/myapp/component/folder/sub-folder/', + 'META-INF/', + 'META-INF/vault/', + 'META-INF/vault/config.xml', + 'META-INF/vault/definition/', + 'META-INF/vault/definition/.content.xml@vlt:PackageDefinition', + 'META-INF/vault/filter.xml', + 'META-INF/vault/nodetypes.cnd', + 'META-INF/vault/properties.xml' + ], + filter: [ + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '' + ] + } + + let result = await this._push(path.join(COMPONENT)) + assert.deepStrictEqual(result, expected) + + result = await this._push(path.join(COMPONENT, 'file.xml')) + assert.deepStrictEqual(result, expected) + + result = await this._push(path.join(COMPONENT, '.content.xml')) + assert.deepStrictEqual(result, expected) + + result = await this._push(path.join(COMPONENT, 'file-node.xml')) + assert.deepStrictEqual(result, expected) + + result = await this._push(path.join(COMPONENT, 'deleted.xml')) + assert.deepStrictEqual(result, expected) + } + + async '+ folder-node, + folder-node/.content.xml' () { + const expected = { + entries: [ + 'jcr_root/', + 'jcr_root/aemsync.txt', + 'jcr_root/apps/.content.xml@nt:folder', + 'jcr_root/apps/myapp/.content.xml@nt:folder', + 'jcr_root/apps/myapp/component/.content.xml@cq:Component', + 'jcr_root/apps/myapp/component/folder-node/', + 'jcr_root/apps/myapp/component/folder-node/.content.xml@nt:unstructured', + 'META-INF/', + 'META-INF/vault/', + 'META-INF/vault/config.xml', + 'META-INF/vault/definition/', + 'META-INF/vault/definition/.content.xml@vlt:PackageDefinition', + 'META-INF/vault/filter.xml', + 'META-INF/vault/nodetypes.cnd', + 'META-INF/vault/properties.xml' ], + filter: [ + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '' + ] + } + + let result = await this._push(path.join(COMPONENT, 'folder-node')) + assert.deepStrictEqual(result, expected) + + result = await this._push(path.join(COMPONENT, 'folder-node', '.content.xml')) + assert.deepStrictEqual(result, expected) + } + + async '+ folder-node-nested' () { + const expected = { + entries: [ + 'jcr_root/', + 'jcr_root/aemsync.txt', + 'jcr_root/apps/.content.xml@nt:folder', + 'jcr_root/apps/myapp/.content.xml@nt:folder', + 'jcr_root/apps/myapp/component/.content.xml@cq:Component', + 'jcr_root/apps/myapp/component/folder-node-nested/', + 'jcr_root/apps/myapp/component/folder-node-nested/.content.xml@nt:unstructured', + 'jcr_root/apps/myapp/component/folder-node-nested/foo/', + 'jcr_root/apps/myapp/component/folder-node-nested/foo/.content.xml@nt:unstructured', + 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/', + 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/.content.xml@nt:unstructured', + 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/', + 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/.content.xml@nt:unstructured', + 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/file-node.xml', + 'META-INF/', + 'META-INF/vault/', + 'META-INF/vault/config.xml', + 'META-INF/vault/definition/', + 'META-INF/vault/definition/.content.xml@vlt:PackageDefinition', + 'META-INF/vault/filter.xml', + 'META-INF/vault/nodetypes.cnd', + 'META-INF/vault/properties.xml' + ], + filter: + [ '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '' + ] + } + + const result = await this._push(path.join(COMPONENT, 'folder-node-nested')) + assert.deepStrictEqual(result, expected) + } + + async '+ folder-node-nested/foo/bar/baz/file-node.xml' () { + const expected = { + entries: [ + 'jcr_root/', + 'jcr_root/aemsync.txt', + 'jcr_root/apps/.content.xml@nt:folder', + 'jcr_root/apps/myapp/.content.xml@nt:folder', + 'jcr_root/apps/myapp/component/.content.xml@cq:Component', + 'jcr_root/apps/myapp/component/folder-node-nested/.content.xml@nt:unstructured', + 'jcr_root/apps/myapp/component/folder-node-nested/foo/.content.xml@nt:unstructured', + 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/.content.xml@nt:unstructured', + 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/', + 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/.content.xml@nt:unstructured', + 'jcr_root/apps/myapp/component/folder-node-nested/foo/bar/baz/file-node.xml', + 'META-INF/', + 'META-INF/vault/', + 'META-INF/vault/config.xml', + 'META-INF/vault/definition/', + 'META-INF/vault/definition/.content.xml@vlt:PackageDefinition', + 'META-INF/vault/filter.xml', + 'META-INF/vault/nodetypes.cnd', + 'META-INF/vault/properties.xml' + ], + filter: [ + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '' + ] + } + + const result = await this._push(path.join(COMPONENT, 'folder-node-nested', 'foo', 'bar', 'baz', 'file-node.xml')) + assert.deepStrictEqual(result, expected) + } + + async '+ _jcr_content, + _jcr_content/.content.xml, + _jcr_content/file-node.xml, - _jcr_content/deleted.xml' () { + const expected = { + entries: [ + 'jcr_root/', + 'jcr_root/aemsync.txt', + 'jcr_root/apps/.content.xml@nt:folder', + 'jcr_root/apps/myapp/.content.xml@nt:folder', + 'jcr_root/apps/myapp/component/.content.xml@cq:Component', + 'jcr_root/apps/myapp/component/_jcr_content/', + 'jcr_root/apps/myapp/component/_jcr_content/.content.xml@cq:PageContent', + 'jcr_root/apps/myapp/component/_jcr_content/file-node.xml', + 'META-INF/', + 'META-INF/vault/', + 'META-INF/vault/config.xml', + 'META-INF/vault/definition/', + 'META-INF/vault/definition/.content.xml@vlt:PackageDefinition', + 'META-INF/vault/filter.xml', + 'META-INF/vault/nodetypes.cnd', + 'META-INF/vault/properties.xml' ], + filter: [ + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '' + ] + } + let result = await this._push(path.join(COMPONENT, '_jcr_content')) + assert.deepStrictEqual(result, expected) + + result = await this._push(path.join(COMPONENT, '_jcr_content', 'file-node.xml')) + assert.deepStrictEqual(result, expected) + + result = await this._push(path.join(COMPONENT, '_jcr_content', '.content.xml')) + assert.deepStrictEqual(result, expected) + + result = await this._push(path.join(COMPONENT, '_jcr_content', 'deleted.xml')) + assert.deepStrictEqual(result, expected) + } + + async '- deleted' () { + const expected = { + entries: [ + 'jcr_root/', + 'jcr_root/aemsync.txt', + 'jcr_root/apps/.content.xml@nt:folder', + 'jcr_root/apps/myapp/.content.xml@nt:folder', + 'jcr_root/apps/myapp/component/.content.xml@cq:Component', + 'META-INF/', + 'META-INF/vault/', + 'META-INF/vault/config.xml', + 'META-INF/vault/definition/', + 'META-INF/vault/definition/.content.xml@vlt:PackageDefinition', + 'META-INF/vault/filter.xml', + 'META-INF/vault/nodetypes.cnd', + 'META-INF/vault/properties.xml' + ], + filter: [ + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '' + ] + } + + const result = await this._push(path.join(COMPONENT, 'deleted')) + assert.deepStrictEqual(result, expected) + } +})