Cancel task, restart task, minor refactoring

pull/1/head
Piero Toffanin 2016-07-07 17:07:17 -05:00
rodzic 514bc654c0
commit c0e239d709
5 zmienionych plików z 224 dodań i 52 usunięć

Wyświetl plik

@ -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!');
});

Wyświetl plik

@ -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
}
}
};

Wyświetl plik

@ -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;
}
};

Wyświetl plik

@ -59,15 +59,21 @@
<p data-bind="visible: tasks().length === 0">No running tasks.</p>
<div data-bind="foreach: tasks">
<div class="task">
<p data-bind="visible: loading()">Retrieving <span data-bind="text: uuid"></span> ... <span class="spinning glyphicon glyphicon glyphicon glyphicon-refresh"></span></p>
<p data-bind="visible: loading()">Retrieving <span data-bind="text: uuid"></span> ... <span class="glyphicon glyphicon-refresh spinning"></span></p>
<div data-bind="visible: !loading() && !info().error">
<div class="taskItem"><strong>Name:</strong> <span data-bind="text: info().name"></span></div>
<div class="taskItem"><strong>Images:</strong> <span data-bind="text: info().imagesCount"></span></div>
<div class="taskItem"><strong>Status:</strong> <span data-bind="text: statusDescr()"></span></div>
<span class="statusIcon spinning glyphicon glyphicon glyphicon-cog"></span>
<span data-bind="css: 'statusIcon glyphicon ' + icon()"></span>
<div class="actionButtons">
<button type="button" class="btn btn-primary btn-sm">
<span class="glyphicon glyphicon glyphicon-remove-circle"></span> Cancel
<button data-bind="click: cancel, visible: !canceled()" type="button" class="btn btn-primary btn-sm" >
<span class="glyphicon glyphicon-remove-circle"></span> Cancel
</button>
<button data-bind="click: restart, visible: canceled()" type="button" class="btn btn-primary btn-sm" >
<span class="glyphicon glyphicon-play"></span> Restart
</button>
</div>
</div>
@ -77,7 +83,7 @@
<div class="alert alert-warning" role="alert" data-bind="text: info().error"></div>
<div class="actionButtons">
<button data-bind="click: remove" type="button" class="btn btn-primary btn-sm">
<span class="glyphicon glyphicon glyphicon-remove-circle"></span> Remove Task
<span class="glyphicon glyphicon-remove-circle"></span> Remove
</button>
</div>
</div>

Wyświetl plik

@ -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){
});
});