foresth-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.
@@ -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