camping 1.4.2 → 1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,51 +1,52 @@
1
- %w[rubygems active_record markaby metaid tempfile uri].each{|l|require l}
2
- module Camping;C=self;F=__FILE__;S=IO.read(F).gsub(/_+FILE_+/,F.dump)
3
- module Helpers;def R c,*args;p=/\(.+?\)/;args.inject(c.urls.find{|x|x.scan(p).
4
- size==args.size}.dup){|str,a|str.sub(p,(a.__send__(a.class.primary_key)rescue
5
- a).to_s)} end;def URL c='/',*a;c=R(c,*a)if c.respond_to?:urls;c=self/c;c=
6
- "//"+@env.HTTP_HOST+c if c[/^\//];URI(c) end;def / p;p[/^\//]?@root+p:p end
7
- def errors_for o;ul.errors{o.errors.each_full{|x|li x}}if o.errors.any? end end
8
- module Base;include Helpers;attr_accessor :input,:cookies,:env,:headers,:body,
9
- :status,:root;def method_missing m,*a,&b;s=m==:render ? markaview(*a,&b):eval(
10
- "markaby.#{m}(*a,&b)");s=markaview(:layout){s} if Views.method_defined?:layout
11
- r 200,s.to_s end;def r s,b,h={};@status=s;@headers.merge!(h);@body=b end;def
12
- redirect *a;r 302,'','Location'=>URL(*a) end;def initialize r,e,m;e=H[e.to_hash]
13
- @status,@method,@env,@headers,@root=200,m.downcase,e,{'Content-Type'=>"text/htm\
14
- l"},e.SCRIPT_NAME.sub(/\/$/,'');@k=C.kp e.HTTP_COOKIE;q=C.qs_parse e.QUERY_STRING
15
- @in=r;if %r|\Amultipart/form-.*boundary=\"?([^\";,]+)|n.match e.CONTENT_TYPE;b=
16
- /(?:\r?\n|\A)#{Regexp::quote("--#$1")}(?:--)?\r$/;until @in.eof?;fh=H[];for l in
17
- @in;case l;when "\r\n":break;when /^Content-Disposition: form-data;/:fh.u H[*$'.
18
- scan(/(?:\s(\w+)="([^"]+)")/).flatten];when /^Content-Type: (.+?)(\r$|\Z)/m;fh[
1
+ %w[active_support markaby tempfile uri].map{|l|require l}
2
+ module Camping;Apps=[];C=self;S=IO.read(__FILE__).sub(/S=I.+$/,'')
3
+ P="Cam\ping Problem!";module Helpers;def R c,*g;p=/\(.+?\)/;g.inject(c.
4
+ urls.find{|x|x.scan(p).size==g.size}.dup){|s,a|s.sub p,C.escape((a[
5
+ a.class.primary_key]rescue a))}end;def URL c='/',*a;c=R(c,*a)if c.
6
+ respond_to?:urls;c=self/c;c="//"+@env.HTTP_HOST+c if c[/^\//];URI(c)end;def/p
7
+ p[/^\//]?@root+p : p end;def errors_for o;ul.errors{o.errors.each_full{|x|li x}
8
+ }if o.errors.any?end end;module Base;include Helpers;attr_accessor:input,
9
+ :cookies,:env,:headers,:body,:status,:root;def method_missing*a,&b
10
+ a.shift if a[0]==:render;m=Mab.new({},self);s=m.capture{send(*a,&b)};s=m.layout{s}if
11
+ /^_/!~a[0].to_s and m.respond_to?:layout;s end;def r s,b,h={};@status=s;@headers.
12
+ merge!h;@body=b end;def redirect*a;r 302,'','Location'=>URL(*a)end;Z="\r\n"
13
+ def initialize r,e,m;e=H[e.to_hash];@status,@method,@env,@headers,@root=200,m.
14
+ downcase,e,{'Content-Type'=>"text/html"},e.SCRIPT_NAME.sub(/\/$/,'')
15
+ @k=C.kp e.HTTP_COOKIE;q=C.qs_parse e.QUERY_STRING;@in=r
16
+ if%r|\Amultipart/form-.*boundary=\"?([^\";,]+)|n.match e.CONTENT_TYPE
17
+ b=/(?:\r?\n|\A)#{Regexp::quote("--#$1")}(?:--)?\r$/;until@in.eof?;fh=H[];for l in@in
18
+ case l;when Z;break;when/^Content-D.+?: form-data;/;fh.u H[*$'.
19
+ scan(/(?:\s(\w+)="([^"]+)")/).flatten];when/^Content-Type: (.+?)(\r$|\Z)/m;fh[
19
20
  :type]=$1;end;end;fn=fh[:name];o=if fh[:filename];o=fh[:tempfile]=Tempfile.new(:C)
20
- o.binmode;else;fh="";end;while l=@in.read(16384);if l=~b;o<<$`.chomp;@in.seek(-$'.
21
+ o.binmode;else;fh=""end;while l=@in.read(16384);if l=~b;o<<$`.chomp;@in.seek(-$'.
21
22
  size,IO::SEEK_CUR);break;end;o<<l;end;q[fn]=fh if fn;fh[:tempfile].rewind if
22
- fh.is_a?H;end;elsif @method=="post";q.u C.qs_parse(@in.read) end;@cookies,@input=
23
- @k.dup,q.dup end;def service *a;@body=send(@method,*a)if respond_to?@method
24
- @headers["Set-Cookie"]=@cookies.map{|k,v|"#{k}=#{C.escape(v)}; path=#{self/"/"}\
25
- " if v != @k[k]}.compact;self end;def to_s;"Status: #{@status}\n#{@headers.map{
26
- |k,v|[*v].map{|x|"#{k}: #{x}"}*"\n"}*"\n"}\n\n#{@body}" end;def markaby;Mab.new(
27
- instance_variables.map{|iv|[iv[1..-1],instance_variable_get(iv)]}) end;def
28
- markaview m,*a,&b;h=markaby;h.send m,*a,&b;h.to_s end end;class R;include Base
29
- end;module Controllers;class NotFound;def get p;r(404,div{h1 "Cam\ping Problem!"
30
- h2 p+" not found"}) end end;class ServerError;include Base;def get k,m,e;r(500,
31
- Mab.new{h1 "Cam\ping Problem!";h2 "#{k}.#{m}";h3 "#{e.class} #{e.message}:";ul{
32
- e.backtrace.each{|bt|li(bt)}}}.to_s)end end;class<<self;def R *urls;Class.new(
33
- R){meta_def(:urls){urls}}end;def D path;constants.inject(nil){|d,c|k=const_get c
34
- k.meta_def(:urls){["/#{c.downcase}"]}if !(k<R);d||([k,$~[1..-1]]if k.urls.find{
35
- |x|path=~/^#{x}\/?$/})}||[NotFound,[path]] end end end;class<<self;def goes m
36
- eval(S.gsub(/Camping/,m.to_s),TOPLEVEL_BINDING)end;def escape s;s.to_s.gsub(
37
- /([^ a-zA-Z0-9_.-]+)/n){'%'+$1.unpack('H2'*$1.size).join('%').upcase}.tr ' ','+'
38
- end;def un s;s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){[$1.delete('%'
39
- )].pack('H*')} end;def qs_parse q,d='&;';m=proc{|_,o,n|o.u(n,&m)rescue([*o
40
- ]<<n)};q.to_s.split(/[#{d}] */n).inject(H[]){|h,p|k,v=un(p).split('=',2)
41
- h.u k.split(/[\]\[]+/).reverse.inject(v){|x,i|H[i,x]},&m}end;def kp(s);c=
42
- qs_parse(s,';,')end;def run r=$stdin,e=ENV;k,a=Controllers.D un("/#{e['PATH_INFO']
43
- }".gsub(/\/+/,'/'));k.send:include,C,Base,Models;k.new(r,e,(m=e['REQUEST_METHOD'
44
- ]||"GET")).service *a;rescue=>x;Controllers::ServerError.new(r,e,'get').service(
45
- k,m,x)end end;module Views;include Controllers,Helpers end;module Models;A=
46
- ActiveRecord;Base=A::Base;def Base.table_name_prefix;"#{name[/^(\w+)/,1]}_".
47
- downcase.sub(/^(#{A}|camping)_/i,'')end end;class Mab<Markaby::Builder;include \
48
- Views;def tag! *g,&b;h=g[-1];[:href,:action,:src].map{|a|(h[a]=self/h[a])rescue
23
+ fh.is_a?H;end;elsif@method=="post";q.u C.qs_parse(@in.read)end;@cookies,@input=
24
+ @k.dup,q.dup end;def service*a;@body=send(@method,*a)if respond_to?@method
25
+ @headers["Set-Cookie"]=@cookies.map{|k,v|"#{k}=#{C.escape(v)}; path=#{self/'/'
26
+ }"if v!=@k[k]}-[nil];self end;def to_s;"Status: #{@status}#{Z+@headers.map{|k,v|
27
+ [*v].map{|x|"#{k}: #{x}"}}*Z+Z*2+@body}"end;end
28
+ X=module Controllers;@r=[];class<<self;def r;@r;end;def R*u;r=@r;Class.new{
29
+ meta_def(:urls){u};meta_def(:inherited){|x|r<<x}}end;def M;def M;end;constants.map{|c|
30
+ k=const_get(c);k.send:include,C,Base,Models;r[0,0]=k if !r.include?k;k.meta_def(
31
+ :urls){["/#{c.downcase}"]}if !k.respond_to?:urls}end;def D p;r.map{|k|k.urls.
32
+ map{|x|return k,$~[1..-1]if p=~/^#{x}\/?$/}};[NotFound,[p]]end end;class
33
+ NotFound<R();def get p;r(404,Mab.new{h1 P;h2 p+" not found"})end end;class
34
+ ServerError<R();def get k,m,e;r(500,Mab.new{h1 P;h2"#{k}.#{m}";h3"#{e.class
35
+ } #{e.message}:";ul{e.backtrace.each{|bt|li(bt)}}}.to_s)end end;self;end;class<<
36
+ self;def goes m;eval S.gsub(/Camping/,m.to_s).gsub("A\pps=[]","Cam\ping::Apps<<\
37
+ self"),TOPLEVEL_BINDING;end;def escape s;s.to_s.gsub(/[^ \w.-]+/n){'%'+($&.
38
+ unpack('H2'*$&.size)*'%').upcase}.tr(' ','+')end;def un s;s.tr('+',' ').gsub(
39
+ /%([\da-f]{2})/in){[$1].pack('H*')}end;def qs_parse q,d='&;';m=proc{|_,o,n|o.u(
40
+ n,&m)rescue([*o]<<n)};q.to_s.split(/[#{d}] */n).inject(H[]){|h,p|k,v=un(p).
41
+ split('=',2);h.u k.split(/[\]\[]+/).reverse.inject(v){|x,i|H[i,x]},&m}end;def
42
+ kp s;c=qs_parse(s,';,')end;def run r=$stdin,e=ENV;X.M;k,a=X.D un("/#{e[
43
+ 'PATH_INFO']}".gsub(/\/+/,'/'));k.new(r,e,(m=e['REQUEST_METHOD']||"GET")).Y.
44
+ service *a;rescue Exception=>x;X::ServerError.new(r,e,'get').service(k,m,x)end
45
+ def method_missing m,c,*a;X.M;k=X.const_get(c).new(StringIO.new,H['HTTP_HOST',
46
+ '','SCRIPT_NAME','','HTTP_COOKIE',''],m.to_s);H.new(a.pop).each{|e,f|k.send(
47
+ "#{e}=",f)}if Hash===a[-1];k.service *a;end;end;module Views;include X,Helpers
48
+ end;module Models;autoload:Base,'camping/db';def Y;self;end;end;class Mab<Markaby::Builder
49
+ include Views;def tag!*g,&b;h=g[-1];[:href,:action,:src].map{|a|(h[a]=self/h[a])rescue
49
50
  0};super end end;H=HashWithIndifferentAccess;class H;def method_missing m,*a
50
- if m.to_s=~/=$/;self[$`]=a[0];elsif a.empty?;self[m];else;raise NoMethodError,
51
- "#{m}" end end;alias_method:u,:regular_update;end end
51
+ m.to_s=~/=$/?self[$`]=a[0]:a==[]?self[m]:raise(NoMethodError,"#{m}")end
52
+ alias_method:u,:regular_update;end end
@@ -0,0 +1,78 @@
1
+ class MissingLibrary < Exception #:nodoc: all
2
+ end
3
+ begin
4
+ require 'active_record'
5
+ rescue LoadError => e
6
+ raise MissingLibrary, "ActiveRecord could not be loaded (is it installed?): #{e.message}"
7
+ end
8
+
9
+ $AR_EXTRAS = %{
10
+ Base = ActiveRecord::Base unless const_defined? :Base
11
+
12
+ def Y; ActiveRecord::Base.verify_active_connections!; self; end
13
+
14
+ class SchemaInfo < Base
15
+ end
16
+
17
+ def self.V(n)
18
+ @final = [n, @final.to_i].max
19
+ m = (@migrations ||= [])
20
+ Class.new(ActiveRecord::Migration) do
21
+ meta_def(:version) { n }
22
+ meta_def(:inherited) { |k| m << k }
23
+ end
24
+ end
25
+
26
+ def self.create_schema(opts = {})
27
+ opts[:assume] ||= 0
28
+ opts[:version] ||= @final
29
+ if @migrations
30
+ unless SchemaInfo.table_exists?
31
+ ActiveRecord::Schema.define do
32
+ create_table SchemaInfo.table_name do |t|
33
+ t.column :version, :float
34
+ end
35
+ end
36
+ end
37
+
38
+ si = SchemaInfo.find(:first) || SchemaInfo.new(:version => opts[:assume])
39
+ if si.version < opts[:version]
40
+ @migrations.each do |k|
41
+ k.migrate(:up) if si.version < k.version and k.version <= opts[:version]
42
+ k.migrate(:down) if si.version > k.version and k.version > opts[:version]
43
+ end
44
+ si.update_attributes(:version => opts[:version])
45
+ end
46
+ end
47
+ end
48
+ }
49
+
50
+ module Camping
51
+ module Models
52
+ A = ActiveRecord
53
+ # Base is an alias for ActiveRecord::Base. The big warning I'm going to give you
54
+ # about this: *Base overloads table_name_prefix.* This means that if you have a
55
+ # model class Blog::Models::Post, it's table name will be <tt>blog_posts</tt>.
56
+ #
57
+ # ActiveRecord is not loaded if you never reference this class. The minute you
58
+ # use the ActiveRecord or Camping::Models::Base class, then the ActiveRecord library
59
+ # is loaded.
60
+ Base = A::Base
61
+
62
+ # The default prefix for Camping model classes is the topmost module name lowercase
63
+ # and followed with an underscore.
64
+ #
65
+ # Tepee::Models::Page.table_name_prefix
66
+ # #=> "tepee_pages"
67
+ #
68
+ def Base.table_name_prefix
69
+ "#{name[/\w+/]}_".downcase.sub(/^(#{A}|camping)_/i,'')
70
+ end
71
+ module_eval $AR_EXTRAS
72
+ end
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
78
+ end
@@ -0,0 +1,198 @@
1
+ # == About camping/fastcgi.rb
2
+ #
3
+ # Camping works very well with FastCGI, since your application is only loaded
4
+ # once -- when FastCGI starts. In addition, this class lets you mount several
5
+ # Camping apps under a single FastCGI process, to help save memory costs.
6
+ #
7
+ # So where do you use the Camping::FastCGI class? Use it in your application's
8
+ # postamble and then you can point your web server directly at your application.
9
+ # See Camping::FastCGI docs for more.
10
+ require 'camping'
11
+ require 'fcgi'
12
+
13
+ module Camping
14
+ # Camping::FastCGI is a small class for hooking one or more Camping apps up to
15
+ # FastCGI. Generally, you'll use this class in your application's postamble.
16
+ #
17
+ # == The Smallest Example
18
+ #
19
+ # if __FILE__ == $0
20
+ # require 'camping/fastcgi'
21
+ # Camping::FastCGI.start(YourApp)
22
+ # end
23
+ #
24
+ # This example is stripped down to the basics. The postamble has no database
25
+ # connection. It just loads this class and calls Camping::FastCGI.start.
26
+ #
27
+ # Now, in Lighttpd or Apache, you can point to your app's file, which will
28
+ # be executed, only to discover that your app now speaks the FastCGI protocol.
29
+ #
30
+ # Here's a sample lighttpd.conf (tested with Lighttpd 1.4.11) to serve as example:
31
+ #
32
+ # server.port = 3044
33
+ # server.bind = "127.0.0.1"
34
+ # server.modules = ( "mod_fastcgi" )
35
+ # server.document-root = "/var/www/camping/blog/"
36
+ # server.errorlog = "/var/www/camping/blog/error.log"
37
+ #
38
+ # #### fastcgi module
39
+ # fastcgi.server = ( "/" => (
40
+ # "localhost" => (
41
+ # "socket" => "/tmp/camping-blog.socket",
42
+ # "bin-path" => "/var/www/camping/blog/blog.rb",
43
+ # "check-local" => "disable",
44
+ # "max-procs" => 1 ) ) )
45
+ #
46
+ # The file <tt>/var/www/camping/blog/blog.rb</tt> is the Camping app with
47
+ # the postamble.
48
+ #
49
+ # == Mounting Many Apps
50
+ #
51
+ # require 'camping/fastcgi'
52
+ # fast = Camping::FastCGI.new
53
+ # fast.mount("/blog", Blog)
54
+ # fast.mount("/tepee", Tepee)
55
+ # fast.mount("/", Index)
56
+ # fast.start
57
+ #
58
+ class FastCGI
59
+ CHUNK_SIZE=(4 * 1024)
60
+
61
+ # Creates a Camping::FastCGI class with empty mounts.
62
+ def initialize
63
+ @mounts = {}
64
+ end
65
+ # Mounts a Camping application. The +dir+ being the name of the directory
66
+ # to serve as the application's root. The +app+ is a Camping class.
67
+ def mount(dir, app)
68
+ dir.gsub!(/\/{2,}/, '/')
69
+ dir.gsub!(/\/+$/, '')
70
+ @mounts[dir] = app
71
+ end
72
+ #
73
+ # Starts the FastCGI main loop.
74
+ def start
75
+ FCGI.each do |req|
76
+ dir, app = nil
77
+ begin
78
+ root, path = "/"
79
+ if ENV['FORCE_ROOT'] and ENV['FORCE_ROOT'].to_i == 1
80
+ path = req.env['REQUEST_URI']
81
+ else
82
+ root = req.env['SCRIPT_NAME']
83
+ path = req.env['PATH_INFO']
84
+ end
85
+
86
+ dir, app = @mounts.max { |a,b| match(path, a[0]) <=> match(path, b[0]) }
87
+ unless dir and app
88
+ dir, app = '/', Camping
89
+ end
90
+ yield dir, app if block_given?
91
+
92
+ req.env['SERVER_SCRIPT_NAME'] = req.env['SCRIPT_NAME']
93
+ req.env['SERVER_PATH_INFO'] = req.env['PATH_INFO']
94
+ req.env['SCRIPT_NAME'] = File.join(root, dir)
95
+ req.env['PATH_INFO'] = path.gsub(/^#{dir}/, '')
96
+
97
+ controller = app.run(req.in, req.env)
98
+ sendfile = nil
99
+ headers = {}
100
+ controller.headers.each do |k, v|
101
+ if k =~ /^X-SENDFILE$/i and !ENV['SERVER_X_SENDFILE']
102
+ sendfile = v
103
+ else
104
+ headers[k] = v
105
+ end
106
+ end
107
+
108
+ body = controller.body
109
+ controller.body = ""
110
+ controller.headers = headers
111
+
112
+ req.out << controller.to_s
113
+ if sendfile
114
+ File.open(sendfile, "rb") do |f|
115
+ while chunk = f.read(CHUNK_SIZE) and chunk.length > 0
116
+ req.out << chunk
117
+ end
118
+ end
119
+ elsif body.respond_to? :read
120
+ while chunk = body.read(CHUNK_SIZE) and chunk.length > 0
121
+ req.out << chunk
122
+ end
123
+ body.close if body.respond_to? :close
124
+ else
125
+ req.out << body.to_s
126
+ end
127
+ rescue Exception => e
128
+ req.out << "Content-Type: text/html\r\n\r\n" +
129
+ "<h1>Camping Problem!</h1>" +
130
+ "<h2><strong>#{root}</strong>#{path}</h2>" +
131
+ "<h3>#{e.class} #{esc e.message}</h3>" +
132
+ "<ul>" + e.backtrace.map { |bt| "<li>#{esc bt}</li>" }.join + "</ul>" +
133
+ "<hr /><p>#{req.env.inspect}</p>"
134
+ ensure
135
+ req.finish
136
+ end
137
+ end
138
+ end
139
+
140
+ # A simple single-app starter mechanism
141
+ #
142
+ # Camping::FastCGI.start(Blog)
143
+ #
144
+ def self.start(app)
145
+ cf = Camping::FastCGI.new
146
+ cf.mount("/", app)
147
+ cf.start
148
+ end
149
+
150
+ # Serve an entire directory of Camping apps. (See
151
+ # http://code.whytheluckystiff.net/camping/wiki/TheCampingServer.)
152
+ #
153
+ # Use this method inside your FastCGI dispatcher:
154
+ #
155
+ # #!/usr/local/bin/ruby
156
+ # require 'rubygems'
157
+ # require 'camping/fastcgi'
158
+ # Camping::Models::Base.establish_connection :adapter => 'sqlite3', :database => "/path/to/db"
159
+ # Camping::FastCGI.serve("/home/why/cvs/camping/examples")
160
+ #
161
+ def self.serve(path, index=nil)
162
+ require 'camping/reloader'
163
+ if File.directory? path
164
+ fast = Camping::FastCGI.new
165
+ script_load = proc do |script|
166
+ app = Camping::Reloader.new(script)
167
+ fast.mount("/#{app.mount}", app)
168
+ app
169
+ end
170
+ Dir[File.join(path, '*.rb')].each &script_load
171
+ fast.mount("/", index) if index
172
+
173
+ fast.start do |dir, app|
174
+ Dir[File.join(path, dir, '*.rb')].each do |script|
175
+ smount = "/" + File.basename(script, '.rb')
176
+ script_load[script] unless @mounts.has_key? smount
177
+ end
178
+ end
179
+ else
180
+ start(Camping::Reloader.new(path))
181
+ end
182
+ end
183
+
184
+ private
185
+
186
+ def match(path, mount)
187
+ m = path.match(/^#{Regexp::quote mount}(\/|$)/)
188
+ if m; m.end(0)
189
+ else -1
190
+ end
191
+ end
192
+
193
+ def esc(str)
194
+ str.gsub(/&/n, '&amp;').gsub(/\"/n, '&quot;').gsub(/>/n, '&gt;').gsub(/</n, '&lt;')
195
+ end
196
+
197
+ end
198
+ end
@@ -0,0 +1,169 @@
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
+ Object.send :remove_const, @klass.name if @klass
52
+ @klass = nil
53
+ end
54
+
55
+ # Loads (or reloads) the application. The reloader will take care of calling
56
+ # this for you. You can certainly call it yourself if you feel it's warranted.
57
+ def load_app
58
+ title = File.basename(@script)[/^([\w_]+)/,1].gsub /_/,''
59
+ begin
60
+ all_requires = $LOADED_FEATURES.dup
61
+ load @script
62
+ @requires = ($LOADED_FEATURES - all_requires).select do |req|
63
+ req.index(File.basename(@script) + "/") == 0 || req.index(title + "/") == 0
64
+ end
65
+ rescue Exception => e
66
+ puts "!! trouble loading #{title}: [#{e.class}] #{e.message}"
67
+ puts e.backtrace.join("\n")
68
+ find_app title
69
+ remove_app
70
+ return
71
+ end
72
+
73
+ @mtime = mtime
74
+ find_app title
75
+ unless @klass and @klass.const_defined? :C
76
+ puts "!! trouble loading #{title}: not a Camping app, no #{title.capitalize} module found"
77
+ remove_app
78
+ return
79
+ end
80
+
81
+ Reloader.conditional_connect
82
+ @klass.create if @klass.respond_to? :create
83
+ @klass
84
+ end
85
+
86
+ # The timestamp of the most recently modified app dependency.
87
+ def mtime
88
+ ((@requires || []) + [@script]).map do |fname|
89
+ fname = fname.gsub(/^#{Regexp::quote File.dirname(@script)}\//, '')
90
+ begin
91
+ File.mtime(File.join(File.dirname(@script), fname))
92
+ rescue Errno::ENOENT
93
+ remove_app
94
+ @mtime
95
+ end
96
+ end.max
97
+ end
98
+
99
+ # Conditional reloading of the app. This gets called on each request and
100
+ # only reloads if the modification times on any of the files is updated.
101
+ def reload_app
102
+ return if @klass and @mtime and mtime <= @mtime
103
+
104
+ if @requires
105
+ @requires.each { |req| $LOADED_FEATURES.delete(req) }
106
+ end
107
+ k = @klass
108
+ Object.send :remove_const, k.name if k
109
+ load_app
110
+ end
111
+
112
+ # Conditionally reloads (using reload_app.) Then passes the request through
113
+ # to the wrapped Camping app.
114
+ def run(*a)
115
+ reload_app
116
+ if @klass
117
+ @klass.run(*a)
118
+ else
119
+ Camping.run(*a)
120
+ end
121
+ end
122
+
123
+ # Returns source code for the main script in the application.
124
+ def view_source
125
+ File.read(@script)
126
+ end
127
+
128
+ class << self
129
+ def database=(db)
130
+ @database = db
131
+ end
132
+ def log=(log)
133
+ @log = log
134
+ end
135
+ def conditional_connect
136
+ # If database models are present, `autoload?` will return nil.
137
+ unless Camping::Models.autoload? :Base
138
+ require 'logger'
139
+ require 'camping/session'
140
+ Camping::Models::Base.establish_connection @database if @database
141
+
142
+ case @log
143
+ when Logger
144
+ Camping::Models::Base.logger = @log
145
+ when String
146
+ Camping::Models::Base.logger = Logger.new(@log == "-" ? STDOUT : @log)
147
+ end
148
+
149
+ begin
150
+ Camping::Models::Session.create_schema
151
+ rescue MissingSourceFile
152
+ puts "** #$0 stopped: SQLite3 not found, please install."
153
+ puts "** See http://code.whytheluckystiff.net/camping/wiki/BeAlertWhenOnSqlite3 for instructions."
154
+ exit
155
+ end
156
+
157
+ if @database and @database[:adapter] == 'sqlite3'
158
+ begin
159
+ require 'sqlite3_api'
160
+ rescue LoadError
161
+ puts "!! Your SQLite3 adapter isn't a compiled extension."
162
+ abort "!! Please check out http://code.whytheluckystiff.net/camping/wiki/BeAlertWhenOnSqlite3 for tips."
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end