serve 0.9.10 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. data/{History.txt → CHANGELOG.rdoc} +9 -0
  2. data/{License.txt → LICENSE} +1 -1
  3. data/{Quickstart.textile → QUICKSTART.rdoc} +61 -80
  4. data/{README.txt → README.rdoc} +10 -13
  5. data/Rakefile +11 -13
  6. data/VERSION +1 -0
  7. data/bin/serve +6 -0
  8. data/lib/serve.rb +2 -8
  9. data/lib/serve/application.rb +13 -6
  10. data/lib/serve/file_resolver.rb +48 -0
  11. data/lib/serve/handlers/dynamic_handler.rb +31 -21
  12. data/lib/serve/handlers/file_type_handler.rb +23 -32
  13. data/lib/serve/handlers/markdown_handler.rb +1 -1
  14. data/lib/serve/handlers/redirect_handler.rb +6 -5
  15. data/lib/serve/handlers/sass_handler.rb +4 -3
  16. data/lib/serve/rack.rb +57 -0
  17. data/lib/serve/rails.rb +4 -0
  18. data/lib/serve/rails/configuration.rb +69 -0
  19. data/lib/serve/rails/mount.rb +38 -0
  20. data/lib/serve/rails/routing.rb +25 -0
  21. data/lib/serve/rails/serve_controller.rb +52 -0
  22. data/lib/serve/response_cache.rb +170 -0
  23. data/lib/serve/version.rb +4 -9
  24. data/lib/serve/webrick/extensions.rb +80 -15
  25. data/lib/serve/webrick/server.rb +17 -0
  26. data/lib/serve/webrick/servlet.rb +19 -0
  27. data/rails/init.rb +6 -0
  28. data/spec/{serve_application_spec.rb → application_spec.rb} +5 -4
  29. data/spec/response_cache_spec.rb +248 -0
  30. data/spec/serve_spec.rb +1 -0
  31. data/spec/spec_helper.rb +10 -9
  32. metadata +51 -69
  33. data.tar.gz.sig +0 -0
  34. data/Manifest.txt +0 -44
  35. data/config/hoe.rb +0 -70
  36. data/config/requirements.rb +0 -17
  37. data/log/debug.log +0 -0
  38. data/script/destroy +0 -14
  39. data/script/generate +0 -14
  40. data/script/txt2html +0 -74
  41. data/setup.rb +0 -1585
  42. data/tasks/deployment.rake +0 -27
  43. data/tasks/environment.rake +0 -7
  44. data/tasks/rspec.rake +0 -21
  45. data/tasks/undefine.rake +0 -5
  46. data/tasks/website.rake +0 -17
  47. data/test_project/_layout.haml +0 -12
  48. data/test_project/erb/_footer.html.erb +0 -2
  49. data/test_project/erb/_layout.html.erb +0 -28
  50. data/test_project/erb/index.html.erb +0 -10
  51. data/test_project/haml/_footer.haml +0 -2
  52. data/test_project/haml/_layout.haml +0 -20
  53. data/test_project/haml/index.haml +0 -9
  54. data/test_project/test.haml +0 -3
  55. data/test_project/test.html.erb +0 -3
  56. data/test_project/view_helpers.rb +0 -5
  57. metadata.gz.sig +0 -0
@@ -1,46 +1,37 @@
1
1
  module Serve #:nodoc:
2
- class FileTypeHandler < ::WEBrick::HTTPServlet::AbstractServlet #:nodoc:
3
-
2
+ class FileTypeHandler #:nodoc:
3
+ def self.handlers
4
+ @handlers ||= {}
5
+ end
6
+
4
7
  def self.extension(*extensions)
5
- extensions.each do |extensions|
6
- ::WEBrick::HTTPServlet::FileHandler.add_handler(extensions, self)
8
+ extensions.each do |ext|
9
+ FileTypeHandler.handlers[ext] = self
7
10
  end
8
11
  end
9
12
 
10
-
11
- def initialize(server, name)
12
- super
13
- @script_filename = name
13
+ def self.find(path)
14
+ if ext = File.extname(path)
15
+ handlers[ext.sub(/\A\./, '')]
16
+ end
14
17
  end
15
18
 
16
- def process(req, res)
17
- data = open(@script_filename){|io| io.read }
18
- res['content-type'] = content_type
19
- res.body = parse(data)
19
+ def initialize(root_path, path)
20
+ @root_path = root_path
21
+ @script_filename = File.join(@root_path, path)
20
22
  end
21
23
 
22
- def do_GET(req, res)
23
- begin
24
- process(req, res)
25
- rescue StandardError => ex
26
- raise
27
- rescue Exception => ex
28
- @logger.error(ex)
29
- raise ::WEBrick::HTTPStatus::InternalServerError, ex.message
30
- end
24
+ def process(request, response)
25
+ response.headers['content-type'] = content_type
26
+ response.body = parse(open(@script_filename){|io| io.read })
31
27
  end
32
28
 
33
- alias do_POST do_GET
34
-
35
- protected
36
-
37
- def content_type
38
- 'text/html'
39
- end
40
-
41
- def parse(string)
42
- string.dup
43
- end
29
+ def content_type
30
+ 'text/html'
31
+ end
44
32
 
33
+ def parse(string)
34
+ string.dup
35
+ end
45
36
  end
46
37
  end
@@ -1,7 +1,7 @@
1
1
  module Serve #:nodoc:
2
2
  class MarkdownHandler < FileTypeHandler #:nodoc:
3
3
  extension 'markdown'
4
-
4
+
5
5
  def parse(string)
6
6
  require 'bluecloth'
7
7
  "<html><body>#{ BlueCloth.new(string).to_html }</body></html>"
@@ -2,11 +2,12 @@ module Serve #:nodoc:
2
2
  class RedirectHandler < FileTypeHandler #:nodoc:
3
3
  extension 'redirect'
4
4
 
5
- def process(req, res)
6
- data = super
7
- res['location'] = data.strip
8
- res.body = ''
9
- raise ::WEBrick::HTTPStatus::Found
5
+ def process(request, response)
6
+ url = super.strip
7
+ unless url =~ %r{^\w[\w\d+.-]*:.*}
8
+ url = request.protocol + request.host_with_port + url
9
+ end
10
+ response.redirect(url, '302')
10
11
  end
11
12
  end
12
13
  end
@@ -1,16 +1,17 @@
1
1
  module Serve #:nodoc:
2
2
  class SassHandler < FileTypeHandler #:nodoc:
3
3
  extension 'sass'
4
-
4
+
5
5
  def parse(string)
6
6
  require 'sass'
7
7
  engine = Sass::Engine.new(string,
8
+ :load_paths => [@root_path],
8
9
  :style => :expanded,
9
10
  :filename => @script_filename
10
11
  )
11
- engine.render
12
+ engine.render
12
13
  end
13
-
14
+
14
15
  def content_type
15
16
  'text/css'
16
17
  end
@@ -0,0 +1,57 @@
1
+ require 'active_support'
2
+ require 'serve'
3
+ require 'rack'
4
+
5
+ module Serve
6
+ class Rack
7
+ def call(env)
8
+ path = Serve.resolve_file(Dir.pwd, env["PATH_INFO"])
9
+ return not_found unless path
10
+
11
+ ext = File.extname(path)[1..-1]
12
+ handler = Serve::FileTypeHandler.handlers[ext]
13
+ return no_handler(ext) unless handler
14
+
15
+ res = Response.new
16
+ handler.new(Dir.pwd, path).process(nil, res)
17
+ [200, res.headers, res.body]
18
+ rescue Exception => e
19
+ return html_response(500, %(
20
+ <h1>Error!</h1>
21
+ <h2>#{h(e.message)}</h2>
22
+ <pre>
23
+ #{h(e.backtrace.join("\n"))}
24
+ </pre>))
25
+ end
26
+
27
+ def no_handler(ext)
28
+ html_response(501, %(
29
+ <h1>No handler</h1>
30
+
31
+ <p>Don't know how to handle resources of type "#{h(ext)}".</p>))
32
+ end
33
+
34
+ def not_found
35
+ html_response(404, %(
36
+ <h1>Not Found</h1>
37
+
38
+ <p>The requested resource was not found.</p>))
39
+ end
40
+
41
+ def html_response(code, body)
42
+ [code, {"Content-Type" => "text/html"}, %(<html><head></head><body>#{body}</body></html>)]
43
+ end
44
+
45
+ def h(input)
46
+ CGI.escapeHTML(input)
47
+ end
48
+
49
+ class Response
50
+ attr_reader :headers
51
+ attr_accessor :body
52
+ def initialize
53
+ @headers = {}
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,4 @@
1
+ require 'serve/rails/configuration'
2
+ require 'serve/rails/mount'
3
+ require 'serve/rails/serve_controller'
4
+ require 'serve/rails/routing'
@@ -0,0 +1,69 @@
1
+ module Serve # :nodoc:
2
+ module Rails
3
+ class Configuration # :nodoc:
4
+ attr_reader :mounts, :view_helpers
5
+
6
+ def initialize
7
+ Serve::ResponseCache.defaults.update(
8
+ :logger => ActionController::Base.logger,
9
+ :perform_caching => ActionController::Base.perform_caching
10
+ )
11
+
12
+ @mounts = []
13
+
14
+ define_find_cache do |request|
15
+ @default_cache ||= Serve::ResponseCache.new(
16
+ :directory => File.join(::Rails.root, 'tmp', 'serve_cache')
17
+ )
18
+ end
19
+ end
20
+
21
+ def define_find_cache(&block)
22
+ metaclass.module_eval do
23
+ define_method :find_cache, &block
24
+ end
25
+ end
26
+
27
+ def mount(route, root_path)
28
+ m = Mount.new(route, root_path)
29
+ @mounts << m
30
+ yield m if block_given?
31
+ m
32
+ end
33
+ end
34
+
35
+ # Answer the cache for the given request. Delegates to the 'find_cache'
36
+ # block, which defaults to a single cache.
37
+ #
38
+ def self.cache(request)
39
+ configuration.find_cache(request)
40
+ end
41
+
42
+ def self.configuration
43
+ @configuration ||= Configuration.new
44
+ end
45
+
46
+ # The most powerful way to configure Serve::Rails, the configuration is
47
+ # yielded to the provided block. Multiple calls are cumulative - there is
48
+ # only one configuration instance.
49
+ #
50
+ def self.configure
51
+ yield configuration
52
+ end
53
+
54
+ # Define the strategy for resolving the cache for a request. This allows
55
+ # applications to provide logic like cache-per-browser. The default is a
56
+ # single cache.
57
+ #
58
+ def self.find_cache(&block)
59
+ configuration.define_find_cache(&block)
60
+ end
61
+
62
+ # Mount a route on a directory. This allows an application to have
63
+ # multiple served directories, each connected to different routes.
64
+ #
65
+ def self.mount(route, root_path, &block)
66
+ configuration.mount(route, root_path, &block)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,38 @@
1
+ module Serve
2
+ module Rails
3
+ class Mount # :nodoc:
4
+ attr_reader :root_path, :route
5
+
6
+ def initialize(route, root)
7
+ @route, @root_path = route, root
8
+ @view_helper_module_names = []
9
+ end
10
+
11
+ # Appends to the collection of view helpers that will be made availabe
12
+ # to the DynamicHandler. These should be module names. They will be
13
+ # constantized in order to allow for re-loading in development mode.
14
+ #
15
+ def append_view_helper(module_name)
16
+ @view_helper_module_names << module_name
17
+ end
18
+
19
+ def connection
20
+ @route == '/' ? '*path' : "#{@route}/*path"
21
+ end
22
+
23
+ def resolve_path(path)
24
+ Serve.resolve_file(@root_path, path)
25
+ end
26
+
27
+ def view_helpers
28
+ helpers = Module.new
29
+ @view_helper_module_names.each do |module_name|
30
+ helpers.module_eval do
31
+ include module_name.constantize
32
+ end
33
+ end
34
+ helpers
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,25 @@
1
+ module Serve
2
+ module Rails
3
+ module Routing
4
+
5
+ module MapperExtensions
6
+ def serve
7
+ serve_mounts = Serve::Rails.configuration.mounts
8
+ default_site_path = File.join(::Rails.root, 'site')
9
+
10
+ if File.directory?(default_site_path) && !serve_mounts.detect {|m| m.route == '/'}
11
+ mount('/', default_site_path)
12
+ end
13
+
14
+ serve_mounts.each do |mount|
15
+ @set.add_route(mount.connection, {
16
+ :controller => 'serve', :action => 'show',
17
+ :serve_route => mount.route
18
+ })
19
+ end
20
+ end
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,52 @@
1
+ module Serve
2
+ module Rails
3
+
4
+ module ServeController
5
+ def show
6
+ response.headers.delete('Cache-Control')
7
+ cache = Serve::Rails.cache(request)
8
+ if cache.response_cached?(request.path)
9
+ cache.update_response(request.path, response, request)
10
+ else
11
+ mount = Serve::Rails.configuration.mounts.detect {|m| m.route == params[:serve_route]}
12
+ if path = mount.resolve_path(params[:path] || '/')
13
+ handler_class = Serve::FileTypeHandler.find(path)
14
+ handler = handler_class.new(mount.root_path, path)
15
+ install_view_helpers(handler, mount.view_helpers) if handler_class == Serve::DynamicHandler
16
+ handler.process(request, response)
17
+ cache.cache_response(request.path, response)
18
+ else
19
+ render_not_found
20
+ end
21
+ end
22
+ @performed_render = true
23
+ end
24
+
25
+ private
26
+ def render_not_found
27
+ render :text => 'not found', :status => 404
28
+ end
29
+
30
+ # This is a quick solution: We need to install the view helpers defined by
31
+ # the Rails application after those of the Serve'd app's view helpers, so
32
+ # that those of the Rails application override them.
33
+ #
34
+ # Ideally, we'll work toward moving some of this Rails stuff further up
35
+ # into Serve, so that a simple Serve'd directory uses almost all of the
36
+ # same code, like the Configuration.
37
+ #
38
+ def install_view_helpers(handler, view_helpers)
39
+ controller = self
40
+ handler.metaclass.module_eval do
41
+ define_method :install_view_helpers do |context|
42
+ super(context)
43
+ # Make available to view helpers
44
+ context.instance_variable_set('@controller', controller)
45
+ context.extend view_helpers
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,170 @@
1
+ # This code and it's tests are adapted from the Radiant CMS project.
2
+
3
+ require 'uri'
4
+
5
+ module Serve
6
+ class ResponseCache
7
+ @@defaults = {
8
+ :expire_time => 5.minutes,
9
+ :default_extension => '.yml',
10
+ :perform_caching => true,
11
+ :use_x_sendfile => false
12
+ }
13
+ cattr_accessor :defaults
14
+
15
+ attr_accessor :directory, :expire_time, :default_extension, :perform_caching, :logger, :use_x_sendfile
16
+ alias :page_cache_directory :directory
17
+ alias :page_cache_extension :default_extension
18
+ private :page_cache_directory, :page_cache_extension
19
+
20
+ # Creates a ResponseCache object with the specified options.
21
+ #
22
+ # Options are as follows:
23
+ # :directory :: the path to the temporary cache directory
24
+ # :expire_time :: the number of seconds a cached response is considered valid (defaults to 5 min)
25
+ # :default_extension :: the extension cached files should use (defaults to '.yml')
26
+ # :perform_caching :: boolean value that turns caching on or off (defaults to true)
27
+ # :logger :: the application logging object
28
+ # :use_x_sendfile :: use X-Sendfile headers to speed up transfer of cached pages (not available on all web servers)
29
+ #
30
+ def initialize(options = {})
31
+ options = options.symbolize_keys.reverse_merge(defaults)
32
+ self.directory = options[:directory]
33
+ self.expire_time = options[:expire_time]
34
+ self.default_extension = options[:default_extension]
35
+ self.perform_caching = options[:perform_caching]
36
+ self.logger = options[:logger]
37
+ self.use_x_sendfile = options[:use_x_sendfile]
38
+ end
39
+
40
+ # Caches a response object for path to disk.
41
+ def cache_response(path, response)
42
+ if perform_caching
43
+ path = clean(path)
44
+ write_response(path, response)
45
+ end
46
+ response
47
+ end
48
+
49
+ # If perform_caching is set to true, updates a response object so that it mirrors the
50
+ # cached version. The request object is required to perform Last-Modified/If-Modified-Since
51
+ # checks--it is left optional to allow for backwards compatability.
52
+ def update_response(path, response, request=nil)
53
+ if perform_caching
54
+ path = clean(path)
55
+ read_response(path, response, request)
56
+ end
57
+ response
58
+ end
59
+
60
+ # Returns metadata for path.
61
+ def read_metadata(path)
62
+ path = clean(path)
63
+ name = "#{page_cache_path(path)}.yml"
64
+ if File.exists?(name) and not File.directory?(name)
65
+ content = File.open(name, "rb") { |f| f.read }
66
+ metadata = YAML::load(content)
67
+ metadata if metadata['expires'] >= Time.now
68
+ end
69
+ rescue
70
+ nil
71
+ end
72
+
73
+ # Returns true if a response is cached at the specified path.
74
+ def response_cached?(path)
75
+ perform_caching && !!read_metadata(path)
76
+ end
77
+
78
+ # Expires the cached response for the specified path.
79
+ def expire_response(path)
80
+ path = clean(path)
81
+ expire_page(path)
82
+ end
83
+
84
+ # Expires the entire cache.
85
+ def clear
86
+ Dir["#{directory}/*"].each do |f|
87
+ FileUtils.rm_rf f
88
+ end
89
+ end
90
+
91
+ private
92
+ # Ensures that path begins with a slash and remove extra slashes.
93
+ def clean(path)
94
+ path = path.gsub(%r{/+}, '/')
95
+ %r{^/?(.*?)/?$}.match(path)
96
+ "/#{$1}"
97
+ end
98
+
99
+ # Reads a cached response from disk and updates a response object.
100
+ def read_response(path, response, request)
101
+ file_path = page_cache_path(path)
102
+ if metadata = read_metadata(path)
103
+ response.headers.merge!(metadata['headers'] || {})
104
+ if client_has_cache?(metadata, request)
105
+ response.headers.merge!('Status' => '304 Not Modified')
106
+ elsif use_x_sendfile
107
+ response.headers.merge!('X-Sendfile' => "#{file_path}.data")
108
+ else
109
+ response.body = File.open("#{file_path}.data", "rb") {|f| f.read}
110
+ end
111
+ end
112
+ response
113
+ end
114
+
115
+ def client_has_cache?(metadata, request)
116
+ return false unless request
117
+ request_time = Time.httpdate(request.env["HTTP_IF_MODIFIED_SINCE"]) rescue nil
118
+ response_time = Time.httpdate(metadata['headers']['Last-Modified']) rescue nil
119
+ return request_time && response_time && response_time <= request_time
120
+ end
121
+
122
+ # Writes a response to disk.
123
+ def write_response(path, response)
124
+ if response.cache_timeout
125
+ if Time === response.cache_timeout
126
+ expires = response.cache_timeout
127
+ else
128
+ expires = Time.now + response.cache_timeout
129
+ end
130
+ else
131
+ expires = Time.now + self.expire_time
132
+ end
133
+ response.headers['Last-Modified'] ||= Time.now.httpdate
134
+ metadata = {
135
+ 'headers' => response.headers,
136
+ 'expires' => expires
137
+ }.to_yaml
138
+ cache_page(metadata, response.body, path)
139
+ end
140
+
141
+ def page_cache_path(path)
142
+ path = (path.empty? || path == "/") ? "/_site-root" : URI.unescape(path)
143
+ root_dir = File.expand_path(page_cache_directory)
144
+ cache_path = File.expand_path(File.join(root_dir, path), root_dir)
145
+ cache_path if cache_path.index(root_dir) == 0
146
+ end
147
+
148
+ def expire_page(path)
149
+ return unless perform_caching
150
+
151
+ if path = page_cache_path(path)
152
+ File.delete("#{path}.yml") if File.exists?("#{path}.yml")
153
+ File.delete("#{path}.data") if File.exists?("#{path}.data")
154
+ end
155
+ end
156
+
157
+ def cache_page(metadata, content, path)
158
+ return unless perform_caching
159
+
160
+ if path = page_cache_path(path)
161
+ FileUtils.makedirs(File.dirname(path))
162
+ #dont want yml without data
163
+ File.open("#{path}.data", "wb+") { |f| f.write(content) }
164
+ File.open("#{path}.yml", "wb+") { |f| f.write(metadata) }
165
+ end
166
+ end
167
+ end
168
+
169
+ NULL_CACHE = ResponseCache.new(:perform_caching => false)
170
+ end