diff --git a/README.md b/README.md index 1052da3..3ecdd66 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,115 @@ -nuhoh +Desirae ===== -Blog Platform. A Ruhoh knock-off written in JavaScript +In development. + +Blog Platform. A Ruhoh knock-off written in JavaScript for the Browser + +Key Features +------------ + +Really good use of `try { ... } catch(e) ...` - it won't all blow up at the slightest parse error (*cough* ruhoh *cough* jekyll)... bless me. + +JavaScript - it's so stable it takes 10 years to get new any features. Won't break every time you upgrade OS X and reinstall brew (*cough* ruby). + +Browser (optional) - using your front-end templates to build in your front-end? Imagine that! + +Node (optional) - if you'd prefer to go headless, you can. + +The server is *very* minimal and could easily be implemented in any language (such as ruby or python). + +Server +====== + +Obviously there has to be a server with some sort of storage and retrieval mechanism. + +I've implemented a very simple node server using the filesystem. + +/api/fs/walk +------------ + +`GET http://local.dear.desi:8080/api/fs/walk?dir=posts&dotfiles=true&extensions=md,markdown,jade,htm,html` + +* `dir` **must** be supplied. returns a flat list of all files, recursively +* `dotfiles` default to `false`. includes dotfiles when `true`. +* `extensions` defaults to `null`. inclode **only** the supplied extensions when `true`. + +```json +[ + { "name": "happy-new-year.md" + , "lastModifiedDate": "2015-01-05T18:19:30.000Z" + , "size": 2121 + , "relativePath": "posts/2015" + } + +, { "name": "tips-for-the-ages.jade" + , "lastModifiedDate": "2014-06-16T18:19:30.000Z" + , "size": 389 + , "relativePath": "posts" + } +, { "name": "my-first-post.html" + , "lastModifiedDate": "2013-08-01T22:47:37.000Z" + , "size": 4118 + , "relativePath": "posts/2013" + } +] +``` + +To retrieve multiple dir listings at once: + +* for a few simple dirs without special chars just change `dir` to `dirs` and separate with commas + +`GET http://local.dear.desi:8080/api/fs/walk?dirs=posts/2015,posts/2013&dotfiles=true&extensions=md,markdown,jade,htm,html` + +* for many dirs, or dirs with special chars, `POST` an object containing an array of `dirs` with `&_method=GET` appended to the url. + +``` +POST http://local.dear.desi:8080/api/fs/walk?dotfiles=true&extensions=md,markdown,jade,htm,html&_method=GET + +{ "dirs": [ "old", "2013,12", "2013,11" ] } +``` + +```javascript +{ + "posts/2015": [ { "name": ... }, { ... } ] +, "posts/2013": [ ... ] +} +``` + +/api/fs/files +------------- + +`GET http://local.dear.desi:8080/api/fs/files?path=posts/happy-new-year.md` + +```json +{ "path": "posts/intro-to-http-with-netcat-node-connect.md" +, "lastModifiedDate": "2013-08-01T22:47:37.000Z" +, "contents": "..." +, "sha1": "6eae3a5b062c6d0d79f070c26e6d62486b40cb46" +} +``` + +To retrieve multiple files at once: + +* for a few simple files without special chars just change `path` to `paths` and separate with commas + +`GET http://local.dear.desi:8080/api/fs/files?paths=posts/foo.md,posts/bar.md` + +* for many files, or files with special chars, `POST` an object containing an array of `pathss` with `&_method=GET` appended to the url. + +``` +POST http://local.dear.desi:8080/api/fs/files?dotfiles=true&extensions=md,markdown,jade,htm,html&_method=GET + +{ "paths": [ "posts/foo.md", "posts/2013,11,30.md" ] } +``` + +```json +[ + { "path": "posts/foo.md" + , "lastModifiedDate": "2013-08-01T22:47:37.000Z" + , "contents": "..." + , "sha1": "6eae3a5b062c6d0d79f070c26e6d62486b40cb46" + } +, ... +] +``` diff --git a/index.html b/index.html index 245654c..6878139 100644 --- a/index.html +++ b/index.html @@ -2,15 +2,20 @@ Dear Desi + + + + + diff --git a/lib/fsapi.js b/lib/fsapi.js new file mode 100644 index 0000000..2643809 --- /dev/null +++ b/lib/fsapi.js @@ -0,0 +1,162 @@ +'use strict'; + +var PromiseA = require('bluebird').Promise + , fs = PromiseA.promisifyAll(require('fs')) + , forEachAsync = require('foreachasync').forEachAsync + , path = require('path') + , walk = require('walk') + , escapeRegExp = require('./deardesi-utils').escapeRegExp + , safeResolve = require('./deardesi-utils').safeResolve + , sha1sum = function (str) { return require('secret-utils').hashsum('sha1', str); } + ; + +function strip(prefix, pathname) { + return pathname.substr(prefix.length + 1); +} + +function walkDir(parent, sub, opts) { + opts = opts || {}; + + var prefix = path.resolve(parent) + , trueRoot = path.resolve(prefix, sub) + , files = [] + ; + + function filter(name) { + if (!name) { + return false; + } + + if ('.' === name[0] && !opts.dotfiles) { + return false; + } + + if (opts.extensions && opts.extensions.length) { + if (!opts.extensions.some(function (ext) { + return new RegExp('\\.' + escapeRegExp(ext) + '$').test(name); + })) { + return false; + } + } + + return true; + } + + return new PromiseA(function (resolve) { + var walker = walk.walk(trueRoot) + ; + + walker.on('nodeError', function (filepath, stat, next) { + //stats.forEach(function (stat) { + if (!filter(stat.name)) { + return; + } + + stat.error.path = path.join(strip(prefix, filepath), stat.name); + files.push({ + name: stat.name + , relativePath: strip(prefix, filepath) + , type: undefined + , error: stat.error + }); + //}); + + next(); + }); + + walker.on('files', function (root, stats, next) { + var dirname = strip(prefix, root) + ; + + function eachFile(stat) { + var file + ; + + + if (!filter(stat.name)) { + return; + } + + file = { + name: stat.name + , lastModifiedDate: stat.mtime.toISOString() + , size: stat.size + , relativePath: dirname + , type: undefined // TODO include mimetype + }; + files.push(file); + + if (opts.contents) { + return fs.readFileAsync(path.join(root, stat.name), 'utf8').then(function (contents) { + file.contents = contents; + file.sha1 = sha1sum(contents); + }); + } + } + + if (!opts.contents) { + stats.forEach(eachFile); + next(); + } else { + forEachAsync(stats, eachFile).then(next); + } + }); + + walker.on('end', function () { + resolve(files); + }); + }); +} + +function walkDirs(parent, subs, opts) { + opts = opts || {}; + + var collections = {} + ; + + return forEachAsync(subs, function (sub) { + return walkDir(parent, sub, opts).then(function (results) { + collections[sub] = results; + }); + }).then(function () { + return collections; + }); +} + + +function getfs(blogdir, filepaths) { + var files = [] + ; + + return forEachAsync(filepaths, function (filepath) { + var pathname = safeResolve(blogdir, filepath) + ; + + return fs.lstatAsync(pathname).then(function (stat) { + return fs.readFileAsync(pathname, null).then(function (buffer) { + files.push({ + path: filepath + , size: buffer.byteLength + , lastModifiedDate: stat.mtime.toISOString() + , contents: buffer.toString('utf8') + , sha1: sha1sum(buffer) + , type: undefined + }); + }); + }).catch(function (e) { + files.push({ path: filepath, error: e.message }); + }); + }).then(function () { + return files; + }); +} +/* +walkDirs('blog', ['posts'], { contents: false }).then(function (stats) { + console.log(JSON.stringify(stats, null, ' ')); +}); +*/ + +module.exports.walk = { walkDirs: walkDirs, walkDir: walkDir }; +module.exports.getfs = getfs; +module.exports.walkDir = walkDir; +module.exports.walkDirs = walkDirs; diff --git a/lib/walk.js b/lib/walk.js deleted file mode 100644 index f13800f..0000000 --- a/lib/walk.js +++ /dev/null @@ -1,173 +0,0 @@ -'use strict'; - -var PromiseA = require('bluebird').Promise - , fs = PromiseA.promisifyAll(require('fs')) - , forEachAsync = require('foreachasync').forEachAsync - , path = require('path') - , walk = require('walk') - , walker - ; - -function strip(prefix, pathname) { - return pathname.substr(prefix.length + 1); -} - -/* -function walkDir(parent, sub, opts) { - opts = opts || {}; - - var prefix = path.resolve(parent) - , trueRoot = path.resolve(prefix, sub) - , things = {} - ; - - return fs.lstatAsync(trueRoot).then(function (stat) { - var name = strip(prefix, trueRoot) - ; - - things[name] = things[name] || {}; - things[name].name = stat.name; - things[name].lastModifiedDate = stat.mtime.toISOString(); - things[name].contents = []; - - return new PromiseA(function (resolve) { - walker = walk.walk(trueRoot); - - walker.on('directories', function (root, stats, next) { - var dirname = strip(prefix, root) - ; - - stats.forEach(function (stat) { - var cdirname = path.join(dirname, stat.name) - ; - - things[cdirname] = things[cdirname] || {}; - things[cdirname].name = stat.name; - things[cdirname].lastModifiedDate = stat.mtime.toISOString(); - things[cdirname].contents = things[cdirname].contents || []; - }); - - next(); - }); - walker.on('directory', function (root, stat, next) { - var dirname = strip(prefix, path.join(root, stat.name)) - ; - - things[dirname] = things[dirname] || {}; - things[dirname].name = stat.name; - things[dirname].lastModifiedDate = stat.mtime.toISOString(); - things[dirname].contents = things[dirname].contents || []; - - next(); - }); - - walker.on('files', function (root, stats, next) { - var dirname = strip(prefix, root) - ; - - function eachFile(stat) { - var file - ; - - file = { - name: stat.name - , lastModifiedDate: stat.mtime.toISOString() - }; - - things[dirname].contents.push(file); - - if (opts.contents) { - return fs.readFileAsync(path.join(root, stat.name), 'utf8').then(function (contents) { - file.contents = contents; - }); - } - } - - if (!opts.contents) { - stats.forEach(eachFile); - next(); - } else { - forEachAsync(stats, eachFile).then(next); - } - }); - - walker.on('end', function () { - resolve(things); - }); - }); - }); -} -*/ - -function walkDir(parent, sub, opts) { - opts = opts || {}; - - var prefix = path.resolve(parent) - , trueRoot = path.resolve(prefix, sub) - , files = [] - ; - - return new PromiseA(function (resolve) { - walker = walk.walk(trueRoot); - - walker.on('files', function (root, stats, next) { - var dirname = strip(prefix, root) - ; - - function eachFile(stat) { - var file - ; - - file = { - name: stat.name - , lastModifiedDate: stat.mtime.toISOString() - , size: stat.size - , path: dirname - }; - files.push(file); - - if (opts.contents) { - return fs.readFileAsync(path.join(root, stat.name), 'utf8').then(function (contents) { - file.contents = contents; - }); - } - } - - if (!opts.contents) { - stats.forEach(eachFile); - next(); - } else { - forEachAsync(stats, eachFile).then(next); - } - }); - - walker.on('end', function () { - resolve(files); - }); - }); -} - -function walkDirs(parent, subs, opts) { - opts = opts || {}; - - var collections = {} - ; - - return forEachAsync(subs, function (sub) { - return walkDir(parent, sub, opts).then(function (results) { - collections[sub] = results; - }); - }).then(function () { - return collections; - }); -} - - -/* -walkDirs('blog', ['posts'], { contents: false }).then(function (stats) { - console.log(JSON.stringify(stats, null, ' ')); -}); -*/ - -module.exports.walkDir = walkDir; -module.exports.walkDirs = walkDirs; diff --git a/package.json b/package.json index d1d502f..61e97e7 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "require-yamljs": "^1.0.1", "secret-utils": "^1.0.2", "serve-static": "^1.7.2", - "walk": "^2.3.5", + "walk": "^2.3.9", "yaml": "^0.2.3", "yamljs": "^0.2.1" } diff --git a/server.js b/server.js index e8058b7..bfdb4a8 100644 --- a/server.js +++ b/server.js @@ -2,23 +2,20 @@ require('require-yaml'); -var PromiseA = require('bluebird').Promise - , connect = require('connect') - , query = require('connect-query') - , bodyParser = require('body-parser') +var connect = require('connect') + //, PromiseA = require('bluebird').Promise + , query = require('connect-query') + , bodyParser = require('body-parser') , serveStatic = require('serve-static') - , forEachAsync = require('foreachasync').forEachAsync - , send = require('connect-send-json') + , send = require('connect-send-json') - , app = connect() - , walk = require('./lib/walk') + , app = connect() + , walk = require('./lib/fsapi').walk + , getfs = require('./lib/fsapi').getfs - , config = require('./config.yml') - , safeResolve = require('./lib/deardesi-utils').safeResolve - , path = require('path') - , blogdir = path.resolve(config.blogdir || __dirname) - , sha1sum = function (str) { return require('secret-utils').hashsum('sha1', str); } - , fs = PromiseA.promisifyAll(require('fs')) + , config = require('./config.yml') + , path = require('path') + , blogdir = path.resolve(config.blogdir || __dirname) ; @@ -33,54 +30,57 @@ app return; } - var filepaths = req.query.path && [req.query.path] || req.body.paths - , files = [] + var filepaths = req.query.path && [req.query.path] || (req.query.paths && req.query.paths.split(/,/g)) || req.body.paths ; if (!filepaths || !filepaths.length) { - res.json({ error: "please specify req.query.path or req.body.paths" }); + res.json({ error: "please specify GET w/ req.query.path or POST _method=GET&paths=path/to/thing,..." }); return; } - return forEachAsync(filepaths, function (filepath) { - var pathname = safeResolve(blogdir, filepath) - ; - - return fs.lstatAsync(pathname).then(function (stat) { - return fs.readFileAsync(pathname, null).then(function (buffer) { - files.push({ - path: filepath - , size: buffer.byteLength - , lastModifiedDate: stat.mtime.toISOString() - , contents: buffer.toString('utf8') - , sha1: sha1sum(buffer) - , - }); - }); - }).catch(function (e) { - files.push({ path: filepath, error: e.message }); - }); - }).then(function () { - res.send(files); + return getfs(blogdir, filepaths).then(function (files) { + if (!req.body.paths && !req.query.paths) { + res.json(files[0]); + } else { + res.send(files); + } }); }) .use('/api/fs/walk', function (req, res, next) { + var opts = {} + ; + if (!(/^GET$/i.test(req.method) || /^GET$/i.test(req.query._method))) { next(); return; } - var dirnames = req.query.dir && [req.query.dir] || req.body.dirs + var dirnames = req.query.dir && [req.query.dir] || (req.query.dirs && req.query.dirs.split(/,/g)) || req.body.dirs ; if (!dirnames || !dirnames.length) { - res.json({ error: "please specify req.query.dir or req.body.dirs" }); + res.json({ error: "please specify GET w/ req.query.dir or POST w/ _method=GET&dirs=path/to/thing,..." }); return; } - walk.walkDirs(blogdir, dirnames, { contents: false }).then(function (stats) { - if (!req.body.dirs) { + /* + if (req.query.excludes) { + opts.excludes = req.query.excludes.split(','); + } + */ + + if (req.query.extensions) { + opts.extensions = req.query.extensions.split(/,/g); + } + + if ('true' === req.query.dotfiles) { + opts.dotfiles = true; + } + + // TODO opts.contents? + walk.walkDirs(blogdir, dirnames, opts).then(function (stats) { + if (!req.body.dirs && !req.query.dirs) { res.json(stats[dirnames[0]]); } else { res.json(stats); @@ -92,11 +92,13 @@ app next(); return; }) + .use('/api/fs/static', serveStatic('.')) + .use(serveStatic('.')) ; module.exports = app; -require('http').createServer(app).listen(8080, function () { - console.log('listening 8080'); +require('http').createServer().on('request', app).listen(80, function () { + console.log('listening 80'); });