foresth-cloudfront_asset_host 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ pkg
@@ -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.
@@ -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
@@ -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 = "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{foresth-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-07-27}
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
+ "cloudfront_asset_host.gemspec",
25
+ "lib/cloudfront_asset_host.rb",
26
+ "lib/cloudfront_asset_host/asset_tag_helper_ext.rb",
27
+ "lib/cloudfront_asset_host/css_rewriter.rb",
28
+ "lib/cloudfront_asset_host/mime_types.yml",
29
+ "lib/cloudfront_asset_host/uploader.rb",
30
+ "test/app/config/s3.yml",
31
+ "test/app/public/images/image.png",
32
+ "test/app/public/javascripts/application.js",
33
+ "test/app/public/stylesheets/style.css",
34
+ "test/cloudfront_asset_host_test.rb",
35
+ "test/css_rewriter_test.rb",
36
+ "test/test_helper.rb",
37
+ "test/uploader_test.rb"
38
+ ]
39
+ s.homepage = %q{http://github.com/foresth/cloudfront_asset_host}
40
+ s.rdoc_options = ["--charset=UTF-8"]
41
+ s.require_paths = ["lib"]
42
+ s.rubygems_version = %q{1.3.6}
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,136 @@
1
+ require 'cloudfront_asset_host/asset_tag_helper_ext'
2
+ require 'digest/md5'
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
+ # Log S3 server access on a bucket
22
+ mattr_accessor :s3_logging
23
+
24
+ # Indicates whether the plugin should be enabled
25
+ mattr_accessor :enabled
26
+
27
+ # Upload gzipped assets and serve those assets when applicable
28
+ mattr_accessor :gzip
29
+
30
+ # Which extensions to serve as gzip
31
+ mattr_accessor :gzip_extensions
32
+
33
+ # Key-prefix under which to store gzipped assets
34
+ mattr_accessor :gzip_prefix
35
+
36
+ # Which extensions are likely to occur in css files
37
+ mattr_accessor :image_extensions
38
+
39
+ class << self
40
+
41
+ def configure
42
+ # default configuration
43
+ self.bucket = nil
44
+ self.cname = nil
45
+ self.key_prefix = ""
46
+ self.s3_config = "#{RAILS_ROOT}/config/s3.yml"
47
+ self.s3_logging = false
48
+ self.enabled = false
49
+
50
+ self.gzip = true
51
+ self.gzip_extensions = %w(js css)
52
+ self.gzip_prefix = "gz"
53
+
54
+ self.image_extensions = %w(jpg jpeg gif png)
55
+
56
+ yield(self)
57
+
58
+ if properly_configured?
59
+ enable!
60
+ end
61
+ end
62
+
63
+ def asset_host(source = nil, request = nil)
64
+ if cname.present?
65
+ if cname.is_a?(Proc)
66
+ host = cname.call(source, request)
67
+ else
68
+ host = (cname =~ /%d/) ? cname % (source.hash % 4) : cname
69
+ end
70
+ else
71
+ host = "http://#{self.bucket_host}"
72
+ end
73
+
74
+ if source && request && CloudfrontAssetHost.gzip
75
+ gzip_allowed = CloudfrontAssetHost.gzip_allowed_for_source?(source)
76
+
77
+ # Only Netscape 4 does not support gzip
78
+ # IE masquerades as Netscape 4, so we check that as well
79
+ user_agent = request.headers['User-Agent'].to_s
80
+ gzip_accepted = !(user_agent =~ /^Mozilla\/4/) || user_agent =~ /\bMSIE/
81
+ gzip_accepted &&= request.headers['Accept-Encoding'].to_s.include?('gzip')
82
+
83
+ if gzip_accepted && gzip_allowed
84
+ host << "/#{CloudfrontAssetHost.gzip_prefix}"
85
+ end
86
+ end
87
+
88
+ host
89
+ end
90
+
91
+ def bucket_host
92
+ "#{self.bucket}.s3.amazonaws.com"
93
+ end
94
+
95
+ def enable!
96
+ if enabled
97
+ ActionController::Base.asset_host = Proc.new { |source, request| CloudfrontAssetHost.asset_host(source, request) }
98
+ ActionView::Helpers::AssetTagHelper.send(:alias_method_chain, :rewrite_asset_path, :cloudfront)
99
+ ActionView::Helpers::AssetTagHelper.send(:alias_method_chain, :rails_asset_id, :cloudfront)
100
+ end
101
+ end
102
+
103
+ def key_for_path(path)
104
+ key_prefix + md5sum(path)[0..8]
105
+ end
106
+
107
+ def gzip_allowed_for_source?(source)
108
+ extension = source.split('.').last
109
+ CloudfrontAssetHost.gzip_extensions.include?(extension)
110
+ end
111
+
112
+ def image?(path)
113
+ extension = path.split('.').last
114
+ CloudfrontAssetHost.image_extensions.include?(extension)
115
+ end
116
+
117
+ def css?(path)
118
+ File.extname(path) == '.css'
119
+ end
120
+
121
+ private
122
+
123
+ def properly_configured?
124
+ raise "You'll need to specify a bucket" if bucket.blank?
125
+ raise "Could not find S3-configuration" unless File.exists?(s3_config)
126
+ true
127
+ end
128
+
129
+ def md5sum(path)
130
+ #`openssl md5 #{path}`.split(/\s/)[1].to_s
131
+ Digest::MD5.hexdigest(File.read(path))
132
+ end
133
+
134
+ end
135
+
136
+ 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,149 @@
1
+ require 'right_aws'
2
+ require 'tempfile'
3
+
4
+ module CloudfrontAssetHost
5
+ module Uploader
6
+
7
+ mattr_accessor :new_keys
8
+
9
+ class << self
10
+
11
+ def upload!(options = {})
12
+ dryrun = options.delete(:dryrun) || false
13
+ verbose = options.delete(:verbose) || false
14
+
15
+ self.new_keys = []
16
+
17
+ puts "-- Updating uncompressed files" if verbose
18
+ upload_keys_with_paths(keys_with_paths, dryrun, verbose, false)
19
+
20
+ if CloudfrontAssetHost.gzip
21
+ puts "-- Updating compressed files" if verbose
22
+ upload_keys_with_paths(gzip_keys_with_paths, dryrun, verbose, true)
23
+ end
24
+
25
+ delete_old_keys
26
+
27
+ @existing_keys = nil
28
+ end
29
+
30
+ def upload_keys_with_paths(keys_paths, dryrun, verbose, gzip)
31
+ keys_paths.each do |key, path|
32
+ new_keys << key
33
+ if !existing_keys.include?(key) || CloudfrontAssetHost.css?(path) && rewrite_all_css?
34
+ puts "+ #{key}" if verbose
35
+
36
+ extension = File.extname(path)[1..-1]
37
+
38
+ path = rewritten_css_path(path)
39
+
40
+ data_path = gzip ? gzipped_path(path) : path
41
+ bucket.put(key, File.read(data_path), {}, 'public-read', headers_for_path(extension, gzip)) unless dryrun
42
+
43
+ File.unlink(data_path) if gzip && File.exists?(data_path)
44
+ else
45
+ puts "= #{key}" if verbose
46
+ end
47
+ end
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 CloudfrontAssetHost.css?(path)
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 delete_old_keys
88
+ puts "-- Removing expired files" if verbose
89
+ (existing_keys - new_keys).uniq.each do |key|
90
+ unless new_keys.include?(key)
91
+ puts "- #{key}" if verbose
92
+ bucket.delete_folder(key)
93
+ end
94
+ end
95
+ end
96
+
97
+ def rewrite_all_css?
98
+ @rewrite_all_css ||= !keys_with_paths.delete_if { |key, path| existing_keys.include?(key) || !CloudfrontAssetHost.image?(path) }.empty?
99
+ end
100
+
101
+ def existing_keys
102
+ @existing_keys ||= begin
103
+ keys = []
104
+ keys.concat bucket.keys('prefix' => CloudfrontAssetHost.key_prefix).map { |key| key.name }
105
+ keys.concat bucket.keys('prefix' => CloudfrontAssetHost.gzip_prefix).map { |key| key.name }
106
+ keys
107
+ end
108
+ end
109
+
110
+ def current_paths
111
+ @current_paths ||= Dir.glob("#{Rails.public_path}/{images,javascripts,stylesheets}/**/*").reject { |path| File.directory?(path) }
112
+ end
113
+
114
+ def headers_for_path(extension, gzip = false)
115
+ mime = ext_to_mime[extension] || 'application/octet-stream'
116
+ headers = {
117
+ 'Content-Type' => mime,
118
+ 'Cache-Control' => "max-age=#{10.years.to_i}",
119
+ 'Expires' => 1.year.from_now.utc.to_s
120
+ }
121
+ headers['Content-Encoding'] = 'gzip' if gzip
122
+
123
+ headers
124
+ end
125
+
126
+ def ext_to_mime
127
+ @ext_to_mime ||= Hash[ *( YAML::load_file(File.join(File.dirname(__FILE__), "mime_types.yml")).collect { |k,vv| vv.collect{ |v| [v,k] } }.flatten ) ]
128
+ end
129
+
130
+ def bucket
131
+ @bucket ||= begin
132
+ bucket = s3.bucket(CloudfrontAssetHost.bucket)
133
+ bucket.disable_logging unless CloudfrontAssetHost.s3_logging
134
+ bucket
135
+ end
136
+ end
137
+
138
+ def s3
139
+ @s3 ||= RightAws::S3.new(config['access_key_id'], config['secret_access_key'])
140
+ end
141
+
142
+ def config
143
+ @config ||= YAML::load_file(CloudfrontAssetHost.s3_config)
144
+ end
145
+
146
+ end
147
+
148
+ end
149
+ 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,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: foresth-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-07-27 00:00:00 +02: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
+ - cloudfront_asset_host.gemspec
50
+ - lib/cloudfront_asset_host.rb
51
+ - lib/cloudfront_asset_host/asset_tag_helper_ext.rb
52
+ - lib/cloudfront_asset_host/css_rewriter.rb
53
+ - lib/cloudfront_asset_host/mime_types.yml
54
+ - lib/cloudfront_asset_host/uploader.rb
55
+ - test/app/config/s3.yml
56
+ - test/app/public/images/image.png
57
+ - test/app/public/javascripts/application.js
58
+ - test/app/public/stylesheets/style.css
59
+ - test/cloudfront_asset_host_test.rb
60
+ - test/css_rewriter_test.rb
61
+ - test/test_helper.rb
62
+ - test/uploader_test.rb
63
+ has_rdoc: true
64
+ homepage: http://github.com/foresth/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