diff --git a/README.md b/README.md index 3ecdd66..d1251ab 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,59 @@ 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). +Widgets +======= + +All widgets should export an object with a `create(widgetConf, desiState)` function that returns a promise. + +```yaml +widgets: + foogizmo: + # only stuff that is intensely specific to foogizmo goes here + # stuff like google ad and disqus ids should go in config.yml or data.yml + config: + foobeep: boop + + handle: + - html + - markdown + handlers: + post: fooposter + page: foopager +``` + +```javascript +'use strict'; + +module.exports.Foogizmo.create = function (foogizmoConf, desiState) { + return new Promise(function (resolve) { + + function pager(desiPageState) { + // Do processing + + return Promise.resolve(); + } + + function poster(desiPostState) { + // Do processing + + desiPostState.fooembedinator = function (fooval) { + // figure out what type of link fooval is and return iframe html + return '' + } + } + + resolve({ foopager: pager, fooposter: poster }); + }); +} +``` + +Overlays +-------- + +For any config a widget uses, it should also check on post.fooconfig and theme.fooconfig to make sure that they don't override the foogizmo.config.fooconfig + + Server ====== @@ -25,7 +78,7 @@ Obviously there has to be a server with some sort of storage and retrieval mecha I've implemented a very simple node server using the filesystem. -/api/fs/walk +GET /api/fs/walk ------------ `GET http://local.dear.desi:8080/api/fs/walk?dir=posts&dotfiles=true&extensions=md,markdown,jade,htm,html` @@ -76,7 +129,7 @@ POST http://local.dear.desi:8080/api/fs/walk?dotfiles=true&extensions=md,markdow } ``` -/api/fs/files +GET /api/fs/files ------------- `GET http://local.dear.desi:8080/api/fs/files?path=posts/happy-new-year.md` @@ -113,3 +166,56 @@ POST http://local.dear.desi:8080/api/fs/files?dotfiles=true&extensions=md,markdo , ... ] ``` + +POST /api/fs/files +------------------ + +By default this should assume that you intended to write to the compiled directory +and return an error if you try to write to any other directory, unless `compiled=false` (not yet implemented). + +`_method=PUT` is just for funzies. + +Including `sha1` is optional, but recommended. + +`lastModifiedDate` is optional and may or may not make any difference. + +`strict` (not yet implemented) fail immediately and completely on any error + +```json +POST http://local.dear.desi:8080/api/fs/files?compiled=true&_method=PUT + +{ + "files": [ + { "path": "posts/foo.md" + , "name": "foo.md" + , "relativePath": "posts" + , "lastModifiedDate": "2013-08-01T22:47:37.000Z" + , "contents": "..." + , "sha1": "6eae3a5b062c6d0d79f070c26e6d62486b40cb46" + , "delete": false + } + , ... + ] +} +``` + +The response may include errors of all shapes and sizes. + +```json +{ "error": { message: "any top-level error", ... } +, "errors": [ + { "type": "file|directory" + , "message": "maybe couldn't create the directory, but maybe still wrote the file. Maybe not" + } + , ... + ] +} +``` + +**TODO** Allow rename and delete? + +TODO +---- + +option for client write to a hidden `.desi-revisions` (as well as indexeddb) +to safeguard against accidental blow-ups for people who aren't using git. diff --git a/lib/fsapi.js b/lib/fsapi.js index 3bece72..740a01e 100644 --- a/lib/fsapi.js +++ b/lib/fsapi.js @@ -8,6 +8,7 @@ var PromiseA = require('bluebird').Promise , escapeRegExp = require('./deardesi-utils').escapeRegExp , safeResolve = require('./deardesi-utils').safeResolve , sha1sum = function (str) { return require('secret-utils').hashsum('sha1', str); } + , mkdirp = PromiseA.promisify(require('mkdirp')) ; function strip(prefix, pathname) { @@ -155,6 +156,89 @@ function getfs(blogdir, filepaths) { return files; }); } + +function putfs(blogdir, files) { + var results = { errors: [] } + , dirpaths = {} + ; + + return forEachAsync(files, function (file) { + var filepath = safeResolve(blogdir, file.path || path.join(file.relativePath, file.name)) + , pathname = path.dirname(filepath) + , filename = file.name || path.basename(filepath) + ; + + file.realPath = filepath; + file.name = filename; + + dirpaths[pathname] = true; + + return Promise.resolve(); + }).then(function () { + // TODO is it better to do this lazy-like or as a batch? + // I figure as batch when there may be hundreds of files, + // likely within 2 or 3 directories + return forEachAsync(Object.keys(dirpaths), function (pathname) { + return mkdirp(pathname).catch(function (e) { + // TODO exclude attempting to write files to this dir? + results.errors.push({ + type: 'directory' + + , directory: pathname + + , message: e.message + , code: e.code + , errno: e.errno + , status: e.status + , syscall: e.syscall + }); + + }); + }); + }).then(function () { + // TODO sort deletes last + return forEachAsync(files, function (file) { + var p + ; + + // TODO use lastModifiedDate as per client request? + // TODO compare sha1 sums for integrity + if (file.delete || !file.contents) { + p = fs.unlinkAsync(file.realPath); + } else { + p = fs.writeFileAsync(file.realPath, file.contents, 'utf8'); + } + + return p.catch(function (e) { + results.errors.push({ + type: 'file' + + , file: file.realPath + , delete: !file.contents + , path: file.path + , relativePath: file.relativePath + , name: file.name + + , message: e.message + , code: e.code + , errno: e.errno + , status: e.status + , syscall: e.syscall + }); + }); + }); + }).catch(function (e) { + results.error = { + message: e.message + , code: e.code + , errno: e.errno + , status: e.status + , syscall: e.syscall + }; + }).then(function () { + return results; + }); +} /* walkDirs('blog', ['posts'], { contents: false }).then(function (stats) { console.log(JSON.stringify(stats, null, ' ')); @@ -163,5 +247,6 @@ walkDirs('blog', ['posts'], { contents: false }).then(function (stats) { module.exports.walk = { walkDirs: walkDirs, walkDir: walkDir }; module.exports.getfs = getfs; +module.exports.putfs = putfs; module.exports.walkDir = walkDir; module.exports.walkDirs = walkDirs; diff --git a/package.json b/package.json index 61e97e7..6fab3a5 100644 --- a/package.json +++ b/package.json @@ -36,10 +36,11 @@ "connect-query": "^0.2.0", "connect-send-json": "^1.0.0", "escape-string-regexp": "^1.0.2", - "foreachasync": "^5.0.2", + "foreachasync": "^5.0.5", "json2yaml": "^1.0.3", "markdown": "^0.5.0", "marked": "^0.3.2", + "mkdirp": "^0.5.0", "mustache": "^1.0.0", "require-yaml": "0.0.1", "require-yamljs": "^1.0.1", diff --git a/server.js b/server.js index bfdb4a8..4a7dcb2 100644 --- a/server.js +++ b/server.js @@ -12,6 +12,7 @@ var connect = require('connect') , app = connect() , walk = require('./lib/fsapi').walk , getfs = require('./lib/fsapi').getfs + , putfs = require('./lib/fsapi').putfs , config = require('./config.yml') , path = require('path') @@ -23,40 +24,14 @@ app .use(send.json()) .use(query()) .use(bodyParser.json()) - - .use('/api/fs/files', function (req, res, next) { - if (!(/^GET$/i.test(req.method) || /^GET$/i.test(req.query._method))) { - next(); - return; - } - - 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 GET w/ req.query.path or POST _method=GET&paths=path/to/thing,..." }); - return; - } - - 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.query.dirs && req.query.dirs.split(/,/g)) || req.body.dirs + var opts = {} + , dirnames = req.query.dir && [req.query.dir] || (req.query.dirs && req.query.dirs.split(/,/g)) || req.body.dirs ; if (!dirnames || !dirnames.length) { @@ -87,6 +62,47 @@ app } }); }) + .use('/api/fs/files', function (req, res, next) { + if (!(/^GET$/i.test(req.method) || /^GET$/i.test(req.query._method))) { + next(); + return; + } + + 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 GET w/ req.query.path or POST _method=GET&paths=path/to/thing,..." }); + return; + } + + 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/files', function (req, res, next) { + if (!(/^POST|PUT$/i.test(req.method) || /^POST|PUT$/i.test(req.query._method))) { + next(); + return; + } + + var opts = {} + , files = req.body.files + ; + + if (!files || !files.length) { + res.json({ error: "please specify POST w/ req.body.files" }); + return; + } + + return putfs(blogdir, files, opts).then(function (results) { + res.json(results); + }); + }) .use('/api/fs', function (req, res, next) { next(); @@ -94,6 +110,7 @@ app }) .use('/api/fs/static', serveStatic('.')) + .use(serveStatic(blogdir)) .use(serveStatic('.')) ;