UNPKG

9.35 kBJavaScriptView Raw
1/*jslint node: true*/
2(function () {
3 "use strict";
4
5 // Built-in packages
6 var os = require("os");
7 var path = require('path');
8 var parseUrl = require('url').parse;
9 var StringDecoder = require('string_decoder').StringDecoder;
10
11 // NPM packages
12 var fs = require('graceful-fs');
13 var watch = require("node-watch");
14 var minimist = require('minimist');
15 var archiver = require('archiver'); // TODO: consider using zip-stream for less dependencies.
16 var FormData = require('form-data');
17 var colors = require('colors');
18
19 // Constants
20 var HELP = "Usage: aemsync -t targets [-i interval] -w path_to_watch\nWebsite: https://github.com/gavoja/aemsync";
21 var NT_FOLDER = __dirname + "/data/nt_folder/.content.xml";
22 var RE_DIR = /^.*\.dir$/;
23 var RE_CONTENT = /.*\.content\.xml$/;
24 // Include files on "jcr_root/xyz/..." path that's outside hidden or target folder.
25 var RE_SAFE_PATH = /^((?!(\/\.)|(\/target\/)).)*\/jcr_root\/[^\/]*\/.*$/;
26 var ZIP_NAME = "/aemsync.zip";
27 var STATUS_REGEX = /code="([0-9]+)">(.*)</
28
29 // Variables.
30 var syncerInterval = 300;
31 var queue = [];
32 var debugMode = false;
33 var maybeeExit = false;
34 var lock = false;
35
36 /** Prints debug message. */
37 function debug(msg) {
38 if (debugMode) {
39 console.log(msg.grey);
40 }
41 }
42
43 /** Recursively walks over directory. */
44 function walkSync(dir, includeDirectories) {
45 var results = includeDirectories ? [dir] : [];
46 var list = fs.readdirSync(dir);
47 list.forEach(function(file) {
48 file = dir + "/" + file;
49 var stat = fs.statSync(file);
50 if (stat && stat.isDirectory()) {
51 results = results.concat(walkSync(file));
52 } else {
53 results.push(file);
54 }
55 });
56 return results;
57 }
58
59 /** Gets a zip path from a local path. */
60 function getZipPath(localPath) {
61 return localPath.replace(/.*\/(jcr_root\/.*)/, "$1");
62 }
63
64 /** Gets a filter path from a local path. */
65 function getFilterPath(localPath) {
66 return localPath.replace(/(.*jcr_root)|(\.xml$)|(\.dir)/g, "").replace(/\/_([^\/]*)_([^\/]*)$/g, "$1:$2");
67 }
68
69 /** Zip wrapper. */
70 function Zip() {
71 var zipPath = debugMode ? __dirname + ZIP_NAME : os.tmpdir() + ZIP_NAME;
72 var zip = archiver("zip");
73
74 debug("Creating archive: " + zipPath);
75 var output = fs.createWriteStream(zipPath);
76 zip.pipe(output);
77
78 this.addLocalFile = function(localPath, zipPath) {
79 debug(" Zipping: " + zipPath);
80 zip.append(fs.createReadStream(localPath), {name: zipPath});
81 };
82
83 this.addFile = function(content, zipPath) {
84 debug(" Zipping: " + zipPath);
85 zip.append(content, {name: zipPath});
86 };
87
88 this.save = function(onSave) {
89 output.on("close", function() {
90 onSave(zipPath);
91 });
92 zip.finalize(); // Trigers the above.
93 };
94 }
95
96 function handleExit() {
97 if (maybeeExit === true) {
98 // Graceful exit.
99 console.log("Exit.");
100 process.exit( );
101 }
102 }
103
104 /** Pushes changes to AEM. */
105 function Syncer(targets, queue) {
106 targets = targets.split(",");
107
108 /** Submits the package manager form. */
109 var sendForm = function(zipPath) {
110 debug("Seding form...");
111 for (var i=0; i<targets.length; ++i) {
112 sendFormToTarget(zipPath, targets[i]);
113 }
114 };
115
116 var sendFormToTarget = function(zipPath, target) {
117 var params = parseUrl(target);
118 var options = {};
119 options.path = "/crx/packmgr/service.jsp";
120 options.port = params.port;
121 options.host = params.hostname;
122 options.headers = {"Authorization":"Basic " + new Buffer(params.auth).toString('base64')};
123
124 var form = new FormData();
125 form.append('file', fs.createReadStream(zipPath));
126 form.append('force', 'true');
127 form.append('install', 'true');
128 form.submit(options, function(err, res) {
129 onSubmit(err, res, zipPath, target);
130 });
131 };
132
133 /** Package install submit callback */
134 var onSubmit = function(err, res, zipPath, target) {
135 var host = res.req._headers.host;
136 console.log("Installing package on " + host + " ...");
137
138 if (!res) {
139 console.log(" " + err.code.red);
140 return;
141 }
142
143 var decoder = new StringDecoder('utf8');
144 res.on("data", function(chunk) {
145 // Get message and remove new line.
146 var textChunk = decoder.write(chunk);
147 textChunk = textChunk.substring(0, textChunk.length - 1);
148 debug(textChunk);
149
150 // Parse message.
151 var match = STATUS_REGEX.exec(textChunk);
152 if (match === null || match.length !== 3) {
153 return;
154 }
155
156 var code = match[1];
157 var msg = match[2];
158
159 // Success.
160 if (code === "200") {
161 console.log(" " + msg.green);
162 lock = false;
163 return;
164 }
165
166 console.log(" " + msg.red);
167 console.log("Retrying.");
168
169 // Retry on error.
170 this.sendFormToTarget(zipPath, target);
171 });
172 };
173
174
175 /** Creates a package. */
176 var createPackage = function() {
177 var zip = new Zip();
178 var path = __dirname + "/data/package_content";
179 var fileList = walkSync(path, false);
180 fileList.forEach(function(subItem) {
181 zip.addLocalFile(subItem, subItem.substr(path.length + 1));
182 });
183 return {zip: zip, filters: ""};
184 };
185
186 /** Installs a package. */
187 var installPackage = function(pack) {
188 // Add filters.
189 // TODO: Add support for rep:policy nodes.
190 pack.filters = '<?xml version="1.0" encoding="UTF-8"?>\n<workspaceFilter version="1.0">\nFILTERS</workspaceFilter>'.replace(/FILTERS/g, pack.filters);
191 pack.zip.addFile(new Buffer(pack.filters), "META-INF/vault/filter.xml");
192
193 debug("\nPackage filters:\n" + pack.filters + "\n");
194
195 // TODO: Make in-memory zip perhaps?
196 pack.zip.save(sendForm);
197 };
198
199 /** Adds item to package. */
200 var addItemInPackage = function(pack, item) {
201
202 console.log("ADD: " + item.yellow);
203 var filterPath = getFilterPath(item);
204 var filter = '';
205 filter += ' <filter root="PARENT">\n';
206 filter += ' <exclude pattern="PARENT/.*" />\n';
207 filter += ' <include pattern="ITEM" />\n';
208 filter += ' <include pattern="ITEM/.*" />\n';
209 filter += ' </filter>\n';
210 pack.filters += filter.replace(/PARENT/g, path.dirname(filterPath)).replace(/ITEM/g, filterPath);
211
212 // Add file.
213 if (fs.lstatSync(item).isFile()) {
214 pack.zip.addLocalFile(item, getZipPath(item));
215 return;
216 }
217
218 // Add files in directory.
219 var fileList = walkSync(item, true);
220 fileList.forEach(function(subItem) {
221
222 // Add files
223 if (fs.lstatSync(subItem).isFile()) {
224 pack.zip.addLocalFile(subItem, getZipPath(subItem));
225 return;
226 }
227
228 // Add NT_FOLDER if no .content.xml.
229 var contentXml = subItem + "/.content.xml";
230 if (!fs.existsSync(contentXml)) {
231 pack.zip.addLocalFile(NT_FOLDER, getZipPath(contentXml));
232 }
233 });
234 };
235
236 /** Deletes item in package. */
237 var deleteItemInPackage = function(pack, item) {
238 console.log("DEL: " + item.yellow);
239
240 var filterPath = getFilterPath(item);
241 pack.filters += ' <filter root="FILE" />\n'.replace(/FILE/g, filterPath);
242 };
243
244 /** Processes queue items; duplicates and descendants are removed. */
245 var processQueueItem = function(item, dict) {
246 var parentItem = path.dirname(item);
247
248 // Check if path is safe (prevents from deleting stuff like "/apps").
249 if (!RE_SAFE_PATH.test(item)) {
250 return;
251 }
252
253 // Try the parent if item is "special".
254 if (item.match(RE_CONTENT) || item.match(RE_DIR) || parentItem.match(RE_DIR)) {
255 processQueueItem(parentItem, dict);
256 return;
257 }
258
259 // Make sure only parent items are processed.
260 for (var dictItem in dict) {
261 // Skip item if ancestor was already added to dict.
262 if (item.indexOf(dictItem + "/") === 0) {
263 item = null;
264 break;
265 }
266
267 // Remove item if item is ancestor.
268 if (dictItem.indexOf(item + "/") === 0) {
269 delete dict[dictItem];
270 }
271 }
272
273 // Add to dictionary.
274 if (item) {
275 dict[item] = true;
276 }
277 };
278
279 /** Processes queue. */
280 this.processQueue = function() {
281 var i, item, dict = {};
282
283 // Wait for the previous package to install.
284 if (lock === true) {
285 return;
286 }
287
288 handleExit();
289
290 // Dequeue items (dictionary takes care of duplicates).
291 while((i = queue.pop())) {
292 processQueueItem(i, dict);
293 }
294
295 // Skip if no items.
296 if (Object.keys(dict).length === 0) {
297 return;
298 }
299
300 lock = true;
301 console.log("");
302
303 var pack = createPackage();
304 for (item in dict) {
305 if (fs.existsSync(item)) {
306 addItemInPackage(pack, item);
307 } else {
308 deleteItemInPackage(pack, item);
309 }
310 }
311 installPackage(pack);
312 };
313
314 setInterval(this.processQueue, syncerInterval);
315 }
316
317 /** Watches for file system changes. */
318 function Watcher(pathToWatch, queue) {
319 pathToWatch = path.resolve(path.normalize(pathToWatch));
320 fs.exists(pathToWatch, function(exists) {
321 if (!exists) {
322 console.error("Invalid path: " + pathToWatch);
323 return;
324 }
325
326 watch(pathToWatch, function(localPath) {
327 localPath = path.normalize(localPath);
328 debug("Change detected: " + localPath);
329 queue.push(localPath);
330 });
331 console.log("Watching: " + pathToWatch + ". Update interval: " + syncerInterval + " ms.");
332 });
333 }
334
335 function main() {
336 var args = minimist(process.argv.slice(2));
337 if (!args.t || !args.w) {
338 console.log(HELP);
339 return;
340 }
341 syncerInterval = args.i || syncerInterval;
342 debugMode = args.d;
343
344 new Watcher(args.w, queue);
345 new Syncer(args.t, queue);
346 }
347
348 process.on("SIGINT", function() {
349 console.log( "\nGracefully shutting down from SIGINT (Ctrl-C)...");
350 maybeeExit = true;
351 });
352
353 main();
354}());