mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-03-25 09:05:47 +00:00
Drafts don’t have reliable slugs until they’re published so give them UUIDs, and lookup posts by ID instead of slug.
356 lines
7.8 KiB
Ruby
Executable file
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
|