slap a sinatra API server in front of HarpBlog

terminate meta_weblog_handler.rb with extreme prejudice
This commit is contained in:
Sami Samhuri 2014-10-18 01:38:45 -07:00
parent 5c6399b558
commit 70e8ff6b18
5 changed files with 230 additions and 176 deletions

View file

@ -3,5 +3,6 @@ source 'https://rubygems.org'
gem 'builder'
gem 'htmlentities'
gem 'rdiscount'
gem 'sinatra'
gem 'rspec'
gem 'guard-rspec'

View file

@ -29,6 +29,9 @@ GEM
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
rack (1.5.2)
rack-protection (1.5.0)
rack
rb-fsevent (0.9.4)
rb-inotify (0.9.5)
ffi (>= 0.5.0)
@ -45,8 +48,13 @@ GEM
rspec-mocks (3.0.4)
rspec-support (~> 3.0.0)
rspec-support (3.0.4)
sinatra (1.3.6)
rack (~> 1.4)
rack-protection (~> 1.3)
tilt (~> 1.3, >= 1.3.3)
slop (3.6.0)
thor (0.19.1)
tilt (1.4.1)
timers (4.0.1)
hitimes
@ -59,3 +67,4 @@ DEPENDENCIES
htmlentities
rdiscount
rspec
sinatra

11
server/auth.rb Normal file
View file

@ -0,0 +1,11 @@
require 'json'
class Auth
def initialize(filename)
@credentials = JSON.parse(File.read(filename))
end
def authenticated?(username, password)
@credentials['username'] == username && @credentials['password'] == password
end
end

View file

@ -1,166 +0,0 @@
require 'fileutils'
require 'json'
require 'open-uri'
require 'xmlrpc/server'
class MetaWeblogHandler
def newPost(site_id, username, password, post_hash, should_publish)
if authenticated?(username, password)
post_hash.each_key do |key|
v = post_hash[key]
if v.to_s.strip == ''
post_hash.delete(key)
end
end
url = post_hash['link']
title = post_hash['title'] || find_title(url)
body = post_hash['description'] || 'No description necessary.'
unless title
raise XMLRPC::FaultException.new(0, "no title given and cannot parse title")
end
create_link_post(url, title, body)
1 # dummy post ID
else
raise XMLRPC::FaultException.new(0, "username or password invalid")
end
end
private
def find_title(url)
body = open(url).read
lines = body.split(/[\r\n]+/)
title_line = lines.grep(/<title/).first.strip
title_line.gsub(/\s*<\/?title[^>]*>\s*/, '')
rescue
nil
end
def create_link_post(url, title, body)
slug = create_slug(title)
time = Time.now
date = time.strftime('%B %d, %Y')
post_dir = File.join(samhuri_net_path, 'public/posts', time.year.to_s, pad(time.month))
relative_url = "/posts/#{time.year}/#{pad(time.month)}/#{slug}"
git_pull
ensure_post_dir_exists(post_dir)
post_filename = File.join(post_dir, "#{slug}.md")
File.open(post_filename, 'w') do |f|
f.puts(body)
end
post_data = read_post_data(post_dir)
post_data[slug] = {
title: title,
date: date,
timestamp: time.to_i,
tags: [],
url: relative_url,
link: url
}
write_post_data(post_dir, post_data)
git_commit(title, post_dir)
git_push
publish
rescue Exception => e
git_reset
raise e
end
def samhuri_net_path
'/Users/sjs/Projects/samhuri.net-publish'
end
def create_slug(title)
# TODO: be intelligent about unicode ... \p{Word} might help. negated char class with it?
title.downcase.gsub(/[^\w]/, '-')
end
def pad(n)
n.to_i < 10 ? "0#{n}" : "#{n}"
end
def ensure_post_dir_exists(post_dir)
FileUtils.mkdir_p(post_dir)
monthly_index_filename = File.join(post_dir, 'index.ejs')
unless File.exists?(monthly_index_filename)
source = File.join(post_dir, '../../2006/02/index.ejs')
FileUtils.cp(source, monthly_index_filename)
end
yearly_index_filename = File.join(post_dir, '../index.ejs')
unless File.exists?(yearly_index_filename)
source = File.join(post_dir, '../../2006/index.ejs')
FileUtils.cp(source, yearly_index_filename)
end
end
def read_post_data(dir)
post_data_filename = File.join(dir, '_data.json')
if File.exists?(post_data_filename)
JSON.parse(File.read(post_data_filename))
else
{}
end
end
def write_post_data(dir, data)
post_data_filename = File.join(dir, '_data.json')
json = JSON.pretty_generate(data)
File.open(post_data_filename, 'w') do |f|
f.puts(json)
end
end
def quote(s)
s.gsub('"', '\\"')
end
def git_commit(title, *files)
quoted_files = files.map { |f| "\"#{quote(f)}\"" }
message = "linked '#{quote(title)}'"
`cd '#{samhuri_net_path}' && git add #{quoted_files.join(' ')} && git commit -m "#{message}"`
end
def git_pull
`cd '#{samhuri_net_path}' && git pull -f`
end
def git_push
`cd '#{samhuri_net_path}' && git push`
end
def git_reset
`cd '#{samhuri_net_path}' && git reset --hard`
end
def publish
`cd '#{samhuri_net_path}' && make publish`
end
def auth_json_filename
File.expand_path('../auth.json', __FILE__)
end
def auth
@auth ||= JSON.parse(File.read(auth_json_filename))
end
def authenticated?(username, password)
auth['username'] == username && auth['password'] == password
end
end

View file

@ -1,16 +1,215 @@
#!/usr/bin/env ruby -w
# MetaWeblog and Blogger API to post to this site.
# An HTTP interface for my Harp blog.
require 'xmlrpc/server'
require 'xmlrpc/client'
require './meta_weblog_handler'
require 'json'
require 'optparse'
require 'sinatra'
require './auth'
require './harp_blog'
def main
port = (ARGV.shift || 6706).to_i
server = XMLRPC::Server.new(port, '0.0.0.0')
server.add_handler('metaWeblog', MetaWeblogHandler.new)
server.serve
$config = {
auth: false,
dry_run: false,
path: File.expand_path('../spec/test-blog', __FILE__),
host: '127.0.0.1',
port: 6706,
}
OptionParser.new do |opts|
opts.banner = "Usage: server.rb [options]"
opts.on("-a", "--[no-]auth", "Enable authentication") do |auth|
$config[:auth] = auth
end
opts.on("-h", "--host [HOST]", "Host to bind") do |host|
$config[:host] = host
end
opts.on("-p", "--port [PORT]", "Port to bind") do |port|
$config[:port] = port.to_i
end
opts.on("-P", "--path [PATH]", "Path to Harp blog") do |path|
$config[:path] = path
end
end.parse!
unless File.exist?($config[:path])
raise RuntimeError.new("file not found: #{$config[:path]}")
end
main if $0 == __FILE__
if $config[:host] == '0.0.0.0' && !$config[:auth]
raise RuntimeError.new("cowardly refusing to bind to 0.0.0.0 without authentication")
end
$auth = Auth.new(File.expand_path('../auth.json', __FILE__))
def authenticated?(auth)
if $config[:auth]
username, password = auth.split('|')
$auth.authenticated?(username, password)
else
true
end
end
real_host = $config[:host] == '0.0.0.0' ? 'h.samhuri.net' : $config[:host]
$url_root = "http://#{real_host}:#{$config[:port]}/"
def url_for(*components)
File.join($url_root, *components)
end
# Server
set :host, $config[:host]
set :port, $config[:port]
blog = HarpBlog.new($config[:path], $config[:dry_run])
# list years
get '/years' do
unless authenticated?(request['Auth'])
status 403
return 'forbidden'
end
JSON.generate(years: blog.years)
end
# list posts
get '/posts/:year/?:month?' do |year, month|
unless authenticated?(request['Auth'])
status 403
return 'forbidden'
end
posts =
if month
blog.posts_for_month(year, month)
else
blog.posts_for_year(year)
end
JSON.generate(posts: posts.map(&:fields))
end
# get a post
get '/posts/:year/:month/:slug' do |year, month, slug|
unless authenticated?(request['Auth'])
status 403
return 'forbidden'
end
begin
post = blog.get_post(year, month, slug)
rescue HarpBlog::InvalidDataError => e
status 500
return "Failed to get post, invalid data on disk: #{e.message}"
end
if post
if request.accept?('application/json')
status 200
headers 'Content-Type' => 'application/json'
JSON.generate(post: post.fields)
elsif request.accept?('text/html')
status 200
headers 'Content-Type' => 'text/html'
blog.render_post(post.fields)
else
status 400
"content not available in an acceptable format: #{request.accept.join(', ')}"
end
else
status 404
'not found'
end
end
# make a post
post '/posts' do
unless authenticated?(request['Auth'])
status 403
return 'forbidden'
end
begin
post = blog.create_post(params[:title], params[:body], params[:link])
rescue HarpBlog::PostExistsError => e
post = HarpBlog::Post.new({
title: params[:title],
body: params[:body],
link: params[:link],
})
status 409
return "refusing to clobber existing post, update it instead: #{post.url}"
rescue HarpBlog::PostSaveError => e
status 500
if orig_err = e.original_error
"#{e.message} -- #{orig_err.class}: #{orig_err.message}"
else
"Failed to create post: #{e.message}"
end
end
if post
url = url_for(post.url)
status 201
headers 'Location' => url, 'Content-Type' => 'application/json'
JSON.generate(post: post.fields)
else
status 500
'failed to create post'
end
end
# update a post
put '/posts/:year/:month/:slug' do |year, month, slug|
unless authenticated?(request['Auth'])
status 403
return 'forbidden'
end
begin
if post = blog.get_post(year, month, slug)
blog.update_post(post, params[:title], params[:body], params[:link])
status 204
else
status 404
'not found'
end
rescue HarpBlog::InvalidDataError => e
status 500
"Failed to update post, invalid data on disk: #{e.message}"
rescue HarpBlog::PostSaveError => e
status 500
if orig_err = e.original_error
"#{e.message} -- #{orig_err.class}: #{orig_err.message}"
else
"Failed to create post: #{e.message}"
end
end
end
# delete a post
delete '/posts/:year/:month/:slug' do |year, month, slug|
unless authenticated?(request['Auth'])
status 403
return 'forbidden'
end
blog.delete_post(year, month, slug)
status 204
end
# publish
post '/publish' do
unless authenticated?(request['Auth'])
status 403
return 'forbidden'
end
production = params[:env] == 'production'
blog.publish(production)
status 204
end