benhutton-cloudfront_asset_host 1.0.3

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/.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 = "wpeterson-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/menno/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,64 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{benhutton-cloudfront_asset_host}
8
+ s.version = "1.0.3"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Menno van der Sman"]
12
+ s.date = %q{2010-08-23}
13
+ s.description = %q{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.}
14
+ s.email = %q{menno@wakoopa.com}
15
+ s.extra_rdoc_files = [
16
+ "README.markdown"
17
+ ]
18
+ s.files = [
19
+ ".gitignore",
20
+ "MIT-LICENSE",
21
+ "README.markdown",
22
+ "Rakefile",
23
+ "VERSION",
24
+ "lib/cloudfront_asset_host.rb",
25
+ "lib/cloudfront_asset_host/asset_tag_helper_ext.rb",
26
+ "lib/cloudfront_asset_host/css_rewriter.rb",
27
+ "lib/cloudfront_asset_host/mime_types.yml",
28
+ "lib/cloudfront_asset_host/uploader.rb",
29
+ "test/app/config/s3.yml",
30
+ "test/app/public/images/image.png",
31
+ "test/app/public/javascripts/application.js",
32
+ "test/app/public/stylesheets/style.css",
33
+ "test/cloudfront_asset_host_test.rb",
34
+ "test/css_rewriter_test.rb",
35
+ "test/test_helper.rb",
36
+ "test/uploader_test.rb",
37
+ "benhutton-cloudfront_asset_host.gemspec"
38
+ ]
39
+ s.homepage = %q{http://github.com/benhutton/cloudfront_asset_host}
40
+ s.rdoc_options = ["--charset=UTF-8"]
41
+ s.require_paths = ["lib"]
42
+ s.rubygems_version = %q{1.3.5}
43
+ s.summary = %q{Rails plugin to easily and efficiently deploy your assets on Amazon's S3 or CloudFront}
44
+ s.test_files = [
45
+ "test/cloudfront_asset_host_test.rb",
46
+ "test/css_rewriter_test.rb",
47
+ "test/test_helper.rb",
48
+ "test/uploader_test.rb"
49
+ ]
50
+
51
+ if s.respond_to? :specification_version then
52
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
53
+ s.specification_version = 3
54
+
55
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
56
+ s.add_runtime_dependency(%q<right_aws>, [">= 0"])
57
+ else
58
+ s.add_dependency(%q<right_aws>, [">= 0"])
59
+ end
60
+ else
61
+ s.add_dependency(%q<right_aws>, [">= 0"])
62
+ end
63
+ end
64
+
@@ -0,0 +1,126 @@
1
+ require 'digest/md5'
2
+ require 'cloudfront_asset_host/asset_tag_helper_ext'
3
+
4
+ module CloudfrontAssetHost
5
+
6
+ autoload :Uploader, 'cloudfront_asset_host/uploader'
7
+ autoload :CssRewriter, 'cloudfront_asset_host/css_rewriter'
8
+
9
+ # Bucket that will be used to store all the assets (required)
10
+ mattr_accessor :bucket
11
+
12
+ # CNAME that is configured for the bucket or CloudFront distribution
13
+ mattr_accessor :cname
14
+
15
+ # Prefix keys
16
+ mattr_accessor :key_prefix
17
+
18
+ # Path to S3 config. Expects an +access_key_id+ and +secret_access_key+
19
+ mattr_accessor :s3_config
20
+
21
+ # Indicates whether the plugin should be enabled
22
+ mattr_accessor :enabled
23
+
24
+ # Upload gzipped assets and serve those assets when applicable
25
+ mattr_accessor :gzip
26
+
27
+ # Which extensions to serve as gzip
28
+ mattr_accessor :gzip_extensions
29
+
30
+ # Key-prefix under which to store gzipped assets
31
+ mattr_accessor :gzip_prefix
32
+
33
+ # Directories to search for asset files in
34
+ mattr_accessor :asset_dirs
35
+
36
+ # Key Regular Expression to filter out/exclude content
37
+ mattr_accessor :exclude_pattern
38
+
39
+
40
+ class << self
41
+
42
+ def configure
43
+ # default configuration
44
+ self.bucket = nil
45
+ self.cname = nil
46
+ self.key_prefix = ""
47
+ self.s3_config = "#{RAILS_ROOT}/config/s3.yml"
48
+ self.enabled = false
49
+
50
+ self.asset_dirs = "images,javascripts,stylesheets"
51
+ self.exclude_pattern = nil
52
+
53
+ self.gzip = true
54
+ self.gzip_extensions = %w(js css)
55
+ self.gzip_prefix = "gz"
56
+
57
+ yield(self)
58
+
59
+ if properly_configured?
60
+ enable!
61
+ end
62
+ end
63
+
64
+ def asset_host(source = nil, request = nil)
65
+ return '' if source && disable_cdn_for_source?(source)
66
+
67
+ cname_for_source = (self.cname =~ /%d/) ? self.cname % (source.hash % 4) : self.cname
68
+ host = cname_for_source.present? ? "http://#{ cname_for_source }" : "http://#{self.bucket_host}"
69
+
70
+ if source && request && CloudfrontAssetHost.gzip
71
+ gzip_allowed = CloudfrontAssetHost.gzip_allowed_for_source?(source)
72
+
73
+ # Only Netscape 4 does not support gzip
74
+ # IE masquerades as Netscape 4, so we check that as well
75
+ user_agent = request.headers['User-Agent'].to_s
76
+ gzip_accepted = !(user_agent =~ /^Mozilla\/4/) || user_agent =~ /\bMSIE/
77
+ gzip_accepted &&= request.headers['Accept-Encoding'].to_s.include?('gzip')
78
+
79
+ if gzip_accepted && gzip_allowed
80
+ host << "/#{CloudfrontAssetHost.gzip_prefix}"
81
+ end
82
+ end
83
+
84
+ host
85
+ end
86
+
87
+ def bucket_host
88
+ "#{self.bucket}.s3.amazonaws.com"
89
+ end
90
+
91
+ def enable!
92
+ if enabled
93
+ ActionController::Base.asset_host = Proc.new { |source, request| CloudfrontAssetHost.asset_host(source, request) }
94
+ ActionView::Helpers::AssetTagHelper.send(:alias_method_chain, :rewrite_asset_path, :cloudfront)
95
+ ActionView::Helpers::AssetTagHelper.send(:alias_method_chain, :rails_asset_id, :cloudfront)
96
+ end
97
+ end
98
+
99
+ def key_for_path(path)
100
+ key_prefix + md5sum(path)[0..8]
101
+ end
102
+
103
+ def gzip_allowed_for_source?(source)
104
+ extension = source.split('.').last
105
+ CloudfrontAssetHost.gzip_extensions.include?(extension)
106
+ end
107
+
108
+ def disable_cdn_for_source?(source)
109
+ source.match(exclude_pattern) if exclude_pattern.present?
110
+ end
111
+
112
+ private
113
+
114
+ def properly_configured?
115
+ raise "You'll need to specify a bucket" if bucket.blank?
116
+ raise "Could not find S3-configuration" unless File.exists?(s3_config)
117
+ true
118
+ end
119
+
120
+ def md5sum(path)
121
+ Digest::MD5.hexdigest(File.read(path))
122
+ end
123
+
124
+ end
125
+
126
+ end
@@ -0,0 +1,38 @@
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
+ rewrite_path = File.exist?(path) && !CloudfrontAssetHost.disable_cdn_for_source?(source)
14
+ asset_id = rewrite_path ? CloudfrontAssetHost.key_for_path(path) : ''
15
+
16
+ if @@cache_asset_timestamps
17
+ @@asset_timestamps_cache_guard.synchronize do
18
+ @@asset_timestamps_cache[source] = asset_id
19
+ end
20
+ end
21
+
22
+ asset_id
23
+ end
24
+ end
25
+
26
+ # Override asset_path so it prepends the asset_id
27
+ def rewrite_asset_path_with_cloudfront(source)
28
+ asset_id = rails_asset_id(source)
29
+ if asset_id.blank?
30
+ source
31
+ else
32
+ "/#{asset_id}#{source}"
33
+ end
34
+ end
35
+
36
+ end
37
+ end
38
+ 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,135 @@
1
+ require 'right_aws'
2
+ require 'tempfile'
3
+
4
+ module CloudfrontAssetHost
5
+ module Uploader
6
+
7
+ class << self
8
+
9
+ def upload!(options = {})
10
+ puts "-- Updating uncompressed files" if options[:verbose]
11
+ upload_keys_with_paths(keys_with_paths, options)
12
+
13
+ if CloudfrontAssetHost.gzip
14
+ puts "-- Updating compressed files" if options[:verbose]
15
+ upload_keys_with_paths(gzip_keys_with_paths, options.merge(:gzip => true))
16
+ end
17
+
18
+ @existing_keys = nil
19
+ end
20
+
21
+ def upload_keys_with_paths(keys_paths, options={})
22
+ gzip = options[:gzip] || false
23
+ dryrun = options[:dryrun] || false
24
+ verbose = options[:verbose] || false
25
+
26
+ keys_paths.each do |key, path|
27
+ if should_upload?(key, options)
28
+ puts "+ #{key}" if verbose
29
+
30
+ extension = File.extname(path)[1..-1]
31
+
32
+ path = rewritten_css_path(path)
33
+
34
+ data_path = gzip ? gzipped_path(path) : path
35
+ bucket.put(key, File.read(data_path), {}, 'public-read', headers_for_path(extension, gzip)) unless dryrun
36
+
37
+ File.unlink(data_path) if gzip && File.exists?(data_path)
38
+ else
39
+ puts "= #{key}" if verbose
40
+ end
41
+ end
42
+ end
43
+
44
+ def should_upload?(key, options={})
45
+ return false if CloudfrontAssetHost.disable_cdn_for_source?(key)
46
+
47
+ options[:force_write] || !existing_keys.include?(key)
48
+ end
49
+
50
+ def gzipped_path(path)
51
+ tmp = Tempfile.new("cfah-gz")
52
+ `gzip #{path} -q -c > #{tmp.path}`
53
+ tmp.path
54
+ end
55
+
56
+ def rewritten_css_path(path)
57
+ if File.extname(path) == '.css'
58
+ tmp = CloudfrontAssetHost::CssRewriter.rewrite_stylesheet(path)
59
+ tmp.path
60
+ else
61
+ path
62
+ end
63
+ end
64
+
65
+ def keys_with_paths
66
+ current_paths.inject({}) do |result, path|
67
+ key = CloudfrontAssetHost.key_for_path(path) + path.gsub(Rails.public_path, '')
68
+
69
+ result[key] = path
70
+ result
71
+ end
72
+ end
73
+
74
+ def gzip_keys_with_paths
75
+ current_paths.inject({}) do |result, path|
76
+ source = path.gsub(Rails.public_path, '')
77
+
78
+ if CloudfrontAssetHost.gzip_allowed_for_source?(source)
79
+ key = "#{CloudfrontAssetHost.gzip_prefix}/" << CloudfrontAssetHost.key_for_path(path) << source
80
+ result[key] = path
81
+ end
82
+
83
+ result
84
+ end
85
+ end
86
+
87
+ def existing_keys
88
+ @existing_keys ||= begin
89
+ keys = []
90
+ keys.concat bucket.keys('prefix' => CloudfrontAssetHost.key_prefix).map { |key| key.name }
91
+ keys.concat bucket.keys('prefix' => CloudfrontAssetHost.gzip_prefix).map { |key| key.name }
92
+ keys
93
+ end
94
+ end
95
+
96
+ def current_paths
97
+ @current_paths ||= Dir.glob("#{Rails.public_path}/{#{ asset_dirs }}/**/*").reject { |path| File.directory?(path) }
98
+ end
99
+
100
+ def headers_for_path(extension, gzip = false)
101
+ mime = ext_to_mime[extension] || 'application/octet-stream'
102
+ headers = {
103
+ 'Content-Type' => mime,
104
+ 'Cache-Control' => "max-age=#{10.years.to_i}",
105
+ 'Expires' => 1.year.from_now.utc.to_s
106
+ }
107
+ headers['Content-Encoding'] = 'gzip' if gzip
108
+
109
+ headers
110
+ end
111
+
112
+ def ext_to_mime
113
+ @ext_to_mime ||= Hash[ *( YAML::load_file(File.join(File.dirname(__FILE__), "mime_types.yml")).collect { |k,vv| vv.collect{ |v| [v,k] } }.flatten ) ]
114
+ end
115
+
116
+ def bucket
117
+ @bucket ||= s3.bucket(CloudfrontAssetHost.bucket)
118
+ end
119
+
120
+ def s3
121
+ @s3 ||= RightAws::S3.new(config['access_key_id'], config['secret_access_key'])
122
+ end
123
+
124
+ def config
125
+ @config ||= YAML::load_file(CloudfrontAssetHost.s3_config)
126
+ end
127
+
128
+ def asset_dirs
129
+ @asset_dirs ||= CloudfrontAssetHost.asset_dirs
130
+ end
131
+
132
+ end
133
+
134
+ end
135
+ 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,6 @@
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"); }
5
+ body { background-image: url("/images/image.png?98732857"); }
6
+ body { background-image: url("/images/image.png?fo0=bar"); }
@@ -0,0 +1,143 @@
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
+ should "default asset_dirs setting" do
35
+ assert_equal "images,javascripts,stylesheets", CloudfrontAssetHost.asset_dirs
36
+ end
37
+
38
+ context "asset-host" do
39
+
40
+ setup do
41
+ @source = "/javascripts/application.js"
42
+ end
43
+
44
+ should "use cname for asset_host" do
45
+ assert_equal "http://assethost.com", CloudfrontAssetHost.asset_host(@source)
46
+ end
47
+
48
+ should "use interpolated cname for asset_host" do
49
+ CloudfrontAssetHost.cname = "assethost-%d.com"
50
+ assert_equal "http://assethost-3.com", CloudfrontAssetHost.asset_host(@source)
51
+ end
52
+
53
+ should "use bucket_host when cname is not present" do
54
+ CloudfrontAssetHost.cname = nil
55
+ assert_equal "http://bucketname.s3.amazonaws.com", CloudfrontAssetHost.asset_host(@source)
56
+ end
57
+
58
+ should "not support gzip for images" do
59
+ request = stub(:headers => {'User-Agent' => 'Mozilla/5.0', 'Accept-Encoding' => 'gzip, compress'})
60
+ source = "/images/logo.png"
61
+ assert_equal "http://assethost.com", CloudfrontAssetHost.asset_host(source, request)
62
+ end
63
+
64
+ context "when taking the headers into account" do
65
+
66
+ should "support gzip for IE" do
67
+ request = stub(:headers => {'User-Agent' => 'Mozilla/4.0 (compatible; MSIE 8.0)', 'Accept-Encoding' => 'gzip, compress'})
68
+ assert_equal "http://assethost.com/gz", CloudfrontAssetHost.asset_host(@source, request)
69
+ end
70
+
71
+ should "support gzip for modern browsers" do
72
+ request = stub(:headers => {'User-Agent' => 'Mozilla/5.0', 'Accept-Encoding' => 'gzip, compress'})
73
+ assert_equal "http://assethost.com/gz", CloudfrontAssetHost.asset_host(@source, request)
74
+ end
75
+
76
+ should "support not support gzip for Netscape 4" do
77
+ request = stub(:headers => {'User-Agent' => 'Mozilla/4.0', 'Accept-Encoding' => 'gzip, compress'})
78
+ assert_equal "http://assethost.com", CloudfrontAssetHost.asset_host(@source, request)
79
+ end
80
+
81
+ should "require gzip in accept-encoding" do
82
+ request = stub(:headers => {'User-Agent' => 'Mozilla/5.0'})
83
+ assert_equal "http://assethost.com", CloudfrontAssetHost.asset_host(@source, request)
84
+ end
85
+
86
+ end
87
+
88
+ end
89
+ end
90
+
91
+ context "An enabled and configured plugin" do
92
+ setup do
93
+ CloudfrontAssetHost.configure do |config|
94
+ config.enabled = true
95
+ config.cname = "assethost.com"
96
+ config.bucket = "bucketname"
97
+ config.key_prefix = ""
98
+ end
99
+ end
100
+
101
+ should "set the asset_host" do
102
+ assert ActionController::Base.asset_host.is_a?(Proc)
103
+ end
104
+
105
+ should "alias methods in asset-tag-helper" do
106
+ assert ActionView::Helpers::AssetTagHelper.private_method_defined?('rails_asset_id_without_cloudfront')
107
+ assert ActionView::Helpers::AssetTagHelper.private_method_defined?('rewrite_asset_path_without_cloudfront')
108
+ assert ActionView::Helpers::AssetTagHelper.private_method_defined?('rails_asset_id')
109
+ assert ActionView::Helpers::AssetTagHelper.private_method_defined?('rewrite_asset_path')
110
+ end
111
+ end
112
+
113
+ context "An improperly configured plugin" do
114
+ should "complain about bucket not being set" do
115
+ assert_raise(RuntimeError) {
116
+ CloudfrontAssetHost.configure do |config|
117
+ config.enabled = false
118
+ config.cname = "assethost.com"
119
+ config.bucket = nil
120
+ end
121
+ }
122
+ end
123
+
124
+ should "complain about missing s3-config" do
125
+ assert_raise(RuntimeError) {
126
+ CloudfrontAssetHost.configure do |config|
127
+ config.enabled = false
128
+ config.cname = "assethost.com"
129
+ config.bucket = "bucketname"
130
+ config.s3_config = "bogus"
131
+ end
132
+ }
133
+ end
134
+ end
135
+
136
+ should "respect custom asset_dirs" do
137
+ CloudfrontAssetHost.configure do |config|
138
+ config.bucket = "bucketname"
139
+ config.asset_dirs = "custom"
140
+ end
141
+ assert_equal "custom", CloudfrontAssetHost.asset_dirs
142
+ end
143
+ 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,155 @@
1
+ require 'test_helper'
2
+
3
+ class UploaderTest < Test::Unit::TestCase
4
+
5
+ context "A configured uploader" do
6
+ setup do
7
+ @css_md5 = md5_key('stylesheets/style.css') #7026e6ce3
8
+ @js_md5 = md5_key('javascripts/application.js') #8ed41cb87
9
+ CloudfrontAssetHost.configure do |config|
10
+ config.cname = "assethost.com"
11
+ config.bucket = "bucketname"
12
+ config.key_prefix = ""
13
+ config.s3_config = "#{RAILS_ROOT}/config/s3.yml"
14
+ config.enabled = false
15
+ end
16
+ end
17
+
18
+ should "be able to retrieve s3-config" do
19
+ config = CloudfrontAssetHost::Uploader.config
20
+ assert_equal 'access_key', config['access_key_id']
21
+ assert_equal 'secret', config['secret_access_key']
22
+ end
23
+
24
+ should "be able to instantiate s3-interface" do
25
+ RightAws::S3.expects(:new).with('access_key', 'secret').returns(mock)
26
+ assert_not_nil CloudfrontAssetHost::Uploader.s3
27
+ end
28
+
29
+ should "glob current files" do
30
+ assert_equal 3, CloudfrontAssetHost::Uploader.current_paths.length
31
+ end
32
+
33
+ should "calculate keys for paths" do
34
+ keys_with_paths = CloudfrontAssetHost::Uploader.keys_with_paths
35
+ assert_equal 3, keys_with_paths.length
36
+ assert_match %r{/test/app/public/javascripts/application\.js$}, keys_with_paths["#{@js_md5}/javascripts/application.js"]
37
+ end
38
+
39
+ should "calculate gzip keys for paths" do
40
+ gz_keys_with_paths = CloudfrontAssetHost::Uploader.gzip_keys_with_paths
41
+ assert_equal 2, gz_keys_with_paths.length
42
+ assert_match %r{/test/app/public/javascripts/application\.js$}, gz_keys_with_paths["gz/#{@js_md5}/javascripts/application.js"]
43
+ assert_match %r{/test/app/public/stylesheets/style\.css$}, gz_keys_with_paths["gz/#{@css_md5}/stylesheets/style.css"]
44
+ end
45
+
46
+ should "return a mimetype for an extension" do
47
+ assert_equal 'application/x-javascript', CloudfrontAssetHost::Uploader.ext_to_mime['js']
48
+ end
49
+
50
+ should "construct the headers for a path" do
51
+ headers = CloudfrontAssetHost::Uploader.headers_for_path('js')
52
+ assert_equal 'application/x-javascript', headers['Content-Type']
53
+ assert_match /max-age=\d+/, headers['Cache-Control']
54
+ assert DateTime.parse(headers['Expires'])
55
+ end
56
+
57
+ should "add gzip-header" do
58
+ headers = CloudfrontAssetHost::Uploader.headers_for_path('js', true)
59
+ assert_equal 'application/x-javascript', headers['Content-Type']
60
+ assert_equal 'gzip', headers['Content-Encoding']
61
+ assert_match /max-age=\d+/, headers['Cache-Control']
62
+ assert DateTime.parse(headers['Expires'])
63
+ end
64
+
65
+ should "retrieve existing keys" do
66
+ bucket_mock = mock
67
+ bucket_mock.expects(:keys).with({'prefix' => ''}).returns([stub(:name => "keyname")])
68
+ bucket_mock.expects(:keys).with({'prefix' => 'gz'}).returns([stub(:name => "gz/keyname")])
69
+
70
+ CloudfrontAssetHost::Uploader.expects(:bucket).times(2).returns(bucket_mock)
71
+ assert_equal ["keyname", "gz/keyname"], CloudfrontAssetHost::Uploader.existing_keys
72
+ end
73
+
74
+ should "upload files when there are no existing keys" do
75
+ bucket_mock = mock
76
+ bucket_mock.expects(:put).times(5)
77
+ CloudfrontAssetHost::Uploader.stubs(:bucket).returns(bucket_mock)
78
+ CloudfrontAssetHost::Uploader.stubs(:existing_keys).returns([])
79
+
80
+ CloudfrontAssetHost::Uploader.upload!
81
+ end
82
+
83
+ should "not re-upload existing keys by default" do
84
+ CloudfrontAssetHost::Uploader.expects(:bucket).never
85
+ CloudfrontAssetHost::Uploader.stubs(:existing_keys).returns(
86
+ ["gz/#{@js_md5}/javascripts/application.js", "#{@js_md5}/javascripts/application.js",
87
+ "d41d8cd98/images/image.png",
88
+ "#{@css_md5}/stylesheets/style.css", "gz/#{@css_md5}/stylesheets/style.css"]
89
+ )
90
+
91
+ CloudfrontAssetHost::Uploader.upload!
92
+ end
93
+
94
+ should "re-upload existing keys w/ force_write" do
95
+ bucket_mock = mock
96
+ bucket_mock.expects(:put).times(5)
97
+ CloudfrontAssetHost::Uploader.stubs(:bucket).returns(bucket_mock)
98
+ CloudfrontAssetHost::Uploader.stubs(:existing_keys).returns(
99
+ ["gz/#{@js_md5}/javascripts/application.js", "#{@js_md5}/javascripts/application.js",
100
+ "d41d8cd98/images/image.png",
101
+ "#{@css_md5}/stylesheets/style.css", "gz/#{@css_md5}/stylesheets/style.css"]
102
+ )
103
+
104
+ CloudfrontAssetHost::Uploader.upload!(:force_write => true)
105
+ end
106
+
107
+ should "correctly gzip files" do
108
+ path = File.join(RAILS_ROOT, 'public', 'javascripts', 'application.js')
109
+ contents = File.read(path)
110
+
111
+ gz_path = CloudfrontAssetHost::Uploader.gzipped_path(path)
112
+ gunzip_contents = `gunzip #{gz_path} -q -c`
113
+
114
+ assert_equal contents, gunzip_contents
115
+ end
116
+
117
+ should "correctly rewrite css files" do
118
+ path = File.join(RAILS_ROOT, 'public', 'stylesheets', 'style.css')
119
+ css_path = CloudfrontAssetHost::Uploader.rewritten_css_path(path)
120
+
121
+ File.read(css_path).split("\n").each do |line|
122
+ assert_equal "body { background-image: url(http://assethost.com/d41d8cd98/images/image.png); }", line
123
+ end
124
+ end
125
+
126
+ end
127
+
128
+ context 'with exclude_pattern' do
129
+ setup do
130
+ @css_md5 = md5_key('stylesheets/style.css') #7026e6ce3
131
+ @js_md5 = md5_key('javascripts/application.js') #8ed41cb87
132
+ CloudfrontAssetHost.configure do |config|
133
+ config.cname = "assethost.com"
134
+ config.bucket = "bucketname"
135
+ config.key_prefix = ""
136
+ config.s3_config = "#{RAILS_ROOT}/config/s3.yml"
137
+ config.enabled = false
138
+ config.exclude_pattern = /style/
139
+ end
140
+ end
141
+
142
+ should "filter keys out" do
143
+ bucket_mock = mock
144
+ bucket_mock.expects(:put).times(3)
145
+ CloudfrontAssetHost::Uploader.stubs(:bucket).returns(bucket_mock)
146
+ CloudfrontAssetHost::Uploader.stubs(:existing_keys).returns([])
147
+
148
+ CloudfrontAssetHost::Uploader.upload!
149
+ end
150
+ end
151
+
152
+ def md5_key(path)
153
+ CloudfrontAssetHost.send(:md5sum, File.join('test/app/public', path))[0..8]
154
+ end
155
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: benhutton-cloudfront_asset_host
3
+ version: !ruby/object:Gem::Version
4
+ hash: 17
5
+ prerelease: false
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 3
10
+ version: 1.0.3
11
+ platform: ruby
12
+ authors:
13
+ - Menno van der Sman
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-08-23 00:00:00 -04:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: right_aws
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ 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.
36
+ email: menno@wakoopa.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - README.markdown
43
+ files:
44
+ - .gitignore
45
+ - MIT-LICENSE
46
+ - README.markdown
47
+ - Rakefile
48
+ - VERSION
49
+ - lib/cloudfront_asset_host.rb
50
+ - lib/cloudfront_asset_host/asset_tag_helper_ext.rb
51
+ - lib/cloudfront_asset_host/css_rewriter.rb
52
+ - lib/cloudfront_asset_host/mime_types.yml
53
+ - lib/cloudfront_asset_host/uploader.rb
54
+ - test/app/config/s3.yml
55
+ - test/app/public/images/image.png
56
+ - test/app/public/javascripts/application.js
57
+ - test/app/public/stylesheets/style.css
58
+ - test/cloudfront_asset_host_test.rb
59
+ - test/css_rewriter_test.rb
60
+ - test/test_helper.rb
61
+ - test/uploader_test.rb
62
+ - benhutton-cloudfront_asset_host.gemspec
63
+ has_rdoc: true
64
+ homepage: http://github.com/benhutton/cloudfront_asset_host
65
+ licenses: []
66
+
67
+ post_install_message:
68
+ rdoc_options:
69
+ - --charset=UTF-8
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ hash: 3
78
+ segments:
79
+ - 0
80
+ version: "0"
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ hash: 3
87
+ segments:
88
+ - 0
89
+ version: "0"
90
+ requirements: []
91
+
92
+ rubyforge_project:
93
+ rubygems_version: 1.3.7
94
+ signing_key:
95
+ specification_version: 3
96
+ summary: Rails plugin to easily and efficiently deploy your assets on Amazon's S3 or CloudFront
97
+ test_files:
98
+ - test/cloudfront_asset_host_test.rb
99
+ - test/css_rewriter_test.rb
100
+ - test/test_helper.rb
101
+ - test/uploader_test.rb