Skip to content
This repository has been archived by the owner on Feb 14, 2018. It is now read-only.

Abstract persistence into Store classes -- FileStore and CouchStore. #204

Merged
merged 13 commits into from
Apr 30, 2012
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ gem "RubyInline"
gem "png"
gem "rest-client"
gem "ruby-openid"
gem "couchrest"

group :development do
gem 'ruby-debug', :require => 'ruby-debug', :platform => :mri_18
Expand Down
5 changes: 5 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ GEM
ffi (~> 1.0.6)
columnize (0.3.4)
configuration (1.2.0)
couchrest (1.1.2)
mime-types (~> 1.15)
multi_json (~> 1.0.0)
rest-client (~> 1.6.1)
daemons (1.1.4)
diff-lcs (1.1.2)
eventmachine (0.12.10)
Expand Down Expand Up @@ -93,6 +97,7 @@ PLATFORMS
DEPENDENCIES
RubyInline
capybara
couchrest
haml
json
launchy
Expand Down
13 changes: 13 additions & 0 deletions server/sinatra/ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,16 @@ The server will create subdirectories with farm for each virtual host name and l
The thin web server cannot handle recursive web requests that can happen with federated sites hosted in the same farm. Use webrick instead. Launch it with this command:

bundle exec rackup -s webrick -p 1111

CouchDB
=======

By default, all pages, favicons, and server claims are stored in the server's local filesystem.
If you'd prefer to use CouchDB for storage, you need to set two environment variables:

STORE_TYPE=CouchStore
COUCHDB_URL=https://username:[email protected]

If you want to run a farm with CouchDB, you should also set this environment variable:

FARM_MODE=true
5 changes: 2 additions & 3 deletions server/sinatra/favicon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

class Favicon
class << self
def create(path)
def create_blob
canvas = PNG::Canvas.new 32, 32
light = PNG::Color.from_hsv(256*rand,200,255).rgb()
dark = PNG::Color.from_hsv(256*rand,200,125).rgb()
Expand All @@ -20,8 +20,7 @@ def create(path)
light[2]*p + dark[2]*(1-p))
end
end
png = PNG.new canvas
png.save path
PNG.new(canvas).to_blob
end
end
end
40 changes: 15 additions & 25 deletions server/sinatra/page.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
require 'json'
require File.expand_path("../random_id", __FILE__)
require File.expand_path("../stores/all", __FILE__)

class PageError < StandardError; end;

# Page Class
# Handles writing and reading JSON data to and from files.
class Page
# class << self

# Directory where pages are to be stored.
attr_accessor :directory
# Directory where default (pre-existing) pages are stored.
Expand All @@ -17,27 +18,21 @@ class Page
# @param [String] name - The name of the file to retrieve, relative to Page.directory.
# @return [Hash] The contents of the retrieved page (parsed JSON).
def get(name)
assert_directories_set

assert_attributes_set
path = File.join(directory, name)

if File.exist? path
load_and_parse path
default_path = File.join(default_directory, name)
page = Store.get_page(path)
if page
page
elsif File.exist?(default_path)
put name, FileStore.get_page(default_path)
else
default_path = File.join(default_directory, name)

if File.exist?(default_path)
FileUtils.mkdir_p File.dirname(path)
FileUtils.cp default_path, path
load_and_parse path
else
put name, {'title'=>name,'story'=>[{'type'=>'factory', 'id'=>RandomId.generate}]} unless File.file? path
end
put name, {'title'=>name,'story'=>[{'type'=>'factory', 'id'=>RandomId.generate}]}
end
end

def exists?(name)
File.exists?(File.join(directory, name)) or File.exist?(File.join(default_directory, name))
Store.exists?(File.join(directory, name)) or File.exist?(File.join(default_directory, name))
end

# Create or update a page
Expand All @@ -46,20 +41,15 @@ def exists?(name)
# @param [Hash] page - The page data to be written to the file (it will be converted to JSON).
# @return [Hash] The contents of the retrieved page (parsed JSON).
def put(name, page)
assert_directories_set
File.open(File.join(directory, name), 'w') { |file| file.write(JSON.pretty_generate(page)) }
page
assert_attributes_set
path = File.join directory, name
Store.put_page(path, page, :name => name, :directory => directory)
end

private

def load_and_parse(path)
JSON.parse(File.read(path))
end

def assert_directories_set
def assert_attributes_set
raise PageError.new('Page.directory must be set') unless directory
raise PageError.new('Page.default_directory must be set') unless default_directory
end
# end
end
61 changes: 30 additions & 31 deletions server/sinatra/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

Encoding.default_external = Encoding::UTF_8

require 'stores/all'
require 'random_id'
require 'page'
require 'favicon'
Expand All @@ -24,37 +25,37 @@ class Controller < Sinatra::Base
set :versions, `git log -10 --oneline` || "no git log"
enable :sessions

Store.set ENV['STORE_TYPE'], APP_ROOT

class << self # overridden in test
def data_root
File.join APP_ROOT, "data"
end
end

def farm_page
data = File.exists?(File.join(self.class.data_root, "farm")) ? File.join(self.class.data_root, "farm", request.host) : self.class.data_root
page = Page.new
page.directory = File.join(data, "pages")
page.directory = File.join data_dir, "pages"
page.default_directory = File.join APP_ROOT, "default-data", "pages"
FileUtils.mkdir_p page.directory
Store.mkdir page.directory
page
end

def farm_status
data = File.exists?(File.join(self.class.data_root, "farm")) ? File.join(self.class.data_root, "farm", request.host) : self.class.data_root
status = File.join(data, "status")
FileUtils.mkdir_p status
status = File.join data_dir, "status"
Store.mkdir status
status
end

def data_dir
Store.farm?(self.class.data_root) ? File.join(self.class.data_root, "farm", request.host) : self.class.data_root
end

def identity
default_path = File.join APP_ROOT, "default-data", "status", "local-identity"
real_path = File.join farm_status, "local-identity"
unless File.exist? real_path
FileUtils.mkdir_p File.dirname(real_path)
FileUtils.cp default_path, real_path
end

JSON.parse(File.read(real_path))
id_data = Store.get_hash real_path
id_data ||= Store.put_hash(real_path, FileStore.get_hash(default_path))
end

helpers do
Expand Down Expand Up @@ -84,7 +85,7 @@ def authenticated?
end

def claimed?
File.exists? "#{farm_status}/open_id.identity"
Store.exists? "#{farm_status}/open_id.identity"
end

def authenticate!
Expand Down Expand Up @@ -123,16 +124,16 @@ def oops status, message
when OpenID::Consumer::SUCCESS
id = params['openid.identity']
id_file = File.join farm_status, "open_id.identity"
if File.exist?(id_file)
stored_id = File.read(id_file)
stored_id = Store.get_text(id_file)
if stored_id
if stored_id == id
# login successful
authenticate!
else
oops 403, "This is not your wiki"
end
else
File.open(id_file, "w") {|f| f << id }
Store.put_text id_file, id
# claim successful
authenticate!
end
Expand All @@ -154,8 +155,7 @@ def oops status, message
content_type 'image/png'
cross_origin
local = File.join farm_status, 'favicon.png'
Favicon.create local unless File.exists? local
File.read local
Store.get_blob(local) || Store.put_blob(local, Favicon.create_blob)
end

get '/random.png' do
Expand All @@ -165,9 +165,8 @@ def oops status, message
end

content_type 'image/png'
local = File.join farm_status, 'favicon.png'
Favicon.create local
File.read local
path = File.join farm_status, 'favicon.png'
Store.put_blob path, Favicon.create_blob
end

get '/' do
Expand Down Expand Up @@ -206,21 +205,21 @@ def oops status, message
content_type 'application/json'
cross_origin
bins = Hash.new {|hash, key| hash[key] = Array.new}
Dir.chdir(farm_page.directory) do
Dir.glob("*").collect do |slug|
dt = Time.now - File.new(slug).mtime
bins[(dt/=60)<1?'Minute':(dt/=60)<1?'Hour':(dt/=24)<1?'Day':(dt/=7)<1?'Week':(dt/=4)<1?'Month':(dt/=3)<1?'Season':(dt/=4)<1?'Year':'Forever']<<slug
end

pages = Store.recently_changed_pages farm_page.directory
pages.each do |page|
dt = Time.now - page['updated_at']
bins[(dt/=60)<1?'Minute':(dt/=60)<1?'Hour':(dt/=24)<1?'Day':(dt/=7)<1?'Week':(dt/=4)<1?'Month':(dt/=3)<1?'Season':(dt/=4)<1?'Year':'Forever']<<page
end

story = []
['Minute', 'Hour', 'Day', 'Week', 'Month', 'Season', 'Year'].each do |key|
next unless bins[key].length>0
story << {'type' => 'paragraph', 'text' => "<h3>Within a #{key}</h3>", 'id' => RandomId.generate}
bins[key].each do |slug|
page = farm_page.get(slug)
next if page['story'].length == 0
bins[key].each do |page|
next if page['story'].empty?
site = "#{request.host}#{request.port==80 ? '' : ':'+request.port.to_s}"
story << {'type' => 'federatedWiki', 'site' => site, 'slug' => slug, 'title' => page['title'], 'text' => "", 'id' => RandomId.generate}
story << {'type' => 'federatedWiki', 'site' => site, 'slug' => page['name'], 'title' => page['title'], 'text' => "", 'id' => RandomId.generate}
end
end
page = {'title' => 'Recent Changes', 'story' => story}
Expand Down Expand Up @@ -266,7 +265,7 @@ def oops status, message
get %r{^/([a-z0-9-]+)\.json$} do |name|
content_type 'application/json'
cross_origin
halt 404 unless File.exists? "#{farm_page.directory}/#{name}" or File.exists? "#{farm_page.default_directory}/#{name}"
halt 404 unless Store.exists?("#{farm_page.directory}/#{name}") || Store.exists?("#{farm_page.default_directory}/#{name}")
JSON.pretty_generate(farm_page.get(name))
end

Expand Down
3 changes: 3 additions & 0 deletions server/sinatra/stores/all.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
require File.expand_path('store', File.dirname(__FILE__))
require File.expand_path('file', File.dirname(__FILE__))
require File.expand_path('couch', File.dirname(__FILE__))
Loading