serve 0.9.10 → 0.10.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.
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