caleb-cloudfront_asset_host 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ pkg
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Menno van der Sman
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,88 @@
1
+ # CloudFront AssetHost
2
+
3
+ Easy deployment of your assets on CloudFront or S3. When enabled in production, your assets will be served from CloudFront/S3 which will result in a speedier front-end.
4
+
5
+ ## Why?
6
+
7
+ Hosting your assets on CloudFront ensures minimum latency for all your visitors. But deploying your assets requires some additional management that this gem provides.
8
+
9
+ ### Expiration
10
+
11
+ The best way to expire your assets on CloudFront is to upload the asset to a new unique url. The gem will calculate the MD5-hash of the asset and incorporate that into the URL.
12
+
13
+ ### Efficient uploading
14
+
15
+ By using the MD5-hash we can easily determined which assets aren't uploaded yet. This speeds up the deployment considerably.
16
+
17
+ ### Compressed assets
18
+
19
+ CloudFront will not serve compressed assets automatically. To counter this, the gem will upload gzipped javascripts and stylesheets and serve them when the user-agent supports it.
20
+
21
+ ## Installing
22
+
23
+ gem install cloudfront_asset_host
24
+
25
+ Include the gem in your app's `environment.rb` or `Gemfile`.
26
+
27
+ ### Dependencies
28
+
29
+ The gem relies on `openssl md5` and `gzip` utilities. Make sure they are available locally and on your servers.
30
+
31
+ ### Configuration
32
+
33
+ Make sure your s3-credentials are stored in _config/s3.yml_ like this:
34
+
35
+ access_key_id: 'access_key'
36
+ secret_access_key: 'secret'
37
+
38
+ Create an initializer to configure the plugin _config/initializers/cloudfront_asset_host.rb_
39
+
40
+ # Simple configuration
41
+ CloudfrontAssetHost.configure do |config|
42
+ config.bucket = "bucketname" # required
43
+ config.enabled = true if Rails.env.production? # only enable in production
44
+ end
45
+
46
+ # Extended configuration
47
+ CloudfrontAssetHost.configure do |config|
48
+ config.bucket = "bucketname" # required
49
+ config.cname = "assets.domain.com" # if you have a cname configured for your distribution or bucket
50
+ config.key_prefix = "app/" # if you share the bucket and want to keep things separated
51
+ config.s3_config = "#{RAILS_ROOT}/config/s3.yml" # Alternative location of your s3-config file
52
+
53
+ # gzip related configuration
54
+ config.gzip = true # enable gzipped assets (defaults to true)
55
+ config.gzip_extensions = ['js', 'css'] # only gzip javascript or css (defaults to %w(js css))
56
+ config.gzip_prefix = "gz" # prefix for gzipped bucket (defaults to "gz")
57
+
58
+ config.enabled = true if Rails.env.production? # only enable in production
59
+ end
60
+
61
+ ## Usage
62
+
63
+ ### Uploading your assets
64
+ Run `CloudfrontAssetHost::Uploader.upload!(:verbose => true, :dryrun => false)` before your deployment. Put it for example in your Rakefile or capistrano-recipe. Verbose output will include information about which keys are being uploaded. Enabling _dryrun_ will skip the actual upload if you're just interested to see what will be uploaded.
65
+
66
+ ### Hooks
67
+ If the plugin is enabled. Rails' internal `asset_host` and `asset_id` functionality will be overridden to point to the location of the assets on Cloudfront.
68
+
69
+ ### Other plugins
70
+ When using in combination with SASS and/or asset_packager it is recommended to generate the css-files and package your assets before uploading them to Cloudfront. For example, call `Sass::Plugin.update_stylesheets` and `Synthesis::AssetPackage.build_all` first.
71
+
72
+ ## Contributing
73
+
74
+ Feel free to fork the project and send pull-requests.
75
+
76
+ ## Known Limitations
77
+
78
+ - Does not delete old assets
79
+
80
+ ## Compatibility
81
+
82
+ Tested on Rails 2.3.5 with SASS and AssetPackager plugins
83
+
84
+ ## Copyright
85
+
86
+ Created at Wakoopa
87
+
88
+ Copyright (c) 2010 Menno van der Sman, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,39 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the cloudfront_asset_host plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.libs << 'test'
12
+ t.pattern = 'test/**/*_test.rb'
13
+ t.verbose = true
14
+ end
15
+
16
+ desc 'Generate documentation for the cloudfront_asset_host plugin.'
17
+ Rake::RDocTask.new(:rdoc) do |rdoc|
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.title = 'CloudfrontAssetHost'
20
+ rdoc.options << '--line-numbers' << '--inline-source'
21
+ rdoc.rdoc_files.include('README')
22
+ rdoc.rdoc_files.include('lib/**/*.rb')
23
+ end
24
+
25
+ begin
26
+ require 'jeweler'
27
+ Jeweler::Tasks.new do |gemspec|
28
+ gemspec.name = "caleb-cloudfront_asset_host"
29
+ gemspec.summary = "Rails plugin to easily and efficiently deploy your assets on Amazon's S3 or CloudFront"
30
+ gemspec.description = "Easy deployment of your assets on CloudFront or S3 using a simple rake-task. When enabled in production, the application's asset_host and public_paths will point to the correct location."
31
+ gemspec.email = "menno@wakoopa.com"
32
+ gemspec.homepage = "http://github.com/caleb/cloudfront_asset_host"
33
+ gemspec.authors = ["Menno van der Sman"]
34
+ gemspec.add_dependency 'right_aws'
35
+ end
36
+ Jeweler::GemcutterTasks.new
37
+ rescue LoadError
38
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
39
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.2
@@ -0,0 +1,116 @@
1
+ require 'cloudfront_asset_host/asset_tag_helper_ext'
2
+
3
+ module CloudfrontAssetHost
4
+
5
+ autoload :Uploader, 'cloudfront_asset_host/uploader'
6
+ autoload :CssRewriter, 'cloudfront_asset_host/css_rewriter'
7
+
8
+ # Bucket that will be used to store all the assets (required)
9
+ mattr_accessor :bucket
10
+
11
+ # CNAME that is configured for the bucket or CloudFront distribution
12
+ mattr_accessor :cname
13
+
14
+ # Prefix keys
15
+ mattr_accessor :key_prefix
16
+
17
+ # Path to S3 config. Expects an +access_key_id+ and +secret_access_key+
18
+ mattr_accessor :s3_config
19
+
20
+ # Indicates whether the plugin should be enabled
21
+ mattr_accessor :enabled
22
+
23
+ # Upload gzipped assets and serve those assets when applicable
24
+ mattr_accessor :gzip
25
+
26
+ # Which extensions to serve as gzip
27
+ mattr_accessor :gzip_extensions
28
+
29
+ # Key-prefix under which to store gzipped assets
30
+ mattr_accessor :gzip_prefix
31
+
32
+ class << self
33
+
34
+ def configure
35
+ # default configuration
36
+ self.bucket = nil
37
+ self.cname = nil
38
+ self.key_prefix = ""
39
+ self.s3_config = "#{RAILS_ROOT}/config/s3.yml"
40
+ self.enabled = false
41
+
42
+ self.gzip = true
43
+ self.gzip_extensions = %w(js css)
44
+ self.gzip_prefix = "gz"
45
+
46
+ yield(self)
47
+
48
+ if properly_configured?
49
+ enable!
50
+ end
51
+ end
52
+
53
+ def asset_host(source = nil, request = nil)
54
+ host = cname.present? ? "http://#{self.cname}" : "http://#{self.bucket_host}"
55
+
56
+ if source && request && CloudfrontAssetHost.gzip
57
+ gzip_allowed = CloudfrontAssetHost.gzip_allowed_for_source?(source)
58
+
59
+ # Only Netscape 4 does not support gzip
60
+ # IE masquerades as Netscape 4, so we check that as well
61
+ user_agent = request.headers['User-Agent'].to_s
62
+ gzip_accepted = !(user_agent =~ /^Mozilla\/4/) || user_agent =~ /\bMSIE/
63
+ gzip_accepted &&= request.headers['Accept-Encoding'].to_s.include?('gzip')
64
+
65
+ if gzip_accepted && gzip_allowed
66
+ host << "/#{CloudfrontAssetHost.gzip_prefix}"
67
+ end
68
+ end
69
+
70
+ host
71
+ end
72
+
73
+ def cname
74
+ if @cname.is_a? Proc
75
+ @cname.call
76
+ else
77
+ @cname
78
+ end
79
+ end
80
+
81
+ def bucket_host
82
+ "#{self.bucket}.s3.amazonaws.com"
83
+ end
84
+
85
+ def enable!
86
+ if enabled
87
+ ActionController::Base.asset_host = Proc.new { |source, request| CloudfrontAssetHost.asset_host(source, request) }
88
+ ActionView::Helpers::AssetTagHelper.send(:alias_method_chain, :rewrite_asset_path, :cloudfront)
89
+ ActionView::Helpers::AssetTagHelper.send(:alias_method_chain, :rails_asset_id, :cloudfront)
90
+ end
91
+ end
92
+
93
+ def key_for_path(path)
94
+ key_prefix + md5sum(path)[0..8]
95
+ end
96
+
97
+ def gzip_allowed_for_source?(source)
98
+ extension = source.split('.').last
99
+ CloudfrontAssetHost.gzip_extensions.include?(extension)
100
+ end
101
+
102
+ private
103
+
104
+ def properly_configured?
105
+ raise "You'll need to specify a bucket" if bucket.blank?
106
+ raise "Could not find S3-configuration" unless File.exists?(s3_config)
107
+ true
108
+ end
109
+
110
+ def md5sum(path)
111
+ `openssl md5 #{path}`.split(/\s/)[1].to_s
112
+ end
113
+
114
+ end
115
+
116
+ end
@@ -0,0 +1,37 @@
1
+ module ActionView
2
+ module Helpers
3
+ module AssetTagHelper
4
+
5
+ private
6
+
7
+ # Override asset_id so it calculates the key by md5 instead of modified-time
8
+ def rails_asset_id_with_cloudfront(source)
9
+ if @@cache_asset_timestamps && (asset_id = @@asset_timestamps_cache[source])
10
+ asset_id
11
+ else
12
+ path = File.join(ASSETS_DIR, source)
13
+ asset_id = File.exist?(path) ? CloudfrontAssetHost.key_for_path(path) : ''
14
+
15
+ if @@cache_asset_timestamps
16
+ @@asset_timestamps_cache_guard.synchronize do
17
+ @@asset_timestamps_cache[source] = asset_id
18
+ end
19
+ end
20
+
21
+ asset_id
22
+ end
23
+ end
24
+
25
+ # Override asset_path so it prepends the asset_id
26
+ def rewrite_asset_path_with_cloudfront(source)
27
+ asset_id = rails_asset_id(source)
28
+ if asset_id.blank?
29
+ source
30
+ else
31
+ "/#{asset_id}#{source}"
32
+ end
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,65 @@
1
+ require 'tempfile'
2
+
3
+ module CloudfrontAssetHost
4
+ module CssRewriter
5
+
6
+ # Location of the stylesheets directory
7
+ mattr_accessor :stylesheets_dir
8
+ self.stylesheets_dir = File.join(Rails.public_path, 'stylesheets')
9
+
10
+ class << self
11
+ # matches optional quoted url(<path>)
12
+ ReplaceRexeg = /url\(["']?([^\)"']+)["']?\)/i
13
+
14
+ # Returns the path to the temporary file that contains the
15
+ # rewritten stylesheet
16
+ def rewrite_stylesheet(path)
17
+ contents = File.read(path)
18
+ contents.gsub!(ReplaceRexeg) do |match|
19
+ rewrite_asset_link(match, path)
20
+ end
21
+
22
+ tmp = Tempfile.new("cfah-css")
23
+ tmp.write(contents)
24
+ tmp.flush
25
+ tmp
26
+ end
27
+
28
+ private
29
+
30
+ def rewrite_asset_link(asset_link, stylesheet_path)
31
+ url = asset_link.match(ReplaceRexeg)[1]
32
+ if url
33
+ path = path_for_url(url, stylesheet_path)
34
+
35
+ if path.present? && File.exists?(path)
36
+ key = CloudfrontAssetHost.key_for_path(path) + path.gsub(Rails.public_path, '')
37
+ "url(#{CloudfrontAssetHost.asset_host}/#{key})"
38
+ else
39
+ puts "Could not extract path: #{path}"
40
+ asset_link
41
+ end
42
+ else
43
+ puts "Could not find url in #{asset_link}"
44
+ asset_link
45
+ end
46
+ end
47
+
48
+ def path_for_url(url, stylesheet_path)
49
+ if url.starts_with?('/')
50
+ # absolute to public path
51
+ File.expand_path(File.join(Rails.public_path, url))
52
+ else
53
+ # relative to stylesheet_path
54
+ File.expand_path(File.join(File.dirname(stylesheet_path), url))
55
+ end
56
+ end
57
+
58
+ def stylesheets_to_rewrite
59
+ Dir.glob("#{stylesheets_dir}/**/*.css")
60
+ end
61
+
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,151 @@
1
+ "text/html":
2
+ - html
3
+ - htm
4
+ - shtml
5
+ "text/css":
6
+ - css
7
+ "text/xml":
8
+ - xml
9
+ - rss
10
+ "image/gif":
11
+ - gif
12
+ "image/jpeg":
13
+ - jpeg
14
+ - jpg
15
+ "application/x-javascript":
16
+ - js
17
+ "application/atom+xml":
18
+ - atom
19
+ "text/mathml":
20
+ - mml
21
+ "text/plain":
22
+ - txt
23
+ "text/vnd.sun.j2me.app-descriptor":
24
+ - jad
25
+ "text/vnd.wap.wml":
26
+ - wml
27
+ "text/x-component":
28
+ - htc
29
+ "image/png":
30
+ - png
31
+ "image/tiff":
32
+ - tif
33
+ - tiff
34
+ "image/vnd.wap.wbmp":
35
+ - wbmp
36
+ "image/x-icon":
37
+ - ico
38
+ "image/x-jng":
39
+ - jng
40
+ "image/x-ms-bmp":
41
+ - bmp
42
+ "image/svg+xml":
43
+ - svg
44
+ "application/java-archive":
45
+ - jar
46
+ - war
47
+ - ear
48
+ "application/mac-binhex40":
49
+ - hqx
50
+ "application/msword":
51
+ - doc
52
+ "application/pdf":
53
+ - pdf
54
+ "application/postscript":
55
+ - ps
56
+ - eps
57
+ - ai
58
+ "application/rtf":
59
+ - rtf
60
+ "application/vnd.ms-excel":
61
+ - xls
62
+ "application/vnd.ms-powerpoint":
63
+ - ppt
64
+ "application/vnd.wap.wmlc":
65
+ - wmlc
66
+ "application/vnd.wap.xhtml+xml":
67
+ - xhtml
68
+ "application/x-cocoa":
69
+ - cco
70
+ "application/x-java-archive-diff":
71
+ - jardiff
72
+ "application/x-java-jnlp-file":
73
+ - jnlp
74
+ "application/x-makeself":
75
+ - run
76
+ "application/x-perl":
77
+ - pl
78
+ - pm
79
+ "application/x-pilot":
80
+ - prc
81
+ - pdb
82
+ "application/x-rar-compressed":
83
+ - rar
84
+ "application/x-redhat-package-manager":
85
+ - rpm
86
+ "application/x-sea":
87
+ - sea
88
+ "application/x-shockwave-flash":
89
+ - swf
90
+ "application/x-stuffit":
91
+ - sit
92
+ "application/x-tcl":
93
+ - tcl
94
+ - tk
95
+ "application/x-x509-ca-cert":
96
+ - der
97
+ - pem
98
+ - crt
99
+ "application/x-xpinstall":
100
+ - xpi
101
+ "application/zip":
102
+ - zip
103
+ "application/octet-stream":
104
+ - bin
105
+ - exe
106
+ - dll
107
+ "application/octet-stream":
108
+ - deb
109
+ "application/octet-stream":
110
+ - dmg
111
+ "application/octet-stream":
112
+ - eot
113
+ "application/octet-stream":
114
+ - iso
115
+ - img
116
+ "application/octet-stream":
117
+ - msi
118
+ - msp
119
+ - msm
120
+ "audio/midi":
121
+ - mid
122
+ - midi
123
+ - kar
124
+ "audio/mpeg":
125
+ - mp3
126
+ "audio/x-realaudio":
127
+ - ra
128
+ "video/3gpp":
129
+ - 3gpp
130
+ - 3gp
131
+ "video/mpeg":
132
+ - mpeg
133
+ - mpg
134
+ "video/quicktime":
135
+ - mov
136
+ "video/x-flv":
137
+ - flv
138
+ "video/x-mng":
139
+ - mng
140
+ "video/x-ms-asf":
141
+ - asx
142
+ - asf
143
+ "video/x-ms-wmv":
144
+ - wmv
145
+ "video/x-msvideo":
146
+ - avi
147
+ "text/plain":
148
+ - py
149
+ - php
150
+ - config
151
+ - txt
@@ -0,0 +1,124 @@
1
+ require 'right_aws'
2
+ require 'tempfile'
3
+
4
+ module CloudfrontAssetHost
5
+ module Uploader
6
+
7
+ class << self
8
+
9
+ def upload!(options = {})
10
+ dryrun = options.delete(:dryrun) || false
11
+ verbose = options.delete(:verbose) || false
12
+
13
+ puts "-- Updating uncompressed files" if verbose
14
+ upload_keys_with_paths(keys_with_paths, dryrun, verbose, false)
15
+
16
+ if CloudfrontAssetHost.gzip
17
+ puts "-- Updating compressed files" if verbose
18
+ upload_keys_with_paths(gzip_keys_with_paths, dryrun, verbose, true)
19
+ end
20
+
21
+ @existing_keys = nil
22
+ end
23
+
24
+ def upload_keys_with_paths(keys_paths, dryrun, verbose, gzip)
25
+ keys_paths.each do |key, path|
26
+ if existing_keys.include?(key)
27
+ puts "= #{key}" if verbose
28
+ else
29
+ puts "+ #{key}" if verbose
30
+
31
+ extension = File.extname(path)[1..-1]
32
+
33
+ path = rewritten_css_path(path)
34
+
35
+ data_path = gzip ? gzipped_path(path) : path
36
+ bucket.put(key, File.read(data_path), {}, 'public-read', headers_for_path(extension, gzip)) unless dryrun
37
+
38
+ File.unlink(data_path) if gzip && File.exists?(data_path)
39
+ end
40
+ end
41
+ end
42
+
43
+ def gzipped_path(path)
44
+ tmp = Tempfile.new("cfah-gz")
45
+ `gzip #{path} -q -c > #{tmp.path}`
46
+ tmp.path
47
+ end
48
+
49
+ def rewritten_css_path(path)
50
+ if File.extname(path) == '.css'
51
+ tmp = CloudfrontAssetHost::CssRewriter.rewrite_stylesheet(path)
52
+ tmp.path
53
+ else
54
+ path
55
+ end
56
+ end
57
+
58
+ def keys_with_paths
59
+ current_paths.inject({}) do |result, path|
60
+ key = CloudfrontAssetHost.key_for_path(path) + path.gsub(Rails.public_path, '')
61
+
62
+ result[key] = path
63
+ result
64
+ end
65
+ end
66
+
67
+ def gzip_keys_with_paths
68
+ current_paths.inject({}) do |result, path|
69
+ source = path.gsub(Rails.public_path, '')
70
+
71
+ if CloudfrontAssetHost.gzip_allowed_for_source?(source)
72
+ key = "#{CloudfrontAssetHost.gzip_prefix}/" << CloudfrontAssetHost.key_for_path(path) << source
73
+ result[key] = path
74
+ end
75
+
76
+ result
77
+ end
78
+ end
79
+
80
+ def existing_keys
81
+ @existing_keys ||= begin
82
+ keys = []
83
+ keys.concat bucket.keys('prefix' => CloudfrontAssetHost.key_prefix).map { |key| key.name }
84
+ keys.concat bucket.keys('prefix' => CloudfrontAssetHost.gzip_prefix).map { |key| key.name }
85
+ keys
86
+ end
87
+ end
88
+
89
+ def current_paths
90
+ @current_paths ||= Dir.glob("#{Rails.public_path}/{images,javascripts,stylesheets}/**/*").reject { |path| File.directory?(path) }
91
+ end
92
+
93
+ def headers_for_path(extension, gzip = false)
94
+ mime = ext_to_mime[extension] || 'application/octet-stream'
95
+ headers = {
96
+ 'Content-Type' => mime,
97
+ 'Cache-Control' => "max-age=#{10.years.to_i}",
98
+ 'Expires' => 1.year.from_now.utc.to_s
99
+ }
100
+ headers['Content-Encoding'] = 'gzip' if gzip
101
+
102
+ headers
103
+ end
104
+
105
+ def ext_to_mime
106
+ @ext_to_mime ||= Hash[ *( YAML::load_file(File.join(File.dirname(__FILE__), "mime_types.yml")).collect { |k,vv| vv.collect{ |v| [v,k] } }.flatten ) ]
107
+ end
108
+
109
+ def bucket
110
+ @bucket ||= s3.bucket(CloudfrontAssetHost.bucket)
111
+ end
112
+
113
+ def s3
114
+ @s3 ||= RightAws::S3.new(config['access_key_id'], config['secret_access_key'])
115
+ end
116
+
117
+ def config
118
+ @config ||= YAML::load_file(CloudfrontAssetHost.s3_config)
119
+ end
120
+
121
+ end
122
+
123
+ end
124
+ end
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+ access_key_id: 'access_key'
3
+ secret_access_key: 'secret'
4
+ options:
5
+ use_ssl: true
File without changes
@@ -0,0 +1 @@
1
+ // Javascripts here
@@ -0,0 +1,4 @@
1
+ body { background-image: url(../images/image.png); }
2
+ body { background-image: url(/images/image.png); }
3
+ body { background-image: url('/images/image.png'); }
4
+ body { background-image: url("/images/image.png"); }
@@ -0,0 +1,127 @@
1
+ require 'test_helper'
2
+
3
+ class CloudfrontAssetHostTest < Test::Unit::TestCase
4
+
5
+ context "A configured plugin" do
6
+ setup do
7
+ CloudfrontAssetHost.configure do |config|
8
+ config.cname = "assethost.com"
9
+ config.bucket = "bucketname"
10
+ config.key_prefix = ""
11
+ config.enabled = false
12
+ end
13
+ end
14
+
15
+ should "add methods to asset-tag-helper" do
16
+ assert ActionView::Helpers::AssetTagHelper.private_method_defined?('rails_asset_id_with_cloudfront')
17
+ assert ActionView::Helpers::AssetTagHelper.private_method_defined?('rewrite_asset_path_with_cloudfront')
18
+ end
19
+
20
+ should "not enable itself by default" do
21
+ assert_equal false, CloudfrontAssetHost.enabled
22
+ assert_equal "", ActionController::Base.asset_host
23
+ end
24
+
25
+ should "return key for path" do
26
+ assert_equal "8ed41cb87", CloudfrontAssetHost.key_for_path(File.join(RAILS_ROOT, 'public', 'javascripts', 'application.js'))
27
+ end
28
+
29
+ should "prepend prefix to key" do
30
+ CloudfrontAssetHost.key_prefix = "prefix/"
31
+ assert_equal "prefix/8ed41cb87", CloudfrontAssetHost.key_for_path(File.join(RAILS_ROOT, 'public', 'javascripts', 'application.js'))
32
+ end
33
+
34
+ context "asset-host" do
35
+
36
+ setup do
37
+ @source = "/javascripts/application.js"
38
+ end
39
+
40
+ should "use cname for asset_host" do
41
+ assert_equal "http://assethost.com", CloudfrontAssetHost.asset_host(@source)
42
+ end
43
+
44
+ should "use bucket_host when cname is not present" do
45
+ CloudfrontAssetHost.cname = nil
46
+ assert_equal "http://bucketname.s3.amazonaws.com", CloudfrontAssetHost.asset_host(@source)
47
+ end
48
+
49
+ should "not support gzip for images" do
50
+ request = stub(:headers => {'User-Agent' => 'Mozilla/5.0', 'Accept-Encoding' => 'gzip, compress'})
51
+ source = "/images/logo.png"
52
+ assert_equal "http://assethost.com", CloudfrontAssetHost.asset_host(source, request)
53
+ end
54
+
55
+ context "when taking the headers into account" do
56
+
57
+ should "support gzip for IE" do
58
+ request = stub(:headers => {'User-Agent' => 'Mozilla/4.0 (compatible; MSIE 8.0)', 'Accept-Encoding' => 'gzip, compress'})
59
+ assert_equal "http://assethost.com/gz", CloudfrontAssetHost.asset_host(@source, request)
60
+ end
61
+
62
+ should "support gzip for modern browsers" do
63
+ request = stub(:headers => {'User-Agent' => 'Mozilla/5.0', 'Accept-Encoding' => 'gzip, compress'})
64
+ assert_equal "http://assethost.com/gz", CloudfrontAssetHost.asset_host(@source, request)
65
+ end
66
+
67
+ should "support not support gzip for Netscape 4" do
68
+ request = stub(:headers => {'User-Agent' => 'Mozilla/4.0', 'Accept-Encoding' => 'gzip, compress'})
69
+ assert_equal "http://assethost.com", CloudfrontAssetHost.asset_host(@source, request)
70
+ end
71
+
72
+ should "require gzip in accept-encoding" do
73
+ request = stub(:headers => {'User-Agent' => 'Mozilla/5.0'})
74
+ assert_equal "http://assethost.com", CloudfrontAssetHost.asset_host(@source, request)
75
+ end
76
+
77
+ end
78
+
79
+ end
80
+ end
81
+
82
+ context "An enabled and configured plugin" do
83
+ setup do
84
+ CloudfrontAssetHost.configure do |config|
85
+ config.enabled = true
86
+ config.cname = "assethost.com"
87
+ config.bucket = "bucketname"
88
+ config.key_prefix = ""
89
+ end
90
+ end
91
+
92
+ should "set the asset_host" do
93
+ assert ActionController::Base.asset_host.is_a?(Proc)
94
+ end
95
+
96
+ should "alias methods in asset-tag-helper" do
97
+ assert ActionView::Helpers::AssetTagHelper.private_method_defined?('rails_asset_id_without_cloudfront')
98
+ assert ActionView::Helpers::AssetTagHelper.private_method_defined?('rewrite_asset_path_without_cloudfront')
99
+ assert ActionView::Helpers::AssetTagHelper.private_method_defined?('rails_asset_id')
100
+ assert ActionView::Helpers::AssetTagHelper.private_method_defined?('rewrite_asset_path')
101
+ end
102
+ end
103
+
104
+ context "An improperly configured plugin" do
105
+ should "complain about bucket not being set" do
106
+ assert_raise(RuntimeError) {
107
+ CloudfrontAssetHost.configure do |config|
108
+ config.enabled = false
109
+ config.cname = "assethost.com"
110
+ config.bucket = nil
111
+ end
112
+ }
113
+ end
114
+
115
+ should "complain about missing s3-config" do
116
+ assert_raise(RuntimeError) {
117
+ CloudfrontAssetHost.configure do |config|
118
+ config.enabled = false
119
+ config.cname = "assethost.com"
120
+ config.bucket = "bucketname"
121
+ config.s3_config = "bogus"
122
+ end
123
+ }
124
+ end
125
+ end
126
+
127
+ end
@@ -0,0 +1,28 @@
1
+ require 'test_helper'
2
+
3
+ class CssRewriterTest < Test::Unit::TestCase
4
+
5
+ context "The CssRewriter" do
6
+
7
+ setup do
8
+ CloudfrontAssetHost.configure do |config|
9
+ config.cname = "assethost.com"
10
+ config.bucket = "bucketname"
11
+ config.key_prefix = ""
12
+ config.enabled = false
13
+ end
14
+
15
+ @stylesheet_path = File.join(Rails.public_path, 'stylesheets', 'style.css')
16
+ end
17
+
18
+ should "rewrite a single css file" do
19
+ tmp = CloudfrontAssetHost::CssRewriter.rewrite_stylesheet(@stylesheet_path)
20
+ contents = File.read(tmp.path)
21
+ contents.split("\n").each do |line|
22
+ assert_equal "body { background-image: url(http://assethost.com/d41d8cd98/images/image.png); }", line
23
+ end
24
+ end
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,26 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+
4
+ require 'action_controller'
5
+ require 'right_aws'
6
+
7
+ require 'shoulda'
8
+ require 'mocha'
9
+ begin require 'redgreen'; rescue LoadError; end
10
+ begin require 'turn'; rescue LoadError; end
11
+
12
+ RAILS_ROOT = File.expand_path(File.join(File.dirname(__FILE__), 'app'))
13
+
14
+ module Rails
15
+ class << self
16
+ def root
17
+ RAILS_ROOT
18
+ end
19
+
20
+ def public_path
21
+ File.join(RAILS_ROOT, 'public')
22
+ end
23
+ end
24
+ end
25
+
26
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'cloudfront_asset_host')
@@ -0,0 +1,113 @@
1
+ require 'test_helper'
2
+
3
+ class UploaderTest < Test::Unit::TestCase
4
+
5
+ context "A configured uploader" do
6
+ setup do
7
+ CloudfrontAssetHost.configure do |config|
8
+ config.cname = "assethost.com"
9
+ config.bucket = "bucketname"
10
+ config.key_prefix = ""
11
+ config.s3_config = "#{RAILS_ROOT}/config/s3.yml"
12
+ config.enabled = false
13
+ end
14
+ end
15
+
16
+ should "be able to retrieve s3-config" do
17
+ config = CloudfrontAssetHost::Uploader.config
18
+ assert_equal 'access_key', config['access_key_id']
19
+ assert_equal 'secret', config['secret_access_key']
20
+ end
21
+
22
+ should "be able to instantiate s3-interface" do
23
+ RightAws::S3.expects(:new).with('access_key', 'secret').returns(mock)
24
+ assert_not_nil CloudfrontAssetHost::Uploader.s3
25
+ end
26
+
27
+ should "glob current files" do
28
+ assert_equal 3, CloudfrontAssetHost::Uploader.current_paths.length
29
+ end
30
+
31
+ should "calculate keys for paths" do
32
+ keys_with_paths = CloudfrontAssetHost::Uploader.keys_with_paths
33
+ assert_equal 3, keys_with_paths.length
34
+ assert_match %r{/test/app/public/javascripts/application\.js$}, keys_with_paths["8ed41cb87/javascripts/application.js"]
35
+ end
36
+
37
+ should "calculate gzip keys for paths" do
38
+ gz_keys_with_paths = CloudfrontAssetHost::Uploader.gzip_keys_with_paths
39
+ assert_equal 2, gz_keys_with_paths.length
40
+ assert_match %r{/test/app/public/javascripts/application\.js$}, gz_keys_with_paths["gz/8ed41cb87/javascripts/application.js"]
41
+ assert_match %r{/test/app/public/stylesheets/style\.css$}, gz_keys_with_paths["gz/bd258f13d/stylesheets/style.css"]
42
+ end
43
+
44
+ should "return a mimetype for an extension" do
45
+ assert_equal 'application/x-javascript', CloudfrontAssetHost::Uploader.ext_to_mime['js']
46
+ end
47
+
48
+ should "construct the headers for a path" do
49
+ headers = CloudfrontAssetHost::Uploader.headers_for_path('js')
50
+ assert_equal 'application/x-javascript', headers['Content-Type']
51
+ assert_match /max-age=\d+/, headers['Cache-Control']
52
+ assert DateTime.parse(headers['Expires'])
53
+ end
54
+
55
+ should "add gzip-header" do
56
+ headers = CloudfrontAssetHost::Uploader.headers_for_path('js', true)
57
+ assert_equal 'application/x-javascript', headers['Content-Type']
58
+ assert_equal 'gzip', headers['Content-Encoding']
59
+ assert_match /max-age=\d+/, headers['Cache-Control']
60
+ assert DateTime.parse(headers['Expires'])
61
+ end
62
+
63
+ should "retrieve existing keys" do
64
+ bucket_mock = mock
65
+ bucket_mock.expects(:keys).with({'prefix' => ''}).returns([stub(:name => "keyname")])
66
+ bucket_mock.expects(:keys).with({'prefix' => 'gz'}).returns([stub(:name => "gz/keyname")])
67
+
68
+ CloudfrontAssetHost::Uploader.expects(:bucket).times(2).returns(bucket_mock)
69
+ assert_equal ["keyname", "gz/keyname"], CloudfrontAssetHost::Uploader.existing_keys
70
+ end
71
+
72
+ should "upload files when there are no existing keys" do
73
+ bucket_mock = mock
74
+ bucket_mock.expects(:put).times(5)
75
+ CloudfrontAssetHost::Uploader.stubs(:bucket).returns(bucket_mock)
76
+ CloudfrontAssetHost::Uploader.stubs(:existing_keys).returns([])
77
+
78
+ CloudfrontAssetHost::Uploader.upload!
79
+ end
80
+
81
+ should "not re-upload existing keys" do
82
+ CloudfrontAssetHost::Uploader.expects(:bucket).never
83
+ CloudfrontAssetHost::Uploader.stubs(:existing_keys).returns(
84
+ ["gz/8ed41cb87/javascripts/application.js", "8ed41cb87/javascripts/application.js",
85
+ "d41d8cd98/images/image.png",
86
+ "bd258f13d/stylesheets/style.css", "gz/bd258f13d/stylesheets/style.css"]
87
+ )
88
+
89
+ CloudfrontAssetHost::Uploader.upload!
90
+ end
91
+
92
+ should "correctly gzip files" do
93
+ path = File.join(RAILS_ROOT, 'public', 'javascripts', 'application.js')
94
+ contents = File.read(path)
95
+
96
+ gz_path = CloudfrontAssetHost::Uploader.gzipped_path(path)
97
+ gunzip_contents = `gunzip #{gz_path} -q -c`
98
+
99
+ assert_equal contents, gunzip_contents
100
+ end
101
+
102
+ should "correctly rewrite css files" do
103
+ path = File.join(RAILS_ROOT, 'public', 'stylesheets', 'style.css')
104
+ css_path = CloudfrontAssetHost::Uploader.rewritten_css_path(path)
105
+
106
+ File.read(css_path).split("\n").each do |line|
107
+ assert_equal "body { background-image: url(http://assethost.com/d41d8cd98/images/image.png); }", line
108
+ end
109
+ end
110
+
111
+ end
112
+
113
+ end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: caleb-cloudfront_asset_host
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 0
8
+ - 2
9
+ version: 1.0.2
10
+ platform: ruby
11
+ authors:
12
+ - Menno van der Sman
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-04-23 00:00:00 -04:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: right_aws
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :runtime
31
+ version_requirements: *id001
32
+ description: Easy deployment of your assets on CloudFront or S3 using a simple rake-task. When enabled in production, the application's asset_host and public_paths will point to the correct location.
33
+ email: menno@wakoopa.com
34
+ executables: []
35
+
36
+ extensions: []
37
+
38
+ extra_rdoc_files:
39
+ - README.markdown
40
+ files:
41
+ - .gitignore
42
+ - MIT-LICENSE
43
+ - README.markdown
44
+ - Rakefile
45
+ - VERSION
46
+ - lib/cloudfront_asset_host.rb
47
+ - lib/cloudfront_asset_host/asset_tag_helper_ext.rb
48
+ - lib/cloudfront_asset_host/css_rewriter.rb
49
+ - lib/cloudfront_asset_host/mime_types.yml
50
+ - lib/cloudfront_asset_host/uploader.rb
51
+ - test/app/config/s3.yml
52
+ - test/app/public/images/image.png
53
+ - test/app/public/javascripts/application.js
54
+ - test/app/public/stylesheets/style.css
55
+ - test/cloudfront_asset_host_test.rb
56
+ - test/css_rewriter_test.rb
57
+ - test/test_helper.rb
58
+ - test/uploader_test.rb
59
+ has_rdoc: true
60
+ homepage: http://github.com/caleb/cloudfront_asset_host
61
+ licenses: []
62
+
63
+ post_install_message:
64
+ rdoc_options:
65
+ - --charset=UTF-8
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ segments:
80
+ - 0
81
+ version: "0"
82
+ requirements: []
83
+
84
+ rubyforge_project:
85
+ rubygems_version: 1.3.6
86
+ signing_key:
87
+ specification_version: 3
88
+ summary: Rails plugin to easily and efficiently deploy your assets on Amazon's S3 or CloudFront
89
+ test_files:
90
+ - test/cloudfront_asset_host_test.rb
91
+ - test/css_rewriter_test.rb
92
+ - test/test_helper.rb
93
+ - test/uploader_test.rb