mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-03-25 09:05:47 +00:00
slap a sinatra API server in front of HarpBlog
terminate meta_weblog_handler.rb with extreme prejudice
This commit is contained in:
parent
5c6399b558
commit
70e8ff6b18
5 changed files with 230 additions and 176 deletions
1
Gemfile
1
Gemfile
|
|
@ -3,5 +3,6 @@ source 'https://rubygems.org'
|
|||
gem 'builder'
|
||||
gem 'htmlentities'
|
||||
gem 'rdiscount'
|
||||
gem 'sinatra'
|
||||
gem 'rspec'
|
||||
gem 'guard-rspec'
|
||||
|
|
|
|||
|
|
@ -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
11
server/auth.rb
Normal 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
|
||||
|
|
@ -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
|
||||
219
server/server.rb
219
server/server.rb
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue