From c0e239d709f835026beb1a4a96a6e3d07402cef0 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Thu, 7 Jul 2016 17:07:17 -0500 Subject: [PATCH] Cancel task, restart task, minor refactoring --- index.js | 42 +++++++++++-- libs/Task.js | 44 +++++++++++++- libs/TaskManager.js | 30 ++++++++- public/index.html | 16 +++-- public/js/main.js | 144 ++++++++++++++++++++++++++++++++------------ 5 files changed, 224 insertions(+), 52 deletions(-) diff --git a/index.js b/index.js index bf262c9..9d7eccb 100644 --- a/index.js +++ b/index.js @@ -6,10 +6,13 @@ let app = express(); let addRequestId = require('./libs/express-request-id')(); let multer = require('multer'); +let bodyParser = require('body-parser'); let taskManager = new (require('./libs/taskManager'))(); let Task = require('./libs/Task'); +app.use(bodyParser.urlencoded({extended: true})); +app.use(bodyParser.json()); app.use(express.static('public')); let upload = multer({ @@ -32,16 +35,21 @@ let upload = multer({ }) }); -app.post('/newTask', addRequestId, upload.array('images'), (req, res, next) => { +app.post('/newTask', addRequestId, upload.array('images'), (req, res) => { if (req.files.length === 0) res.json({error: "Need at least 1 file."}); else{ console.log(`Received ${req.files.length} files`); // Move to data - fs.rename(`tmp/${req.id}`, `data/${req.id}`, (err) => { + fs.rename(`tmp/${req.id}`, `data/${req.id}`, err => { if (!err){ - taskManager.addNew(new Task(req.id, req.body.name)); - res.json({uuid: req.id, success: true}); + new Task(req.id, req.body.name, (err, task) => { + if (err) res.json({error: err.message}); + else{ + taskManager.addNew(task); + res.json({uuid: req.id, success: true}); + } + }); }else{ res.json({error: "Could not move images folder."}); } @@ -49,13 +57,37 @@ app.post('/newTask', addRequestId, upload.array('images'), (req, res, next) => { } }); -app.get('/taskInfo/:uuid', (req, res, next) => { +app.get('/taskInfo/:uuid', (req, res) => { let task = taskManager.find(req.params.uuid); if (task){ res.json(task.getInfo()); }else res.json({error: `${req.params.uuid} not found`}); }); +let uuidCheck = (req, res, next) => { + if (!req.body.uuid) res.json({error: "uuid param missing."}); + else next(); +}; + +let successHandler = res => { + return err => { + if (!err) res.json({success: true}); + else res.json({error: err.message}); + }; +}; + +app.post('/cancelTask', uuidCheck, (req, res) => { + taskManager.cancel(req.body.uuid, successHandler(res)); +}); + +app.post('/removeTask', uuidCheck, (req, res) => { + taskManager.remove(req.body.uuid, successHandler(res)); +}); + +app.post('/restartTask', uuidCheck, (req, res) => { + taskManager.restart(req.body.uuid, successHandler(res)); +}); + app.listen(3000, () => { console.log('Example app listening on port 3000!'); }); \ No newline at end of file diff --git a/libs/Task.js b/libs/Task.js index e185eaf..12fd370 100644 --- a/libs/Task.js +++ b/libs/Task.js @@ -1,16 +1,19 @@ "use strict"; let assert = require('assert'); +let fs = require('fs'); let statusCodes = { QUEUED: 10, RUNNING: 20, FAILED: 30, - COMPLETED: 40 + COMPLETED: 40, + CANCELED: 50 }; module.exports = class Task{ - constructor(uuid, name){ + constructor(uuid, name, readyCb){ assert(uuid !== undefined, "uuid must be set"); + assert(readyCb !== undefined, "ready must be set"); this.uuid = uuid; this.name = name != "" ? name : "Task of " + (new Date()).toISOString(); @@ -19,6 +22,40 @@ module.exports = class Task{ code: statusCodes.QUEUED }; this.options = {}; + + // Read images info + fs.readdir(`data/${this.uuid}`, (err, files) => { + if (err) readyCb(err); + else{ + this.images = files; + readyCb(null, this); + } + }); + } + + cancel(cb){ + if (this.status.code !== statusCodes.CANCELED){ + this.status.code = statusCodes.CANCELED; + + console.log("Requested to cancel " + this.name); + // TODO + cb(null); + }else{ + cb(new Error("Task already cancelled")); + } + } + + restart(cb){ + if (this.status.code === statusCodes.CANCELED){ + this.status.code = statusCodes.QUEUED; + + console.log("Requested to restart " + this.name); + // TODO + + cb(null); + }else{ + cb(new Error("Task cannot be restarted")); + } } getInfo(){ @@ -27,7 +64,8 @@ module.exports = class Task{ name: this.name, dateCreated: this.dateCreated, status: this.status, - options: this.options + options: this.options, + imagesCount: this.images.length } } }; \ No newline at end of file diff --git a/libs/TaskManager.js b/libs/TaskManager.js index 42c76e7..80ed15d 100644 --- a/libs/TaskManager.js +++ b/libs/TaskManager.js @@ -12,7 +12,33 @@ module.exports = class TaskManager{ this.tasks[task.uuid] = task; } - find(uuid){ - return this.tasks[uuid]; + cancel(uuid, cb){ + let task; + if (task = this.find(uuid, cb)){ + task.cancel(cb); + } + } + + remove(uuid, cb){ + this.cancel(uuid, err => { + if (!err){ + delete(this.tasks[uuid]); + // TODO: other cleanup + cb(null); + }else cb(err); + }); + } + + restart(uuid, cb){ + let task; + if (task = this.find(uuid, cb)){ + task.restart(cb); + } + } + + find(uuid, errCb){ + let task = this.tasks[uuid]; + if (!task && errCb) errCb(new Error(`${uuid} not found`)); + return task; } }; \ No newline at end of file diff --git a/public/index.html b/public/index.html index dd2b889..a14c11f 100644 --- a/public/index.html +++ b/public/index.html @@ -59,15 +59,21 @@

No running tasks.

-

Retrieving ...

+

Retrieving ...

Name:
+
Images:
Status:
- + +
- + +
@@ -77,7 +83,7 @@
diff --git a/public/js/main.js b/public/js/main.js index 605d975..92c1003 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -28,71 +28,141 @@ $(function(){ this.uuid = uuid; this.loading = ko.observable(true); this.info = ko.observable({}); + + var statusCodes = { + 10: { + descr: "Queued", + icon: "glyphicon-hourglass" + }, + 20: { + descr: "Running", + icon: "glyphicon-refresh spinning" + }, + 30: { + descr: "Failed", + icon: "glyphicon-remove-circle" + }, + 40: { + descr: "Completed", + icon: "glyphicon-ok-circle" + }, + 50: { + descr: "Canceled", + icon: "glyphicon-ban-circle" + } + }; + this.statusDescr = ko.pureComputed(function(){ if (this.info().status && this.info().status.code){ - switch(this.info().status.code){ - case 10: return "Queued"; - case 20: return "Running"; - case 30: return "Failed"; - case 40: return "Completed"; - default: return "Unknown (Status Code: " + this.info().status.code + ")"; - } + if(statusCodes[this.info().status.code]){ + return statusCodes[this.info().status.code].descr; + }else return "Unknown (Status Code: " + this.info().status.code + ")"; }else return "-"; }, this); + this.icon = ko.pureComputed(function(){ + if (this.info().status && this.info().status.code){ + if(statusCodes[this.info().status.code]){ + console.log(statusCodes[this.info().status.code].icon); + return statusCodes[this.info().status.code].icon; + }else return "glyphicon-question-sign"; + }else return ""; + }, this); + this.canceled = ko.pureComputed(function(){ + return this.info().status && this.info().status.code === 50; + }, this); + this.refreshInfo(); + } + Task.prototype.refreshInfo = function(){ var self = this; - var url = "/taskInfo/" + uuid; + var url = "/taskInfo/" + this.uuid; $.get(url) .done(self.info) .fail(function(){ self.info({error: url + " is unreachable."}); }) .always(function(){ self.loading(false); }); - } - Task.prototype.remove = function() { - taskList.remove(this); }; + Task.prototype.remove = function() { + var self = this; + var url = "/removeTask"; + + $.post(url, { + uuid: this.uuid + }) + .done(function(json){ + if (json.success || self.info().error){ + taskList.remove(self); + }else{ + self.info({error: json.error}); + } + }) + .fail(function(){ + self.info({error: url + " is unreachable."}); + }); + }; + + function genApiCall(url){ + return function(){ + var self = this; + + $.post(url, { + uuid: this.uuid + }) + .done(function(json){ + if (json.success){ + self.refreshInfo(); + }else{ + self.info({error: json.error}); + } + }) + .fail(function(){ + self.info({error: url + " is unreachable."}); + }); + } + }; + Task.prototype.cancel = genApiCall("/cancelTask"); + Task.prototype.restart = genApiCall("/restartTask"); var taskList = new TaskList(); ko.applyBindings(taskList); - // Handle uploads $("#images").fileinput({ - uploadUrl: '/newTask', - showPreview: false, + uploadUrl: '/newTask', + showPreview: false, allowedFileExtensions: ['jpg', 'jpeg'], elErrorContainer: '#errorBlock', showUpload: false, uploadAsync: false, uploadExtraData: function(){ - return { - name: $("#taskName").val() - }; + return { + name: $("#taskName").val() + }; } }); $("#btnUpload").click(function(){ - var btnUploadLabel = $("#btnUpload").val(); - $("#btnUpload").attr('disabled', true) - .val("Uploading..."); + $("#btnUpload").attr('disabled', true) + .val("Uploading..."); - $("#images") - .fileinput('upload') - .on('filebatchuploadsuccess', function(e, params){ - $("#images").fileinput('reset'); + // Start upload + $("#images").fileinput('upload'); + }); - // TODO: this is called multiple times - // consider switching file upload plugin. - if (params.response.success && params.response.uuid){ - taskList.addNew(new Task(params.response.uuid)); - } - }) - .on('filebatchuploadcomplete', function(){ - $("#btnUpload").removeAttr('disabled') - .val(btnUploadLabel); - }) - .on('filebatchuploaderror', function(e, data, msg){ - }); - }); + var btnUploadLabel = $("#btnUpload").val(); + $("#images") + .on('filebatchuploadsuccess', function(e, params){ + $("#images").fileinput('reset'); + + if (params.response.success && params.response.uuid){ + taskList.addNew(new Task(params.response.uuid)); + } + }) + .on('filebatchuploadcomplete', function(){ + $("#btnUpload").removeAttr('disabled') + .val(btnUploadLabel); + }) + .on('filebatchuploaderror', function(e, data, msg){ + }); });