picnic 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/History.txt +10 -0
  2. data/Manifest.txt +30 -13
  3. data/Rakefile +2 -0
  4. data/lib/picnic.rb +5 -120
  5. data/lib/picnic/authentication.rb +40 -4
  6. data/lib/picnic/cli.rb +86 -43
  7. data/lib/picnic/conf.rb +45 -43
  8. data/lib/picnic/logger.rb +41 -0
  9. data/lib/picnic/server.rb +99 -0
  10. data/lib/picnic/version.rb +2 -2
  11. data/picnic.gemspec +44 -0
  12. data/vendor/{camping-1.5.180 → camping-2.0.20090212}/CHANGELOG +17 -10
  13. data/vendor/{camping-1.5.180 → camping-2.0.20090212}/COPYING +0 -0
  14. data/vendor/{camping-1.5.180 → camping-2.0.20090212}/README +2 -2
  15. data/vendor/{camping-1.5.180 → camping-2.0.20090212}/Rakefile +62 -5
  16. data/vendor/camping-2.0.20090212/bin/camping +99 -0
  17. data/vendor/camping-2.0.20090212/doc/camping.1.gz +0 -0
  18. data/vendor/camping-2.0.20090212/examples/README +5 -0
  19. data/vendor/camping-2.0.20090212/examples/blog.rb +375 -0
  20. data/vendor/camping-2.0.20090212/examples/campsh.rb +629 -0
  21. data/vendor/camping-2.0.20090212/examples/tepee.rb +242 -0
  22. data/vendor/camping-2.0.20090212/extras/Camping.gif +0 -0
  23. data/vendor/camping-2.0.20090212/extras/flipbook_rdoc.rb +491 -0
  24. data/vendor/camping-2.0.20090212/extras/permalink.gif +0 -0
  25. data/vendor/{camping-1.5.180 → camping-2.0.20090212}/lib/camping-unabridged.rb +168 -294
  26. data/vendor/camping-2.0.20090212/lib/camping.rb +54 -0
  27. data/vendor/{camping-1.5.180/lib/camping/db.rb → camping-2.0.20090212/lib/camping/ar.rb} +4 -4
  28. data/vendor/{camping-1.5.180/lib/camping → camping-2.0.20090212/lib/camping/ar}/session.rb +23 -14
  29. data/vendor/camping-2.0.20090212/lib/camping/mab.rb +26 -0
  30. data/vendor/camping-2.0.20090212/lib/camping/reloader.rb +163 -0
  31. data/vendor/camping-2.0.20090212/lib/camping/server.rb +158 -0
  32. data/vendor/camping-2.0.20090212/lib/camping/session.rb +74 -0
  33. data/vendor/camping-2.0.20090212/setup.rb +1551 -0
  34. data/vendor/camping-2.0.20090212/test/apps/env_debug.rb +65 -0
  35. data/vendor/camping-2.0.20090212/test/apps/forms.rb +95 -0
  36. data/vendor/camping-2.0.20090212/test/apps/misc.rb +86 -0
  37. data/vendor/camping-2.0.20090212/test/apps/sessions.rb +38 -0
  38. data/vendor/camping-2.0.20090212/test/test_camping.rb +54 -0
  39. metadata +43 -16
  40. data/lib/picnic/postambles.rb +0 -292
  41. data/lib/picnic/utils.rb +0 -36
  42. data/vendor/camping-1.5.180/lib/camping.rb +0 -57
  43. data/vendor/camping-1.5.180/lib/camping/fastcgi.rb +0 -244
  44. data/vendor/camping-1.5.180/lib/camping/reloader.rb +0 -163
  45. data/vendor/camping-1.5.180/lib/camping/webrick.rb +0 -65
@@ -0,0 +1,54 @@
1
+ %w[uri stringio rack].map{|l|require l};class Object;def meta_def m,&b
2
+ (class<<self;self end).send:define_method,m,&b end end;module Camping;C=self
3
+ S=IO.read(__FILE__)rescue nil;P="<h1>Cam\ping Problem!</h1><h2>%s</h2>"
4
+ U=Rack::Utils;Apps=[];class H<Hash
5
+ def method_missing m,*a;m.to_s=~/=$/?self[$`]=a[0]:a==[]?self[m.to_s]:super end
6
+ undef id,type;end;module Helpers;def R c,*g
7
+ p,h=/\(.+?\)/,g.grep(Hash);g-=h;raise"bad route"unless u=c.urls.find{|x|
8
+ break x if x.scan(p).size==g.size&&/^#{x}\/?$/=~(x=g.inject(x){|x,a|
9
+ x.sub p,U.escape((a[a.class.primary_key]rescue a))})}
10
+ h.any?? u+"?"+U.build_query(h[0]):u end;def / p
11
+ p[0]==?/?@root+p:p end;def URL c='/',*a;c=R(c, *a) if c.respond_to?:urls
12
+ c=self/c;c=@request.url[/.{8,}?(?=\/)/]+c if c[0]==?/;URI c end
13
+ end;module Base;attr_accessor:input,:cookies,:headers,:body,:status,:root
14
+ def render v,*a,&b;mab(/^_/!~v.to_s){send(v,*a,&b)} end
15
+ def mab l=nil,&b;m=Mab.new({},self);s=m.capture(&b)
16
+ s=m.capture{layout{s}} if l && m.respond_to?(:layout);s end
17
+ def r s,b,h={};b,h=h,b if Hash===b;@status=s;
18
+ @headers.merge!(h);@body=b;end;def redirect *a;r 302,'','Location'=>URL(*a).
19
+ to_s;end;def r404 p=env.PATH;r 404,P%"#{p} not found"end;def r500 k,m,x
20
+ r 500,P%"#{k}.#{m}"+"<h3>#{x.class} #{x.message}: <ul>#{x.
21
+ backtrace.map{|b|"<li>#{b}</li>"}}</ul></h3>"end;def r501 m=@method
22
+ r 501,P%"#{m.upcase} not implemented"end;def to_a
23
+ @response.body=@body.respond_to?(:each)?@body:""
24
+ @response.status=@status;@response.headers.merge!(@headers)
25
+ @cookies.each{|k,v|v={:value=>v,:path=>self/"/"} if String===v
26
+ @response.set_cookie(k,v) if @request.cookies[k]!=v}
27
+ @response.to_a;end;def initialize(env)
28
+ @request,@response,@env=Rack::Request.new(env),Rack::Response.new,env
29
+ @root,@input,@cookies,@headers,@status=
30
+ @env.SCRIPT_NAME.sub(/\/$/,''),H[@request.params],
31
+ H[@request.cookies],@response.headers,@response.status
32
+ @input.each{|k,v|if k[-2..-1]=="[]";@input[k[0..-3]]=
33
+ @input.delete(k)elsif k=~/(.*)\[([^\]]+)\]$/
34
+ (@input[$1]||=H[])[$2]=@input.delete(k)end};end;def service *a
35
+ r=catch(:halt){send(@env.REQUEST_METHOD.downcase,*a)};@body||=r
36
+ self;end;end;module Controllers;@r=[];class<<self;def r;@r end;def R *u;r=@r
37
+ Class.new{meta_def(:urls){u};meta_def(:inherited){|x|r<<x}}end
38
+ def D p,m;p='/'if !p||!p[0]
39
+ r.map{|k|k.urls.map{|x|return(k.instance_method(m)rescue nil)?
40
+ [k,m,*$~[1..-1]]:[I,'r501',m]if p=~/^#{x}\/?$/}};[I,'r404',p] end
41
+ N=H.new{|_,x|x.downcase}.merge! "N"=>'(\d+)',"X"=>'(\w+)',"Index"=>''
42
+ def M;def M;end;constants.map{|c|k=const_get(c)
43
+ k.send:include,C,Base,Helpers,Models;@r=[k]+r if r-[k]==r
44
+ k.meta_def(:urls){["/#{c.scan(/.[^A-Z]*/).map(&N.method(:[]))*'/'}"]
45
+ }if !k.respond_to?:urls}end end;class I<R()
46
+ end; end;X=Controllers;class<<self;def goes m
47
+ Apps<<eval(S.gsub(/Camping/,m.to_s),TOPLEVEL_BINDING) end;def call(
48
+ e)X.M;e=H[e.to_hash];k,m,*a=X.D e.PATH_INFO,(e.REQUEST_METHOD||'get').downcase
49
+ e.REQUEST_METHOD=m;k.new(e).service(*a).to_a;end
50
+ def method_missing m,c,*a;X.M;h=Hash===a[-1]?H[a.pop]:{};e=
51
+ H[h[:env]||{}].merge!({'rack.input'=>StringIO.new,'REQUEST_METHOD'=>m.to_s})
52
+ k=X.const_get(c).new(H[e]);k.send("input=",h[:input])if h[:input]
53
+ k.service(*a);end;end;module Views;include X,Helpers end;module Models
54
+ autoload:Base,'camping/ar';def Y;self;end end;autoload:Mab,'camping/mab';C end
@@ -71,8 +71,8 @@ module Camping
71
71
  module_eval $AR_EXTRAS
72
72
  end
73
73
  end
74
- Camping::S.sub! "autoload:Base,'camping/db'", ""
75
- Camping::S.sub! "def Y;self;end", $AR_EXTRAS
76
- Camping::Apps.each do |app|
77
- app::Models.module_eval $AR_EXTRAS
74
+ Camping::S.sub! /autoload\s*:Base\s*,\s*['"]camping\/ar['"]/, ""
75
+ Camping::S.sub! /def\s*Y[;\s]*self[;\s]*end/, $AR_EXTRAS
76
+ Camping::Apps.each do |c|
77
+ c::Models.module_eval $AR_EXTRAS
78
78
  end
@@ -1,22 +1,25 @@
1
- # == About camping/session.rb
1
+ # == About camping/ar/session.rb
2
2
  #
3
3
  # This file contains two modules which supply basic sessioning to your Camping app.
4
4
  # Again, we're dealing with a pretty little bit of code: approx. 60 lines.
5
5
  #
6
6
  # * Camping::Models::Session is a module which adds a single <tt>sessions</tt> table
7
7
  # to your database.
8
- # * Camping::Session is a module which you will mix into your application (or into
8
+ # * Camping::ARSession is a module which you will mix into your application (or into
9
9
  # specific controllers which require sessions) to supply a <tt>@state</tt> variable
10
10
  # you can use in controllers and views.
11
11
  #
12
- # For a basic tutorial, see the *Getting Started* section of the Camping::Session module.
12
+ # For a basic tutorial, see the *Getting Started* section of the Camping::ARSession module.
13
13
  require 'camping'
14
+ require 'camping/ar'
14
15
 
15
16
  module Camping::Models
16
17
  # A database table for storing Camping sessions. Contains a unique 32-character hashid, a
17
18
  # creation timestamp, and a column of serialized data called <tt>ivars</tt>.
18
19
  class Session < Base
19
20
  serialize :ivars
21
+ set_primary_key :hashid
22
+
20
23
  def []=(k, v) # :nodoc:
21
24
  self.ivars[k] = v
22
25
  end
@@ -24,13 +27,17 @@ class Session < Base
24
27
  self.ivars[k] rescue nil
25
28
  end
26
29
 
30
+ protected
27
31
  RAND_CHARS = [*'A'..'Z'] + [*'0'..'9'] + [*'a'..'z']
32
+ def before_create
33
+ rand_max = RAND_CHARS.size
34
+ sid = (0...32).inject("") { |ret,_| ret << RAND_CHARS[rand(rand_max)] }
35
+ write_attribute('hashid', sid)
36
+ end
28
37
 
29
38
  # Generates a new session ID and creates a row for the new session in the database.
30
39
  def self.generate cookies
31
- rand_max = RAND_CHARS.size
32
- sid = (0...32).inject("") { |ret,_| ret << RAND_CHARS[rand(rand_max)] }
33
- sess = Session.create :hashid => sid, :ivars => Camping::H[]
40
+ sess = Session.create :ivars => Camping::H[]
34
41
  cookies.camping_sid = sess.hashid
35
42
  sess
36
43
  end
@@ -38,6 +45,7 @@ class Session < Base
38
45
  # Gets the existing session based on the <tt>camping_sid</tt> available in cookies.
39
46
  # If none is found, generates a new session.
40
47
  def self.persist cookies
48
+ session = nil
41
49
  if cookies.camping_sid
42
50
  session = Camping::Models::Session.find_by_hashid cookies.camping_sid
43
51
  end
@@ -62,21 +70,22 @@ class Session < Base
62
70
  def self.create_schema
63
71
  unless table_exists?
64
72
  ActiveRecord::Schema.define do
65
- create_table :sessions, :force => true do |t|
66
- #t.column :id, :integer, :null => false
67
- t.column :hashid, :string, :limit => 32
73
+ create_table :sessions, :force => true, :id => false do |t|
74
+ t.column :hashid, :string, :limit => 32, :null => false
68
75
  t.column :created_at, :datetime
69
76
  t.column :ivars, :text
70
77
  end
78
+ add_index :sessions, [:hashid], :unique => true
71
79
  end
72
80
  reset_column_information
73
81
  end
74
82
  end
75
83
  end
84
+ Session.partial_updates = false if Session.respond_to?(:partial_updates=)
76
85
  end
77
86
 
78
87
  module Camping
79
- # The Camping::Session module is designed to be mixed into your application or into specific
88
+ # The Camping::ARSession module is designed to be mixed into your application or into specific
80
89
  # controllers which require sessions. This module defines a <tt>service</tt> method which
81
90
  # intercepts all requests handed to those controllers.
82
91
  #
@@ -85,7 +94,7 @@ module Camping
85
94
  # To get sessions working for your application:
86
95
  #
87
96
  # 1. <tt>require 'camping/session'</tt>
88
- # 2. Mixin the module: <tt>module YourApp; include Camping::Session end</tt>
97
+ # 2. Mixin the module: <tt>module YourApp; include Camping::ARSession end</tt>
89
98
  # 3. In your application's <tt>create</tt> method, add a call to <tt>Camping::Models::Session.create_schema</tt>
90
99
  # 4. Throughout your application, use the <tt>@state</tt> var like a hash to store your application's data.
91
100
  #
@@ -99,7 +108,7 @@ module Camping
99
108
  # * All mounted Camping apps using this class will use the same database table.
100
109
  # * However, your application's data is stored in its own hash.
101
110
  # * Session data is only saved if it has changed.
102
- module Session
111
+ module ARSession
103
112
  # This <tt>service</tt> method, when mixed into controllers, intercepts requests
104
113
  # and wraps them with code to start and close the session. If a session isn't found
105
114
  # in the database it is created. The <tt>@state</tt> variable is set and if it changes,
@@ -109,7 +118,8 @@ module Session
109
118
  app = self.class.name.gsub(/^(\w+)::.+$/, '\1')
110
119
  @state = (session[app] ||= Camping::H[])
111
120
  hash_before = Marshal.dump(@state).hash
112
- s = super(*a)
121
+ return super(*a)
122
+ ensure
113
123
  if session
114
124
  hash_after = Marshal.dump(@state).hash
115
125
  unless hash_before == hash_after
@@ -117,7 +127,6 @@ module Session
117
127
  session.save
118
128
  end
119
129
  end
120
- s
121
130
  end
122
131
  end
123
132
  end
@@ -0,0 +1,26 @@
1
+ class MissingLibrary < Exception #:nodoc: all
2
+ end
3
+ begin
4
+ require 'markaby'
5
+ rescue LoadError => e
6
+ raise MissingLibrary, "Markaby could not be loaded (is it installed?): #{e.message}"
7
+ end
8
+
9
+ $MAB_CODE = %{
10
+ # The Mab class wraps Markaby, allowing it to run methods from Camping::Views
11
+ # and also to replace :href, :action and :src attributes in tags by prefixing the root
12
+ # path.
13
+ class Mab < Markaby::Builder
14
+ include Views
15
+ def tag!(*g,&b)
16
+ h=g[-1]
17
+ [:href,:action,:src].map{|a|(h[a]&&=self/h[a])rescue 0}
18
+ super
19
+ end
20
+ end
21
+ }
22
+
23
+ Camping::S.sub! /autoload\s*:Mab\s*,\s*['"]camping\/mab['"]/, $MAB_CODE
24
+ Camping::Apps.each do |c|
25
+ c.module_eval $MAB_CODE
26
+ end
@@ -0,0 +1,163 @@
1
+ module Camping
2
+ # == The Camping Reloader
3
+ #
4
+ # Camping apps are generally small and predictable. Many Camping apps are
5
+ # contained within a single file. Larger apps are split into a handful of
6
+ # other Ruby libraries within the same directory.
7
+ #
8
+ # Since Camping apps (and their dependencies) are loaded with Ruby's require
9
+ # method, there is a record of them in $LOADED_FEATURES. Which leaves a
10
+ # perfect space for this class to manage auto-reloading an app if any of its
11
+ # immediate dependencies changes.
12
+ #
13
+ # == Wrapping Your Apps
14
+ #
15
+ # Since bin/camping and the Camping::FastCGI class already use the Reloader,
16
+ # you probably don't need to hack it on your own. But, if you're rolling your
17
+ # own situation, here's how.
18
+ #
19
+ # Rather than this:
20
+ #
21
+ # require 'yourapp'
22
+ #
23
+ # Use this:
24
+ #
25
+ # require 'camping/reloader'
26
+ # Camping::Reloader.new('/path/to/yourapp.rb')
27
+ #
28
+ # The reloader will take care of requiring the app and monitoring all files
29
+ # for alterations.
30
+ class Reloader
31
+ attr_accessor :klass, :mtime, :mount, :requires
32
+
33
+ # Creates the reloader, assigns a +script+ to it and initially loads the
34
+ # application. Pass in the full path to the script, otherwise the script
35
+ # will be loaded relative to the current working directory.
36
+ def initialize(script)
37
+ @script = File.expand_path(script)
38
+ @mount = File.basename(script, '.rb')
39
+ @requires = nil
40
+ load_app
41
+ end
42
+
43
+ # Find the application, based on the script name.
44
+ def find_app(title)
45
+ @klass = Object.const_get(Object.constants.grep(/^#{title}$/i)[0]) rescue nil
46
+ end
47
+
48
+ # If the file isn't found, if we need to remove the app from the global
49
+ # namespace, this will be sure to do so and set @klass to nil.
50
+ def remove_app
51
+ if @klass
52
+ Camping::Apps.delete(@klass)
53
+ Object.send :remove_const, @klass.name
54
+ @klass = nil
55
+ end
56
+ end
57
+
58
+ # Loads (or reloads) the application. The reloader will take care of calling
59
+ # this for you. You can certainly call it yourself if you feel it's warranted.
60
+ def load_app
61
+ title = File.basename(@script)[/^([\w_]+)/,1].gsub /_/,''
62
+ begin
63
+ all_requires = $LOADED_FEATURES.dup
64
+ load @script
65
+ @requires = ($LOADED_FEATURES - all_requires).select do |req|
66
+ req.index(File.basename(@script) + "/") == 0 || req.index(title + "/") == 0
67
+ end
68
+ rescue Exception => e
69
+ puts "!! trouble loading #{title.inspect}: [#{e.class}] #{e.message}"
70
+ puts e.backtrace.join("\n")
71
+ find_app title
72
+ remove_app
73
+ return
74
+ end
75
+
76
+ @mtime = mtime
77
+ find_app title
78
+ unless @klass and @klass.const_defined? :C
79
+ puts "!! trouble loading #{title.inspect}: not a Camping app, no #{title.capitalize} module found"
80
+ remove_app
81
+ return
82
+ end
83
+
84
+ Reloader.conditional_connect
85
+ @klass.create if @klass.respond_to? :create
86
+ puts "** #{title.inspect} app loaded"
87
+ @klass
88
+ end
89
+
90
+ # The timestamp of the most recently modified app dependency.
91
+ def mtime
92
+ ((@requires || []) + [@script]).map do |fname|
93
+ fname = fname.gsub(/^#{Regexp::quote File.dirname(@script)}\//, '')
94
+ begin
95
+ File.mtime(File.join(File.dirname(@script), fname))
96
+ rescue Errno::ENOENT
97
+ remove_app
98
+ @mtime
99
+ end
100
+ end.reject{|t| t > Time.now }.max
101
+ end
102
+
103
+ # Conditional reloading of the app. This gets called on each request and
104
+ # only reloads if the modification times on any of the files is updated.
105
+ def reload_app
106
+ return if @klass and @mtime and mtime <= @mtime
107
+
108
+ if @requires
109
+ @requires.each { |req| $LOADED_FEATURES.delete(req) }
110
+ end
111
+ remove_app
112
+ load_app
113
+ end
114
+
115
+ # Conditionally reloads (using reload_app.) Then passes the request through
116
+ # to the wrapped Camping app.
117
+ def call(*a)
118
+ reload_app
119
+ if @klass
120
+ @klass.call(*a)
121
+ else
122
+ Camping.call(*a)
123
+ end
124
+ end
125
+
126
+ # Returns source code for the main script in the application.
127
+ def view_source
128
+ File.read(@script)
129
+ end
130
+
131
+ class << self
132
+ attr_writer :database, :log
133
+
134
+ def conditional_connect
135
+ # If database models are present, `autoload?` will return nil.
136
+ unless Camping::Models.autoload? :Base
137
+ if @database and @database[:adapter] == 'sqlite3'
138
+ begin
139
+ require 'sqlite3_api'
140
+ rescue LoadError
141
+ puts "!! Your SQLite3 adapter isn't a compiled extension."
142
+ abort "!! Please check out http://code.whytheluckystiff.net/camping/wiki/BeAlertWhenOnSqlite3 for tips."
143
+ end
144
+ end
145
+
146
+ case @log
147
+ when Logger
148
+ Camping::Models::Base.logger = @log
149
+ when String
150
+ require 'logger'
151
+ Camping::Models::Base.logger = Logger.new(@log == "-" ? STDOUT : @log)
152
+ end
153
+
154
+ Camping::Models::Base.establish_connection @database if @database
155
+
156
+ if Camping::Models.const_defined?(:Session)
157
+ Camping::Models::Session.create_schema
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,158 @@
1
+ require 'irb'
2
+ require 'rack'
3
+ require 'camping/reloader'
4
+
5
+ module Camping::Server
6
+ class Base < Hash
7
+ include Enumerable
8
+
9
+ attr_reader :paths
10
+ attr_accessor :conf
11
+
12
+ def initialize(conf, paths = [])
13
+ unless conf.database
14
+ raise "!! No home directory found. Please specify a database file, see --help."
15
+ end
16
+
17
+ @conf = conf
18
+ Camping::Reloader.database = conf.database
19
+ Camping::Reloader.log = conf.log
20
+
21
+ @paths = []
22
+ paths.each { |script| add_app script }
23
+ # TODO exception instead of abort()
24
+ # abort("** No apps successfully loaded") unless self.detect { |app| app.klass }
25
+
26
+ end
27
+
28
+ def add_app(path)
29
+ @paths << path
30
+ if File.directory? path
31
+ Dir[File.join(path, '*.rb')].each { |s| insert_app(s)}
32
+ else
33
+ insert_app(path)
34
+ end
35
+ # TODO check to see if the application is created or not... exception perhaps?
36
+ end
37
+
38
+ def find_new_scripts
39
+ self.values.each { |app| app.reload_app }
40
+ @paths.each do |path|
41
+ Dir[File.join(path, '*.rb')].each do |script|
42
+ smount = File.basename(script, '.rb')
43
+ next if detect { |x| x.mount == smount }
44
+
45
+ puts "** Discovered new #{script}"
46
+ # TODO hmm. the next should be handled by the add_app thingy
47
+ app = insert_app(script)
48
+ next unless app
49
+
50
+ yield app
51
+
52
+ end
53
+ end
54
+ self.values.sort! { |x, y| x.mount <=> y.mount }
55
+ end
56
+ def index_page
57
+ welcome = "You are Camping"
58
+ apps = self
59
+ <<-HTML
60
+ <html>
61
+ <head>
62
+ <title>#{welcome}</title>
63
+ <style type="text/css">
64
+ body {
65
+ font-family: verdana, arial, sans-serif;
66
+ padding: 10px 40px;
67
+ margin: 0;
68
+ }
69
+ h1, h2, h3, h4, h5, h6 {
70
+ font-family: utopia, georgia, serif;
71
+ }
72
+ </style>
73
+ </head>
74
+ <body>
75
+ <h1>#{welcome}</h1>
76
+ <p>Good day. These are the Camping apps you've mounted.</p>
77
+ <ul>
78
+ #{apps.values.select{|app|app.klass}.map do |app|
79
+ "<li><h3 style=\"display: inline\"><a href=\"/#{app.mount}\">#{app.klass.name}</a></h3><small> / <a href=\"/code/#{app.mount}\">View source</a></small></li>"
80
+ end.join("\n")}
81
+ </ul>
82
+ </body>
83
+ </html>
84
+ HTML
85
+ end
86
+
87
+ def each(&b)
88
+ self.values.each(&b)
89
+ end
90
+
91
+ # for RSpec tests
92
+ def apps
93
+ self.values
94
+ end
95
+
96
+ def start
97
+ handler, conf = case @conf.server
98
+ when "console"
99
+ ARGV.clear
100
+ IRB.start
101
+ exit
102
+ when "mongrel"
103
+ puts "** Starting Mongrel on #{@conf.host}:#{@conf.port}"
104
+ [Rack::Handler::Mongrel, {:Port => @conf.port, :Host => @conf.host}]
105
+ when "webrick"
106
+ [Rack::Handler::WEBrick, {:Port => @conf.port, :BindAddress => @conf.host}]
107
+ end
108
+
109
+ rapp = if apps.length > 1
110
+ hash = {
111
+ "/" => proc {|env|[200,{'Content-Type'=>'text/html'},index_page]}
112
+ }
113
+ apps.each do |app|
114
+ hash["/#{app.mount}"] = app
115
+ hash["/code/#{app.mount}"] = proc do |env|
116
+ [200,{'Content-Type'=>'text/plain'},app.view_source]
117
+ end
118
+ end
119
+ Rack::URLMap.new(hash)
120
+ else
121
+ apps.first
122
+ end
123
+ rapp = Rack::Lint.new(rapp)
124
+ rapp = XSendfile.new(rapp)
125
+ rapp = Rack::ShowExceptions.new(rapp)
126
+ handler.run(rapp, conf)
127
+ end
128
+
129
+ private
130
+
131
+ def insert_app(script)
132
+ self[script] = Camping::Reloader.new(script)
133
+ end
134
+ end
135
+
136
+ # A Rack middleware for reading X-Sendfile. Should only be used in
137
+ # development.
138
+ class XSendfile
139
+
140
+ HEADERS = [
141
+ "X-Sendfile",
142
+ "X-Accel-Redirect",
143
+ "X-LIGHTTPD-send-file"
144
+ ]
145
+
146
+ def initialize(app)
147
+ @app = app
148
+ end
149
+
150
+ def call(env)
151
+ status, headers, body = @app.call(env)
152
+ if path = headers.values_at(*HEADERS).compact.first
153
+ body = File.read(path)
154
+ end
155
+ [status, headers, body]
156
+ end
157
+ end
158
+ end