├── .gitignore ├── .jshintrc ├── .travis.yml ├── README.md ├── app.js ├── bin └── www ├── config.json ├── lib ├── authorize.js ├── handleFiles.js ├── middleware │ └── .keep ├── multiParse.js ├── publishPost.js ├── repo.js ├── server.js └── stagePost.js ├── notes.md ├── package.json ├── prototypes ├── fmTest.js ├── repoSpec.js ├── repoTest.js └── testStart.js ├── public └── stylesheets │ └── style.css ├── routes ├── index.js ├── micropub.js └── post.js ├── settings.js ├── test ├── .keep └── server.js └── views ├── error.jade ├── index.jade ├── layout.jade ├── micropub.jade └── post.jade /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | 27 | repo/* 28 | *.sublime-project 29 | *.sublime-workspace 30 | .DS_Store -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "jquery": true, 4 | "strict": false, 5 | "globalstrict": false, 6 | "asi": true 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | 5 | script: 6 | - npm run cover 7 | 8 | after_script: 9 | - cat coverage/lcov.info | node_modules/.bin/codeclimate 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gitpub 2 | ====== 3 | 4 | [![release][release-image]][release-url] 5 | [![npm][npm-image]][npm-url] 6 | [![travis][travis-image]][travis-url] 7 | [![coverage][coverage-image]][coverage-url] 8 | [![david][david-image]][david-url] 9 | [![stability][stability-image]][stability-url] 10 | 11 | [release-image]: https://img.shields.io/github/release/bcomnes/gitpub.svg?style=flat-square 12 | [release-url]: https://github.com/bcomnes/gitpub/releases/latest 13 | [npm-image]: https://img.shields.io/npm/v/gitpub.svg?style=flat-square 14 | [npm-url]: https://www.npmjs.com/package/gitpub 15 | [travis-image]: https://img.shields.io/travis/bcomnes/gitpub.svg?style=flat-square 16 | [travis-url]: https://travis-ci.org/bcomnes/gitpub 17 | [coverage-image]: https://img.shields.io/codeclimate/coverage/github/bcomnes/gitpub.svg?style=flat-square 18 | [coverage-url]: https://codeclimate.com/github/bcomnes/gitpub 19 | [david-image]: https://img.shields.io/david/bcomnes/gitpub.svg?style=flat-square 20 | [david-url]: https://david-dm.org/bcomnes/gitpub 21 | [stability-image]: https://img.shields.io/badge/stability-1%20--%20experimental-orange.svg?style=flat-square 22 | [stability-url]: https://nodejs.org/api/documentation.html#documentation_stability_index 23 | 24 | An experimental publishing tool that takes incoming http requests, authorizes them and then turns them into static files inside a remote git repository for later consumption by a static site generator running in a [gh-pages](https://pages.github.com/) like environment or a dynamic web app that renders content from static files on disk. 25 | 26 | It currently works but generates an inflexible Jekyll post file in a specific location in a repository and is undocumented! 27 | 28 | ## Currently Working 29 | 30 | - [micropub](http://indiewebcamp.com/micropub) (posting) 31 | - Simple File handling for small files in git. 32 | - Support for a simple jekyll post schema. 33 | 34 | ## Active Development 35 | 36 | - Breaking down into small modules: 37 | - https://www.npmjs.com/package/quick-gits 38 | - https://www.npmjs.com/package/bepo 39 | 40 | ## On the table: 41 | 42 | - Documentation: Micropub is still a baby. 43 | - Work out all the kinks. 44 | - Testing 45 | - Example Jekyll Template. 46 | 47 | ## Future plans: 48 | 49 | - [micropub](http://indiewebcamp.com/micropub) (editing) 50 | - [webmention](http://indiewebcamp.com/micropub) 51 | - Robust file handling (S3, Dropbox, BitTorrent Sync, [Camlistore](https://camlistore.org/)) 52 | - Advanced syndication options ([POSSE](http://indiewebcamp.com/POSSE), [PESOS](http://indiewebcamp.com/PESOS)) 53 | - Support for arbitrary post templates and schema. 54 | - Flexible pathing. 55 | - Static Site Generator Agnostic 56 | - Site Provisioning 57 | - [One click deploy](https://blog.heroku.com/archives/2014/8/7/heroku-button) 58 | - Administration Panel 59 | - Automated Conflict Management and 3 way merges. 60 | - Get added to the [Fork n Go](http://jlord.github.io/forkngo/) listing 61 | - Migrate from express to [Hapi](https://github.com/hapijs/hapi) 62 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var logger = require('morgan'); 5 | var bodyParser = require('body-parser'); 6 | var busboy = require('connect-busboy'); 7 | var debug = require('debug')('gitpub:app'); 8 | 9 | var routes = require('./routes/index'); 10 | var micropub = require('./routes/micropub'); 11 | var post = require('./routes/post'); 12 | 13 | var app = express(); 14 | 15 | app.set('views', path.join(__dirname, 'views')); 16 | app.set('view engine', 'jade'); 17 | //app.use(favicon(__dirname + '/public/favicon.ico')); 18 | app.use(logger('dev')); 19 | app.use(bodyParser.json()); 20 | app.use(bodyParser.urlencoded({ extended: true })); 21 | app.use(busboy()); 22 | app.use(express.static(path.join(__dirname, 'public'))); 23 | 24 | // Routes 25 | app.use('/', routes); 26 | app.use('/micropub', micropub); 27 | app.use('/post', post); 28 | //TODO: webmention client 29 | 30 | 31 | /// catch 404 and forwarding to error handler 32 | app.use(function(req, res, next) { 33 | var err = new Error('Not Found'); 34 | err.status = 404; 35 | next(err); 36 | }); 37 | 38 | /// error handlers 39 | 40 | // development error handlers 41 | // will print stacktrace 42 | debug('env: ' + app.get('env')); 43 | if (app.get('env') === 'development') { 44 | app.use(function(err, req, res, next) { 45 | res.status(err.status || 500); 46 | res.render('error', { 47 | message: err.message, 48 | error: err 49 | }); 50 | }); 51 | } 52 | 53 | // production error handler 54 | // no stacktraces leaked to user 55 | app.use(function(err, req, res, next) { 56 | res.status(err.status || 500); 57 | res.render('error', { 58 | message: err.message, 59 | error: {} 60 | }); 61 | }); 62 | 63 | module.exports = app; 64 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var server = require('../lib/server'); 3 | 4 | server.start(); -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "http://bret.io", 3 | "github": { 4 | "user": "bcomnes", 5 | "repo": "bcomnes.github.io" 6 | }, 7 | "tokenUrl": "https://tokens.oauth.net/token", 8 | "email": "bcomnes@gmail.com", 9 | "name": "Bret Comnes", 10 | "mediaFolder": "media", 11 | "syndicateTo": [ "twitter.com/bretolius" ] 12 | } 13 | -------------------------------------------------------------------------------- /lib/authorize.js: -------------------------------------------------------------------------------- 1 | // authorize.js 2 | 3 | var request = require ('request'); 4 | var qs = require('querystring'); 5 | var debug = require('debug')('lib:auth'); 6 | var url = require('url'); 7 | 8 | var settings = require ('../settings'); 9 | 10 | module.exports = function (req, res, next) { 11 | req.tokenData = {}; 12 | req.tokenData.token = req.get('Authorization') || 'Bearer ' + req.body.access_token; 13 | debug('Content-Type: ' + req.get('Content-Type')); 14 | var options = { 15 | method: 'GET', 16 | url: settings.tokenUrl, 17 | headers: { 'Authorization': req.tokenData.token } 18 | }; 19 | 20 | if (req.tokenData.token) { 21 | 22 | req.tokenData.endPoint = settings.tokenUrl; 23 | debug('Token Verrification Request'); 24 | request(options, processTokenCheck); 25 | } else { 26 | if (req.is('multipart')) { 27 | debug('Missing multipart header token'); 28 | res.send(401, 'Unauthorized: Tokens must arrive in the header for multipart forms.'); 29 | } else { 30 | debug('Missing Token'); 31 | res.send(401, 'Unauthorized: No token was provided'); 32 | } 33 | } 34 | 35 | function processTokenCheck (error, response, body) { 36 | debug('Err: ' + error); 37 | debug('Status: ' + response.statusCode); 38 | // TODO: Actually handle all the codes 39 | if (!error && response.statusCode === 200) { 40 | var parsedTokenResponse = qs.parse(body); 41 | 42 | req.tokenData.me = parsedTokenResponse.me; 43 | req.tokenData.client_id = parsedTokenResponse.client_id; 44 | req.tokenData.client = url.parse(parsedTokenResponse.client_id).hostname; 45 | req.tokenData.scope = parsedTokenResponse.scope; 46 | req.tokenData.date_issued = parsedTokenResponse.date_issued; 47 | req.tokenData.nonce = parsedTokenResponse.nonce; 48 | 49 | if (url.parse(req.tokenData.me).href === url.parse(settings.domain).href) { 50 | debug('Token Verrified'); 51 | next(); 52 | } else { 53 | res.status(403).send('You gotta be authorized to do that'); 54 | debug('Valid but unauthorized token'); 55 | } 56 | } else { 57 | res.status(response.statusCode).send(qs.parse(body)); 58 | } 59 | } 60 | }; -------------------------------------------------------------------------------- /lib/handleFiles.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('lib:files'); 2 | var fs = require('fs-extra'); 3 | var path = require('path'); 4 | var async = require('async'); 5 | 6 | var settings = require('../settings'); 7 | 8 | // TODO: File handler selector and handlers 9 | module.exports = function (req, res, next) { 10 | debug('Looking for Files'); 11 | if (req.files) { 12 | debug('Yes there are files') 13 | // Git FS File Handler 14 | var saveDir = path.join(settings.worktree, 15 | settings.mediaFolder, 16 | req.tokenData.client); 17 | var webDir = path.join(settings.mediaFolder, 18 | req.tokenData.client); 19 | debug('saveDir: ' + saveDir); 20 | debug('webDir: ' + webDir); 21 | 22 | 23 | function saveToDisk (file, cb) { 24 | debug('Trying to Save ' + file.filename + ' to Disk'); 25 | 26 | var filename = path.basename(file.filename); 27 | file.savePath = path.join(saveDir, filename); 28 | file.webPath = path.join('/',webDir, filename); 29 | 30 | debug('savePath: ' + file.savePath); 31 | debug('webPath:' + file.webPath); 32 | 33 | var writeStream = fs.createWriteStream(file.savePath) 34 | 35 | writeStream.on('finish', function(cb) { 36 | debug(file.filename + ' saved to Disk.'); 37 | cb(); 38 | }); 39 | 40 | writeStream.on('error', cb); 41 | writeStream.on('start', function() { 42 | debug('fs Start'); 43 | }); 44 | writeStream.on('end', function() { 45 | debug('End event caught'); 46 | }); 47 | 48 | file.fileData.pipe(writeStream); 49 | } 50 | 51 | fs.ensureDir(saveDir, function(err) { 52 | if (err) next(err); 53 | debug('Saving files'); 54 | 55 | async.each(req.files, saveToDisk, function (err) { 56 | if (err){ 57 | debug(err); 58 | next(err); 59 | } 60 | debug('Files Saved to Disk'); 61 | next(); 62 | }); 63 | }); 64 | 65 | } else { 66 | debug('No files to handle.'); 67 | next(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/middleware/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonance-cascade/gitpub/75e390a1f7c1fb9a411d606b0f9db82c3f7e97a0/lib/middleware/.keep -------------------------------------------------------------------------------- /lib/multiParse.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('lib:multiParse'); 2 | var inspect = require('util').inspect; 3 | var path = require('path'); 4 | var fs = require('fs-extra'); 5 | 6 | var settings = require('../settings'); 7 | 8 | module.exports = function (req, res, next) { 9 | debug('Content-Type: ' + req.get('Content-Type')); 10 | if ( req.is('multipart') ) { 11 | debug('Its multipart!') 12 | // Handle file 13 | req.busboy.on('file', function(fieldname,fileStream,filename,encoding,mimetype) { 14 | debug('The was a file field'); 15 | 16 | if (filename) { 17 | debug('And it contains a file'); 18 | 19 | if (!req.files) req.files = []; 20 | var fileInfo = { 21 | "filename": filename, 22 | "fieldname": fieldname, 23 | "encoding": encoding, 24 | "mimetype": mimetype 25 | }; 26 | req.files.push(fileInfo); 27 | // Handle the readableStream; 28 | gitFs(req, fileInfo, fileStream, next); 29 | } else { 30 | debug('No file was submitted however'); 31 | fileStream.resume(); // Kill the readableStream for these 32 | } 33 | 34 | // Debug Info 35 | debug('File [' + fieldname + ']: filename: ' + filename + ', encoding: ' + encoding); 36 | 37 | fileStream.on('end', function() { 38 | debug('File [' + fieldname + '] Finished'); 39 | }); 40 | 41 | }); 42 | 43 | // Handle fields 44 | req.busboy.on('field', function(fieldname,val,fieldnameTruncated,valTruncated) { 45 | debug('Field [' + fieldname + ']: value: ' + inspect(val)); 46 | req.body[fieldname] = val; 47 | }); 48 | 49 | // Handle the finished event. 50 | req.busboy.on('finish', function() { 51 | debug('Busboy: finish - Done parsing form.'); 52 | next(); 53 | }) 54 | 55 | // Busboy, Do it! 56 | req.pipe(req.busboy); 57 | 58 | } else { 59 | debug('Nope, not multipart') 60 | next(); 61 | } 62 | } 63 | 64 | function gitFs(req, fileInfo, fileStream, cb) { 65 | var fsDir = path.join(settings.worktree, 66 | settings.mediaFolder, 67 | req.tokenData.client); 68 | var srcDir = path.join( settings.mediaFolder, 69 | req.tokenData.client); 70 | 71 | debug('Trying to Save ' + fileInfo.filename + ' to Disk'); 72 | 73 | var filename = path.basename(fileInfo.filename); 74 | fileInfo.fsPath = path.join(fsDir, filename); 75 | fileInfo.workPath = path.join('/', 76 | settings.mediaFolder, 77 | req.tokenData.client); 78 | fileInfo.src = path.join('/',srcDir, filename); 79 | 80 | debug('fsPath: ' + fileInfo.fsPath); 81 | debug('workPath: '+ fileInfo.workPath); 82 | debug('src:' + fileInfo.src); 83 | 84 | fs.ensureDir(fsDir, function(err) { 85 | if (err) cb(err); 86 | var writeStream = fs.createWriteStream(fileInfo.fsPath); 87 | writeStream.on('error', cb); 88 | debug('Saving file ' + filename); 89 | fileStream.pipe(writeStream); 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /lib/publishPost.js: -------------------------------------------------------------------------------- 1 | // publishPost.js 2 | // 3 | // Commits new files to repo, pulls repo, then pushes and 4 | // Returns the published URL 5 | 6 | var debug = require('debug')('lib:publish') 7 | var Git = require('git-wrapper'); 8 | 9 | var Repo = require('./repo'); 10 | 11 | var settings = require('../settings'); 12 | 13 | var repo = new Repo(settings); 14 | repo.git = new Git({ 15 | 'work-tree': repo.worktree, 16 | 'git-dir': repo.gitdir 17 | }); 18 | 19 | module.exports = function(req, res, next){ 20 | debug('Trying to publish post'); 21 | repo.publish(next); 22 | } -------------------------------------------------------------------------------- /lib/repo.js: -------------------------------------------------------------------------------- 1 | // Repo.js 2 | 3 | // TODO: Experiment with child_process.exec() 4 | // Git-wrapper is funky. 5 | var Git = require('git-wrapper'); 6 | var url = require('url'); 7 | var fs = require('fs'); 8 | var async = require('async'); 9 | var debug = require('debug')('lib:repo'); 10 | 11 | // Pass in the settings to get a new Repo object 12 | function Repo(obj) { 13 | for (var key in obj) { 14 | this[key] = obj[key]; 15 | } 16 | 17 | this.git = new Git({ 18 | 'work-tree': obj.worktree, 19 | 'git-dir': obj.gitdir 20 | }); 21 | } 22 | 23 | var folders = ['media', '_posts']; 24 | var comMsg = ["'gitpub posted a new post'"]; 25 | 26 | Repo.prototype.clone = function (cb) { 27 | // TODO Audit how safe this is.... 28 | // Caution! Passwords! 29 | // 30 | debug('Cloning to ' + this.worktree); 31 | 32 | var that = this; 33 | var repoUrl = url.parse(that.repo); 34 | if (process.env.ENV !== 'development') { 35 | repoUrl.auth = process.env.USERNAME + ':' + process.env.PASSWORD; 36 | } 37 | var cloneGit = new Git(); 38 | cloneGit.exec('clone',{},[url.format(repoUrl), this.worktree], function(err, stdout) { 39 | if (err) throw(err); 40 | if (stdout) debug(stdout); 41 | debug('Done cloning '+ that.github.repo); 42 | if (cb) cb(null); 43 | }) 44 | } 45 | 46 | Repo.prototype.pull = function(cb) { 47 | debug('Pulling ' + this.worktree); 48 | this.git.exec('pull', this.remoteBranch, function(err, stdout) { 49 | if (err) cb(err); 50 | if (stdout) debug(stdout); 51 | debug('Finished pulling repo'); 52 | if (cb) cb(); 53 | }); 54 | } 55 | 56 | Repo.prototype.setUser = function(cb) { 57 | var that = this; 58 | debug(that.github.repo + ' User: ' + that.name || 'GitPub'); 59 | this.git.exec('config', null, ['user.name',that.name || 'GitPub'], function(err, stdout) { 60 | if (err) cb(err); 61 | if (stdout) debug(stdout); 62 | debug('User Set'); 63 | if (cb) cb(null); 64 | }) 65 | }; 66 | 67 | Repo.prototype.setEmail = function(cb) { 68 | var that = this; 69 | debug(that.github.repo + ' email: ' + that.email); 70 | this.git.exec('config', null, ['user.email',that.email],function (err, stdout) { 71 | if (err) cb(err); 72 | if (stdout) debug(stdout); 73 | debug('Email Set'); 74 | if (cb) cb(null); 75 | }) 76 | }; 77 | 78 | Repo.prototype.add = function(cb) { 79 | // folders to commit: 80 | // folders = ['media', '_posts/ownyourgram'] 81 | debug('Adding ' + folders); 82 | this.git.exec('add', {A: true}, folders, function (err, stdout) { 83 | if (err) cb(err); 84 | if (stdout) debug(stdout); 85 | debug('Added ' + folders); 86 | if (cb) cb(); 87 | }); 88 | }; 89 | 90 | Repo.prototype.commit = function (cb) { 91 | // comMsg: commit message 92 | // pitfall: "'Ownyourgram posted a file'" 93 | // The string must be delimited 94 | debug('Comitting the changes') 95 | this.git.exec('commit', {m: true}, comMsg, function (err, stdout) { 96 | if (err) cb(err); 97 | if (stdout) debug(stdout); 98 | debug ('Changes committed'); 99 | if (cb) cb(); 100 | }) 101 | } 102 | 103 | Repo.prototype.push = function(cb) { 104 | debug('Pushing to ' + this.remoteBranch); 105 | this.git.exec('push', this.remoteBranch, function (err, stdout) { 106 | if (err) debug(err); 107 | debug('Push Succeeded'); 108 | if (stdout) debug(stdout); 109 | if (cb) cb(); 110 | }); 111 | }; 112 | 113 | Repo.prototype.publish = function(cb) { 114 | var that = this; 115 | 116 | async.series([function(acb){that.pull(acb)}, 117 | function(acb){that.add(acb)}, 118 | function(acb){that.commit(acb)}, 119 | function(acb){that.push(acb)}], 120 | cb); 121 | }; 122 | 123 | Repo.prototype.init = function(cb) { 124 | var that = this; 125 | fs.exists(this.gitdir, function(exists) { 126 | if (exists === true) { 127 | debug(that.github.repo + ' aready cloned. Pulling.'); 128 | that.pull(cb); 129 | } else { 130 | async.series([function(acb){that.clone(acb)}, function(acb){that.setUser(acb)}, function(acb){that.setEmail(acb)}], cb) 131 | } 132 | }); 133 | } 134 | 135 | module.exports = Repo; 136 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug')('gitpub'); 2 | var app = require('../app'); 3 | var settings = require('../settings'); 4 | 5 | var server 6 | var Repo = require('../lib/repo'); 7 | 8 | function startServer (cb) { 9 | app.set('port', process.env.PORT || 3000); 10 | 11 | app.locals.repo = new Repo(settings); 12 | app.locals.repo.init(function() { 13 | server = app.listen(app.get('port'), function() { 14 | debug('Express server listening on port ' + server.address().port); 15 | if (cb) cb(); 16 | }); 17 | }); 18 | } 19 | 20 | function stopServer (cb) { 21 | var port = server.address().port; 22 | server.close(function() { 23 | debug('Express server running on port ' + port + ' closed.'); 24 | if (cb) cb(); 25 | }); 26 | } 27 | 28 | exports.start = startServer; 29 | exports.stop = stopServer; -------------------------------------------------------------------------------- /lib/stagePost.js: -------------------------------------------------------------------------------- 1 | // stagePost.js 2 | // 3 | // This module takes an incoming request object, prepares a post file referencing 4 | // any files, and schedules other tasks such as POSSE or PESSOS and webmentions. 5 | 6 | 7 | var moment = require('moment'); 8 | var yaml = require('js-yaml'); 9 | var async = require('async'); 10 | var fs = require('fs-extra'); 11 | var jade = require('jade'); 12 | var url = require('url'); 13 | var path = require('path'); 14 | 15 | var debug = require('debug')('lib:stage'); 16 | 17 | var separator = '---'; 18 | var settings = require('../settings'); 19 | 20 | module.exports = function (req, res, next) { 21 | var pub = moment(req.body.published); 22 | debug('Staging Post'); 23 | 24 | async.series([setFm,generateSlug,createPostFile],next) 25 | 26 | function setFm(cb) { 27 | debug('Setting Front Matter') 28 | var fmObj = { 29 | h: req.body.h || null, 30 | date: pub.format() || moment().format() || null, 31 | tags: req.body.category || null, 32 | location: req.body.location || null, 33 | 'place-name': req.body['place-name'] || null, 34 | 'in-reply-to': req.body['in-reply-to'] || null, 35 | 'syndicate-to': req.body['syndicate-to'] || null, 36 | client_id: req.tokenData.client_id || null, 37 | client: req.tokenData.client || null, 38 | scope: req.tokenData.scope || null, 39 | files: req.files || null, 40 | published: true, 41 | syndicate: syndObj(req.tokenData.syndication) || null 42 | }; 43 | req.fm = yaml.safeDump(fmObj, {skipInvalid: true}) 44 | cb(null); 45 | } 46 | 47 | function generateSlug(cb) { 48 | debug('Generating Slug') 49 | var tcontent = req.body.content.split('\n')[0].substring(0,20); 50 | debug('tcontent: ' + tcontent); 51 | req.body.tcontent = tcontent; 52 | var tcontentFileSafe = tcontent.toLowerCase().replace(/[^a-z0-9]+/gi,'-').replace(/-$/,''); 53 | var fileName = pub.format('YYYY-MM-DD') + '-' + tcontentFileSafe + '.md'; 54 | debug('fileName: ' + fileName); 55 | req.body.postFileName = fileName; 56 | req.slug = [pub.format('/YYYY/MM/DD'),tcontentFileSafe].join('/'); 57 | cb(null); 58 | } 59 | 60 | function createPostFile(cb){ 61 | debug('Rendering Post'); 62 | var yaml = req.fm; 63 | var content = req.body.content; 64 | fullPost = [null,yaml,content].join(separator + '\n'); 65 | var postDir = path.join(settings.worktree, 66 | '_posts', 67 | req.tokenData.client); 68 | var postPath = path.join(postDir, req.body.postFileName); 69 | debug('Writing Post to: '+ postPath); 70 | fs.ensureDir(postDir, function(err) { 71 | if (err) next(err); 72 | fs.writeFile(postPath, fullPost, cb); 73 | }) 74 | } 75 | } 76 | 77 | function syndObj (syndication) { 78 | var obj 79 | if (syndication) { 80 | obj = { 81 | name: url.parse(syndication).hostname, 82 | url: url.parse(syndication).href 83 | } 84 | } 85 | return obj 86 | } 87 | 88 | // Micropub Fields: 89 | // ================ 90 | // See: http://indiewebcamp.com/micropub 91 | // (# indicates values that are not used yet) 92 | // 93 | // req.body: 94 | // h (entry, #card, #event, #cite) 95 | // #name 96 | // #summary 97 | // content (string) 98 | // published (Date String) 99 | // #updated 100 | // category = tag1, tag2, tag3 (CSV string?) 101 | // #slug 102 | // location 103 | // as a Geo URI, for example geo:45.51533714,-122.646538633 104 | // place-name 105 | // in-reply-to 106 | // syndicate-to = http://twitter.com/bretolius 107 | // 108 | // req.tokenData: 109 | // me 110 | // client_id 111 | // scope 112 | // #date_issued 113 | // 114 | // req.files[file] 115 | // file 116 | // filename 117 | // file 118 | // fieldname 119 | // encoding 120 | // mimetype 121 | // -------- 122 | // savePath 123 | // webPath 124 | -------------------------------------------------------------------------------- /notes.md: -------------------------------------------------------------------------------- 1 | curl ref 2 | 3 | curl --header "Authorization: Bearer xxxxxxxx" 4 | curl https://example.com/micropub -d h=entry -d "content=Hello World" -H "Authorization: Bearer XXXXXXX" 5 | 6 | http://ownyourgram.com/dashboard 7 | http://expressjs.com/4x/api.html#req.params 8 | http://indiewebcamp.com/auth-brainstorming 9 | http://indiewebcamp.com/login-brainstorming 10 | http://indiewebcamp.com/micropub 11 | 12 | 13 | curl http://localhost:3000 -d h=entry -d "content=Hello World" -H "Authorization: Bearer XXXXXXX" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitpub", 3 | "version": "0.1.0", 4 | "description": "A simple git based micropub endpoint", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node_modules/.bin/mocha", 8 | "cover": "node_modules/.bin/istanbul cover node_modules/.bin/_mocha", 9 | "start": "node bin/www", 10 | "dev": "DEBUG=gitpub*,routes:*,lib:* ENV=development node ./bin/www", 11 | "devmon": "DEBUG=gitpub*,routes:*,lib:* ENV=development nodemon ./bin/www", 12 | "debug": "DEBUG=gitpub*,routes:*,lib:* ENV=development node-debug ./bin/www", 13 | "windev": "win-spawn DEBUG=gitpub*,routes:*,lib:* ENV=development nodemon ./bin/www", 14 | "production": "ENV=production node ./bin/www" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/bcomnes/gitpub.git" 19 | }, 20 | "author": "Bret Comnes", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/bcomnes/gitpub/issues" 24 | }, 25 | "homepage": "https://github.com/bcomnes/gitpub", 26 | "dependencies": { 27 | "async": "^1.3.0", 28 | "body-parser": "^1.5.1", 29 | "connect-busboy": "0.0.1", 30 | "cookie-parser": "^1.3.2", 31 | "debug": "^2.2.0", 32 | "ejs": "^1.0.0", 33 | "express": "4.8.1", 34 | "form-urlencoded": "0.0.6", 35 | "fs-extra": "^0.22.1", 36 | "git-wrapper": "^0.1.1", 37 | "jade": "^1.5.0", 38 | "js-yaml": "^3.1.0", 39 | "marked": "^0.3.3", 40 | "moment": "^2.6.0", 41 | "morgan": "^1.2.1", 42 | "request": "^2.35.0", 43 | "serve-favicon": "^2.0.1", 44 | "type-is": "^1.2.0" 45 | }, 46 | "devDependencies": { 47 | "codeclimate-test-reporter": "0.0.3", 48 | "istanbul": "^0.3.0", 49 | "mocha": "^1.21.4", 50 | "request-mocha": "^0.2.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /prototypes/fmTest.js: -------------------------------------------------------------------------------- 1 | var req = {}; 2 | req.files = [ {stuff: 'derp', ho:'hi',bloop: 'blop'}, {stuff: 'derp', ho:'hi', bloop: 'blop' }] 3 | 4 | function setFm(cb) { 5 | var fm = { 6 | amessage: 'Hi dude!', 7 | anothermsg: 'hey', 8 | anothermsga: 'hey', 9 | anotherms: 'hey', 10 | anotherm: 'hey', 11 | anothesg: 'hey', 12 | anothermsg: 'hey', 13 | anotrmsg: 'hey', 14 | anothermsg: 'hey', 15 | anhermsg: 'hey', 16 | anothermsg: 'hey', 17 | aohermsg: 'hey', 18 | anfdfdothermsg: 'hey', 19 | anothefdsrmsg: 'hey', 20 | anothermsfdsfg: 'hey', 21 | 22 | files: [] 23 | }; 24 | 25 | req.files.map(function (file) { 26 | var fileInfo = { 27 | stuff: file.stuff, 28 | ho: file.ho 29 | }; 30 | fm.files.push(fileInfo); 31 | }); 32 | cb(fm); 33 | } 34 | 35 | setFm(function(fm) { 36 | console.log(fm); 37 | }); -------------------------------------------------------------------------------- /prototypes/repoSpec.js: -------------------------------------------------------------------------------- 1 | var Repo = require("../lib/repo"); 2 | var expect = require("chai").expect; 3 | var settings = require("../settings"); 4 | 5 | describe("Repo", function() { 6 | describe("repo", function() { 7 | it("should have corrent settings", function(){ 8 | var repo = new Repo(settings); 9 | 10 | expect(repo).to.have.a.property("repo",settings.repo); 11 | expect(repo).to.have.a.property("worktree", settings.worktree); 12 | expect(repo).to.have.a.property("gitdir", settings.gitdir); 13 | expect(repo).to.have.a.property("remoteBranch", settings.remoteBranch); 14 | }); 15 | describe("#clone", function(){ 16 | before(function() { 17 | (!fs.existsSync(".test_files")) 18 | }) 19 | } 20 | }); 21 | }); -------------------------------------------------------------------------------- /prototypes/repoTest.js: -------------------------------------------------------------------------------- 1 | var Repo = require('../lib/repo'); 2 | var settings = require('../settings'); 3 | var repo = new Repo(settings); 4 | var inspect = require('util').inspect; 5 | var debug = require('debug')('test:repoTest'); 6 | 7 | repo.init(function () { 8 | debug('Initialized'); 9 | }) 10 | 11 | -------------------------------------------------------------------------------- /prototypes/testStart.js: -------------------------------------------------------------------------------- 1 | startServer = require('../lib/server') 2 | 3 | startServer.start(function() { console.log('ale done')}); -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | 10 | #menu { 11 | position: absolute; 12 | top: 15px; 13 | right: 20px; 14 | font-size: 12px; 15 | color: #888; 16 | } 17 | 18 | #menu .name:after { 19 | content: ' -'; 20 | } 21 | 22 | #menu a { 23 | text-decoration: none; 24 | margin-left: 5px; 25 | color: black; 26 | } 27 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | // Gitpub Default Route 2 | var express = require('express'); 3 | var router = express.Router(); 4 | var debug = require('debug')('routes:index'); 5 | 6 | /* GET home page. */ 7 | router.get('/', function(req, res) { 8 | 9 | res.render('index', { title: 'Gitpub' }); 10 | }); 11 | 12 | module.exports = router; -------------------------------------------------------------------------------- /routes/micropub.js: -------------------------------------------------------------------------------- 1 | // Micropub route 2 | var express = require('express'); 3 | var router = express.Router(); 4 | var url = require('url'); 5 | var formEncoder = require('form-urlencoded'); 6 | 7 | var authorize = require('../lib/authorize'); 8 | var multiParse = require('../lib/multiParse'); 9 | var handleFiles = require('../lib/handleFiles'); 10 | var stagePost = require('../lib/stagePost'); 11 | var publishPost = require('../lib/publishPost'); 12 | 13 | var settings = require('../settings'); 14 | 15 | 16 | 17 | /* GET micropub landing page */ 18 | router.get('/', function(req, res) { 19 | switch (req.query['q']){ 20 | case "syndicate-to": 21 | res.set('Content-Type', 'application/x-www-form-urlencoded'); 22 | res.send( 23 | formEncoder.encode({'syndicate-to': settings.syndicateTo.join(',')}) 24 | ); 25 | break; 26 | default: 27 | res.render('micropub', { title: 'Gitpub µPub Endpoint' }); 28 | } 29 | }); 30 | 31 | /* POST micropub */ 32 | router.post('/', authorize, 33 | multiParse, 34 | stagePost,publishPost, 35 | microRes); 36 | 37 | function microRes (req, res) { 38 | res.set('Location', postUrl()); 39 | res.status(201).send('Created Post at ' + postString()); 40 | 41 | function postUrl() { 42 | return url.parse([settings.domain + req.slug].join('')).format(); 43 | } 44 | 45 | function postString() { 46 | return [settings.domain + req.slug].join(''); 47 | } 48 | } 49 | 50 | module.exports = router; 51 | -------------------------------------------------------------------------------- /routes/post.js: -------------------------------------------------------------------------------- 1 | // Test Post Route 2 | var express = require('express'); 3 | var router = express.Router(); 4 | var url = require('url'); 5 | 6 | var settings = require('../settings'); 7 | 8 | /* GET Test Form */ 9 | router.get('/', function(req, res) { 10 | res.render('post', { title: 'Test Post'}); 11 | }); 12 | 13 | module.exports = router; -------------------------------------------------------------------------------- /settings.js: -------------------------------------------------------------------------------- 1 | var config = require('./config.json'); 2 | var path = require('path'); 3 | 4 | var settings = clone(config); 5 | 6 | // Full URL to git repo 7 | if (process.env.ENV === 'development') { 8 | settings.repo = 'git@github.com:' + config.github.user + '/' + config.github.repo + '.git'; 9 | } else { 10 | settings.repo = 'https://github.com/' + config.github.user + '/' + config.github.repo + '.git'; 11 | } 12 | // Path to git repo on disk 13 | settings.worktree = path.join(__dirname, 'repo', settings.github.repo); 14 | // Path to .git dir in git repo 15 | settings.gitdir = path.join(settings.worktree, '.git'); 16 | // Remote branch to push/pull from 17 | settings.remoteBranch = [config.remote || 'origin', config.branch || 'master']; 18 | 19 | function clone(o) { 20 | var ret = {}; 21 | Object.keys(o).forEach(function (val) { 22 | ret[val] = o[val]; 23 | }); 24 | return ret; 25 | } 26 | 27 | module.exports = settings; -------------------------------------------------------------------------------- /test/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/resonance-cascade/gitpub/75e390a1f7c1fb9a411d606b0f9db82c3f7e97a0/test/.keep -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var request = require('request'); 3 | var httpUtils = require('request-mocha')(request); 4 | var formEncoder = require('form-urlencoded'); 5 | 6 | var server = require('../lib/server') 7 | 8 | var settings = require('../settings.js') 9 | 10 | describe('Start the server', function() { 11 | before(server.start); 12 | this.timeout(10000); 13 | 14 | describe('GET /', function() { 15 | httpUtils.save('http://localhost:3000/'); 16 | 17 | it ('should respond without error', function() { 18 | assert(this.err === null) 19 | }); 20 | 21 | it ('should respond with 202 status code', function() { 22 | assert(this.res.statusCode === 200); 23 | }); 24 | 25 | it ('should respond with "Welcome to Gitpub"', function(){ 26 | var pos = this.body.indexOf("Welcome to Gitpub") 27 | assert(pos > -1 ) 28 | }) 29 | }); 30 | 31 | describe('GET /micropub', function() { 32 | httpUtils.save('http://localhost:3000/micropub'); 33 | 34 | it ('should respond without error', function() { 35 | assert(this.err === null) 36 | }); 37 | 38 | it ('should respond with 202 status code', function() { 39 | assert(this.res.statusCode === 200); 40 | }); 41 | 42 | it ('should respond with "...µPub Endpoint"', function(){ 43 | var pos = this.body.indexOf("Welcome to Gitpub µPub Endpoint") 44 | assert(pos > -1 ) 45 | }) 46 | }); 47 | 48 | describe('GET /micropub?q=syndicate-to', function() { 49 | httpUtils.save('http://localhost:3000/micropub?q=syndicate-to'); 50 | 51 | it ('should respond without error', function() { 52 | assert(this.err === null) 53 | }); 54 | it ('should respond with 202 status code', function() { 55 | assert(this.res.statusCode === 200); 56 | }); 57 | it ('should response with csv syndicate-to list', function() { 58 | var expected = formEncoder.encode({'syndicate-to': settings.syndicateTo.join(',')}); 59 | assert(this.res.body === expected); 60 | }) 61 | }); 62 | 63 | after(server.stop); 64 | }); 65 | -------------------------------------------------------------------------------- /views/error.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= title 5 | p Welcome to #{title} 6 | 7 | :markdown 8 | [![Build Status](https://travis-ci.org/bcomnes/gitpub.svg?branch=master)](https://travis-ci.org/bcomnes/gitpub) 9 | [![Dependency Status](https://david-dm.org/bcomnes/gitpub.svg?style)](https://david-dm.org/bcomnes/gitpub) 10 | [![devDependency Status](https://david-dm.org/bcomnes/gitpub/dev-status.svg)](https://david-dm.org/bcomnes/gitpub#info=devDependencies) 11 | [![Code Climate](https://codeclimate.com/github/bcomnes/gitpub/badges/gpa.svg)](https://codeclimate.com/github/bcomnes/gitpub) 12 | [![Test Coverage](https://codeclimate.com/github/bcomnes/gitpub/badges/coverage.svg)](https://codeclimate.com/github/bcomnes/gitpub) 13 | 14 | For mor information please see the [gitpub repository](https://github.com/bcomnes/gitpub). 15 | 16 | - [Micropub Endpoint](/micropub) 17 | - [Test Form](/post) 18 | -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | body 7 | block content -------------------------------------------------------------------------------- /views/micropub.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= title 5 | p Welcome to #{title} 6 | 7 | :markdown 8 | This is a micropub endpoint. Example clients: 9 | 10 | - [Quill](https://quill.p3k.io) 11 | - [Ownyourgram](https://ownyourgram.com) 12 | - [Taproot Notes](http://waterpigs.co.uk/notes/new/) 13 | - [Taproot Articles](http://waterpigs.co.uk/articles/new/) 14 | - [Shrewdness](http://indiewebcamp.com/Shrewdness) 15 | -------------------------------------------------------------------------------- /views/post.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= title 5 | p Welcome to #{title} 6 | p This form was used for testing. 7 | 8 | 9 | form(action="post", method="post") 10 | p Content 11 | textarea(type="text", name="content" cols="20" rows="4" ) 12 | p in-reply-to 13 | input(type="text", name="in-reply-to") 14 | p category 15 | input(type="text", name="category") 16 | p slug 17 | input(type="text", name="slug") 18 | p location 19 | input(type="text", name="location") 20 | p token 21 | input(type="text" name="access_token") 22 | input(type="submit" name="post" value="Post") 23 | --------------------------------------------------------------------------------