add some README, fsapi works!
This commit is contained in:
parent
3b19c4e877
commit
a44478de87
115
README.md
115
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"
|
||||
}
|
||||
, ...
|
||||
]
|
||||
```
|
||||
|
|
|
@ -2,15 +2,20 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Dear Desi</title>
|
||||
<!-- Deps -->
|
||||
<script src="./bower_components/bluebird/js/browser/bluebird.js"></script>
|
||||
<script src="./bower_components/mustache/mustache.js"></script>
|
||||
<script src="./bower_components/marked/lib/marked.js"></script>
|
||||
<script src="./bower_components/js-yaml/dist/js-yaml.js"></script>
|
||||
<script src="./bower_components/path/path.js"></script>
|
||||
|
||||
<!-- Libs -->
|
||||
<script src="./lib/deardesi-utils.js"></script>
|
||||
<script src="./lib/verify-config.js"></script>
|
||||
<script src="./lib/deardesi-browser.js"></script>
|
||||
<script src="./lib/frontmatter.js"></script>
|
||||
|
||||
<!-- Desi -->
|
||||
<script src="./deardesi.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -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;
|
173
lib/walk.js
173
lib/walk.js
|
@ -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;
|
|
@ -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"
|
||||
}
|
||||
|
|
88
server.js
88
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');
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue