# -*- coding: utf-8 -*-
require 'digest/md5'
module Juicer
#
# Assists in creating filenames that reflect the last change to the file. These
# kinds of filenames are useful when serving static content through a web server.
# If the filename changes everytime the file is modified, you can safely configure
# the web server to cache files indefinately, and know that the updated filename
# will cause the file to be downloaded again - only once - when it has changed.
#
# = Types of cache busters
#
# == Query string / "soft" cache busters
# Soft cache busters require no web server configuration. However, it is not
# guaranteed to work in all settings. For example, older default
# configurations for popular proxy server Squid does not consider a known URL
# with a new query string a new URL, and thus will not download the file over.
#
# The soft cache busters transforms
# /images/logo.png to /images/logo.png?cb=1232923789
#
# == Filename change / "hard" cache busters
# Hard cache busters change the file name itself, and thus requires either
# the web server to (internally) rewrite requests for these files to the
# original ones, or the file names to actually change. Hard cache busters
# transforms /images/logo.png to /images/logo-1232923789.png
#
# Hard cache busters are guaranteed to work, and is the recommended variant.
# An example configuration for the Apache web server that does not require
# you to actually change the filenames can be seen below.
#
#
# # Application/website configuration
#
# # Cache static resources for a year
#
# ExpiresActive On
# ExpiresDefault "access plus 1 year"
#
#
# # Rewrite URLs like /images/logo-cb1234567890.png to /images/logo.png
# RewriteEngine On
# RewriteRule (.*)-cb\d+\.(.*)$ $1.$2 [L]
# ])
#
# = Consecutive calls
#
# Consecutive calls to add a cache buster to a path will replace the existing
# cache buster *as long as the parameter name is the same*. Consider this:
#
# file = Juicer::CacheBuster.hard("/home/file.png") #=> "/home/file-cb1234567890.png"
# Juicer::CacheBuster.hard(file) #=> "/home/file-cb1234567891.png"
#
# # Changing the parameter name breaks this
# Juicer::CacheBuster.hard(file, :juicer) #=> "/home/file-cb1234567891-juicer1234567892.png"
#
# Avoid this type of trouble simply be cleaning the URL with the old name first:
#
# Juicer::CacheBuster.clean(file) #=> "/home/file.png"
# file = Juicer::CacheBuster.hard(file, :juicer) #=> "/home/file-juicer1234567892.png"
# Juicer::CacheBuster.clean(file, :juicer) #=> "/home/file.png"
#
# Author:: Christian Johansen (christian@cjohansen.no)
# Copyright:: Copyright (c) 2009 Christian Johansen
# License:: BSD
#
module CacheBuster
DEFAULT_PARAMETER = "jcb"
#
# Creates a unique file name for every revision to the files contents.
# Raises an ArgumentError if the file can not be found.
#
# The type indicates which type of cache buster you want, :soft
# or :hard. Default is :soft. If an unsupported value
# is specified, :soft will be used.
#
# See #hard and #soft for explanation of the parameter
# argument.
#
def self.path(file, type = :soft, parameter = DEFAULT_PARAMETER)
return file if file =~ /data:.*;base64/
type = [:soft, :hard, :rails, :md5].include?(type) ? type : :soft
parameter = nil if type == :rails
file = self.clean(file, parameter)
filename = file.split("?").first
raise ArgumentError.new("#{file} could not be found") unless File.exists?(filename)
mtime = File.mtime(filename).to_i
if type == :soft
parameter = "#{parameter}=".sub(/^=$/, '')
return "#{file}#{file.index('?') ? '&' : '?'}#{parameter}#{mtime}"
elsif type == :rails
return "#{file}#{file.index('?') ? '' : "?#{mtime}"}"
elsif type == :md5
md5 = Digest::MD5.hexdigest(File.read(filename))
return file.sub(/(\.[^\.]+$)/, "-#{parameter}#{md5}" + '\1')
end
file.sub(/(\.[^\.]+$)/, "-#{parameter}#{mtime}" + '\1')
end
#
# Add a md5 cache buster to a filename. The parameter is an optional prefix
# that is added before the md5 digest. It results in filenames of the form:
# file-[parameter name][md5].suffix, ie
# images/logo-cb4fdbd4c637ad377adf0fc0c88f6854b3.png which is the case for the default
# parameter name "cb" (as in *c*ache *b*uster).
#
def self.md5(file, parameter = DEFAULT_PARAMETER)
self.path(file, :md5, parameter)
end
#
# Add a hard cache buster to a filename. The parameter is an optional prefix
# that is added before the mtime timestamp. It results in filenames of the form:
# file-[parameter name][timestamp].suffix, ie
# images/logo-cb1234567890.png which is the case for the default
# parameter name "cb" (as in *c*ache *b*uster).
#
def self.hard(file, parameter = DEFAULT_PARAMETER)
self.path(file, :hard, parameter)
end
#
# Add a soft cache buster to a filename. The parameter is an optional name
# for the mtime timestamp value. It results in filenames of the form:
# file.suffix?[parameter name]=[timestamp], ie
# images/logo.png?cb=1234567890 which is the case for the default
# parameter name "cb" (as in *c*ache *b*uster).
#
def self.soft(file, parameter = DEFAULT_PARAMETER)
self.path(file, :soft, parameter)
end
#
# Add a Rails-style cache buster to a filename. Results in filenames of the
# form: file.suffix?[timestamp], ie images/logo.png?1234567890
# which is the format used by Rails' image_tag helper.
#
def self.rails(file)
self.path(file, :rails)
end
#
# Remove cache buster from a URL for a given parameter name. Parameter name is
# "cb" by default.
#
def self.clean(file, parameter = DEFAULT_PARAMETER)
if "#{parameter}".length == 0
return file.sub(/\?\d+$/, '')
else
query_param = "#{parameter}="
new_file = file.sub(/#{query_param}\d+&?/, "").sub(/(\?|&)$/, "")
return new_file unless new_file == file
file.sub(/-#{parameter}[0-9a-f]+(\.\w+)($|\?)/, '\1\2')
end
end
end
end