stapler 0.1.0 → 0.4.1

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.
data/README.md CHANGED
@@ -1,31 +1,54 @@
1
- = stapler
2
-
3
- == DESCRIPTION:
1
+ Stapler
2
+ =======
4
3
 
5
4
  Stapler uses YUI Compressor to automatically compress cached assets in Rails
6
5
 
7
- == FEATURES/PROBLEMS:
8
6
 
9
- * Support both CSS and Javascript
7
+ Features/Problems
8
+ -----------------
9
+
10
+ * Support both CSS and Javascript
11
+
10
12
 
11
- == REQUIREMENTS:
13
+ Requirements
14
+ ------------
12
15
 
13
16
  * yuicompressor gem
14
17
 
15
- == INSTALL:
18
+ TODO
19
+ ------------
20
+
21
+ * warnings if the bundle URL is longer than 2048 chars (for IE)
22
+ * make sure that Config.secret is not a class variable
23
+ * logging
24
+ * specs
25
+ * pass 404 errors to the underlying app
26
+
27
+ Install
28
+ -------
16
29
 
17
30
  * gem install stapler
18
31
 
19
- == REFERENCES:
20
32
 
21
- * YUI Compressor
22
- http://developer.yahoo.com/yui/compressor/
33
+ References
34
+ ----------
35
+
36
+ * YUI Compressor - <http://developer.yahoo.com/yui/compressor/>
37
+ * yuicompressor gem - <http://github.com/mjijackson/yuicompressor>
38
+
39
+
40
+ Author
41
+ ------
42
+
43
+ Michael Rykov :: missingfeature.com :: @michaelrykov
44
+
23
45
 
24
- == LICENSE:
46
+ License
47
+ -------
25
48
 
26
49
  (The MIT License)
27
50
 
28
- Copyright (c) 2009 Michael Rykov
51
+ Copyright (c) 2009 Michael Rykov
29
52
 
30
53
  Permission is hereby granted, free of charge, to any person obtaining
31
54
  a copy of this software and associated documentation files (the
@@ -5,12 +5,13 @@
5
5
  $:.unshift File.dirname(__FILE__)
6
6
 
7
7
  require 'yuicompressor'
8
+ require 'stapler/utils'
9
+ require 'stapler/asset'
10
+ require 'stapler/bundle/key'
11
+ require 'stapler/bundle'
8
12
  require 'stapler/config'
9
- require 'stapler/compressor'
10
- require 'stapler/stapler'
11
- require 'stapler/helper'
12
13
  require 'stapler/middleware'
13
14
 
14
- if defined?(::ActionView::Helpers)
15
- ::ActionView::Helpers.send(:include, Stapler::Helper)
16
- end
15
+ if defined?(Rails)
16
+ require 'stapler/rails'
17
+ end
@@ -0,0 +1,37 @@
1
+ #--
2
+ # Copyright (c) Michael Rykov
3
+ #++
4
+
5
+ module Stapler
6
+ class Asset
7
+ FORBIDDEN_PATHS = %w(.. WEB-INF META-INF)
8
+ REWRITE_KEYS = %w(PATH_INFO REQUEST_PATH REQUEST_URI)
9
+
10
+ def initialize(config, path)
11
+ @config = config
12
+ @path = Utils.groom_path(path)
13
+ end
14
+
15
+ def response(env)
16
+ if forbidden?
17
+ @config.rack_file.not_found
18
+ else
19
+ @config.rack_file.call(rewrite_env(env))
20
+ end
21
+ end
22
+
23
+ private
24
+ def forbidden?
25
+ FORBIDDEN_PATHS.any? { |fp| @path.include?(fp) }
26
+ end
27
+
28
+ def rewrite_env(env)
29
+ out = env.dup
30
+ REWRITE_KEYS.each do |key|
31
+ next unless out[key].is_a?(String)
32
+ out[key] = out[key].gsub(@config.path_regex, @path)
33
+ end
34
+ out
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,41 @@
1
+ #--
2
+ # Copyright (c) Michael Rykov
3
+ #++
4
+ require 'rack/utils'
5
+ require 'rack/file'
6
+
7
+ module Stapler
8
+ class Bundle
9
+ def initialize(config, bundle)
10
+ @config = config
11
+ @paths = Key.to_paths(bundle.to_s)
12
+ end
13
+
14
+ # Collect asset responses and build the collective response
15
+ def response(env)
16
+ if not @paths.is_a?(Array)
17
+ @config.rack_file.not_found
18
+ else
19
+ @paths.inject([200, {}, '']) do |sum, path|
20
+ # Fetch the response
21
+ item = Stapler::Asset.new(@config, path).response(env)
22
+
23
+ # Add the file title
24
+ sum[2] << "\n\n\n/***** #{path} *****/\n\n\n"
25
+
26
+ if item[0] != 200
27
+ sum[2] << "/* [#{item[0]}] Asset problem */\n\n\n"
28
+ else
29
+ # Headers are merged
30
+ sum[1].merge!(item[1])
31
+ # Body is concatenated
32
+ sum[2] << Stapler::Utils.response_body(item)
33
+ end
34
+
35
+ # Return the response
36
+ sum
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,12 @@
1
+ #--
2
+ # Copyright (c) Michael Rykov
3
+ #++
4
+
5
+ require 'stapler/bundle/serialized_key'
6
+
7
+ # Placeholder that will be configurable in the future
8
+ module Stapler
9
+ class Bundle
10
+ Key = SerializedKey
11
+ end
12
+ end
@@ -0,0 +1,38 @@
1
+ #--
2
+ # Copyright (c) Michael Rykov
3
+ #++
4
+
5
+ module Stapler
6
+ class Bundle
7
+ class SerializedKey; end
8
+ @@asset_id = Time.now.to_i.to_s
9
+
10
+ class << SerializedKey
11
+ def to_key(paths)
12
+ key = Utils.url_encode(paths.map { |a| Utils.groom_path(a) })
13
+ File.join(key, signature(key), @@asset_id)
14
+ end
15
+
16
+ def to_paths(bundle)
17
+ key, signature = bundle.split(/[\.\/]/, 3)
18
+ url_decode_with_signature(key, signature)
19
+ rescue Utils::BadString
20
+ nil
21
+ end
22
+
23
+ private
24
+ # Security signature for URLs #
25
+ def signature(key)
26
+ Digest::SHA1.hexdigest("#{key}#{Config.secret}")[0...8]
27
+ end
28
+
29
+ def url_decode_with_signature(key, key_signature)
30
+ if signature(key.to_s) == key_signature.to_s
31
+ Utils.url_decode(key)
32
+ else
33
+ raise BadString, "Invalid signature"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -5,11 +5,64 @@
5
5
  #++
6
6
 
7
7
  module Stapler
8
- module Config
9
- WHITELIST = [%r{^/javascripts/.*\.js$}, %r{^/stylesheets/.*\.css$}]
8
+ class Config
9
+ PREFIX = 'stapler'
10
+ WHITELIST_DIR = %w(javascripts stylesheets)
11
+ WHITELIST_RE = Regexp.union(
12
+ %r{^/javascripts/.*\.js($|\?)},
13
+ %r{^/stylesheets/.*\.css($|\?)}
14
+ )
10
15
 
11
- def self.stapleable?(source)
12
- ActionController::Base.perform_caching && WHITELIST.any? { |re| source =~ re }
16
+ STAPLIZE_RE = Regexp.union(
17
+ %r{/javascripts/.*\.js($|\?)},
18
+ %r{/stylesheets/.*\.css($|\?)}
19
+ )
20
+
21
+ class << self
22
+ def stapleable_path?(source)
23
+ source =~ WHITELIST_RE
24
+ end
25
+
26
+ def stapleable_dir?(dir)
27
+ WHITELIST_DIR.include?(dir)
28
+ end
29
+
30
+ def staplize_url(url)
31
+ url.gsub(STAPLIZE_RE, "/#{PREFIX}\\0")
32
+ end
33
+ end
34
+
35
+ DEFAULT_ROOT = defined?(Rails) ? Rails.public_path : 'public'
36
+ DEFAULT_PREFIX = 'stapler'
37
+
38
+ attr_accessor :path_regex, :bundle_regex, :rack_file,
39
+ :perform_caching, :perform_compress
40
+
41
+ def initialize(opts = {})
42
+ # The root directory of where all the public files are stored
43
+ asset_dir = opts[:public_path] || DEFAULT_ROOT
44
+
45
+ # The path prefix and correcsponding RegEx for stapled assets
46
+ path_prefix = opts[:path_prefix] || DEFAULT_PREFIX
47
+ @path_regex = %r(^/#{path_prefix}/(.+)$)
48
+ @bundle_regex = %r(^/#{path_prefix}/bundle/(.+)$)
49
+
50
+ # Rack::File will pull all the source files
51
+ @rack_file = Rack::File.new(asset_dir)
52
+
53
+ # Should we write stapled results and where?
54
+ @perform_caching = opts[:cache_assets] || false
55
+
56
+ # Should we compress stapled results
57
+ @perform_compress = opts[:compress_assets] || false
58
+
59
+ # Bundle URL signature secret
60
+ @@secret = opts[:secret] || 'nakedemperor'
61
+ end
62
+
63
+ # FIXME: This should not be class variable
64
+ def self.secret
65
+ @@secret
13
66
  end
14
67
  end
15
68
  end
@@ -1,35 +1,70 @@
1
+ require 'rack/utils'
2
+ require 'rack/file'
3
+
1
4
  module Stapler
2
5
  class Middleware
3
- ASSETS_DIR = defined?(Rails.public_path) ? Rails.public_path : "public"
4
-
5
- def initialize(app)
6
+ def initialize(app, opts = {})
6
7
  @app = app
8
+ @config = Stapler::Config.new(opts)
7
9
  end
8
10
 
9
11
  def call(env)
10
- path = env['PATH_INFO']
11
- puts "LOOKING AT PATH #{path}"
12
+ path_info = env["PATH_INFO"]
12
13
 
13
- if path =~ %r(^/stapler/(.*)$)
14
- stapler_call(env, $1)
14
+ if path_info =~ @config.bundle_regex
15
+ _call_bundle(env, Rack::Utils.unescape($1))
16
+ elsif path_info =~ @config.path_regex
17
+ _call_asset(env, Rack::Utils.unescape($1))
15
18
  else
16
19
  @app.call(env)
17
20
  end
18
21
  end
19
22
 
20
23
  private
21
- def stapler_call(env, path)
22
- file_path = asset_file_path(path)
24
+ def _call_bundle(env, path)
25
+ bundle = Stapler::Bundle.new(@config, path)
26
+ staple_response(bundle.response(env), "bundle/#{path}")
27
+ end
28
+
29
+ def _call_asset(env, path)
30
+ assets = Stapler::Asset.new(@config, path)
31
+ staple_response(assets.response(env), path)
32
+ end
33
+
34
+ def staple_response(response, asset_path)
35
+ if response[0] == 200
36
+ # Pull out the body
37
+ body = Stapler::Utils.response_body(response)
38
+
39
+ # Compress the body and update response
40
+ body = compress(body, asset_path) if @config.perform_compress
41
+
42
+ # Prepare the response and headers
43
+ response[1]['Content-Length'] = Rack::Utils.bytesize(body).to_s
44
+ response[2] = body
45
+
46
+ # Add cache headers for Rack::Cache, etc
47
+ response[1].merge!(cache_headers(asset_path))
48
+ end
49
+
50
+ response
51
+ end
23
52
 
24
- if File.file?(file_path)
25
- [200, {"Content-Type" => "text/plain"}, ['NOT DONE!']]
53
+ def compress(content, path)
54
+ # Compress using the path as an indicator for format
55
+ if path =~ /\.css$/
56
+ YUICompressor.compress_css(content)
57
+ elsif path =~ /\.js$/
58
+ YUICompressor.compress_js(content, :munge => true)
26
59
  else
27
- [404, {"Content-Type" => "text/html"}, ["Not Found!"]]
60
+ content
28
61
  end
29
62
  end
30
63
 
31
- def asset_file_path(path)
32
- File.join(ASSETS_DIR, path.split('?').first)
64
+ def cache_headers(path)
65
+ duration = @config.perform_caching ? 31536000 : 0
66
+ { "Cache-Control" => "public, max-age=#{duration.to_s}",
67
+ "ETag" => %("#{Digest::SHA1.hexdigest(path)}") }
33
68
  end
34
69
  end
35
70
  end
@@ -0,0 +1,65 @@
1
+ #--
2
+ # Copyright (c) 2010 Michael Rykov
3
+ #++
4
+
5
+ module Stapler
6
+ module Rails
7
+ class << self
8
+ def activate!
9
+ ::ActionView::Base.send(:include, RailsHelper)
10
+ end
11
+
12
+ def asset_host_proc(image_host = nil)
13
+ prefix = File.join('/', Config::PREFIX)
14
+ if image_host
15
+ Proc.new { |source|
16
+ host = image_host
17
+ host << prefix if Config.stapleable_path?(source)
18
+ host
19
+ }
20
+ else
21
+ Proc.new { |source, request|
22
+ host = request.host_with_port
23
+ host << prefix if Config.stapleable_path?(source)
24
+ host
25
+ }
26
+ end
27
+ end
28
+ end
29
+
30
+ module RailsHelper
31
+ def javascript_include_tag(*sources)
32
+ options = sources.extract_options!.stringify_keys
33
+
34
+ if options.delete('cache') || options.delete('concat')
35
+ recursive = options.delete('recursive')
36
+ paths = compute_javascript_paths(sources, recursive)
37
+ javascript_src_tag(Utils.bundle_path(paths), options)
38
+ else
39
+ sources << options
40
+ super(*sources)
41
+ end
42
+ end
43
+
44
+ def stylesheet_link_tag(*sources)
45
+ options = sources.extract_options!.stringify_keys
46
+
47
+ if options.delete('cache') || options.delete('concat')
48
+ recursive = options.delete('recursive')
49
+ paths = compute_stylesheet_paths(sources, recursive)
50
+ stylesheet_tag(Utils.bundle_path(paths), options)
51
+ else
52
+ sources << options
53
+ super(*sources)
54
+ end
55
+ end
56
+
57
+ private
58
+ def compute_public_path(src, dir, ext = nil, include_host = true)
59
+ staple = include_host && src !~ %r{^[-a-z]+://} && Config.stapleable_dir?(dir)
60
+ path = super(src, dir, ext, include_host)
61
+ staple ? Config.staplize_url(path) : path
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,54 @@
1
+ #--
2
+ # Copyright (c) Michael Rykov
3
+ #
4
+ # Some methods copied from Dragonfly gem Serializer
5
+ #++
6
+
7
+ require 'base64'
8
+ require 'cgi'
9
+ require 'digest/sha1'
10
+
11
+ module Stapler
12
+ module Utils
13
+ # Exceptions
14
+ class BadString < RuntimeError; end
15
+
16
+ class << self
17
+ def response_body(response)
18
+ body = response[2]
19
+ body.is_a?(Rack::File) ? File.read(body.path) : body
20
+ end
21
+
22
+ #### Base64 encoding for URLS ####
23
+ def b64_encode(string)
24
+ Base64.encode64(string).tr("\n=",'')
25
+ end
26
+
27
+ def b64_decode(string)
28
+ padding_length = string.length % 4
29
+ Base64.decode64(string + '=' * padding_length)
30
+ end
31
+
32
+ def url_encode(object)
33
+ CGI.escape(b64_encode(Marshal.dump(object)))
34
+ end
35
+
36
+ def url_decode(string)
37
+ Marshal.load(b64_decode(CGI.unescape(string)))
38
+ rescue TypeError, ArgumentError => e
39
+ raise BadString, "couldn't decode #{string} - got #{e}"
40
+ end
41
+
42
+ def groom_path(path)
43
+ # Remove leading slashes and query params
44
+ path.gsub(/^\//, '').gsub(/\?.*$/, '')
45
+ end
46
+
47
+ # Convert a list of assets into an URL
48
+ # Note: Rails helpers adds the appropriate extension
49
+ def bundle_path(assets)
50
+ File.join('/stapler/bundle/', Bundle::Key.to_key(assets))
51
+ end
52
+ end
53
+ end
54
+ end
metadata CHANGED
@@ -1,94 +1,71 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: stapler
3
- version: !ruby/object:Gem::Version
4
- hash: 27
5
- prerelease: false
6
- segments:
7
- - 0
8
- - 1
9
- - 0
10
- version: 0.1.0
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.1
5
+ prerelease:
11
6
  platform: ruby
12
- authors:
7
+ authors:
13
8
  - Michael Rykov
14
9
  autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
-
18
- date: 2010-08-27 00:00:00 -07:00
19
- default_executable:
20
- dependencies:
21
- - !ruby/object:Gem::Dependency
12
+ date: 2011-12-19 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
22
15
  name: yuicompressor
23
- prerelease: false
24
- requirement: &id001 !ruby/object:Gem::Requirement
16
+ requirement: &70096179375860 !ruby/object:Gem::Requirement
25
17
  none: false
26
- requirements:
18
+ requirements:
27
19
  - - ~>
28
- - !ruby/object:Gem::Version
29
- hash: 31
30
- segments:
31
- - 1
32
- - 2
33
- - 0
20
+ - !ruby/object:Gem::Version
34
21
  version: 1.2.0
35
22
  type: :runtime
36
- version_requirements: *id001
37
- description: |
38
- Dynamic asset compression for Rails using YUI Compressor
23
+ prerelease: false
24
+ version_requirements: *70096179375860
25
+ description: ! 'Dynamic asset compression for Rails using YUI Compressor
39
26
 
27
+ '
40
28
  email: mrykov@gmail
41
29
  executables: []
42
-
43
30
  extensions: []
44
-
45
31
  extra_rdoc_files: []
46
-
47
- files:
32
+ files:
48
33
  - README.md
49
34
  - Rakefile
50
35
  - LICENSE
51
- - lib/stapler/compressor.rb
36
+ - lib/stapler/asset.rb
37
+ - lib/stapler/bundle/key.rb
38
+ - lib/stapler/bundle/serialized_key.rb
39
+ - lib/stapler/bundle.rb
52
40
  - lib/stapler/config.rb
53
- - lib/stapler/helper.rb
54
41
  - lib/stapler/middleware.rb
55
- - lib/stapler/stapler.rb
42
+ - lib/stapler/rails.rb
43
+ - lib/stapler/utils.rb
56
44
  - lib/stapler.rb
57
45
  - lib/tasks/stapler.rake
58
46
  - lib/tasks/tasks.rb
59
- has_rdoc: false
60
47
  homepage: http://github.com/rykov/stapler
61
48
  licenses: []
62
-
63
49
  post_install_message:
64
50
  rdoc_options: []
65
-
66
- require_paths:
51
+ require_paths:
67
52
  - lib
68
- required_ruby_version: !ruby/object:Gem::Requirement
53
+ required_ruby_version: !ruby/object:Gem::Requirement
69
54
  none: false
70
- requirements:
71
- - - ">="
72
- - !ruby/object:Gem::Version
73
- hash: 3
74
- segments:
75
- - 0
76
- version: "0"
77
- required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ! '>='
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
60
  none: false
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- hash: 3
83
- segments:
84
- - 0
85
- version: "0"
61
+ requirements:
62
+ - - ! '>='
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
86
65
  requirements: []
87
-
88
66
  rubyforge_project:
89
- rubygems_version: 1.3.7
67
+ rubygems_version: 1.8.11
90
68
  signing_key:
91
69
  specification_version: 3
92
70
  summary: Dynamic asset compression for Rails
93
71
  test_files: []
94
-
@@ -1,40 +0,0 @@
1
- #--
2
- # Copyright (c) 2009 Michael Rykov
3
- #++
4
-
5
- module Stapler
6
- class Compressor
7
- def initialize(from, to)
8
- @from_path = from
9
- @to_path = to
10
- end
11
-
12
- def compress!
13
- compress(@from_path, @to_path)
14
- logger.info("[Stapler] Compressed #{@from_path}")
15
- rescue => e
16
- logger.warn("[Stapler] Compression failure for #{@from_path}: #{e}")
17
- FileUtil.cp(@from_path, @to_path)
18
- end
19
-
20
- protected
21
- def compress(from, to)
22
- out = File.open(from) do |from_file|
23
- if from =~ /\.css$/
24
- YUICompressor.compress_css(from_file)
25
- elsif from =~ /\.js$/
26
- YUICompressor.compress_js(from_file, :munge => true)
27
- else
28
- raise StandardError.new("Unrecognized file type")
29
- end
30
- end
31
-
32
- File.open(to, 'w') { |f| f.write(out) }
33
- end
34
-
35
- def logger
36
- Rails.logger
37
- end
38
- end
39
- end
40
-
@@ -1,35 +0,0 @@
1
- #--
2
- # Copyright (c) 2009 Michael Rykov
3
- #++
4
-
5
- module Stapler
6
- module Helper
7
- StaplerRoot = "/stapler"
8
-
9
- def self.included(base)
10
- base.send :alias_method_chain, :rewrite_asset_path, :stapler
11
- #base.send :alias_method_chain, :compute_asset_host, :stapler
12
- end
13
-
14
- private
15
- def rewrite_asset_path_with_stapler(source)
16
- if Config.stapleable?(source)
17
- @@stapler ||= Stapler.new
18
- stapled_source = "#{StaplerRoot}#{source}"
19
- stapled_path = asset_file_path(stapled_source)
20
-
21
- if @@stapler.ready?(stapled_path)
22
- source = stapled_source
23
- else
24
- @@stapler.process(asset_file_path(source), stapled_path)
25
- end
26
- end
27
-
28
- rewrite_asset_path_without_stapler(source)
29
- end
30
-
31
- #def compute_asset_host_with_stapler(source)
32
- # compute_asset_host_without_stapler(source)
33
- #end
34
- end
35
- end
@@ -1,33 +0,0 @@
1
- #--
2
- # Copyright (c) 2009 Michael Rykov
3
- #++
4
-
5
- module Stapler
6
- class Stapler
7
-
8
- def initialize
9
- @in_progress = {}
10
- @in_progress_guard = Mutex.new
11
- end
12
-
13
- def ready?(path)
14
- !@in_progress[path] && File.exists?(path)
15
- end
16
-
17
- def process(from, to)
18
- @in_progress_guard.synchronize do
19
- return if @in_progress[to]
20
- @in_progress[to] = true
21
- end
22
-
23
- Thread.new { self.compress(from, to) }
24
- end
25
-
26
- protected
27
- def compress(from, to)
28
- FileUtils.mkdir_p(File.dirname(to))
29
- Compressor.new(from, to).compress!
30
- @in_progress.delete(to)
31
- end
32
- end
33
- end