picnic 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.txt +3 -0
- data/LICENSE.txt +165 -0
- data/Manifest.txt +28 -0
- data/README.txt +31 -0
- data/Rakefile +58 -0
- data/lib/picnic.rb +122 -0
- data/lib/picnic/authentication.rb +80 -0
- data/lib/picnic/cli.rb +112 -0
- data/lib/picnic/conf.rb +133 -0
- data/lib/picnic/controllers.rb +31 -0
- data/lib/picnic/postambles.rb +227 -0
- data/lib/picnic/service_control.rb +269 -0
- data/lib/picnic/utils.rb +36 -0
- data/lib/picnic/version.rb +9 -0
- data/setup.rb +1585 -0
- data/test/picnic_test.rb +11 -0
- data/test/test_helper.rb +2 -0
- data/vendor/camping-1.5.180/CHANGELOG +99 -0
- data/vendor/camping-1.5.180/COPYING +18 -0
- data/vendor/camping-1.5.180/README +119 -0
- data/vendor/camping-1.5.180/Rakefile +117 -0
- data/vendor/camping-1.5.180/lib/camping-unabridged.rb +762 -0
- data/vendor/camping-1.5.180/lib/camping.rb +55 -0
- data/vendor/camping-1.5.180/lib/camping/db.rb +78 -0
- data/vendor/camping-1.5.180/lib/camping/fastcgi.rb +244 -0
- data/vendor/camping-1.5.180/lib/camping/reloader.rb +163 -0
- data/vendor/camping-1.5.180/lib/camping/session.rb +123 -0
- data/vendor/camping-1.5.180/lib/camping/webrick.rb +65 -0
- metadata +77 -0
@@ -0,0 +1,55 @@
|
|
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,h=/\(.+?\)/,g.grep(Hash)
|
4
|
+
(g-=h).inject(c.urls.find{|x|x.scan(p).size==g.size}.dup){|s,a|s.sub p,C.
|
5
|
+
escape((a[a.class.primary_key]rescue a))}+(h.any?? "?"+h[0].map{|x|x.map{|z|C.
|
6
|
+
escape z}*"="}*"&": "")end;def URL c='/',*a;c=R(c,*a)if c.
|
7
|
+
respond_to?:urls;c=self/c;c="//"+@env.HTTP_HOST+c if c[/^\//];URI(c)end;def/p
|
8
|
+
p[/^\//]?@root+p : p end;def errors_for o;ul.errors{o.errors.each_full{|x|li x}
|
9
|
+
}if o.errors.any?end end;module Base;include Helpers;attr_accessor:input,
|
10
|
+
:cookies,:env,:headers,:body,:status,:root;def method_missing*a,&b
|
11
|
+
a.shift if a[0]==:render;m=Mab.new({},self);s=m.capture{send(*a,&b)}
|
12
|
+
s=m.capture{send(:layout){s}} if /^_/!~a[0].to_s and m.respond_to?:layout
|
13
|
+
s end;def r s,b,h={};@status=s;@headers.merge!h;@body=b end
|
14
|
+
def redirect*a;r 302,'','Location'=>URL(*a)end;Z="\r\n"
|
15
|
+
def to_a;[@status,@body,@headers]end
|
16
|
+
def initialize r,e,m;e=H[e.to_hash];@status,@method,@env,@headers,@root=200,m.
|
17
|
+
downcase,e,{'Content-Type'=>"text/html"},e.SCRIPT_NAME.sub(/\/$/,'')
|
18
|
+
@k=C.kp e.HTTP_COOKIE;q=C.qsp e.QUERY_STRING;@in=r
|
19
|
+
if%r|\Amultipart/form-.*boundary=\"?([^\";,]+)|n.match e.CONTENT_TYPE
|
20
|
+
b=/(?:\r?\n|\A)#{Regexp::quote("--#$1")}(?:--)?\r$/;until@in.eof?;fh=H[];for l in@in
|
21
|
+
case l;when Z;break;when/^Content-D.+?: form-data;/;fh.u H[*$'.
|
22
|
+
scan(/(?:\s(\w+)="([^"]+)")/).flatten];when/^Content-Type: (.+?)(\r$|\Z)/m;fh[
|
23
|
+
:type]=$1;end;end;fn=fh[:name];o=if fh[:filename];o=fh[:tempfile]=Tempfile.new(:C)
|
24
|
+
o.binmode;else;fh=""end;while l=@in.read(16384);if l=~b;o<<$`.chomp;@in.seek(-$'.
|
25
|
+
size,IO::SEEK_CUR);break;end;o<<l;end;C.qsp(fn,'&;',fh,q) if fn;fh[:tempfile].rewind if
|
26
|
+
fh.is_a?H;end;elsif@method=="post";q.u C.qsp(@in.read)end;@cookies,@input=
|
27
|
+
@k.dup,q.dup end;def service*a;@body=send(@method,*a)if respond_to?@method
|
28
|
+
@headers["Set-Cookie"]=@cookies.map{|k,v|"#{k}=#{C.escape(v)}; path=#{self/'/'
|
29
|
+
}"if v!=@k[k]}-[nil];self end;def to_s;a=[];@headers.map{|k,v|[*v].map{|x|a<<
|
30
|
+
"#{k}: #{x}"}};"Status: #{@status}#{Z+a*Z+Z*2+@body}"end;end
|
31
|
+
X=module Controllers;@r=[];class<<self;def r;@r;end;def R*u;r=@r;Class.new{
|
32
|
+
meta_def(:urls){u};meta_def(:inherited){|x|r<<x}}end;def M;def M;end;constants.map{|c|
|
33
|
+
k=const_get(c);k.send:include,C,Base,Models;r[0,0]=k if !r.include?k;k.meta_def(
|
34
|
+
:urls){["/#{c.downcase}"]}if !k.respond_to?:urls}end;def D p;r.map{|k|k.urls.
|
35
|
+
map{|x|return k,$~[1..-1]if p=~/^#{x}\/?$/}};[NotFound,[p]]end end;class
|
36
|
+
NotFound<R();def get p;r(404,Mab.new{h1 P;h2 p+" not found"})end end;class
|
37
|
+
ServerError<R();def get k,m,e;r(500,Mab.new{h1 P;h2"#{k}.#{m}";h3"#{e.class
|
38
|
+
} #{e.message}:";ul{e.backtrace.each{|bt|li(bt)}}}.to_s)end end;self;end;class<<
|
39
|
+
self;def goes m;eval S.gsub(/Camping/,m.to_s).gsub("A\pps=[]","Cam\ping::Apps<<\
|
40
|
+
self"),TOPLEVEL_BINDING;end;def escape s;s.to_s.gsub(/[^ \w.-]+/n){'%'+($&.
|
41
|
+
unpack('H2'*$&.size)*'%').upcase}.tr(' ','+')end;def un s;s.tr('+',' ').gsub(
|
42
|
+
/%([\da-f]{2})/in){[$1].pack('H*')}end;def qsp q,d='&;',y=nil,z=H[];m=proc{|_,o,n|o.u(
|
43
|
+
n,&m)rescue([*o]<<n)};q.to_s.split(/[#{d}] */n).inject((b,z=z,H[])[0]){|h,p|k,v=un(p).
|
44
|
+
split('=',2);h.u k.split(/[\]\[]+/).reverse.inject(y||v){|x,i|H[i,x]},&m}end;def
|
45
|
+
kp s;c=qsp(s,';,')end;def run r=$stdin,e=ENV;X.M;k,a=X.D un("/#{e[
|
46
|
+
'PATH_INFO']}".gsub(/\/+/,'/'));k.new(r,e,(m=e['REQUEST_METHOD']||"GET")).Y.
|
47
|
+
service *a;rescue Object=>x;X::ServerError.new(r,e,'get').service(k,m,x)end
|
48
|
+
def method_missing m,c,*a;X.M;k=X.const_get(c).new(StringIO.new,H['HTTP_HOST',
|
49
|
+
'','SCRIPT_NAME','','HTTP_COOKIE',''],m.to_s);H.new(a.pop).each{|e,f|k.send(
|
50
|
+
"#{e}=",f)}if Hash===a[-1];k.service *a;end;end;module Views;include X,Helpers
|
51
|
+
end;module Models;autoload:Base,'camping/db';def Y;self;end;end;class Mab<Markaby::Builder
|
52
|
+
include Views;def tag!*g,&b;h=g[-1];[:href,:action,:src].map{|a|(h[a]=self/h[a])rescue
|
53
|
+
0};super end end;H=HashWithIndifferentAccess;class H;def method_missing m,*a
|
54
|
+
m.to_s=~/=$/?self[$`]=a[0]:a==[]?self[m]:raise(NoMethodError,"#{m}")end
|
55
|
+
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,244 @@
|
|
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
|
+
attr_reader :mounts
|
62
|
+
|
63
|
+
# Creates a Camping::FastCGI class with empty mounts.
|
64
|
+
def initialize
|
65
|
+
@mounts = {}
|
66
|
+
end
|
67
|
+
# Mounts a Camping application. The +dir+ being the name of the directory
|
68
|
+
# to serve as the application's root. The +app+ is a Camping class.
|
69
|
+
def mount(dir, app)
|
70
|
+
dir.gsub!(/\/{2,}/, '/')
|
71
|
+
dir.gsub!(/\/+$/, '')
|
72
|
+
@mounts[dir] = app
|
73
|
+
end
|
74
|
+
|
75
|
+
#
|
76
|
+
# Starts the FastCGI main loop.
|
77
|
+
def start(&blk)
|
78
|
+
FCGI.each do |req|
|
79
|
+
camp_do(req, &blk)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# A simple single-app starter mechanism
|
84
|
+
#
|
85
|
+
# Camping::FastCGI.start(Blog)
|
86
|
+
#
|
87
|
+
def self.start(app)
|
88
|
+
cf = Camping::FastCGI.new
|
89
|
+
cf.mount("/", app)
|
90
|
+
cf.start
|
91
|
+
end
|
92
|
+
|
93
|
+
# Serve an entire directory of Camping apps. (See
|
94
|
+
# http://code.whytheluckystiff.net/camping/wiki/TheCampingServer.)
|
95
|
+
#
|
96
|
+
# Use this method inside your FastCGI dispatcher:
|
97
|
+
#
|
98
|
+
# #!/usr/local/bin/ruby
|
99
|
+
# require 'rubygems'
|
100
|
+
# require 'camping/fastcgi'
|
101
|
+
# Camping::Models::Base.establish_connection :adapter => 'sqlite3', :database => "/path/to/db"
|
102
|
+
# Camping::FastCGI.serve("/home/why/cvs/camping/examples")
|
103
|
+
#
|
104
|
+
def self.serve(path, index=nil)
|
105
|
+
require 'camping/reloader'
|
106
|
+
if File.directory? path
|
107
|
+
fast = Camping::FastCGI.new
|
108
|
+
script_load = proc do |script|
|
109
|
+
app = Camping::Reloader.new(script)
|
110
|
+
fast.mount("/#{app.mount}", app)
|
111
|
+
app
|
112
|
+
end
|
113
|
+
Dir[File.join(path, '*.rb')].each &script_load
|
114
|
+
fast.mount("/", index) if index
|
115
|
+
|
116
|
+
fast.start do |dir, app|
|
117
|
+
Dir[File.join(path, dir, '*.rb')].each do |script|
|
118
|
+
smount = "/" + File.basename(script, '.rb')
|
119
|
+
script_load[script] unless fast.mounts.has_key? smount
|
120
|
+
end
|
121
|
+
end
|
122
|
+
else
|
123
|
+
start(Camping::Reloader.new(path))
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
def camp_do(req)
|
130
|
+
root, path, dir, app = "/"
|
131
|
+
if ENV['FORCE_ROOT'] and ENV['FORCE_ROOT'].to_i == 1
|
132
|
+
path = req.env['SCRIPT_NAME']
|
133
|
+
else
|
134
|
+
root = req.env['SCRIPT_NAME']
|
135
|
+
path = req.env['PATH_INFO']
|
136
|
+
end
|
137
|
+
|
138
|
+
dir, app = @mounts.max { |a,b| match(path, a[0]) <=> match(path, b[0]) }
|
139
|
+
unless dir and app
|
140
|
+
dir, app = '/', Camping
|
141
|
+
end
|
142
|
+
yield dir, app if block_given?
|
143
|
+
|
144
|
+
req.env['SERVER_SCRIPT_NAME'] = req.env['SCRIPT_NAME']
|
145
|
+
req.env['SERVER_PATH_INFO'] = req.env['PATH_INFO']
|
146
|
+
req.env['SCRIPT_NAME'] = File.join(root, dir)
|
147
|
+
req.env['PATH_INFO'] = path.gsub(/^#{dir}/, '')
|
148
|
+
|
149
|
+
controller = app.run(SeekStream.new(req.in), req.env)
|
150
|
+
sendfile = nil
|
151
|
+
headers = {}
|
152
|
+
controller.headers.each do |k, v|
|
153
|
+
if k =~ /^X-SENDFILE$/i and !ENV['SERVER_X_SENDFILE']
|
154
|
+
sendfile = v
|
155
|
+
else
|
156
|
+
headers[k] = v
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
body = controller.body
|
161
|
+
controller.body = ""
|
162
|
+
controller.headers = headers
|
163
|
+
|
164
|
+
req.out << controller.to_s
|
165
|
+
if sendfile
|
166
|
+
File.open(sendfile, "rb") do |f|
|
167
|
+
while chunk = f.read(CHUNK_SIZE) and chunk.length > 0
|
168
|
+
req.out << chunk
|
169
|
+
end
|
170
|
+
end
|
171
|
+
elsif body.respond_to? :read
|
172
|
+
while chunk = body.read(CHUNK_SIZE) and chunk.length > 0
|
173
|
+
req.out << chunk
|
174
|
+
end
|
175
|
+
body.close if body.respond_to? :close
|
176
|
+
else
|
177
|
+
req.out << body.to_s
|
178
|
+
end
|
179
|
+
rescue Exception => e
|
180
|
+
req.out << server_error(root, path, exc, req)
|
181
|
+
ensure
|
182
|
+
req.finish
|
183
|
+
end
|
184
|
+
|
185
|
+
def server_error(root, path, exc, req)
|
186
|
+
"Content-Type: text/html\r\n\r\n" +
|
187
|
+
"<h1>Camping Problem!</h1>" +
|
188
|
+
"<h2><strong>#{root}</strong>#{path}</h2>" +
|
189
|
+
"<h3>#{exc.class} #{esc exc.message}</h3>" +
|
190
|
+
"<ul>" + exc.backtrace.map { |bt| "<li>#{esc bt}</li>" }.join + "</ul>" +
|
191
|
+
"<hr /><p>#{req.env.inspect}</p>"
|
192
|
+
end
|
193
|
+
|
194
|
+
def match(path, mount)
|
195
|
+
m = path.match(/^#{Regexp::quote mount}(\/|$)/)
|
196
|
+
if m; m.end(0)
|
197
|
+
else -1
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def esc(str)
|
202
|
+
str.gsub(/&/n, '&').gsub(/\"/n, '"').gsub(/>/n, '>').gsub(/</n, '<')
|
203
|
+
end
|
204
|
+
|
205
|
+
class SeekStream
|
206
|
+
def initialize(stream)
|
207
|
+
@last_read = ""
|
208
|
+
@stream = stream
|
209
|
+
@buffer = ""
|
210
|
+
end
|
211
|
+
def eof?
|
212
|
+
@buffer.empty? && @stream.eof?
|
213
|
+
end
|
214
|
+
def each
|
215
|
+
while true
|
216
|
+
pull(1024) until eof? or @buffer.index("\n")
|
217
|
+
return nil if eof?
|
218
|
+
yield @buffer.slice!(0..(@buffer.index("\n") || -1))
|
219
|
+
end
|
220
|
+
end
|
221
|
+
def pull(len)
|
222
|
+
@buffer += @stream.read(len).to_s
|
223
|
+
end
|
224
|
+
def read(len = 16384)
|
225
|
+
pull(len)
|
226
|
+
@last_read =
|
227
|
+
if eof?
|
228
|
+
nil
|
229
|
+
else
|
230
|
+
@buffer.slice!(0...len)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
def seek(len, typ)
|
234
|
+
raise NotImplementedError, "only IO::SEEK_CUR is supported with SeekStream" if typ != IO::SEEK_CUR
|
235
|
+
raise NotImplementedError, "only rewinding is supported with SeekStream" if len > 0
|
236
|
+
raise NotImplementedError, "rewinding #{-len} past the buffer #{@last_read.size} start not supported with SeekStream" if -len > @last_read.size
|
237
|
+
@buffer = @last_read[len..-1] + @buffer
|
238
|
+
@last_read = ""
|
239
|
+
self
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
end
|
244
|
+
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
|
+
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
|
+
Camping::Models::Session.create_schema
|
150
|
+
|
151
|
+
if @database and @database[:adapter] == 'sqlite3'
|
152
|
+
begin
|
153
|
+
require 'sqlite3_api'
|
154
|
+
rescue LoadError
|
155
|
+
puts "!! Your SQLite3 adapter isn't a compiled extension."
|
156
|
+
abort "!! Please check out http://code.whytheluckystiff.net/camping/wiki/BeAlertWhenOnSqlite3 for tips."
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|