samhuri.net/server/server.rb
Sami Samhuri c905f5c414 use IDs instead of slugs for posts
Drafts don’t have reliable slugs until they’re published so give them
UUIDs, and lookup posts by ID instead of slug.
2015-03-29 19:42:38 -07:00

356 lines
7.8 KiB
Ruby
Executable file

#!/usr/bin/env ruby -w
# An HTTP interface for my Harp blog.
require 'json'
require 'sinatra'
require './auth'
require './harp_blog'
CONFIG_DEFAULTS = {
auth: false,
dry_run: false,
path: File.expand_path('../test-blog', __FILE__),
host: '127.0.0.1',
port: 6706,
}
def env_value(name)
env_name = "BLOG_#{name.to_s.upcase}"
raw_value = ENV[env_name]
case name
when :auth, :dry_run
raw_value ? raw_value.to_i != 0 : false
when :port
raw_value ? raw_value.to_i : nil
else
raw_value
end
end
$config = CONFIG_DEFAULTS.dup
$config.each_key do |name|
value = env_value(name)
unless value.nil?
$config[name] = value
end
end
unless File.directory?($config[:path])
raise RuntimeError.new("file not found: #{$config[:path]}")
end
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])
before do
if request.body.size > 0
type = request['HTTP_CONTENT_TYPE']
@fields =
case
when type =~ /^application\/json\b/
request.body.rewind
JSON.parse(request.body.read)
else
params
end
end
end
# status
get '/status' do
status 200
headers 'Content-Type' => 'application/json'
JSON.generate(status: blog.status)
end
# publish the site
post '/publish' do
unless authenticated?(request['Auth'])
status 403
return 'forbidden'
end
blog.publish(@fields['env'])
status 204
end
# list years
get '/years' do
status 200
headers 'Content-Type' => 'application/json'
JSON.generate(years: blog.years)
end
# list months
get '/months' do
status 200
headers 'Content-Type' => 'application/json'
JSON.generate(months: blog.months)
end
# list published posts
get '/posts/:year/?:month?' do |year, month|
posts =
if month
blog.posts_for_month(year, month)
else
blog.posts_for_year(year)
end
status 200
headers 'Content-Type' => 'application/json'
JSON.generate(posts: posts.map(&:fields))
end
# list all published posts
get '/posts' do
posts = blog.months.map do |year, month|
blog.posts_for_month(year, month)
end.flatten
status 200
headers 'Content-Type' => 'application/json'
JSON.generate(posts: posts.map(&:fields))
end
# list drafts
get '/drafts' do
posts = blog.drafts
status 200
headers 'Content-Type' => 'application/json'
JSON.generate(posts: posts.map(&:fields))
end
# get a post
get '/posts/:year/:month/:id' do |year, month, id|
begin
post = blog.get_post(year, month, id)
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
# get a draft
get '/drafts/:id' do |id|
begin
post = blog.get_draft(id)
rescue HarpBlog::InvalidDataError => e
status 500
return "Failed to get draft, 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 draft
post '/drafts' do
unless authenticated?(request['Auth'])
status 403
return 'forbidden'
end
id, title, body, link = @fields.values_at('id', 'title', 'body', 'link')
begin
if post = blog.create_post(title, body, link, id: id, draft: true)
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
rescue HarpBlog::PostExistsError => e
post = HarpBlog::Post.new({
title: title,
body: body,
link: link,
})
status 409
"refusing to clobber existing draft, 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 draft: #{e.message}"
end
end
end
# update a post
put '/posts/:year/:month/:id' do |year, month, id|
unless authenticated?(request['Auth'])
status 403
return 'forbidden'
end
title, body, link = @field.values_at('title', 'body', 'link')
begin
if post = blog.get_post(year, month, id)
blog.update_post(post, title, body, 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 update post: #{e.message}"
end
end
end
# update a draft
put '/drafts/:id' do |id|
unless authenticated?(request['Auth'])
status 403
return 'forbidden'
end
title, body, link = @field.values_at('title', 'body', 'link')
begin
if post = blog.get_draft(id)
blog.update_post(post, title, body, link)
status 204
else
status 404
'not found'
end
rescue HarpBlog::InvalidDataError => e
status 500
"Failed to update draft, 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 update draft: #{e.message}"
end
end
end
# delete a post
delete '/posts/:year/:month/:id' do |year, month, id|
unless authenticated?(request['Auth'])
status 403
return 'forbidden'
end
blog.delete_post(year, month, id)
status 204
end
# delete a draft
delete '/drafts/:id' do |id|
unless authenticated?(request['Auth'])
status 403
return 'forbidden'
end
blog.delete_draft(id)
status 204
end
# publish a post
post '/drafts/:id/publish' do |id|
unless authenticated?(request['Auth'])
status 403
return 'forbidden'
end
if post = blog.get_draft(id)
new_post = blog.publish_post(post)
status 201
headers 'Location' => url_for(new_post.url), 'Content-Type' => 'application/json'
JSON.generate(post: new_post.fields)
else
status 404
'not found'
end
end
# unpublish a post
post '/posts/:year/:month/:id/unpublish' do |year, month, id|
unless authenticated?(request['Auth'])
status 403
return 'forbidden'
end
if post = blog.get_post(year, month, id)
new_post = blog.unpublish_post(post)
status 201
headers 'Location' => url_for(new_post.url), 'Content-Type' => 'application/json'
JSON.generate(post: new_post.fields)
else
status 404
'not found'
end
end