QWiki/QCore.js

280 lines
8.9 KiB
JavaScript

/*********************************
* QCore *
*********************************/
var http = require('http');
var fs = require('fs');
var m_path = require('path');
var m_url = require('url');
var formidable = require('formidable');
var QCore = function() {
var acts = {};
this.acts = acts;
self = this;
this.mimetypes = {};
this.index = null;
this.index_parts = [];
this.last_part = { start: 0, end: 0 };
this.etags = {}; // etags cache, obj: uri { etag: '...', timestamp: }
this.res_cache = {}; // resource cache ('/view.png' => '03xc345');
this.rules = {};
// TODO: make file calls explicitly refer to this.cwd
this.cwd = process.cwd();
this.http = http.createServer(function(req, res) {
// parse the URL into proper segments and attach these to the request
var purl = m_url.parse(req.url, true);
req.hash = purl.hash; req.pathname = purl.pathname;
req.path = purl.path; req.query = purl.query;
// set up req.area from pathname (pathname - act)
var last = req.pathname.lastIndexOf("/");
var act = req.pathname.substr(last+1);
if (acts[act]) {
req.area = unescape(req.pathname.substr(0, last)); // area is pathname without the act
} else {
act = "";
req.area = unescape(req.pathname);
}
req.act = act; // attach act to req
req.fields = {}; // fields from POST
req.files = {}; // files from POST
if (req.method == 'POST') { // POST handler
var form = new formidable.IncomingForm();
form.multiples = true;
form.uploadDir = self.cwd+'/tmp';
form.parse(req, function(err, fields, files) {
// TODO: handle err
req.fields = fields;
req.files = files;
for (var i = 0, keys = Object.keys(acts[act]); i < keys.length; i++) {
acts[act][i](req, res);
}
});
return;
} else { // GET and otherwise
// if QRULE query is passed, we attempt to read only the requested rule
if ('QRULE' in req.query) {
if (typeof self.rules[act] !== 'undefined' && self.rules[act][req.query['QRULE']] !== 'undefined') {
self.rules[act][req.query['QRULE']](req, res, null, function() { res.end()});
}
} else {
for (var i = 0, keys = Object.keys(acts[act]); i < keys.length; i++) {
acts[act][i](req, res);
}
}
}
});
this.act = function(act, callback) {
if (typeof acts[act] === 'undefined') {
acts[act] = [];
}
acts[act].push(callback);
};
this.rule = function(zone, replace, callback) {
if (typeof this.rules[zone] === 'undefined') {
this.rules[zone] = {};
}
this.rules[zone][replace] = callback;
};
var fs_timeout; // timer to bypass multiple fs.watch triggering due to unstable API
this.loadIndex = function(event, filename) {
if (fs_timeout) return;
fs_timeout = setTimeout(function() { fs_timeout = null; }, 100);
var reg = /@@[a-z]+@@/ig;
fs.readFile('index.html', function(err, data) {
if (err) throw err;
self.index = data;
var search;
self.index_parts = [];
while ((search = reg.exec(data)) !== null) {
self.index_parts.push({ replace: search[0], start: search.index, end: reg.lastIndex });
}
});
};
// TODO: loadActs should allow for dynamic updates by using fs.watch and clearing the require cache
this.loadActs = function(cb) {
var self = this;
fs.readdir('acts', function(err, files) {
if (err) throw err;
for (var i = 0; i < files.length; i++) {
var ext = self.getExt(files[i]);
if (ext == 'js') {
console.log('Loading act: ' + files[i]);
require(self.cwd+'/acts/'+files[i])(self);
}
}
(typeof cb !== 'undefined' ? cb() : '');
});
};
fs.watch('index.html', this.loadIndex);
};
QCore.prototype.defaults = {};
QCore.prototype.listen = function(port, cb) {
this.loadActs();
this.loadIndex();
if (typeof cb !== 'undefined') cb();
try {
this.http.listen(port);
} catch(err) {
console.log('uhoh, could not listen on port '+port);
console.log(err);
}
};
QCore.prototype.setDefault = function(key, value) {
this.defaults[key] = value;
};
QCore.prototype.getDefault = function(key) {
if (key in this.defaults) {
return this.defaults[key];
}
return '';
};
QCore.prototype.addMIMEtype = function(ext, mimetype) {
if (typeof this.mimetypes[ext] === 'undefined') this.mimetypes[ext] = [];
this.mimetypes[ext].push(mimetype);
};
QCore.prototype.getMIMEtype = function(ext) {
if (this.mimetypes[ext]) return this.mimetypes[ext][0];
return '';
};
QCore.prototype.parsePage = function(zone, fallback_zone, req, res) {
var qparse = new QCore.prototype.QParse();
qparse.indices = this.index_parts.slice(); // clone it TODO: just make an indices (number-only) array
var context = this;
var cb = function(qparse, callback) {
res.write(context.index.slice(qparse.offset, qparse.current.start));
if (typeof context.rules[zone] === 'undefined') { zone = fallback_zone; }
if (typeof context.rules[zone] === 'undefined') { zone = ''; }
if (typeof context.rules[zone] !== 'undefined') {
if (typeof context.rules[zone][qparse.current.replace] !== 'undefined') {
context.rules[zone][qparse.current.replace](req, res, qparse, callback);
} else {
// fallback action
if (typeof context.rules[fallback_zone][qparse.current.replace] !== 'undefined') {
context.rules[fallback_zone][qparse.current.replace](req, res, qparse, callback);
// default action
} else if (typeof context.rules[''][qparse.current.replace] !== 'undefined') {
context.rules[''][qparse.current.replace](req, res, qparse, callback);
} else {
callback();
}
}
}
};
var series = function(qparse) {
if (qparse.current) {
cb(qparse, function(next) {
if (qparse.next) {
res.write(context.index.slice(qparse.current.end, qparse.next.start));
qparse.offset = qparse.next.end;
qparse.current = qparse.indices.shift();
qparse.next = qparse.indices[0];
series(qparse);
} else {
res.write(context.index.slice(qparse.current.end));
qparse.current = qparse.indices.shift();
qparse.next = qparse.indices[0];
series(qparse);
}
});
} else {
res.end();
}
};
qparse.current = qparse.indices.shift();
qparse.next = qparse.indices[0];
series(qparse);
};
QCore.prototype.readFile = function(stream, path, cb) {
fs.exists(path, function(exists) {
if (exists) {
var rs = fs.createReadStream(path);
rs.on('data', function(chunk) {
stream.write(chunk);
});
rs.on('end', function() {
cb('EOF');
});
rs.on('error', function(err) {
cb('ERR', err);
});
} else {
cb('FNF'); // file not found
}
});
};
QCore.prototype.readFiles = function(dir, callback) {
fs.readdir(dir, function(err, files) {
files.forEach(function(file) {
var stat_file = dir+'/'+file
fs.stat(stat_file, function(err, stats) {
if (stats.isDirectory()) {
QCore.prototype.readFiles(stat_file, callback);
callback(err, stat_file, true);
} else if (stats.isFile()) {
callback(err, stat_file, false);
}
});
});
});
};
QCore.prototype.readDirectory = function(dir, callback) {
var all_files = [];
fs.readdir(dir, function(err, files) {
if (err) {
callback(err, null);
return;
}
var len = files.length;
var total = 0;
if (len == 0) callback(err, all_files);
files.forEach(function(file, index) {
var stat_file = dir + '/' + file;
fs.stat(stat_file, function(err, stats) {
if (stats.isDirectory()) {
all_files.push({'is_dir': true, 'name': file});
} else if (stats.isFile()) {
all_files.push({'is_dir': false, 'name': file});
}
if (++total == len) {
callback(err, all_files);
}
});
});
});
};
QCore.prototype.getExt = function(file) {
if (typeof file === 'undefined') return '';
var last = file.lastIndexOf(".");
if (last == -1) return '';
var ext = file.substr(last+1);
return ext;
};
QCore.prototype.r_mkdir = function(dir, mode, cb) {
console.log('creating '+dir);
fs.mkdir(dir, mode, function(err) {
if (err && err.code == 'ENOENT') {
QCore.r_mkdir(m_path.dirname(dir), mode, QCore.r_mkdir.bind(this, dir, mode, cb));
} else if (typeof cb !== 'undefined') {
cb(err);
}
});
};
/*********************************
* QParse *
*********************************/
QCore.prototype.QParse = function() {};
QCore.prototype.QParse.prototype.indices = {};
QCore.prototype.QParse.prototype.offset = 0;
QCore.prototype.QParse.prototype.current = null;
QCore.prototype.QParse.prototype.next = null;
module.exports = QCore;