benhutton-cloudfront_asset_host 1.0.3

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 = "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