camping 2.0 → 2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +11 -0
- data/Rakefile +21 -3
- data/bin/camping +2 -85
- data/book/51_upgrading +17 -0
- data/lib/camping-unabridged.rb +56 -16
- data/lib/camping.rb +37 -32
- data/lib/camping/mab.rb +2 -0
- data/lib/camping/reloader.rb +39 -45
- data/lib/camping/server.rb +217 -130
- data/lib/camping/session.rb +5 -6
- data/lib/camping/template.rb +17 -0
- data/test/app_markup.rb +51 -0
- data/test/app_route_generating.rb +22 -0
- data/test/app_sessions.rb +46 -0
- data/test/app_simple.rb +97 -0
- data/test/test_helper.rb +51 -0
- metadata +9 -16
- data/doc/api.html +0 -1953
- data/doc/book.html +0 -73
- data/doc/book/01_introduction.html +0 -57
- data/doc/book/02_getting_started.html +0 -573
- data/doc/book/51_upgrading.html +0 -146
- data/doc/created.rid +0 -1
- data/doc/images/Camping.gif +0 -0
- data/doc/images/loadingAnimation.gif +0 -0
- data/doc/images/permalink.gif +0 -0
- data/doc/index.html +0 -148
- data/doc/js/camping.js +0 -79
- data/doc/js/jquery.js +0 -32
- data/doc/rdoc.css +0 -117
data/lib/camping/mab.rb
CHANGED
data/lib/camping/reloader.rb
CHANGED
@@ -32,28 +32,7 @@ module Camping
|
|
32
32
|
#
|
33
33
|
# You can also give Reloader more than one script.
|
34
34
|
class Reloader
|
35
|
-
attr_reader :scripts
|
36
|
-
|
37
|
-
# This is a simple wrapper which causes the script to reload (if needed)
|
38
|
-
# on any method call. Then the method call will be forwarded to the
|
39
|
-
# app.
|
40
|
-
class App < (defined?(BasicObject) ? BasicObject : Object) # :nodoc:
|
41
|
-
if superclass == ::Object
|
42
|
-
instance_methods.each { |m| undef_method m unless m =~ /^__/ }
|
43
|
-
end
|
44
|
-
|
45
|
-
attr_accessor :app, :script
|
46
|
-
|
47
|
-
def initialize(script)
|
48
|
-
@script = script
|
49
|
-
end
|
50
|
-
|
51
|
-
# Reloads if needed, before calling the method on the app.
|
52
|
-
def method_missing(meth, *args, &blk)
|
53
|
-
@script.reload!
|
54
|
-
@app.send(meth, *args, &blk)
|
55
|
-
end
|
56
|
-
end
|
35
|
+
attr_reader :scripts, :apps
|
57
36
|
|
58
37
|
# This class is doing all the hard work; however, it only works on
|
59
38
|
# single files. Reloader just wraps up support for multiple scripts
|
@@ -61,13 +40,14 @@ module Camping
|
|
61
40
|
class Script # :nodoc:
|
62
41
|
attr_reader :apps, :file, :dir, :extras
|
63
42
|
|
64
|
-
def initialize(file)
|
43
|
+
def initialize(file, callback = nil)
|
65
44
|
@file = File.expand_path(file)
|
66
45
|
@dir = File.dirname(@file)
|
67
46
|
@extras = File.join(@dir, File.basename(@file, ".rb"))
|
68
47
|
@mtime = Time.at(0)
|
69
48
|
@requires = []
|
70
49
|
@apps = {}
|
50
|
+
@callback = callback
|
71
51
|
end
|
72
52
|
|
73
53
|
# Loads the apps availble in this script. Use <tt>apps</tt> to get
|
@@ -94,20 +74,25 @@ module Camping
|
|
94
74
|
|
95
75
|
new_apps = (Camping::Apps - all_apps)
|
96
76
|
old_apps = @apps.dup
|
77
|
+
|
97
78
|
@apps = new_apps.inject({}) do |hash, app|
|
98
79
|
key = app.name.to_sym
|
99
|
-
hash[key] =
|
100
|
-
|
101
|
-
|
80
|
+
hash[key] = app
|
81
|
+
|
82
|
+
if !old_apps.include?(key)
|
83
|
+
@callback.call(app) if @callback
|
84
|
+
app.create if app.respond_to?(:create)
|
85
|
+
end
|
102
86
|
hash
|
103
87
|
end
|
88
|
+
|
104
89
|
self
|
105
90
|
end
|
106
91
|
|
107
92
|
# Removes all the apps defined in this script.
|
108
93
|
def remove_apps
|
109
94
|
@apps.each do |name, app|
|
110
|
-
Camping::Apps.delete(app
|
95
|
+
Camping::Apps.delete(app)
|
111
96
|
Object.send :remove_const, name
|
112
97
|
end
|
113
98
|
end
|
@@ -149,42 +134,51 @@ module Camping
|
|
149
134
|
# will be loaded relative to the current working directory.
|
150
135
|
def initialize(*scripts)
|
151
136
|
@scripts = []
|
137
|
+
@apps = {}
|
152
138
|
update(*scripts)
|
153
139
|
end
|
154
140
|
|
141
|
+
def on_reload(&blk)
|
142
|
+
@callback = blk
|
143
|
+
end
|
144
|
+
|
155
145
|
# Updates the reloader to only use the scripts provided:
|
156
146
|
#
|
157
147
|
# reloader.update("examples/blog.rb", "examples/wiki.rb")
|
158
148
|
def update(*scripts)
|
159
|
-
|
149
|
+
old_scripts = @scripts.dup
|
160
150
|
clear
|
151
|
+
|
161
152
|
@scripts = scripts.map do |script|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
old[pos]
|
166
|
-
else
|
167
|
-
s.load_apps
|
168
|
-
end
|
153
|
+
file = File.expand_path(script)
|
154
|
+
old_scripts.detect { |s| s.file == file } or
|
155
|
+
Script.new(script, @callback)
|
169
156
|
end
|
157
|
+
|
158
|
+
reload!
|
170
159
|
end
|
171
160
|
|
172
161
|
# Removes all the scripts from the reloader.
|
173
162
|
def clear
|
174
|
-
@
|
163
|
+
@scripts = []
|
164
|
+
@apps = {}
|
175
165
|
end
|
176
166
|
|
177
|
-
#
|
178
|
-
def
|
179
|
-
@scripts.each
|
167
|
+
# Returns the script which provided the given app.
|
168
|
+
def script(app)
|
169
|
+
@scripts.each do |script|
|
170
|
+
return script if script.apps.values.include?(app)
|
171
|
+
end
|
172
|
+
|
173
|
+
false
|
180
174
|
end
|
181
175
|
|
182
|
-
#
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
176
|
+
# Simply calls reload! on all the Script objects.
|
177
|
+
def reload!
|
178
|
+
@apps = {}
|
179
|
+
@scripts.each do |script|
|
180
|
+
script.reload!
|
181
|
+
@apps.update(script.apps)
|
188
182
|
end
|
189
183
|
end
|
190
184
|
end
|
data/lib/camping/server.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'irb'
|
2
|
+
require 'erb'
|
2
3
|
require 'rack'
|
3
4
|
require 'camping/reloader'
|
4
5
|
|
@@ -22,41 +23,182 @@ require 'camping/reloader'
|
|
22
23
|
# camping blog.rb # Mounts Blog at /
|
23
24
|
#
|
24
25
|
# And visit http://localhost:3301/ in your browser.
|
25
|
-
|
26
|
-
|
27
|
-
|
26
|
+
module Camping
|
27
|
+
class Server < Rack::Server
|
28
|
+
class Options
|
29
|
+
if home = ENV['HOME'] # POSIX
|
30
|
+
DB = File.join(home, '.camping.db')
|
31
|
+
RC = File.join(home, '.campingrc')
|
32
|
+
elsif home = ENV['APPDATA'] # MSWIN
|
33
|
+
DB = File.join(home, 'Camping.db')
|
34
|
+
RC = File.join(home, 'Campingrc')
|
35
|
+
else
|
36
|
+
DB = nil
|
37
|
+
RC = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
HOME = File.expand_path(home) + '/'
|
41
|
+
|
42
|
+
def parse!(args)
|
43
|
+
args = args.dup
|
44
|
+
options = {}
|
45
|
+
|
46
|
+
opt_parser = OptionParser.new("", 24, ' ') do |opts|
|
47
|
+
opts.banner = "Usage: camping app1.rb app2.rb..."
|
48
|
+
opts.define_head "#{File.basename($0)}, the microframework ON-button for ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
|
49
|
+
opts.separator ""
|
50
|
+
opts.separator "Specific options:"
|
51
|
+
|
52
|
+
opts.on("-h", "--host HOSTNAME",
|
53
|
+
"Host for web server to bind to (default is all IPs)") { |v| options[:Host] = v }
|
54
|
+
|
55
|
+
opts.on("-p", "--port NUM",
|
56
|
+
"Port for web server (defaults to 3301)") { |v| options[:Port] = v }
|
57
|
+
|
58
|
+
db = DB.sub(HOME, '~/') if DB
|
59
|
+
opts.on("-d", "--database FILE",
|
60
|
+
"SQLite3 database path (defaults to #{db ? db : '<none>'})") { |db_path| options[:database] = db_path }
|
61
|
+
|
62
|
+
opts.on("-C", "--console",
|
63
|
+
"Run in console mode with IRB") { options[:server] = "console" }
|
64
|
+
|
65
|
+
server_list = ["mongrel", "webrick", "console"]
|
66
|
+
opts.on("-s", "--server NAME",
|
67
|
+
"Server to force (#{server_list.join(', ')})") { |v| options[:server] = v }
|
28
68
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
69
|
+
opts.separator ""
|
70
|
+
opts.separator "Common options:"
|
71
|
+
|
72
|
+
# No argument, shows at tail. This will print an options summary.
|
73
|
+
# Try it and see!
|
74
|
+
opts.on_tail("-?", "--help", "Show this message") do
|
75
|
+
puts opts
|
76
|
+
exit
|
77
|
+
end
|
78
|
+
|
79
|
+
# Another typical switch to print the version.
|
80
|
+
opts.on_tail("-v", "--version", "Show version") do
|
81
|
+
puts Gem.loaded_specs['camping'].version
|
82
|
+
exit
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
opt_parser.parse!(args)
|
87
|
+
|
88
|
+
if args.empty?
|
89
|
+
puts opt_parser
|
90
|
+
exit
|
91
|
+
end
|
92
|
+
|
93
|
+
options[:scripts] = args
|
94
|
+
options
|
95
|
+
end
|
39
96
|
end
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
97
|
+
|
98
|
+
def initialize(*)
|
99
|
+
super
|
100
|
+
@reloader = Camping::Reloader.new
|
101
|
+
@reloader.on_reload do |app|
|
102
|
+
if !app.options.has_key?(:dynamic_templates)
|
103
|
+
app.options[:dynamic_templates] = true
|
104
|
+
end
|
105
|
+
|
106
|
+
if !Camping::Models.autoload?(:Base) && options[:database]
|
107
|
+
Camping::Models::Base.establish_connection(
|
108
|
+
:adapter => 'sqlite3',
|
109
|
+
:database => options[:database]
|
110
|
+
)
|
111
|
+
end
|
49
112
|
end
|
50
|
-
end
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
113
|
+
end
|
114
|
+
|
115
|
+
def opt_parser
|
116
|
+
Options.new
|
117
|
+
end
|
118
|
+
|
119
|
+
def default_options
|
120
|
+
super.merge({
|
121
|
+
:Port => 3301,
|
122
|
+
:database => Options::DB
|
123
|
+
})
|
124
|
+
end
|
125
|
+
|
126
|
+
def middleware
|
127
|
+
h = super
|
128
|
+
h["development"].unshift [XSendfile]
|
129
|
+
h
|
130
|
+
end
|
131
|
+
|
132
|
+
def start
|
133
|
+
if options[:server] == "console"
|
134
|
+
puts "** Starting console"
|
135
|
+
reload!
|
136
|
+
this = self
|
137
|
+
eval("self", TOPLEVEL_BINDING).meta_def(:reload!) { this.reload!; nil }
|
138
|
+
ARGV.clear
|
139
|
+
IRB.start
|
140
|
+
exit
|
141
|
+
else
|
142
|
+
name = server.name[/\w+$/]
|
143
|
+
puts "** Starting #{name} on #{options[:Host]}:#{options[:Port]}"
|
144
|
+
super
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def find_scripts
|
149
|
+
scripts = options[:scripts].map do |path|
|
150
|
+
if File.file?(path)
|
151
|
+
path
|
152
|
+
elsif File.directory?(path)
|
153
|
+
Dir[File.join(path, '*.rb')]
|
154
|
+
end
|
155
|
+
end.flatten.compact
|
156
|
+
|
157
|
+
@reloader.update(*scripts)
|
158
|
+
end
|
159
|
+
|
160
|
+
def reload!
|
161
|
+
find_scripts
|
162
|
+
end
|
163
|
+
|
164
|
+
def app
|
165
|
+
self
|
166
|
+
end
|
167
|
+
|
168
|
+
def call(env)
|
169
|
+
reload!
|
170
|
+
apps = @reloader.apps
|
171
|
+
|
172
|
+
case apps.length
|
173
|
+
when 0
|
174
|
+
index_page(apps)
|
175
|
+
when 1
|
176
|
+
apps.values.first.call(env)
|
177
|
+
else
|
178
|
+
apps.each do |name, app|
|
179
|
+
mount = name.to_s.downcase
|
180
|
+
case env["PATH_INFO"]
|
181
|
+
when %r{^/#{mount}}
|
182
|
+
env["SCRIPT_NAME"] = env["SCRIPT_NAME"] + $&
|
183
|
+
env["PATH_INFO"] = $'
|
184
|
+
return app.call(env)
|
185
|
+
when %r{^/code/#{mount}}
|
186
|
+
return [200, {'Content-Type' => 'text/plain', 'X-Sendfile' => @reloader.script(app).file}, []]
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
index_page(apps)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def index_page(apps)
|
195
|
+
[200, {'Content-Type' => 'text/html'}, [TEMPLATE.result(binding)]]
|
196
|
+
end
|
197
|
+
|
198
|
+
SOURCE = <<-HTML
|
57
199
|
<html>
|
58
200
|
<head>
|
59
|
-
<title
|
201
|
+
<title>You are Camping</title>
|
60
202
|
<style type="text/css">
|
61
203
|
body {
|
62
204
|
font-family: verdana, arial, sans-serif;
|
@@ -66,113 +208,58 @@ class Camping::Server
|
|
66
208
|
h1, h2, h3, h4, h5, h6 {
|
67
209
|
font-family: utopia, georgia, serif;
|
68
210
|
}
|
211
|
+
h3 { display: inline; }
|
69
212
|
</style>
|
70
213
|
</head>
|
71
214
|
<body>
|
72
|
-
|
215
|
+
<% if apps.empty? %>
|
216
|
+
<p>Good day. I'm sorry, but I could not find any Camping apps.
|
217
|
+
You might want to take a look at the console to see if any errors
|
218
|
+
have been raised.</p>
|
219
|
+
<% else %>
|
220
|
+
<p>Good day. These are the Camping apps you've mounted.</p>
|
221
|
+
<ul>
|
222
|
+
<% apps.each do |name, app| %>
|
223
|
+
<li>
|
224
|
+
<h3><a href="/<%= name.to_s.downcase %>"><%= app %></a></h3>
|
225
|
+
<small> / <a href="/code/<%= name.to_s.downcase %>">View source</a></small>
|
226
|
+
</li>
|
227
|
+
<% end %>
|
228
|
+
</ul>
|
229
|
+
<% end %>
|
230
|
+
</body>
|
231
|
+
</html>
|
73
232
|
HTML
|
74
|
-
footer = '</body></html>'
|
75
|
-
main = if apps.empty?
|
76
|
-
"<p>Good day. I'm sorry, but I could not find any Camping apps. "\
|
77
|
-
"You might want to take a look at the console to see if any errors "\
|
78
|
-
"have been raised.</p>"
|
79
|
-
else
|
80
|
-
"<p>Good day. These are the Camping apps you've mounted.</p><ul>" +
|
81
|
-
apps.map do |mount, app|
|
82
|
-
"<li><h3 style=\"display: inline\"><a href=\"/#{mount}\">#{app}</a></h3><small> / <a href=\"/code/#{mount}\">View source</a></small></li>"
|
83
|
-
end.join("\n") + '</ul>'
|
84
|
-
end
|
85
233
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
all_apps.each do |mount, wrapp|
|
102
|
-
# We're doing @reloader.reload! ourself, so we don't need the wrapper.
|
103
|
-
app = wrapp.app
|
104
|
-
hash["/#{mount}"] = app
|
105
|
-
hash["/code/#{mount}"] = proc do |env|
|
106
|
-
[200,{'Content-Type'=>'text/plain','X-Sendfile'=>wrapp.script.file},'']
|
234
|
+
TEMPLATE = ERB.new(SOURCE)
|
235
|
+
|
236
|
+
class XSendfile
|
237
|
+
def initialize(app)
|
238
|
+
@app = app
|
239
|
+
end
|
240
|
+
|
241
|
+
def call(env)
|
242
|
+
status, headers, body = @app.call(env)
|
243
|
+
|
244
|
+
if key = headers.keys.grep(/X-Sendfile/i).first
|
245
|
+
filename = headers[key]
|
246
|
+
content = open(filename,'rb') { | io | io.read}
|
247
|
+
headers['Content-Length'] = size(content).to_s
|
248
|
+
body = [content]
|
107
249
|
end
|
250
|
+
|
251
|
+
return status, headers, body
|
108
252
|
end
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
@reloader.apps.inject({}) do |h, (mount, wrapp)|
|
119
|
-
h[mount.to_s.downcase] = wrapp
|
120
|
-
h
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
def call(env)
|
125
|
-
app.call(env)
|
126
|
-
end
|
127
|
-
|
128
|
-
def start
|
129
|
-
handler, conf = case @conf.server
|
130
|
-
when "console"
|
131
|
-
puts "** Starting console"
|
132
|
-
reload!
|
133
|
-
this = self; eval("self", TOPLEVEL_BINDING).meta_def(:reload!) { this.reload!; nil }
|
134
|
-
ARGV.clear
|
135
|
-
IRB.start
|
136
|
-
exit
|
137
|
-
when "mongrel"
|
138
|
-
puts "** Starting Mongrel on #{@conf.host}:#{@conf.port}"
|
139
|
-
[Rack::Handler::Mongrel, {:Port => @conf.port, :Host => @conf.host}]
|
140
|
-
when "webrick"
|
141
|
-
puts "** Starting WEBrick on #{@conf.host}:#{@conf.port}"
|
142
|
-
[Rack::Handler::WEBrick, {:Port => @conf.port, :BindAddress => @conf.host}]
|
143
|
-
end
|
144
|
-
reload!
|
145
|
-
handler.run(self, conf)
|
146
|
-
end
|
147
|
-
|
148
|
-
def reload!
|
149
|
-
find_scripts
|
150
|
-
@reloader.reload!
|
151
|
-
end
|
152
|
-
|
153
|
-
# A Rack middleware for reading X-Sendfile. Should only be used in
|
154
|
-
# development.
|
155
|
-
class XSendfile
|
156
|
-
|
157
|
-
HEADERS = [
|
158
|
-
"X-Sendfile",
|
159
|
-
"X-Accel-Redirect",
|
160
|
-
"X-LIGHTTPD-send-file"
|
161
|
-
]
|
162
|
-
|
163
|
-
def initialize(app)
|
164
|
-
@app = app
|
165
|
-
end
|
166
|
-
|
167
|
-
def call(env)
|
168
|
-
status, headers, body = @app.call(env)
|
169
|
-
headers = Rack::Utils::HeaderHash.new(headers)
|
170
|
-
if header = HEADERS.detect { |header| headers.include?(header) }
|
171
|
-
path = headers[header]
|
172
|
-
body = File.read(path)
|
173
|
-
headers['Content-Length'] = body.length.to_s
|
253
|
+
|
254
|
+
if "".respond_to?(:bytesize)
|
255
|
+
def size(str)
|
256
|
+
str.bytesize
|
257
|
+
end
|
258
|
+
else
|
259
|
+
def size(str)
|
260
|
+
str.size
|
261
|
+
end
|
174
262
|
end
|
175
|
-
[status, headers, body]
|
176
263
|
end
|
177
|
-
end
|
264
|
+
end
|
178
265
|
end
|