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