stapler 0.1.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
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