manveru-ramaze 2008.10 → 2008.12

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 (77) hide show
  1. data/benchmark/run.rb +1 -1
  2. data/examples/app/blog/spec/blog.rb +2 -2
  3. data/examples/app/rapaste/spec/rapaste.rb +1 -1
  4. data/examples/app/rapaste/start.rb +2 -2
  5. data/examples/app/todolist/spec/todolist.rb +1 -1
  6. data/examples/app/whywiki/spec/whywiki.rb +1 -1
  7. data/examples/app/wikore/spec/wikore.rb +1 -1
  8. data/examples/app/wikore/src/model.rb +1 -1
  9. data/examples/app/wiktacular/spec/wiktacular.rb +1 -1
  10. data/examples/app/wiktacular/src/model.rb +1 -1
  11. data/examples/basic/partial.rb +28 -0
  12. data/examples/misc/ramaise.rb +2 -2
  13. data/examples/templates/template_amrita2.rb +1 -1
  14. data/examples/templates/template_erubis.rb +1 -1
  15. data/examples/templates/template_ezamar.rb +1 -1
  16. data/examples/templates/template_haml.rb +2 -2
  17. data/examples/templates/template_liquid.rb +1 -1
  18. data/examples/templates/template_markaby.rb +2 -2
  19. data/examples/templates/template_nagoro.rb +1 -1
  20. data/examples/templates/template_redcloth.rb +1 -1
  21. data/examples/templates/template_remarkably.rb +2 -2
  22. data/examples/templates/template_tenjin.rb +1 -1
  23. data/examples/templates/template_xslt.rb +1 -1
  24. data/lib/proto/public/dispatch.fcgi +2 -2
  25. data/lib/proto/spec/main.rb +3 -3
  26. data/lib/ramaze/action.rb +7 -1
  27. data/lib/ramaze/cache/file.rb +71 -0
  28. data/lib/ramaze/cache.rb +1 -0
  29. data/lib/ramaze/contrib/email.rb +2 -0
  30. data/lib/ramaze/contrib/facebook.rb +2 -2
  31. data/lib/ramaze/contrib/file_cache.rb +2 -64
  32. data/lib/ramaze/contrib/sequel/image.rb +1 -1
  33. data/lib/ramaze/controller.rb +6 -1
  34. data/lib/ramaze/current/request.rb +85 -68
  35. data/lib/ramaze/current/session/hash.rb +7 -11
  36. data/lib/ramaze/current/session.rb +3 -5
  37. data/lib/ramaze/dispatcher/action.rb +2 -0
  38. data/lib/ramaze/dispatcher/file.rb +4 -0
  39. data/lib/ramaze/helper/aspect.rb +2 -2
  40. data/lib/ramaze/helper/form.rb +5 -2
  41. data/lib/ramaze/helper/formatting.rb +4 -0
  42. data/lib/ramaze/helper/gravatar.rb +18 -1
  43. data/lib/ramaze/helper/maruku.rb +2 -0
  44. data/lib/ramaze/helper/redirect.rb +22 -4
  45. data/lib/ramaze/helper.rb +9 -2
  46. data/lib/ramaze/reloader/watch_inotify.rb +73 -0
  47. data/lib/ramaze/reloader/watch_stat.rb +62 -0
  48. data/lib/ramaze/reloader.rb +25 -41
  49. data/lib/ramaze/snippets/object/__dir__.rb +3 -3
  50. data/lib/ramaze/snippets/ramaze/acquire.rb +31 -0
  51. data/lib/ramaze/spec/helper/mock_http.rb +6 -5
  52. data/lib/ramaze/template/ezamar/render_partial.rb +8 -0
  53. data/lib/ramaze.rb +4 -0
  54. data/ramaze.gemspec +757 -756
  55. data/spec/contrib/profiling.rb +1 -1
  56. data/spec/ramaze/action/file_cache.rb +1 -1
  57. data/spec/ramaze/action/layout.rb +1 -1
  58. data/spec/ramaze/controller/actionless_templates.rb +1 -1
  59. data/spec/ramaze/controller/resolve.rb +1 -1
  60. data/spec/ramaze/controller/template_resolving.rb +1 -1
  61. data/spec/ramaze/dispatcher/directory.rb +3 -3
  62. data/spec/ramaze/helper/aspect.rb +1 -1
  63. data/spec/ramaze/helper/partial.rb +1 -1
  64. data/spec/ramaze/localize.rb +1 -1
  65. data/spec/ramaze/rewrite.rb +1 -1
  66. data/spec/ramaze/template/amrita2.rb +1 -1
  67. data/spec/ramaze/template/erubis.rb +1 -1
  68. data/spec/ramaze/template/ezamar.rb +1 -1
  69. data/spec/ramaze/template/haml.rb +2 -2
  70. data/spec/ramaze/template/nagoro.rb +1 -1
  71. data/spec/ramaze/template/redcloth.rb +1 -1
  72. data/spec/ramaze/template/sass.rb +1 -1
  73. data/spec/ramaze/template/tenjin.rb +1 -1
  74. data/spec/ramaze/template.rb +3 -3
  75. data/spec/snippets/object/__dir__.rb +6 -0
  76. data/spec/snippets/ramaze/acquire.rb +77 -0
  77. metadata +8 -8
@@ -28,10 +28,14 @@ module Ramaze
28
28
  super
29
29
  end
30
30
 
31
+ # the full request URI provided by Rack::Request e.g. http://localhost:7000/controller/action?foo=bar.xhtml
32
+
31
33
  def request_uri
32
34
  env['REQUEST_URI'] || path_info
33
35
  end
34
36
 
37
+ # the IP address(s) making the request provided by Rack::Request. You shouldn't trust it
38
+
35
39
  def ip
36
40
  if addr = env['HTTP_X_FORWARDED_FOR']
37
41
  addr.split(',').last.strip
@@ -47,9 +51,8 @@ module Ramaze
47
51
  ipv6 = %w[ fc00::/7 fe80::/10 fec0::/10 ::1 ]
48
52
  LOCAL = (ipv4 + ipv6).map{|a| IPAddr.new(a)} unless defined?(LOCAL)
49
53
 
50
- # --
51
- # Mongrel somehow puts together multiple IPs when proxy is involved.
52
- # ++
54
+ # returns true if the IP address making the request is from local network.
55
+ # Optional argument address can be used to check any IP address.
53
56
 
54
57
  def local_net?(address = ip)
55
58
  addr = IPAddr.new(address)
@@ -63,6 +66,15 @@ module Ramaze
63
66
  [key, *rest].map{|k| params[k.to_s] }
64
67
  end
65
68
 
69
+ # Sets any arguments passed as @instance_variables for the current action.
70
+ #
71
+ # Usage:
72
+ # request.params # => {'name' => 'manveru', 'q' => 'google', 'lang' => 'de'}
73
+ # to_ivs(:name, :q)
74
+ # @q # => 'google'
75
+ # @name # => 'manveru'
76
+ # @lang # => nil
77
+
66
78
  def to_ivs(*args)
67
79
  instance = Action.current.instance
68
80
  args.each do |arg|
@@ -71,81 +83,77 @@ module Ramaze
71
83
  end
72
84
  end
73
85
 
74
- unless method_defined?(:rack_params)
75
- alias rack_params params
76
-
77
- # Wrapping Request#params to support a one-level hash notation.
78
- # It doesn't support anything really fancy, so be conservative in its use.
79
- #
80
- # See if following provides something useful for us:
81
- # http://redhanded.hobix.com/2006/01/25.html
82
- #
83
- # Example Usage:
84
- #
85
- # # Template:
86
- #
87
- # <form action="/paste">
88
- # <input type="text" name="paste[name]" />
89
- # <input type="text" name="paste[syntax]" />
90
- # <input type="submit" />
91
- # </form>
92
- #
93
- # # In your Controller:
94
- #
95
- # def paste
96
- # name, syntax = request['paste'].values_at('name', 'syntax')
97
- # paste = Paste.create_with(:name => name, :syntax => syntax)
98
- # redirect '/'
99
- # end
100
- #
101
- # # Or, easier:
102
- #
103
- # def paste
104
- # paste = Paste.create_with(request['paste'])
105
- # redirect '/'
106
- # end
107
-
108
- def params
109
- return {} if put?
110
- return @ramaze_params if @ramaze_params
111
-
112
- begin
113
- @rack_params ||= rack_params
114
- rescue EOFError => ex
115
- @rack_params = {}
116
- Log.error(ex)
117
- end
86
+ # Wrapping Request#params to support a one-level hash notation.
87
+ # It doesn't support anything really fancy, so be conservative in its use.
88
+ #
89
+ # See if following provides something useful for us:
90
+ # http://redhanded.hobix.com/2006/01/25.html
91
+ #
92
+ # Example Usage:
93
+ #
94
+ # # Template:
95
+ #
96
+ # <form action="/paste">
97
+ # <input type="text" name="paste[name]" />
98
+ # <input type="text" name="paste[syntax]" />
99
+ # <input type="submit" />
100
+ # </form>
101
+ #
102
+ # # In your Controller:
103
+ #
104
+ # def paste
105
+ # name, syntax = request['paste'].values_at('name', 'syntax')
106
+ # paste = Paste.create_with(:name => name, :syntax => syntax)
107
+ # redirect '/'
108
+ # end
109
+ #
110
+ # # Or, easier:
111
+ #
112
+ # def paste
113
+ # paste = Paste.create_with(request['paste'])
114
+ # redirect '/'
115
+ # end
116
+
117
+ def params
118
+ return {} if put?
119
+ return @ramaze_params if @ramaze_params
120
+
121
+ begin
122
+ @rack_params ||= super
123
+ rescue EOFError => ex
124
+ @rack_params = {}
125
+ Log.error(ex)
126
+ end
118
127
 
119
- @ramaze_params = {}
128
+ @ramaze_params = {}
120
129
 
121
- @rack_params.each do |key, value|
122
- if key =~ /^(.*?)(\[.*\])/
123
- prim, nested = $~.captures
124
- ref = @ramaze_params
130
+ @rack_params.each do |key, value|
131
+ if key =~ /^(.*?)(\[.*\])/
132
+ prim, nested = $~.captures
133
+ ref = @ramaze_params
125
134
 
126
- keys = nested.scan(/\[([^\]]+)\]/).flatten
127
- keys.unshift prim
135
+ keys = nested.scan(/\[([^\]]+)\]/).flatten
136
+ keys.unshift prim
128
137
 
129
- keys.each_with_index do |k, i|
130
- if i + 1 >= keys.size
131
- ref[k] = value
138
+ keys.each_with_index do |k, i|
139
+ if i + 1 >= keys.size
140
+ ref[k] = value
141
+ else
142
+ # in case the value is a string we cannot let it be ref next
143
+ # time, so throw it away
144
+ if ref[k].is_a?(String)
145
+ ref = ref[k] = {}
132
146
  else
133
- # in case the value is a string we cannot let it be ref next
134
- # time, so throw it away
135
- if ref[k].is_a?(String)
136
- ref = ref[k] = {}
137
- else
138
- ref = ref[k] ||= {}
139
- end
147
+ ref = ref[k] ||= {}
140
148
  end
141
149
  end
142
- else
143
- @ramaze_params[key] = value
144
150
  end
151
+ else
152
+ @ramaze_params[key] = value
145
153
  end
146
-
147
- @ramaze_params
148
154
  end
155
+
156
+ @ramaze_params
149
157
  end
150
158
 
151
159
  # Interesting HTTP variables from env
@@ -156,12 +164,18 @@ module Ramaze
156
164
  }
157
165
  end
158
166
 
167
+ # Returns a string presentation of the request, useful for debugging
168
+ # parameters of the action.
169
+
159
170
  def to_s
160
171
  p, c, e = params.inspect, cookies.inspect, http_vars.inspect
161
172
  %{#<Ramaze::Request params=#{p} cookies=#{c} env=#{e}>}
162
173
  end
163
174
  alias inspect to_s
164
175
 
176
+ # Pretty prints current action with parameters, cookies and
177
+ # enviroment variables.
178
+
165
179
  def pretty_print pp
166
180
  p, c, e = params, cookies, http_vars
167
181
  pp.object_group(self){
@@ -198,6 +212,9 @@ module Ramaze
198
212
  URI("#{scheme}://#{host}#{path}")
199
213
  end
200
214
 
215
+ # Returns and array of locales from env['HTTP_ACCEPT_LANGUAGE].
216
+ # e.g. ["fi", "en", "ja", "fr", "de", "es", "it", "nl", "sv"]
217
+
201
218
  def locales
202
219
  env['HTTP_ACCEPT_LANGUAGE'].to_s.split(/(?:,|;q=[\d.,]+)/)
203
220
  end
@@ -9,8 +9,8 @@ module Ramaze
9
9
 
10
10
  # Sets @hash to an empty Hash
11
11
 
12
- def initialize sess
13
- @session = sess
12
+ def initialize(session)
13
+ @session = session
14
14
  @hash = {}
15
15
  end
16
16
 
@@ -18,11 +18,8 @@ module Ramaze
18
18
  # Session.current.sessions if anything changes.
19
19
 
20
20
  def method_missing(*args, &block)
21
- old = @hash.dup
22
21
  result = @hash.send(*args, &block)
23
- unless old == @hash
24
- Cache.sessions[@session.session_id] = self
25
- end
22
+ Cache.sessions[@session.session_id] = self
26
23
  result
27
24
  end
28
25
 
@@ -48,11 +45,10 @@ module Ramaze
48
45
 
49
46
  # Unmarshal cookie data to a hash and verify its integrity.
50
47
  def unmarshal(cookie)
51
- if cookie
52
- data, digest = cookie.split('--')
53
- return nil unless digest == generate_digest(data)
54
- Marshal.load(data.unpack('m').first)
55
- end
48
+ return unless cookie
49
+ data, digest = cookie.split('--')
50
+ return nil unless digest == generate_digest(data)
51
+ Marshal.load(data.unpack('m').first)
56
52
  end
57
53
 
58
54
  # Generate the inline SHA512 message digest. Larger (128 bytes) than SHA256
@@ -96,7 +96,7 @@ module Ramaze
96
96
  def initialize(sess_or_request = Current.request)
97
97
  return unless Global.sessions
98
98
 
99
- if sess_or_request.is_a?(Request)
99
+ if sess_or_request.respond_to?(:cookies)
100
100
  request = sess_or_request
101
101
  @session_id = request.cookies[SESSION_KEY] || Session.random_key
102
102
  else
@@ -125,10 +125,8 @@ module Ramaze
125
125
  # existing already, the session itself is an instance of SessionHash
126
126
 
127
127
  def current
128
- unless @current
129
- @current = ( sessions[session_id] ||= Session::Hash.new(self) )
130
- end
131
- @current
128
+ return @current if @current
129
+ @current = ( sessions[session_id] ||= Session::Hash.new(self) )
132
130
  end
133
131
 
134
132
  # shortcut to Cache.sessions
@@ -35,6 +35,8 @@ module Ramaze
35
35
  ex
36
36
  end
37
37
 
38
+ # Logs the request via Log#info unless it's boring.
39
+
38
40
  def log(path)
39
41
  case path
40
42
  when *Global.boring
@@ -79,6 +79,10 @@ module Ramaze
79
79
 
80
80
  private
81
81
 
82
+ # Shortcut for Ramaze::Dispatcher::File#expand_path(path).
83
+ # Returns the absolute path of passed argument e.g.
84
+ # "/Users/rikur/ramaze_tests/public/foo.txt"
85
+
82
86
  def expand(path)
83
87
  ::File.expand_path(path)
84
88
  end
@@ -76,7 +76,7 @@ module Ramaze
76
76
  class Action
77
77
 
78
78
  # overwrites the default Action hook and runs the neccesary blocks in its
79
- # scope. before actions are run starting from Ramaze::Controller down the
79
+ # scope before actions are run, starting from Ramaze::Controller down the
80
80
  # ancestor chain.
81
81
  def before_process
82
82
  common_aspect(:before)
@@ -84,7 +84,7 @@ module Ramaze
84
84
 
85
85
 
86
86
  # overwrites the default Action hook and runs the neccesary blocks in its
87
- # scope. before actions are run starting from Ramaze::Controller down the
87
+ # scope after actions are run, starting from Ramaze::Controller down the
88
88
  # ancestor chain.
89
89
  def after_process
90
90
  common_aspect(:after)
@@ -219,16 +219,19 @@ module Ramaze
219
219
 
220
220
  # Form for instances of the model class
221
221
  class InstanceForm < Form
222
- # <input type='text' name='name' value='value' />
222
+ # returns <input type='text' name='name' value='value' />
223
223
  def field_input(name, value)
224
224
  "<input type='text' name='#{name}' value='#{value}'/>"
225
225
  end
226
226
 
227
+ # returns <textarea name='name'>#{value}</textarea>
228
+
227
229
  def field_textarea(name, value)
228
230
  "<textarea name='#{name}'>#{value}</textarea>"
229
231
  end
230
232
 
231
- # <input type="text" name="name" value="value" />
233
+ # returns <input type="text" name="name" value="value" />
234
+
232
235
  def field_integer(name, value)
233
236
  field_input(name, value)
234
237
  end
@@ -111,6 +111,10 @@ module Ramaze
111
111
  end
112
112
  alias autolink auto_link
113
113
 
114
+ # takes a string and optional argument for outputting compliance HTML
115
+ # instead of XHTML.
116
+ # e.g nl2br "a\nb\n\c" #=> 'a<br />b<br />c'
117
+
114
118
  def nl2br(string, xhtml = true)
115
119
  br = xhtml ? '<br />' : '<br>'
116
120
  string.gsub(/\n/, br)
@@ -1,12 +1,29 @@
1
1
  module Ramaze
2
2
  module Helper
3
3
  module Gravatar
4
+
5
+ # fetches a gravatar from http//www.gravatar.com based on 'email'
6
+ # and 'size'. Falls back to 'fallback_path' if no gravatar is found.
7
+ # default 'fallback_path' is "/images/gravatar_default.jpg".
8
+ # example:
9
+ #
10
+ # class GravatarController < Ramaze::Controller
11
+ # helper :gravatar
12
+ #
13
+ # def index
14
+ # @gravatar_thumbnail_src = gravatar(session[:email] || 'riku@helloit.fi')
15
+ # end
16
+ # end
17
+ #
18
+ # /view/gravatar/index.html:
19
+ # <img src="#{@gravatar_thumbnail_src}" />
20
+
4
21
  def gravatar(email, size = 32, fallback_path = "/images/gravatar_default.jpg")
5
22
  emailhash = Digest::MD5.hexdigest(email)
6
23
 
7
24
  fallback = Request.current.domain
8
25
  fallback.path = fallback_path
9
- default = h(fallback.to_s)
26
+ default = Rack::Utils.escape(fallback.to_s)
10
27
 
11
28
  return "http://www.gravatar.com/avatar.php?gravatar_id=#{emailhash}&default=#{default}&size=#{size}"
12
29
  end
@@ -1,3 +1,5 @@
1
+ require 'maruku'
2
+
1
3
  module Ramaze
2
4
  module Helper::Maruku
3
5
  def maruku(text)
@@ -59,7 +59,7 @@ module Ramaze
59
59
  def raw_redirect(target, opts = {})
60
60
  target = target.to_s
61
61
  header = {'Location' => target}
62
- status = opts[:status] || STATUS_CODE["Moved Temporarily"]
62
+ status = opts[:status] || 302 # Found
63
63
  body = %{You are being redirected, please follow <a href="#{target}">this link to: #{target}</a>!}
64
64
 
65
65
  Log.info("Redirect to '#{target}'")
@@ -72,10 +72,28 @@ module Ramaze
72
72
  request[:redirected]
73
73
  end
74
74
 
75
- # redirect to the location the browser says it's coming from.
75
+ # Redirect to the location the browser says it's coming from.
76
+ # If the current address is the same as the referrer or no referrer exists
77
+ # yet, we will redirect to +fallback+.
78
+ #
79
+ # NOTE:
80
+ # * In some cases this may result in a double redirect, given that the
81
+ # request query parameters may change order. We don't have a nice way
82
+ # of handling that yet, but it should be very, very rare
83
+
84
+ def redirect_referer(fallback = R(:/))
85
+ if referer = request.referer and url = request.url
86
+ referer_uri = URI(referer)
87
+ request_uri = URI(url)
76
88
 
77
- def redirect_referer
78
- redirect request.referer
89
+ if referer_uri == request_uri
90
+ redirect fallback
91
+ else
92
+ redirect referer
93
+ end
94
+ else
95
+ redirect fallback
96
+ end
79
97
  end
80
98
  alias redirect_referrer redirect_referer
81
99
  end
data/lib/ramaze/helper.rb CHANGED
@@ -45,7 +45,7 @@ module Ramaze
45
45
  if require_helper(name)
46
46
  redo
47
47
  else
48
- raise LoadError, "#{name} not found"
48
+ raise LoadError, "helper #{name} not found"
49
49
  end
50
50
  end
51
51
  end
@@ -53,6 +53,8 @@ module Ramaze
53
53
 
54
54
  private
55
55
 
56
+ # returns the Ramaze::Helper::Name Module Constant if exists.
57
+
56
58
  def find_helper(name)
57
59
  name = name.to_s.camel_case
58
60
  ramaze_helper_consts = ::Ramaze::Helper.constants.grep(/^#{name}$/i)
@@ -61,16 +63,21 @@ module Ramaze
61
63
  end
62
64
  end
63
65
 
66
+ # Loads helper from /lib/ramaze/helper/name.(so, bundle, rb)
67
+ # raises LoadError if helper not found.
68
+
64
69
  def require_helper(name)
65
70
  paths = (PATH + [Global.root, "#{BASEDIR}/ramaze"]).join(',')
66
71
  glob = "{#{paths}}/helper/#{name}.{so,bundle,rb}"
67
72
  files = Dir[glob]
68
73
  ignore = Helper.trait[:ignore]
69
74
  files.reject!{|f| ignore.any?{|i| f =~ i }}
70
- raise LoadError, "#{name} not found" unless file = files.first
75
+ raise LoadError, "file for #{name} not found" unless file = files.first
71
76
  require(file)
72
77
  end
73
78
 
79
+ # injects the helper via include and extend, takes Constant as argument.
80
+
74
81
  def use_helper(mod)
75
82
  include mod
76
83
  extend mod
@@ -0,0 +1,73 @@
1
+ module Ramaze
2
+ class Reloader
3
+ class WatchInotify
4
+ POLL_INTERVAL = 2 # seconds
5
+
6
+ def initialize
7
+ @watcher = RInotify.new
8
+ @changed = []
9
+ @mutex = Mutex.new
10
+ @watcher_thread = start_watcher
11
+ end
12
+
13
+ def call(cooldown)
14
+ yield if @changed.any?
15
+ end
16
+
17
+ # TODO: define a finalizer to cleanup? -- reloader never calls #close
18
+
19
+ def start_watcher
20
+ Thread.new do
21
+ loop do
22
+ watcher_cycle
23
+ sleep POLL_INTERVAL
24
+ end
25
+ end
26
+ end
27
+
28
+ def watcher_cycle
29
+ return unless @watcher.wait_for_events(0)
30
+ changed_descriptors = []
31
+
32
+ @watcher.each_event do |event|
33
+ changed_descriptors << event.watch_descriptor
34
+ end
35
+
36
+ @mutex.synchronize do
37
+ changed_descriptors.each do |descriptor|
38
+ @changed << @watcher.watch_descriptors[descriptor]
39
+ end
40
+ end
41
+ end
42
+
43
+ def watch(file)
44
+ return false if @watcher.watch_descriptors.has_value?(file)
45
+ return false unless File.exist?(file)
46
+
47
+ @mutex.synchronize{ @watcher.add_watch(file, RInotify::MODIFY) }
48
+
49
+ true
50
+ end
51
+
52
+ def remove_watch(file)
53
+ @mutex.synchronize{ @watcher.rm_watch(file) }
54
+ true
55
+ end
56
+
57
+ def close
58
+ @watcher_thread.terminate
59
+ @watcher.close
60
+ true
61
+ end
62
+
63
+ def changed_files
64
+ @mutex.synchronize do
65
+ @tmp = @changed
66
+ @changed = []
67
+ end
68
+ @tmp.uniq!
69
+ @tmp
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,62 @@
1
+ module Ramaze
2
+ class Reloader
3
+ class WatchStat
4
+ def initialize
5
+ # @files[file_path] = stat
6
+ @files = {}
7
+ @last = Time.now
8
+ end
9
+
10
+ def call(cooldown)
11
+ if cooldown and Time.now > @last + cooldown
12
+ yield
13
+ @last = Time.now
14
+ end
15
+ end
16
+
17
+ # start watching a file for changes
18
+ # true if succeeded, false if failure
19
+ def watch(file)
20
+ return true if watching?(file) # if already watching
21
+ if stat = safe_stat(file)
22
+ @files[file] = stat
23
+ end
24
+ end
25
+
26
+ def watching?(file)
27
+ @files.has_key?(file)
28
+ end
29
+
30
+ # stop watching a file for changes
31
+ def remove_watch(file)
32
+ @files.delete(file)
33
+ end
34
+
35
+ # no need for cleanup
36
+ def close
37
+ end
38
+
39
+ # return files changed since last call
40
+ def changed_files
41
+ changed = []
42
+
43
+ @files.each do |file, stat|
44
+ if new_stat = safe_stat(file)
45
+ if new_stat.mtime > stat.mtime
46
+ changed << file
47
+ @files[file] = new_stat
48
+ end
49
+ end
50
+ end
51
+
52
+ changed
53
+ end
54
+
55
+ def safe_stat(file)
56
+ File.stat(file)
57
+ rescue Errno::ENOENT, Errno::ENOTDIR
58
+ nil
59
+ end
60
+ end
61
+ end
62
+ end