diff --git a/README/README.md b/README/README.md index 1eb10a7..c569ea0 100644 --- a/README/README.md +++ b/README/README.md @@ -17,10 +17,16 @@ Read [this](http://lifepluslinux.blogspot.in/2015/06/crontab-ui-easy-and-safe-wa npm install -g crontab-ui crontab-ui - + If you need to set/use an alternate port, you may do so by setting an environment variable before starting the process: PORT=9000 crontab-ui + +Also, you may have to **set permissions** for your `node_modules` folder. Refer [this](https://docs.npmjs.com/getting-started/fixing-npm-permissions). + +If you need to autosave your changes to crontab directly: + + crontab-ui --autosave ###Adding, deleting, pausing and resuming jobs. diff --git a/README/issues.md b/README/issues.md index 60b4573..9ac439c 100644 --- a/README/issues.md +++ b/README/issues.md @@ -8,3 +8,7 @@ __crontab-ui is running but is not accessible on browser__ - This is usually because the place where your crontab-ui is installed does not give access to others. It can be resolved by either giving permission to the user (Recommended) or running crontab-ui as root. Refer [this](https://github.com/alseambusher/crontab-ui/issues/8) __Hosting crontab-ui : it works on localhost but not outside the server__ - You have to host it using nginx, apache2, etc. Refer [this](nginx.md). + +__crontab-ui stopped working__ - It can happen that your crontab-ui can stop working for some reason like adding incorrect jobs or timings. In order to fix it, you can just go ahead a remove the job from `crontab.db` or `env.db` in "crontabs" folder and restart crontab-ui. + +__Where is my root node_modules folder__ - You can find it by `npm root -g` diff --git a/README/mail.md b/README/mail.md new file mode 100644 index 0000000..2e72454 --- /dev/null +++ b/README/mail.md @@ -0,0 +1,2 @@ +Mailing and Hooks +================= diff --git a/app.js b/app.js index 113fbf8..e95ee28 100755 --- a/app.js +++ b/app.js @@ -1,3 +1,4 @@ +/*jshint esversion: 6*/ var express = require('express'); var app = express(); var crontab = require("./crontab"); @@ -9,7 +10,6 @@ var mime = require('mime'); var fs = require('fs'); var busboy = require('connect-busboy'); // for file upload - // include the routes var routes = require("./routes").routes; @@ -17,7 +17,7 @@ var routes = require("./routes").routes; app.set('view engine', 'ejs'); var bodyParser = require('body-parser'); -app.use( bodyParser.json() ); // to support JSON-encoded bodies +app.use(bodyParser.json()); // to support JSON-encoded bodies app.use(bodyParser.urlencoded({ // to support URL-encoded bodies extended: true })); @@ -27,14 +27,17 @@ app.use(busboy()); // to support file uploads app.use(express.static(__dirname + '/public')); app.use(express.static(__dirname + '/public/css')); app.use(express.static(__dirname + '/public/js')); +app.use(express.static(__dirname + '/config')); app.set('views', __dirname + '/views'); -//set port +// set port to 8000 or the value set by environment var PORT app.set('port', (process.env.PORT || 8000)); +// root page handler app.get(routes.root, function(req, res) { - // get all the crontabs + // reload the database before rendering crontab.reload_db(); + // send all the required parameters crontab.crontabs( function(docs){ res.render('index', { routes : JSON.stringify(routes), @@ -46,10 +49,15 @@ app.get(routes.root, function(req, res) { }); }); +/* +Handle to save crontab to database +If it is a new job @param _id is set to -1 +@param name, command, schedule, logging has to be sent with _id (if exists) +*/ app.post(routes.save, function(req, res) { // new job if(req.body._id == -1){ - crontab.create_new(req.body.name, req.body.command, req.body.schedule, req.body.logging); + crontab.create_new(req.body.name, req.body.command, req.body.schedule, req.body.logging, req.body.mailing); } // edit job else{ @@ -58,30 +66,39 @@ app.post(routes.save, function(req, res) { res.end(); }); +// set stop to job app.post(routes.stop, function(req, res) { crontab.status(req.body._id, true); res.end(); }); +// set start to job app.post(routes.start, function(req, res) { crontab.status(req.body._id, false); res.end(); }); +// remove a job app.post(routes.remove, function(req, res) { crontab.remove(req.body._id); res.end(); }); -app.get(routes.crontab, function(req, res) { - crontab.set_crontab(req.query.env_vars); - res.end(); + +// set crontab. Needs env_vars to be passed +app.get(routes.crontab, function(req, res, next) { + crontab.set_crontab(req.query.env_vars, function(err) { + if (err) next(err); + else res.end(); + }); }); +// backup crontab db app.get(routes.backup, function(req, res) { crontab.backup(); res.end(); }); +// This renders the restore page similar to backup page app.get(routes.restore, function(req, res) { // get all the crontabs restore.crontabs(req.query.db, function(docs){ @@ -94,16 +111,19 @@ app.get(routes.restore, function(req, res) { }); }); +// delete backup db app.get(routes.delete_backup, function(req, res) { restore.delete(req.query.db); res.end(); }); +// restore from backup db app.get(routes.restore_backup, function(req, res) { crontab.restore(req.query.db); res.end(); }); +// export current crontab db so that user can download it app.get(routes.export, function(req, res) { var file = __dirname + '/crontabs/crontab.db'; @@ -117,7 +137,7 @@ app.get(routes.export, function(req, res) { filestream.pipe(res); }); - +// import from exported crontab db app.post(routes.import, function(req, res) { var fstream; req.pipe(req.busboy); @@ -127,17 +147,18 @@ app.post(routes.import, function(req, res) { fstream.on('close', function () { crontab.reload_db(); res.redirect(routes.root); - }); - }); + }); + }); }); +// import from current ACTUALL crontab app.get(routes.import_crontab, function(req, res) { crontab.import_crontab(); res.end(); }); +// get the log file a given job. id passed as query param app.get(routes.logger, function(req, res) { - var fs = require("fs"); _file = crontab.log_folder +"/"+req.query.id+".log"; if (fs.existsSync(_file)) res.sendFile(_file); @@ -145,6 +166,34 @@ app.get(routes.logger, function(req, res) { res.end("No errors logged yet"); }); +// error handler +app.use(function(err, req, res, next) { + var data = {}; + var statusCode = err.statusCode || 500; + + data.message = err.message || 'Internal Server Error'; + + if (process.env.NODE_ENV === 'development' && err.stack) { + data.stack = err.stack; + } + + if (parseInt(data.statusCode) >= 500) { + console.error(err); + } + + res.status(statusCode).json(data); +}); + app.listen(app.get('port'), function() { - console.log("Crontab UI is running at http://localhost:" + app.get('port')); + // If --autosave is used then we will also save whatever is in the db automatically without having to mention it explictly + // we do this by watching log file and setting a on change hook to it + if (process.argv.includes("--autosave")){ + crontab.autosave_crontab(()=>{}); + fs.watchFile(__dirname + '/crontabs/crontab.db', () => { + crontab.autosave_crontab(()=>{ + console.log("Attempted to autosave crontab"); + }); + }); + } + console.log("Crontab UI is running at http://localhost:" + app.get('port')); }); diff --git a/bin/crontab-ui-mailer.js b/bin/crontab-ui-mailer.js new file mode 100644 index 0000000..7025224 --- /dev/null +++ b/bin/crontab-ui-mailer.js @@ -0,0 +1,36 @@ +#!/usr/bin/env node + +var defaults = require("../config/mailconfig.js"); + +var nodemailer = require('nodemailer'); + +// create reusable transporter object using the default SMTP transport +var transporter = nodemailer.createTransport(defaults.transporterStr); +var mailOptions = defaults.mailOptions; + +var stdin = process.stdin, + stdout = process.stdout, + inputChunks = []; + +stdin.resume(); +stdin.setEncoding('utf8'); + +stdin.on('data', function (chunk) { + inputChunks.push(chunk); +}); + +stdin.on('end', function () { + var inputJSON = inputChunks.join(), + mailOptions = JSON.parse(inputJSON); + + // outputJSON = JSON.stringify(parsedData, null, ' '); + // stdout.write(outputJSON); + // stdout.write('\n'); +}); + +transporter.sendMail(mailOptions, function(error, info){ + if(error){ + return console.log(error); + } + console.log('Message sent: ' + info.response); +}); diff --git a/config/mailconfig.js b/config/mailconfig.js new file mode 100644 index 0000000..2adffb4 --- /dev/null +++ b/config/mailconfig.js @@ -0,0 +1,22 @@ +/*jshint esversion: 6*/ +// refer nodemailer for more info + +var transporterStr = 'smtps://user%40gmail.com:pass@smtp.gmail.com'; + +var mailOptions = { + from: '"Fred Foo 👥" ', // sender address + to: 'bar@blurdybloop.com, baz@blurdybloop.com', // list of receivers + subject: 'Hello ✔', // Subject line + text: 'Hello world 🐴', // plaintext body + html: 'Hello world 🐴' // html body +}; + +if (typeof window === 'undefined') { + exports.transporterStr = transporterStr; + exports.mailOptions = mailOptions; +} else { + if (!window.config) + window.config = {}; + window.config.transporterStr = transporterStr; + window.config.mailOptions = mailOptions; +} diff --git a/crontab.js b/crontab.js index 253c760..5707610 100644 --- a/crontab.js +++ b/crontab.js @@ -1,8 +1,12 @@ +/*jshint esversion: 6*/ //load database var Datastore = require('nedb'); var db = new Datastore({ filename: __dirname + '/crontabs/crontab.db' }); + db.loadDatabase(function (err) { + if (err) throw err; // no hope, just terminate }); + var exec = require('child_process').exec; var fs = require('fs'); var cron_parser = require("cron-parser"); @@ -11,7 +15,7 @@ var os = require("os"); exports.log_folder = __dirname + '/crontabs/logs'; exports.env_file = __dirname + '/crontabs/env.db'; -crontab = function(name, command, schedule, stopped, logging){ +crontab = function(name, command, schedule, stopped, logging, mailing){ var data = {}; data.name = name; data.command = command; @@ -21,17 +25,20 @@ crontab = function(name, command, schedule, stopped, logging){ } data.timestamp = (new Date()).toString(); data.logging = logging; + if (!mailing) + mailing = {}; + data.mailing = mailing; return data; }; -exports.create_new = function(name, command, schedule, logging){ - var tab = crontab(name, command, schedule, false, logging); +exports.create_new = function(name, command, schedule, logging, mailing){ + var tab = crontab(name, command, schedule, false, logging, mailing); tab.created = new Date().valueOf(); db.insert(tab); }; exports.update = function(data){ - db.update({_id: data._id}, crontab(data.name, data.command, data.schedule, null, data.logging)); + db.update({_id: data._id}, crontab(data.name, data.command, data.schedule, null, data.logging, data.mailing)); }; exports.status = function(_id, stopped){ @@ -41,6 +48,8 @@ exports.status = function(_id, stopped){ exports.remove = function(_id){ db.remove({_id: _id}, {}); }; + +// Iterates through all the crontab entries in the db and calls the callback with the entries exports.crontabs = function(callback){ db.find({}).sort({ created: -1 }).exec(function(err, docs){ for(var i=0; i/tmp/.log|| {if test -f /tmp/; then date >> ; cat /tmp/.log >> ; rm /tmp.log } - crontab_string += tab.schedule + " { " + tab.command + " } 2> " + tmp_log +"; if test -f " + tmp_log +"; then date >> " + log_file + "; cat " + tmp_log + " >> " + log_file + "; rm " + tmp_log + "; fi \n"; + // hook is in beta + if (tab.hook){ + let tmp_hook = "/tmp/" + tab._id + ".hook"; + crontab_string += tab.schedule + " ({ " + tab.command + " } | tee " + tmp_hook + ") 3>&1 1>&2 2>&3 | tee " + tmp_log + + "; if test -f " + tmp_log + + "; then date >> " + log_file + + "; cat " + tmp_log + " >> " + log_file + + "; rm " + tmp_log + + "; fi; if test -f " + tmp_hook + + "; then " + tab.hook + " < " + tmp_hook + + "; rm " + tmp_hook + + "; fi \n"; + } else { + crontab_string += tab.schedule + " { " + tab.command + " } 2> " + tmp_log + + "; if test -f " + tmp_log + + "; then date >> " + log_file + + "; cat " + tmp_log + " >> " + log_file + + "; rm " + tmp_log + + "; fi \n"; } - else + } + else { crontab_string += tab.schedule + " " + tab.command + "\n"; + } } }); - fs.writeFile(exports.env_file, env_vars); - fs.writeFile("/tmp/crontab", crontab_string, function(err) { - exec("crontab /tmp/crontab"); - }); + fs.writeFile(exports.env_file, env_vars, function(err) { + if (err) callback(err); + + fs.writeFile("/tmp/crontab", crontab_string, function(err) { + if (err) return callback(err); + exec("crontab /tmp/crontab", function(err) { + if (err) return callback(err); + else callback(); + }); + }); + }); }); }; @@ -85,7 +122,7 @@ exports.get_backup_names = function(){ var backups = []; fs.readdirSync(__dirname + '/crontabs').forEach(function(file){ // file name begins with backup - if(file.indexOf("backup") == 0){ + if(file.indexOf("backup") === 0){ backups.push(file); } }); @@ -118,7 +155,7 @@ exports.restore = function(db_name){ db.loadDatabase(); // reload the database }; -exports.reload_db= function(){ +exports.reload_db = function(){ db.loadDatabase(); }; @@ -139,7 +176,10 @@ exports.import_crontab = function(){ var command = line.replace(regex, '').trim(); var schedule = line.replace(command, '').trim(); - if(command && schedule){ + var is_valid = false; + try { is_valid = cron_parser.parseString(line).expressions.length > 0; } catch (e){} + + if(command && schedule && is_valid){ var name = namePrefix + '_' + index; db.findOne({ command: command, schedule: schedule }, function(err, doc) { @@ -159,3 +199,8 @@ exports.import_crontab = function(){ }); }); }; + +exports.autosave_crontab = function(callback) { + let env_vars = exports.get_env(); + exports.set_crontab(env_vars, callback); +}; diff --git a/package.json b/package.json index ec00bc1..c1aa392 100755 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "node": "latest" }, "bin": { - "crontab-ui": "bin/crontab-ui.js" + "crontab-ui": "bin/crontab-ui.js", + "crontab-ui-mailer": "bin/crontab-ui-mailer.js" }, "repository": { "type": "git", diff --git a/public/js/script.js b/public/js/script.js index c704a95..9777b19 100644 --- a/public/js/script.js +++ b/public/js/script.js @@ -6,6 +6,13 @@ function infoMessageBox(message, title){ $("#info-title").html(title); $("#info-popup").modal('show'); } +// like info, but for errors. +function errorMessageBox(message) { + var msg = + "Operation failed: " + message + ". " + + "Please see error log for details."; + infoMessageBox(msg, "Error"); +} // modal with full control function messageBox(body, title, ok_text, close_text, callback){ $("#modal-body").html(body); @@ -19,6 +26,9 @@ function messageBox(body, title, ok_text, close_text, callback){ /*********** crontab actions ****************/ +// TODO get rid of global variables +var schedule = ""; +var job_command = ""; function deleteJob(_id){ // TODO fix this. pass callback properly @@ -50,6 +60,8 @@ function setCrontab(){ $.get(routes.crontab, { "env_vars": $("#env_vars").val() }, function(){ // TODO show only if success infoMessageBox("Successfuly set crontab file!","Information"); + }).fail(function(response) { + errorMessageBox(response.statusText,"Error"); }); }); } @@ -75,7 +87,7 @@ function editJob(_id){ $("#job-name").val(job.name); $("#job-command").val(job.command); // if macro not used - if(job.schedule.indexOf("@") != 0){ + if(job.schedule.indexOf("@") !== 0){ var components = job.schedule.split(" "); $("#job-minute").val(components[0]); $("#job-hour").val(components[1]); @@ -83,6 +95,9 @@ function editJob(_id){ $("#job-month").val(components[3]); $("#job-week").val(components[4]); } + if (job.mailing) { + $("#job-mailing").attr("data-json", JSON.stringify(job.mailing)); + } schedule = job.schedule; job_command = job.command; if (job.logging && job.logging != "false") @@ -93,7 +108,13 @@ function editJob(_id){ $("#job-save").unbind("click"); // remove existing events attached to this $("#job-save").click(function(){ // TODO good old boring validations - $.post(routes.save, {name: $("#job-name").val(), command: job_command , schedule: schedule, _id: _id, logging: $("#job-logging").prop("checked")}, function(){ + if (!schedule) { + schedule = "* * * * *"; + } + let name = $("#job-name").val(); + let mailing = JSON.parse($("#job-mailing").attr("data-json")); + let logging = $("#job-logging").prop("checked"); + $.post(routes.save, {name: name, command: job_command , schedule: schedule, _id: _id, logging: logging, mailing: mailing}, function(){ location.reload(); }); }); @@ -111,11 +132,18 @@ function newJob(){ $("#job").modal("show"); $("#job-name").val(""); $("#job-command").val(""); + $("#job-mailing").attr("data-json", "{}"); job_string(); $("#job-save").unbind("click"); // remove existing events attached to this $("#job-save").click(function(){ // TODO good old boring validations - $.post(routes.save, {name: $("#job-name").val(), command: job_command , schedule: schedule, _id: -1, logging: $("#job-logging").prop("checked")}, function(){ + if (!schedule) { + schedule = "* * * * *"; + } + let name = $("#job-name").val(); + let mailing = JSON.parse($("#job-mailing").attr("data-json")); + let logging = $("#job-logging").prop("checked"); + $.post(routes.save, {name: name, command: job_command , schedule: schedule, _id: -1, logging: logging, mailing: mailing}, function(){ location.reload(); }); }); @@ -151,10 +179,65 @@ function import_db(){ }); } +function setMailConfig(a){ + let data = JSON.parse(a.getAttribute("data-json")); + let container = document.createElement("div"); + + let message = "

This is based on nodemailer. Refer this for more details.

"; + container.innerHTML += message; + + let transporterLabel = document.createElement("label"); + transporterLabel.innerHTML = "Transporter"; + let transporterInput = document.createElement("input"); + transporterInput.type = "text"; + transporterInput.id = "transporterInput"; + transporterInput.setAttribute("placeholder", config.transporterStr); + transporterInput.className = "form-control"; + if (data.transporterStr){ + transporterInput.setAttribute("value", data.transporterStr); + } + container.appendChild(transporterLabel); + container.appendChild(transporterInput); + + container.innerHTML += "
"; + + let mailOptionsLabel = document.createElement("label"); + mailOptionsLabel.innerHTML = "Mail Config"; + let mailOptionsInput = document.createElement("textarea"); + mailOptionsInput.setAttribute("placeholder", JSON.stringify(config.mailOptions, null, 2)); + mailOptionsInput.className = "form-control"; + mailOptionsInput.id = "mailOptionsInput"; + mailOptionsInput.setAttribute("rows", "10"); + if (data.mailOptions) + mailOptionsInput.innerHTML = JSON.stringify(data.mailOptions, null, 2); + container.appendChild(mailOptionsLabel); + container.appendChild(mailOptionsInput); + + container.innerHTML += "
"; + + let button = document.createElement("a"); + button.className = "btn btn-primary btn-small"; + button.innerHTML = "Use Defaults"; + button.onclick = function(){ + document.getElementById("transporterInput").value = config.transporterStr; + document.getElementById("mailOptionsInput").innerHTML = JSON.stringify(config.mailOptions, null, 2); + }; + container.appendChild(button); + + messageBox(container, "Mailing", null, null, function(){ + let transporterStr = document.getElementById("transporterInput").value; + let mailOptions = JSON.parse(document.getElementById("mailOptionsInput").innerHTML); + if (transporterStr && mailOptions){ + a.setAttribute("data-json", JSON.stringify({transporterStr: transporterStr, mailOptions: mailOptions})); + } + }); +} + +function setHookConfig(a){ + messageBox("

Coming Soon

", "Hooks", null, null, null); +} // script corresponding to job popup management -var schedule = ""; -var job_command = ""; function job_string(){ $("#job-string").val(schedule + " " + job_command); return schedule + " " + job_command; diff --git a/views/index.ejs b/views/index.ejs index 47e081c..ee9be6d 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -4,6 +4,7 @@ + diff --git a/views/popup.ejs b/views/popup.ejs index 931f979..06bc9d7 100644 --- a/views/popup.ejs +++ b/views/popup.ejs @@ -1,36 +1,3 @@ - - - - + + + +