| 1 |
require 'fileutils' |
|---|
| 2 |
require 'uri' |
|---|
| 3 |
require 'set' |
|---|
| 4 |
|
|---|
| 5 |
module ActionController |
|---|
| 6 |
|
|---|
| 7 |
|
|---|
| 8 |
|
|---|
| 9 |
|
|---|
| 10 |
|
|---|
| 11 |
|
|---|
| 12 |
module Caching |
|---|
| 13 |
def self.included(base) |
|---|
| 14 |
base.send(:include, Pages, Actions, Fragments, Sweeping) |
|---|
| 15 |
|
|---|
| 16 |
base.class_eval do |
|---|
| 17 |
@@perform_caching = true |
|---|
| 18 |
cattr_accessor :perform_caching |
|---|
| 19 |
end |
|---|
| 20 |
end |
|---|
| 21 |
|
|---|
| 22 |
|
|---|
| 23 |
|
|---|
| 24 |
|
|---|
| 25 |
|
|---|
| 26 |
|
|---|
| 27 |
|
|---|
| 28 |
|
|---|
| 29 |
|
|---|
| 30 |
|
|---|
| 31 |
|
|---|
| 32 |
|
|---|
| 33 |
|
|---|
| 34 |
|
|---|
| 35 |
|
|---|
| 36 |
|
|---|
| 37 |
|
|---|
| 38 |
|
|---|
| 39 |
|
|---|
| 40 |
|
|---|
| 41 |
|
|---|
| 42 |
|
|---|
| 43 |
|
|---|
| 44 |
|
|---|
| 45 |
|
|---|
| 46 |
|
|---|
| 47 |
|
|---|
| 48 |
|
|---|
| 49 |
|
|---|
| 50 |
|
|---|
| 51 |
|
|---|
| 52 |
|
|---|
| 53 |
|
|---|
| 54 |
|
|---|
| 55 |
|
|---|
| 56 |
|
|---|
| 57 |
|
|---|
| 58 |
|
|---|
| 59 |
|
|---|
| 60 |
|
|---|
| 61 |
module Pages |
|---|
| 62 |
def self.included(base) |
|---|
| 63 |
base.extend(ClassMethods) |
|---|
| 64 |
base.class_eval do |
|---|
| 65 |
@@page_cache_directory = defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/public" : "" |
|---|
| 66 |
cattr_accessor :page_cache_directory |
|---|
| 67 |
|
|---|
| 68 |
@@page_cache_extension = '.html' |
|---|
| 69 |
cattr_accessor :page_cache_extension |
|---|
| 70 |
end |
|---|
| 71 |
end |
|---|
| 72 |
|
|---|
| 73 |
module ClassMethods |
|---|
| 74 |
|
|---|
| 75 |
|
|---|
| 76 |
def expire_page(path) |
|---|
| 77 |
return unless perform_caching |
|---|
| 78 |
|
|---|
| 79 |
benchmark "Expired page: #{page_cache_file(path)}" do |
|---|
| 80 |
File.delete(page_cache_path(path)) if File.exists?(page_cache_path(path)) |
|---|
| 81 |
end |
|---|
| 82 |
end |
|---|
| 83 |
|
|---|
| 84 |
|
|---|
| 85 |
|
|---|
| 86 |
def cache_page(content, path) |
|---|
| 87 |
return unless perform_caching |
|---|
| 88 |
|
|---|
| 89 |
benchmark "Cached page: #{page_cache_file(path)}" do |
|---|
| 90 |
FileUtils.makedirs(File.dirname(page_cache_path(path))) |
|---|
| 91 |
File.open(page_cache_path(path), "wb+") { |f| f.write(content) } |
|---|
| 92 |
end |
|---|
| 93 |
end |
|---|
| 94 |
|
|---|
| 95 |
|
|---|
| 96 |
|
|---|
| 97 |
def caches_page(*actions) |
|---|
| 98 |
return unless perform_caching |
|---|
| 99 |
actions.each do |action| |
|---|
| 100 |
class_eval "after_filter { |c| c.cache_page if c.action_name == '#{action}' }" |
|---|
| 101 |
end |
|---|
| 102 |
end |
|---|
| 103 |
|
|---|
| 104 |
private |
|---|
| 105 |
def page_cache_file(path) |
|---|
| 106 |
name = ((path.empty? || path == "/") ? "/index" : URI.unescape(path)) |
|---|
| 107 |
name << page_cache_extension unless (name.split('/').last || name).include? '.' |
|---|
| 108 |
return name |
|---|
| 109 |
end |
|---|
| 110 |
|
|---|
| 111 |
def page_cache_path(path) |
|---|
| 112 |
page_cache_directory + page_cache_file(path) |
|---|
| 113 |
end |
|---|
| 114 |
end |
|---|
| 115 |
|
|---|
| 116 |
|
|---|
| 117 |
|
|---|
| 118 |
def expire_page(options = {}) |
|---|
| 119 |
return unless perform_caching |
|---|
| 120 |
if options[:action].is_a?(Array) |
|---|
| 121 |
options[:action].dup.each do |action| |
|---|
| 122 |
self.class.expire_page(url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :action => action))) |
|---|
| 123 |
end |
|---|
| 124 |
else |
|---|
| 125 |
self.class.expire_page(url_for(options.merge(:only_path => true, :skip_relative_url_root => true))) |
|---|
| 126 |
end |
|---|
| 127 |
end |
|---|
| 128 |
|
|---|
| 129 |
|
|---|
| 130 |
|
|---|
| 131 |
|
|---|
| 132 |
def cache_page(content = nil, options = {}) |
|---|
| 133 |
return unless perform_caching && caching_allowed |
|---|
| 134 |
self.class.cache_page(content || response.body, url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :format => params[:format]))) |
|---|
| 135 |
end |
|---|
| 136 |
|
|---|
| 137 |
private |
|---|
| 138 |
def caching_allowed |
|---|
| 139 |
request.get? && response.headers['Status'].to_i == 200 |
|---|
| 140 |
end |
|---|
| 141 |
end |
|---|
| 142 |
|
|---|
| 143 |
|
|---|
| 144 |
|
|---|
| 145 |
|
|---|
| 146 |
|
|---|
| 147 |
|
|---|
| 148 |
|
|---|
| 149 |
|
|---|
| 150 |
|
|---|
| 151 |
|
|---|
| 152 |
|
|---|
| 153 |
|
|---|
| 154 |
|
|---|
| 155 |
|
|---|
| 156 |
|
|---|
| 157 |
|
|---|
| 158 |
|
|---|
| 159 |
|
|---|
| 160 |
|
|---|
| 161 |
|
|---|
| 162 |
|
|---|
| 163 |
|
|---|
| 164 |
module Actions |
|---|
| 165 |
def self.included(base) |
|---|
| 166 |
base.extend(ClassMethods) |
|---|
| 167 |
base.class_eval do |
|---|
| 168 |
attr_accessor :rendered_action_cache, :action_cache_path |
|---|
| 169 |
alias_method_chain :protected_instance_variables, :action_caching |
|---|
| 170 |
end |
|---|
| 171 |
end |
|---|
| 172 |
|
|---|
| 173 |
def protected_instance_variables_with_action_caching |
|---|
| 174 |
protected_instance_variables_without_action_caching + %w(@action_cache_path) |
|---|
| 175 |
end |
|---|
| 176 |
|
|---|
| 177 |
module ClassMethods |
|---|
| 178 |
|
|---|
| 179 |
|
|---|
| 180 |
def caches_action(*actions) |
|---|
| 181 |
return unless perform_caching |
|---|
| 182 |
action_cache_filter = ActionCacheFilter.new(*actions) |
|---|
| 183 |
before_filter action_cache_filter |
|---|
| 184 |
after_filter action_cache_filter |
|---|
| 185 |
end |
|---|
| 186 |
end |
|---|
| 187 |
|
|---|
| 188 |
def expire_action(options = {}) |
|---|
| 189 |
return unless perform_caching |
|---|
| 190 |
if options[:action].is_a?(Array) |
|---|
| 191 |
options[:action].dup.each do |action| |
|---|
| 192 |
expire_fragment(ActionCachePath.path_for(self, options.merge({ :action => action }))) |
|---|
| 193 |
end |
|---|
| 194 |
else |
|---|
| 195 |
expire_fragment(ActionCachePath.path_for(self, options)) |
|---|
| 196 |
end |
|---|
| 197 |
end |
|---|
| 198 |
|
|---|
| 199 |
class ActionCacheFilter |
|---|
| 200 |
def initialize(*actions) |
|---|
| 201 |
@actions = Set.new actions |
|---|
| 202 |
end |
|---|
| 203 |
|
|---|
| 204 |
def before(controller) |
|---|
| 205 |
return unless @actions.include?(controller.action_name.to_sym) |
|---|
| 206 |
cache_path = ActionCachePath.new(controller, {}) |
|---|
| 207 |
if cache = controller.read_fragment(cache_path.path) |
|---|
| 208 |
controller.rendered_action_cache = true |
|---|
| 209 |
set_content_type!(controller, cache_path.extension) |
|---|
| 210 |
controller.send(:render_text, cache) |
|---|
| 211 |
false |
|---|
| 212 |
else |
|---|
| 213 |
controller.action_cache_path = cache_path |
|---|
| 214 |
end |
|---|
| 215 |
end |
|---|
| 216 |
|
|---|
| 217 |
def after(controller) |
|---|
| 218 |
return if !@actions.include?(controller.action_name.to_sym) || controller.rendered_action_cache |
|---|
| 219 |
controller.write_fragment(controller.action_cache_path.path, controller.response.body) |
|---|
| 220 |
end |
|---|
| 221 |
|
|---|
| 222 |
private |
|---|
| 223 |
def set_content_type!(controller, extension) |
|---|
| 224 |
controller.response.content_type = Mime::EXTENSION_LOOKUP[extension].to_s if extension |
|---|
| 225 |
end |
|---|
| 226 |
|
|---|
| 227 |
end |
|---|
| 228 |
|
|---|
| 229 |
class ActionCachePath |
|---|
| 230 |
attr_reader :path, :extension |
|---|
| 231 |
|
|---|
| 232 |
class << self |
|---|
| 233 |
def path_for(controller, options) |
|---|
| 234 |
new(controller, options).path |
|---|
| 235 |
end |
|---|
| 236 |
end |
|---|
| 237 |
|
|---|
| 238 |
def initialize(controller, options = {}) |
|---|
| 239 |
@extension = extract_extension(controller.request.path) |
|---|
| 240 |
path = controller.url_for(options).split('://').last |
|---|
| 241 |
normalize!(path) |
|---|
| 242 |
add_extension!(path, @extension) |
|---|
| 243 |
@path = URI.unescape(path) |
|---|
| 244 |
end |
|---|
| 245 |
|
|---|
| 246 |
private |
|---|
| 247 |
def normalize!(path) |
|---|
| 248 |
path << 'index' if path[-1] == ?/ |
|---|
| 249 |
end |
|---|
| 250 |
|
|---|
| 251 |
def add_extension!(path, extension) |
|---|
| 252 |
path << ".#{extension}" if extension |
|---|
| 253 |
end |
|---|
| 254 |
|
|---|
| 255 |
def extract_extension(file_path) |
|---|
| 256 |
|
|---|
| 257 |
|
|---|
| 258 |
file_path[/^[^.]+\.(.+)$/, 1] |
|---|
| 259 |
end |
|---|
| 260 |
end |
|---|
| 261 |
end |
|---|
| 262 |
|
|---|
| 263 |
|
|---|
| 264 |
|
|---|
| 265 |
|
|---|
| 266 |
|
|---|
| 267 |
|
|---|
| 268 |
|
|---|
| 269 |
|
|---|
| 270 |
|
|---|
| 271 |
|
|---|
| 272 |
|
|---|
| 273 |
|
|---|
| 274 |
|
|---|
| 275 |
|
|---|
| 276 |
|
|---|
| 277 |
|
|---|
| 278 |
|
|---|
| 279 |
|
|---|
| 280 |
|
|---|
| 281 |
|
|---|
| 282 |
|
|---|
| 283 |
|
|---|
| 284 |
|
|---|
| 285 |
|
|---|
| 286 |
|
|---|
| 287 |
|
|---|
| 288 |
|
|---|
| 289 |
|
|---|
| 290 |
|
|---|
| 291 |
|
|---|
| 292 |
|
|---|
| 293 |
|
|---|
| 294 |
|
|---|
| 295 |
|
|---|
| 296 |
|
|---|
| 297 |
|
|---|
| 298 |
|
|---|
| 299 |
|
|---|
| 300 |
|
|---|
| 301 |
|
|---|
| 302 |
|
|---|
| 303 |
|
|---|
| 304 |
|
|---|
| 305 |
|
|---|
| 306 |
|
|---|
| 307 |
module Fragments |
|---|
| 308 |
def self.included(base) |
|---|
| 309 |
base.class_eval do |
|---|
| 310 |
@@fragment_cache_store = MemoryStore.new |
|---|
| 311 |
cattr_reader :fragment_cache_store |
|---|
| 312 |
|
|---|
| 313 |
def self.fragment_cache_store=(store_option) |
|---|
| 314 |
store, *parameters = *([ store_option ].flatten) |
|---|
| 315 |
@@fragment_cache_store = if store.is_a?(Symbol) |
|---|
| 316 |
store_class_name = (store == :drb_store ? "DRbStore" : store.to_s.camelize) |
|---|
| 317 |
store_class = ActionController::Caching::Fragments.const_get(store_class_name) |
|---|
| 318 |
store_class.new(*parameters) |
|---|
| 319 |
else |
|---|
| 320 |
store |
|---|
| 321 |
end |
|---|
| 322 |
end |
|---|
| 323 |
end |
|---|
| 324 |
end |
|---|
| 325 |
|
|---|
| 326 |
def fragment_cache_key(name) |
|---|
| 327 |
name.is_a?(Hash) ? url_for(name).split("://").last : name |
|---|
| 328 |
end |
|---|
| 329 |
|
|---|
| 330 |
|
|---|
| 331 |
def cache_erb_fragment(block, name = {}, options = nil) |
|---|
| 332 |
unless perform_caching then block.call; return end |
|---|
| 333 |
|
|---|
| 334 |
buffer = eval("_erbout", block.binding) |
|---|
| 335 |
|
|---|
| 336 |
if cache = read_fragment(name, options) |
|---|
| 337 |
buffer.concat(cache) |
|---|
| 338 |
else |
|---|
| 339 |
pos = buffer.length |
|---|
| 340 |
block.call |
|---|
| 341 |
write_fragment(name, buffer[pos..-1], options) |
|---|
| 342 |
end |
|---|
| 343 |
end |
|---|
| 344 |
|
|---|
| 345 |
def write_fragment(name, content, options = nil) |
|---|
| 346 |
return unless perform_caching |
|---|
| 347 |
|
|---|
| 348 |
key = fragment_cache_key(name) |
|---|
| 349 |
self.class.benchmark "Cached fragment: #{key}" do |
|---|
| 350 |
fragment_cache_store.write(key, content, options) |
|---|
| 351 |
end |
|---|
| 352 |
content |
|---|
| 353 |
end |
|---|
| 354 |
|
|---|
| 355 |
def read_fragment(name, options = nil) |
|---|
| 356 |
return unless perform_caching |
|---|
| 357 |
|
|---|
| 358 |
key = fragment_cache_key(name) |
|---|
| 359 |
self.class.benchmark "Fragment read: #{key}" do |
|---|
| 360 |
fragment_cache_store.read(key, options) |
|---|
| 361 |
end |
|---|
| 362 |
end |
|---|
| 363 |
|
|---|
| 364 |
|
|---|
| 365 |
|
|---|
| 366 |
|
|---|
| 367 |
|
|---|
| 368 |
|
|---|
| 369 |
|
|---|
| 370 |
|
|---|
| 371 |
|
|---|
| 372 |
|
|---|
| 373 |
def expire_fragment(name, options = nil) |
|---|
| 374 |
return unless perform_caching |
|---|
| 375 |
|
|---|
| 376 |
key = fragment_cache_key(name) |
|---|
| 377 |
|
|---|
| 378 |
if key.is_a?(Regexp) |
|---|
| 379 |
self.class.benchmark "Expired fragments matching: #{key.source}" do |
|---|
| 380 |
fragment_cache_store.delete_matched(key, options) |
|---|
| 381 |
end |
|---|
| 382 |
else |
|---|
| 383 |
self.class.benchmark "Expired fragment: #{key}" do |
|---|
| 384 |
fragment_cache_store.delete(key, options) |
|---|
| 385 |
end |
|---|
| 386 |
end |
|---|
| 387 |
end |
|---|
| 388 |
|
|---|
| 389 |
|
|---|
| 390 |
def expire_matched_fragments(matcher = /.*/, options = nil) |
|---|
| 391 |
expire_fragment(matcher, options) |
|---|
| 392 |
end |
|---|
| 393 |
deprecate :expire_matched_fragments => :expire_fragment |
|---|
| 394 |
|
|---|
| 395 |
|
|---|
| 396 |
class UnthreadedMemoryStore |
|---|
| 397 |
def initialize |
|---|
| 398 |
@data = {} |
|---|
| 399 |
end |
|---|
| 400 |
|
|---|
| 401 |
def read(name, options=nil) |
|---|
| 402 |
@data[name] |
|---|
| 403 |
end |
|---|
| 404 |
|
|---|
| 405 |
def write(name, value, options=nil) |
|---|
| 406 |
@data[name] = value |
|---|
| 407 |
end |
|---|
| 408 |
|
|---|
| 409 |
def delete(name, options=nil) |
|---|
| 410 |
@data.delete(name) |
|---|
| 411 |
end |
|---|
| 412 |
|
|---|
| 413 |
def delete_matched(matcher, options=nil) |
|---|
| 414 |
@data.delete_if { |k,v| k =~ matcher } |
|---|
| 415 |
end |
|---|
| 416 |
end |
|---|
| 417 |
|
|---|
| 418 |
module ThreadSafety |
|---|
| 419 |
def read(name, options=nil) |
|---|
| 420 |
@mutex.synchronize { super } |
|---|
| 421 |
end |
|---|
| 422 |
|
|---|
| 423 |
def write(name, value, options=nil) |
|---|
| 424 |
@mutex.synchronize { super } |
|---|
| 425 |
end |
|---|
| 426 |
|
|---|
| 427 |
def delete(name, options=nil) |
|---|
| 428 |
@mutex.synchronize { super } |
|---|
| 429 |
end |
|---|
| 430 |
|
|---|
| 431 |
def delete_matched(matcher, options=nil) |
|---|
| 432 |
@mutex.synchronize { super } |
|---|
| 433 |
end |
|---|
| 434 |
end |
|---|
| 435 |
|
|---|
| 436 |
class MemoryStore < UnthreadedMemoryStore |
|---|
| 437 |
def initialize |
|---|
| 438 |
super |
|---|
| 439 |
if ActionController::Base.allow_concurrency |
|---|
| 440 |
@mutex = Mutex.new |
|---|
| 441 |
MemoryStore.send(:include, ThreadSafety) |
|---|
| 442 |
end |
|---|
| 443 |
end |
|---|
| 444 |
end |
|---|
| 445 |
|
|---|
| 446 |
class DRbStore < MemoryStore |
|---|
| 447 |
attr_reader :address |
|---|
| 448 |
|
|---|
| 449 |
def initialize(address = 'druby://localhost:9192') |
|---|
| 450 |
super() |
|---|
| 451 |
@address = address |
|---|
| 452 |
@data = DRbObject.new(nil, address) |
|---|
| 453 |
end |
|---|
| 454 |
end |
|---|
| 455 |
|
|---|
| 456 |
class MemCacheStore < MemoryStore |
|---|
| 457 |
attr_reader :addresses |
|---|
| 458 |
|
|---|
| 459 |
def initialize(*addresses) |
|---|
| 460 |
super() |
|---|
| 461 |
addresses = addresses.flatten |
|---|
| 462 |
addresses = ["localhost"] if addresses.empty? |
|---|
| 463 |
@addresses = addresses |
|---|
| 464 |
@data = MemCache.new(*addresses) |
|---|
| 465 |
end |
|---|
| 466 |
end |
|---|
| 467 |
|
|---|
| 468 |
class UnthreadedFileStore |
|---|
| 469 |
attr_reader :cache_path |
|---|
| 470 |
|
|---|
| 471 |
def initialize(cache_path) |
|---|
| 472 |
@cache_path = cache_path |
|---|
| 473 |
end |
|---|
| 474 |
|
|---|
| 475 |
def write(name, value, options = nil) |
|---|
| 476 |
ensure_cache_path(File.dirname(real_file_path(name))) |
|---|
| 477 |
File.open(real_file_path(name), "wb+") { |f| f.write(value) } |
|---|
| 478 |
rescue => e |
|---|
| 479 |
Base.logger.error "Couldn't create cache directory: #{name} (#{e.message})" if Base.logger |
|---|
| 480 |
end |
|---|
| 481 |
|
|---|
| 482 |
def read(name, options = nil) |
|---|
| 483 |
File.open(real_file_path(name), 'rb') { |f| f.read } rescue nil |
|---|
| 484 |
end |
|---|
| 485 |
|
|---|
| 486 |
def delete(name, options) |
|---|
| 487 |
File.delete(real_file_path(name)) |
|---|
| 488 |
rescue SystemCallError => e |
|---|
| 489 |
|
|---|
| 490 |
end |
|---|
| 491 |
|
|---|
| 492 |
def delete_matched(matcher, options) |
|---|
| 493 |
search_dir(@cache_path) do |f| |
|---|
| 494 |
if f =~ matcher |
|---|
| 495 |
begin |
|---|
| 496 |
File.delete(f) |
|---|
| 497 |
rescue SystemCallError => e |
|---|
| 498 |
|
|---|
| 499 |
end |
|---|
| 500 |
end |
|---|
| 501 |
end |
|---|
| 502 |
end |
|---|
| 503 |
|
|---|
| 504 |
private |
|---|
| 505 |
def real_file_path(name) |
|---|
| 506 |
'%s/%s.cache' % [@cache_path, name.gsub('?', '.').gsub(':', '.')] |
|---|
| 507 |
end |
|---|
| 508 |
|
|---|
| 509 |
def ensure_cache_path(path) |
|---|
| 510 |
FileUtils.makedirs(path) unless File.exists?(path) |
|---|
| 511 |
end |
|---|
| 512 |
|
|---|
| 513 |
def search_dir(dir, &callback) |
|---|
| 514 |
Dir.foreach(dir) do |d| |
|---|
| 515 |
next if d == "." || d == ".." |
|---|
| 516 |
name = File.join(dir, d) |
|---|
| 517 |
if File.directory?(name) |
|---|
| 518 |
search_dir(name, &callback) |
|---|
| 519 |
else |
|---|
| 520 |
callback.call name |
|---|
| 521 |
end |
|---|
| 522 |
end |
|---|
| 523 |
end |
|---|
| 524 |
end |
|---|
| 525 |
|
|---|
| 526 |
class FileStore < UnthreadedFileStore |
|---|
| 527 |
def initialize(cache_path) |
|---|
| 528 |
super(cache_path) |
|---|
| 529 |
if ActionController::Base.allow_concurrency |
|---|
| 530 |
@mutex = Mutex.new |
|---|
| 531 |
FileStore.send(:include, ThreadSafety) |
|---|
| 532 |
end |
|---|
| 533 |
end |
|---|
| 534 |
end |
|---|
| 535 |
end |
|---|
| 536 |
|
|---|
| 537 |
|
|---|
| 538 |
|
|---|
| 539 |
|
|---|
| 540 |
|
|---|
| 541 |
|
|---|
| 542 |
|
|---|
| 543 |
|
|---|
| 544 |
|
|---|
| 545 |
|
|---|
| 546 |
|
|---|
| 547 |
|
|---|
| 548 |
|
|---|
| 549 |
|
|---|
| 550 |
|
|---|
| 551 |
|
|---|
| 552 |
|---|