diff --git a/lib/roger/release/processors.rb b/lib/roger/release/processors.rb
index 8508f8f..def3f5b 100644
--- a/lib/roger/release/processors.rb
+++ b/lib/roger/release/processors.rb
@@ -26,5 +26,7 @@ def self.map
end
end
end
+
+require File.dirname(__FILE__) + "/processors/fingerprint"
require File.dirname(__FILE__) + "/processors/mockup"
require File.dirname(__FILE__) + "/processors/url_relativizer"
diff --git a/lib/roger/release/processors/fingerprint.rb b/lib/roger/release/processors/fingerprint.rb
new file mode 100644
index 0000000..3e7b6ed
--- /dev/null
+++ b/lib/roger/release/processors/fingerprint.rb
@@ -0,0 +1,145 @@
+require "hpricot"
+require "digest"
+require "fileutils"
+
+require File.dirname(__FILE__) + "../../../resolver"
+
+module Roger::Release::Processors
+ # Fingerprint processor
+ # This process can be used to fingerprint assets, for caching benefits
+ # It does this fingerprinting based on the following approach:
+ # > It expects that urls are ...
+ # > Adds all to a fingerprint blaat ]
+ # > Rewrites the href or src attributes
+ # > At last it writes the assets files to a fingered printed file and
+ # removes the ...
+ class Fingerprint < Base
+ def initialize(options = {})
+ @options = {
+ url_attributes: %w(data-fingerprint),
+ match: ["**/*.html"],
+ skip: []
+ }
+
+ @fingerprinted_files = []
+
+ @options.update(options) if options
+ end
+
+ def call(release, options = {})
+ options = {}.update(@options).update(options)
+
+ log_call(release, options)
+
+ @resolver = Roger::Resolver.new(release.build_path)
+
+ fingerprint_attributes_in_html_files(
+ release,
+ options[:url_attributes],
+ release.get_files(options[:match], options[:skip])
+ )
+
+ fingerprint_asset_files!
+ end
+
+ protected
+
+ def log_call(release, options)
+ log_message = "Fingerprinting all in #{options[:match].inspect} "
+ log_message << "files in attributes #{options[:url_attributes].inspect}, "
+ log_message << "skipping #{options[:skip].any? ? options[:skip].inspect : 'none'}"
+ release.log(self, log_message)
+ end
+
+ def fingerprint_attributes_in_html_files(release, attributes, files)
+ files.map do |file_path|
+ release.log self, "Processing #{file_path}"
+ fingerprint_attributes_in_html_file(release, attributes, file_path)
+ end
+ end
+
+ def fingerprint_attributes_in_html_file(release, attributes, file_path)
+ orig_source = File.read(file_path)
+ File.open(file_path, "w") do |f|
+ f.write(fingerprint_attributes_in_source(release, attributes, file_path, orig_source))
+ end
+ end
+
+ def fingerprint_attributes_in_source(release, attributes, file_path, source)
+ doc = Hpricot(source)
+ attributes.each do |attribute|
+ (doc / "*[@#{attribute}]").each do |tag|
+ link_attr, url = get_url_from_tag(tag)
+
+ asset_file_path = asset_file_path(url, file_path)
+ release.debug(self, "Converting '#{tag[link_attr]}' to '#{asset_file_path}'")
+
+ if asset_file_path.nil?
+ return release.log(self, "Could not resolve link #{tag[link_attr]} in #{file_path}")
+ end
+
+ digest = create_fingerprint_digest(asset_file_path)
+ url_with_digest = append_digest(url, digest)
+
+ @fingerprinted_files.push(asset_file_path: asset_file_path,
+ digest: digest)
+
+ tag[link_attr] = url_with_digest
+ end
+ end
+ doc.to_original_html
+ end
+
+ def get_url_from_tag(tag)
+ tag_name = tag.name
+ case tag_name
+ when "link"
+ return ["href", tag["href"]]
+ when "script"
+ return ["src", tag["src"]]
+ else
+ fail "I don't know how which html attribute to use for url extraction"
+ end
+ end
+
+ def create_fingerprint_digest(file_path)
+ Digest::SHA256.file(file_path).hexdigest
+ end
+
+ def append_digest(url, digest)
+ ext = Pathname.new(url).extname
+
+ # Remove extension and dot
+ url_without_ext = url[0..ext.length * -1 - 1]
+
+ # Add digest and ext back
+ url_without_ext + "-" + digest + ext
+ end
+
+ # Translate the given path to a fysiek file on file system
+ def asset_file_path(url, source_file_path)
+ # Absolute url, starting with /
+ if url.match(%r{^\/[^\/]})
+ absolute_url = url
+ else
+ # When relative url is given transform this to an absolute path
+ # based of the file in which the link is included
+ absolute_url = File.join(
+ Pathname.new(@resolver.path_to_url source_file_path).dirname,
+ url
+ )
+ end
+
+ @resolver.url_to_path(absolute_url, exact_match: true)
+ end
+
+ def fingerprint_asset_files!
+ @fingerprinted_files.uniq.each do |file|
+ FileUtils.mv file[:asset_file_path],
+ append_digest(file[:asset_file_path].to_s, file[:digest])
+ end
+ end
+ end
+end
+
+Roger::Release::Processors.register(:finger, Roger::Release::Processors::Fingerprint)
diff --git a/test/unit/release/processors/fingerprint_test.rb b/test/unit/release/processors/fingerprint_test.rb
new file mode 100644
index 0000000..f842936
--- /dev/null
+++ b/test/unit/release/processors/fingerprint_test.rb
@@ -0,0 +1,78 @@
+require "test_helper"
+require "roger/testing/mock_release"
+
+require "digest"
+
+module Roger
+ # Test UrlRelativizer
+ class FingerprintTest < ::Test::Unit::TestCase
+ def setup
+ @release = Testing::MockRelease.new
+ @processor = Roger::Release::Processors::Fingerprint.new
+
+ @release.project.construct.directory "build" do |dir|
+ test_css = dir.file "style.css", "body { background: pink; }"
+ @hex_test_css = Digest::SHA256.file(test_css).hexdigest
+
+ test_js = dir.file "javascripts/site.js", "console.log('fingerprinting')"
+ @hex_test_js = Digest::SHA256.file(test_js).hexdigest
+ end
+ end
+
+ def teardown
+ @release.destroy
+ @release = nil
+ end
+
+ def test_empty_release_runs
+ files = @processor.call(@release)
+ assert_equal 0, files.length
+ end
+
+ def test_basic_fingerprinting_with_absolute_path
+ @release.project.construct.directory "build/sub" do |dir|
+ dir.file "test.html", ''
+ dir.file "test2.html", ''
+ end
+
+ @processor.call(@release)
+
+ contents = File.read((@release.build_path + "sub/test.html").to_s)
+
+ output_tag_href = "href=\"/style-#{@hex_test_css}.css\""
+ assert contents.include?(output_tag_href)
+ assert File.exist? "build/style-#{@hex_test_css}.css"
+ assert !(File.exist? "build/style.css")
+ end
+
+ def test_basic_fingerprinting_with_relative_path
+ @release.project.construct.directory "build/sub" do |dir|
+ dir.file "test.html", ''
+ end
+
+ @processor.call(@release)
+
+ contents = File.read((@release.build_path + "sub/test.html").to_s)
+
+ output_tag_href = "href=\"../style-#{@hex_test_css}.css\""
+ assert contents.include?(output_tag_href)
+ assert File.exist? "build/style-#{@hex_test_css}.css"
+ assert !(File.exist? "build/style.css")
+ end
+
+ def test_fingerprint_js
+ @release.project.construct.directory "build/sub" do |dir|
+ dir.file "test.html", '