asset_sync 0.5.4 → 1.0.0

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.
@@ -13,33 +13,34 @@ module AssetSync
13
13
  elsif !File.exists?( app_initializer ) && !File.exists?( app_yaml )
14
14
  AssetSync.log "AssetSync: using default configuration from built-in initializer"
15
15
  AssetSync.configure do |config|
16
- config.fog_provider = ENV['FOG_PROVIDER']
17
- config.fog_directory = ENV['FOG_DIRECTORY']
18
- config.fog_region = ENV['FOG_REGION']
16
+ config.fog_provider = ENV['FOG_PROVIDER'] if ENV.has_key?('FOG_PROVIDER')
17
+ config.fog_directory = ENV['FOG_DIRECTORY'] if ENV.has_key?('FOG_DIRECTORY')
18
+ config.fog_region = ENV['FOG_REGION'] if ENV.has_key?('FOG_REGION')
19
19
 
20
- config.aws_access_key_id = ENV['AWS_ACCESS_KEY_ID']
21
- config.aws_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
20
+ config.aws_access_key_id = ENV['AWS_ACCESS_KEY_ID'] if ENV.has_key?('AWS_ACCESS_KEY_ID')
21
+ config.aws_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY'] if ENV.has_key?('AWS_SECRET_ACCESS_KEY')
22
+ config.aws_reduced_redundancy = ENV['AWS_REDUCED_REDUNDANCY'] == true if ENV.has_key?('AWS_REDUCED_REDUNDANCY')
22
23
 
23
- config.rackspace_username = ENV['RACKSPACE_USERNAME']
24
- config.rackspace_api_key = ENV['RACKSPACE_API_KEY']
24
+ config.rackspace_username = ENV['RACKSPACE_USERNAME'] if ENV.has_key?('RACKSPACE_USERNAME')
25
+ config.rackspace_api_key = ENV['RACKSPACE_API_KEY'] if ENV.has_key?('RACKSPACE_API_KEY')
25
26
 
26
- config.google_storage_access_key_id = ENV['GOOGLE_STORAGE_ACCESS_KEY_ID']
27
- config.google_storage_secret_access_key = ENV['GOOGLE_STORAGE_SECRET_ACCESS_KEY']
27
+ config.google_storage_access_key_id = ENV['GOOGLE_STORAGE_ACCESS_KEY_ID'] if ENV.has_key?('GOOGLE_STORAGE_ACCESS_KEY_ID')
28
+ config.google_storage_secret_access_key = ENV['GOOGLE_STORAGE_SECRET_ACCESS_KEY'] if ENV.has_key?('GOOGLE_STORAGE_SECRET_ACCESS_KEY')
29
+
30
+ config.enabled = (ENV['ASSET_SYNC_ENABLED'] == 'true') if ENV.has_key?('ASSET_SYNC_ENABLED')
28
31
 
29
- if ENV.has_key? 'ASSET_SYNC_ENABLED'
30
- config.enabled = ENV['ASSET_SYNC_ENABLED'] == 'true'
31
- end
32
32
  config.existing_remote_files = ENV['ASSET_SYNC_EXISTING_REMOTE_FILES'] || "keep"
33
- config.gzip_compression = ENV['ASSET_SYNC_GZIP_COMPRESSION'] == 'true'
34
- config.manifest = ENV['ASSET_SYNC_MANIFEST'] == 'true'
35
- end
36
33
 
37
- if ENV.has_key? 'ASSET_SYNC_PREFIX'
38
- config.prefix = ENV['ASSET_SYNC_PREFIX']
34
+ config.gzip_compression = (ENV['ASSET_SYNC_GZIP_COMPRESSION'] == 'true') if ENV.has_key?('ASSET_SYNC_GZIP_COMPRESSION')
35
+ config.manifest = (ENV['ASSET_SYNC_MANIFEST'] == 'true') if ENV.has_key?('ASSET_SYNC_MANIFEST')
39
36
  end
37
+
38
+ config.prefix = ENV['ASSET_SYNC_PREFIX'] if ENV.has_key?('ASSET_SYNC_PREFIX')
39
+
40
40
  config.existing_remote_files = ENV['ASSET_SYNC_EXISTING_REMOTE_FILES'] || "keep"
41
- config.gzip_compression = ENV['ASSET_SYNC_GZIP_COMPRESSION'] == 'true'
42
- config.manifest = ENV['ASSET_SYNC_MANIFEST'] == 'true'
41
+
42
+ config.gzip_compression = (ENV['ASSET_SYNC_GZIP_COMPRESSION'] == 'true') if ENV.has_key?('ASSET_SYNC_GZIP_COMPRESSION')
43
+ config.manifest = (ENV['ASSET_SYNC_MANIFEST'] == 'true') if ENV.has_key?('ASSET_SYNC_MANIFEST')
43
44
 
44
45
  end
45
46
 
@@ -1,7 +1,9 @@
1
1
  module AssetSync
2
2
  class Storage
3
+ REGEXP_FINGERPRINTED_FILES = /^(.*)\/([^-]+)-[^\.]+\.([^\.]+)$/
3
4
 
4
- class BucketNotFound < StandardError; end
5
+ class BucketNotFound < StandardError;
6
+ end
5
7
 
6
8
  attr_accessor :config
7
9
 
@@ -34,37 +36,57 @@ module AssetSync
34
36
  files = []
35
37
  Array(self.config.ignored_files).each do |ignore|
36
38
  case ignore
37
- when Regexp
38
- files += self.local_files.select do |file|
39
- file =~ ignore
40
- end
41
- when String
42
- files += self.local_files.select do |file|
43
- file.split('/').last == ignore
44
- end
45
- else
46
- log "Error: please define ignored_files as string or regular expression. #{ignore} (#{ignore.class}) ignored."
39
+ when Regexp
40
+ files += self.local_files.select do |file|
41
+ file =~ ignore
42
+ end
43
+ when String
44
+ files += self.local_files.select do |file|
45
+ file.split('/').last == ignore
46
+ end
47
+ else
48
+ log "Error: please define ignored_files as string or regular expression. #{ignore} (#{ignore.class}) ignored."
47
49
  end
48
50
  end
49
51
  files.uniq
50
52
  end
51
53
 
52
54
  def local_files
53
- @local_files ||= get_local_files
55
+ @local_files ||= get_local_files.uniq
54
56
  end
55
57
 
56
58
  def always_upload_files
57
59
  self.config.always_upload.map { |f| File.join(self.config.assets_prefix, f) }
58
60
  end
59
61
 
62
+ def files_with_custom_headers
63
+ self.config.custom_headers.inject({}) { |h,(k, v)| h[File.join(self.config.assets_prefix, k)] = v; h; }
64
+ end
65
+
66
+ def files_to_invalidate
67
+ self.config.invalidate.map { |filename| File.join("/", self.config.assets_prefix, filename) }
68
+ end
69
+
60
70
  def get_local_files
61
71
  if self.config.manifest
62
- if File.exists?(self.config.manifest_path)
63
- yml = YAML.load(IO.read(self.config.manifest_path))
72
+ if ActionView::Base.respond_to?(:assets_manifest)
73
+ log "Using: Rails 4.0 manifest access"
74
+ manifest = Sprockets::Manifest.new(ActionView::Base.assets_manifest.environment, ActionView::Base.assets_manifest.dir)
75
+ return manifest.assets.values.map { |f| File.join(self.config.assets_prefix, f) }
76
+ elsif File.exists?(self.config.manifest_path)
64
77
  log "Using: Manifest #{self.config.manifest_path}"
65
- return yml.values.map { |f| File.join(self.config.assets_prefix, f) }
78
+ yml = YAML.load(IO.read(self.config.manifest_path))
79
+
80
+ return yml.map do |original, compiled|
81
+ # Upload font originals and compiled
82
+ if original =~ /^.+(eot|svg|ttf|woff)$/
83
+ [original, compiled]
84
+ else
85
+ compiled
86
+ end
87
+ end.flatten.map { |f| File.join(self.config.assets_prefix, f) }.uniq!
66
88
  else
67
- log "Warning: manifest.yml not found at #{self.config.manifest_path}"
89
+ log "Warning: Manifest could not be found"
68
90
  end
69
91
  end
70
92
  log "Using: Directory Search of #{path}/#{self.config.assets_prefix}"
@@ -93,7 +115,7 @@ module AssetSync
93
115
  log "Fetching files to flag for delete"
94
116
  remote_files = get_remote_files
95
117
  # fixes: https://github.com/rumblelabs/asset_sync/issues/19
96
- from_remote_files_to_delete = remote_files - local_files - ignored_files
118
+ from_remote_files_to_delete = remote_files - local_files - ignored_files - always_upload_files
97
119
 
98
120
  log "Flagging #{from_remote_files_to_delete.size} file(s) for deletion"
99
121
  # Delete unneeded remote files
@@ -111,11 +133,31 @@ module AssetSync
111
133
  :key => f,
112
134
  :body => File.open("#{path}/#{f}"),
113
135
  :public => true,
114
- :cache_control => "public, max-age=#{one_year}",
115
- :expires => CGI.rfc1123_date(Time.now + one_year),
116
136
  :content_type => mime
117
137
  }
118
138
 
139
+ if /-[0-9a-fA-F]{32}$/.match(File.basename(f,File.extname(f)))
140
+ file.merge!({
141
+ :cache_control => "public, max-age=#{one_year}",
142
+ :expires => CGI.rfc1123_date(Time.now + one_year)
143
+ })
144
+ end
145
+
146
+ # overwrite headers if applicable, you probably shouldn't specific key/body, but cache-control headers etc.
147
+
148
+ if files_with_custom_headers.has_key? f
149
+ file.merge! files_with_custom_headers[f]
150
+ log "Overwriting #{f} with custom headers #{files_with_custom_headers[f].to_s}"
151
+ elsif key = self.config.custom_headers.keys.detect {|k| f.match(Regexp.new(k))}
152
+ headers = {}
153
+ self.config.custom_headers[key].each do |key, value|
154
+ headers[key.to_sym] = value
155
+ end
156
+ file.merge! headers
157
+ log "Overwriting matching file #{f} with custom headers #{headers.to_s}"
158
+ end
159
+
160
+
119
161
  gzipped = "#{path}/#{f}.gz"
120
162
  ignore = false
121
163
 
@@ -126,15 +168,15 @@ module AssetSync
126
168
  ignore = true
127
169
  elsif config.gzip? && File.exists?(gzipped)
128
170
  original_size = File.size("#{path}/#{f}")
129
- gzipped_size = File.size(gzipped)
171
+ gzipped_size = File.size(gzipped)
130
172
 
131
173
  if gzipped_size < original_size
132
174
  percentage = ((gzipped_size.to_f/original_size.to_f)*100).round(2)
133
175
  file.merge!({
134
- :key => f,
135
- :body => File.open(gzipped),
136
- :content_encoding => 'gzip'
137
- })
176
+ :key => f,
177
+ :body => File.open(gzipped),
178
+ :content_encoding => 'gzip'
179
+ })
138
180
  log "Uploading: #{gzipped} in place of #{f} saving #{percentage}%"
139
181
  else
140
182
  percentage = ((original_size.to_f/gzipped_size.to_f)*100).round(2)
@@ -155,6 +197,12 @@ module AssetSync
155
197
  log "Uploading: #{f}"
156
198
  end
157
199
 
200
+ if config.aws? && config.aws_rrs?
201
+ file.merge!({
202
+ :storage_class => 'REDUCED_REDUNDANCY'
203
+ })
204
+ end
205
+
158
206
  file = bucket.files.create( file ) unless ignore
159
207
  end
160
208
 
@@ -163,12 +211,20 @@ module AssetSync
163
211
  remote_files = ignore_existing_remote_files? ? [] : get_remote_files
164
212
  # fixes: https://github.com/rumblelabs/asset_sync/issues/19
165
213
  local_files_to_upload = local_files - ignored_files - remote_files + always_upload_files
214
+ local_files_to_upload = (local_files_to_upload + get_non_fingerprinted(local_files_to_upload)).uniq
166
215
 
167
216
  # Upload new files
168
217
  local_files_to_upload.each do |f|
169
218
  next unless File.file? "#{path}/#{f}" # Only files.
170
219
  upload_file f
171
220
  end
221
+
222
+ if self.config.cdn_distribution_id && files_to_invalidate.any?
223
+ log "Invalidating Files"
224
+ cdn ||= Fog::CDN.new(self.config.fog_options.except(:region))
225
+ data = cdn.post_invalidation(self.config.cdn_distribution_id, files_to_invalidate)
226
+ log "Invalidation id: #{data.body["Id"]}"
227
+ end
172
228
  end
173
229
 
174
230
  def sync
@@ -184,5 +240,13 @@ module AssetSync
184
240
  def ignore_existing_remote_files?
185
241
  self.config.existing_remote_files == 'ignore'
186
242
  end
243
+
244
+ def get_non_fingerprinted(files)
245
+ files.map do |file|
246
+ match_data = file.match(REGEXP_FINGERPRINTED_FILES)
247
+ match_data && "#{match_data[1]}/#{match_data[2]}.#{match_data[3]}"
248
+ end.compact
249
+ end
250
+
187
251
  end
188
252
  end
@@ -1,3 +1,3 @@
1
1
  module AssetSync
2
- VERSION = "0.5.4"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -3,6 +3,8 @@ AssetSync.configure do |config|
3
3
  config.fog_provider = 'AWS'
4
4
  config.aws_access_key_id = ENV['AWS_ACCESS_KEY_ID']
5
5
  config.aws_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
6
+ # To use AWS reduced redundancy storage.
7
+ # config.aws_reduced_redundancy = true
6
8
  <%- elsif google? -%>
7
9
  config.fog_provider = 'Google'
8
10
  config.google_storage_access_key_id = ENV['GOOGLE_STORAGE_ACCESS_KEY_ID']
@@ -16,7 +18,11 @@ AssetSync.configure do |config|
16
18
  # config.rackspace_auth_url = "lon.auth.api.rackspacecloud.com"
17
19
  <%- end -%>
18
20
  config.fog_directory = ENV['FOG_DIRECTORY']
19
-
21
+
22
+ # Invalidate a file on a cdn after uploading files
23
+ # config.cdn_distribution_id = "12345"
24
+ # config.invalidate = ['file1.js']
25
+
20
26
  # Increase upload performance by configuring your region
21
27
  # config.fog_region = 'eu-west-1'
22
28
  #
@@ -26,7 +32,7 @@ AssetSync.configure do |config|
26
32
  # Automatically replace files with their equivalent gzip compressed version
27
33
  # config.gzip_compression = true
28
34
  #
29
- # Use the Rails generated 'manifest.yml' file to produce the list of files to
35
+ # Use the Rails generated 'manifest.yml' file to produce the list of files to
30
36
  # upload instead of searching the assets directory.
31
37
  # config.manifest = true
32
38
  #
@@ -3,6 +3,8 @@ defaults: &defaults
3
3
  fog_provider: 'AWS'
4
4
  aws_access_key_id: "<%= aws_access_key_id %>"
5
5
  aws_secret_access_key: "<%= aws_secret_access_key %>"
6
+ # To use AWS reduced redundancy storage.
7
+ # aws_reduced_redundancy: true
6
8
  <%- elsif google? -%>
7
9
  fog_provider: 'Google'
8
10
  google_storage_access_key_id: "<%= google_storage_access_key_id %>"
@@ -12,14 +14,14 @@ defaults: &defaults
12
14
  rackspace_username: "<%= rackspace_username %>"
13
15
  rackspace_api_key: "<%= rackspace_api_key %>"
14
16
  # if you need to change rackspace_auth_url (e.g. if you need to use Rackspace London)
15
- # rackspace_auth_url: "lon.auth.api.rackspacecloud.com"
17
+ # rackspace_auth_url: "https://lon.identity.api.rackspacecloud.com/v2.0"
16
18
  <%- end -%>
17
19
  fog_directory: "<%= app_name %>-assets"
18
20
  # You may need to specify what region your storage bucket is in
19
21
  # fog_region: "eu-west-1"
20
22
  existing_remote_files: keep
21
23
  # To delete existing remote files.
22
- # existing_remote_files: delete
24
+ # existing_remote_files: delete
23
25
  # Automatically replace files with their equivalent gzip compressed version
24
26
  # gzip_compression: true
25
27
  # Fail silently. Useful for environments such as Heroku
@@ -1,6 +1,23 @@
1
+ namespace :assets do
2
+
3
+ desc 'Synchronize assets to remote (assumes assets are already compiled)'
4
+ task :sync => :environment do
5
+ AssetSync.sync
6
+ end
7
+ namespace :sync do
8
+ desc 'Delete out-of-sync files on remote'
9
+ task :clean => :environment do
10
+ AssetSync.clean
11
+ end
12
+ end
13
+
14
+ end
15
+
1
16
  if Rake::Task.task_defined?("assets:precompile:nondigest")
2
17
  Rake::Task["assets:precompile:nondigest"].enhance do
3
- AssetSync.sync
18
+ # Conditional execution needs to be inside the enhance block because the enhance block
19
+ # will get executed before yaml or Rails initializers.
20
+ Rake::Task["assets:sync"].invoke if defined?(AssetSync) && AssetSync.config.run_on_precompile
4
21
  end
5
22
  else
6
23
  Rake::Task["assets:precompile"].enhance do
@@ -8,6 +25,6 @@ else
8
25
  # RAILS_GROUP and RAILS_ENV are not defined. We need to reload the
9
26
  # assets environment in this case.
10
27
  # Rake::Task["assets:environment"].invoke if Rake::Task.task_defined?("assets:environment")
11
- AssetSync.sync
28
+ Rake::Task["assets:sync"].invoke if defined?(AssetSync) && AssetSync.config.run_on_precompile
12
29
  end
13
30
  end
@@ -14,6 +14,7 @@ module AssetSyncTest
14
14
  class Application < Rails::Application
15
15
  config.encoding = "utf-8"
16
16
  config.filter_parameters += [:password]
17
+ config.eager_load = false
17
18
  config.assets.enabled = true
18
19
  config.assets.version = '1.0'
19
20
  config.secret_token = 'bf196b4383deefa4e0120a6ef1d9af1cc45f5c4ebd04405'
@@ -26,4 +27,4 @@ module AssetSyncTest
26
27
  end
27
28
  AssetSyncTest::Application.initialize!
28
29
  AssetSyncTest::Application.load_tasks
29
- # Rake::Task['assets:precompile:all'].invoke
30
+ # Rake::Task['assets:precompile:all'].invoke
@@ -3,6 +3,7 @@ defaults: &defaults
3
3
  aws_access_key_id: "xxxx"
4
4
  aws_secret_access_key: "zzzz"
5
5
  region: "eu-west-1"
6
+ run_on_precompile: false
6
7
 
7
8
  development:
8
9
  <<: *defaults
@@ -0,0 +1,24 @@
1
+ defaults: &defaults
2
+ fog_provider= "AWS"
3
+ aws_access_key_id: "xxxx"
4
+ aws_secret_access_key: "zzzz"
5
+ region: "eu-west-1"
6
+
7
+ development:
8
+ <<: *defaults
9
+ fog_directory: "rails_app_development"
10
+ existing_remote_files: keep
11
+
12
+ test:
13
+ <<: *defaults
14
+ fog_directory: "rails_app_test"
15
+ existing_remote_files: keep
16
+
17
+ production:
18
+ <<: *defaults
19
+ fog_directory: "rails_app_production"
20
+ existing_remote_files: delete
21
+
22
+ hybrid:
23
+ <<: *defaults
24
+ enabled: false
@@ -23,6 +23,17 @@ describe "AssetSync" do
23
23
  @prefix = SecureRandom.hex(6)
24
24
  end
25
25
 
26
+ let(:app_js_regex){
27
+ /#{@prefix}\/application-[a-zA-Z0-9]*.js$/
28
+ }
29
+
30
+ let(:app_js_gz_regex){
31
+ /#{@prefix}\/application-[a-zA-Z0-9]*.js.gz$/
32
+ }
33
+
34
+ let(:files){ bucket(@prefix).files }
35
+
36
+
26
37
  after(:each) do
27
38
  @directory = bucket(@prefix)
28
39
  @directory.files.each do |f|
@@ -32,12 +43,17 @@ describe "AssetSync" do
32
43
 
33
44
  it "sync" do
34
45
  execute "rake ASSET_SYNC_PREFIX=#{@prefix} assets:precompile"
35
- bucket(@prefix).files.size.should == 5
46
+ # bucket(@prefix).files.size.should == 5
47
+
48
+ files = bucket(@prefix).files
49
+
50
+ app_js_path = files.select{ |f| f.key =~ app_js_regex }.first
51
+ app_js_gz_path = files.select{ |f| f.key =~ app_js_gz_regex }.first
36
52
 
37
- app_js = bucket(@prefix).files.get("#{@prefix}/application.js")
53
+ app_js = files.get( app_js_path.key )
38
54
  app_js.content_type.should == "text/javascript"
39
55
 
40
- app_js_gz = bucket(@prefix).files.get("#{@prefix}/application.js.gz")
56
+ app_js_gz = files.get( app_js_gz_path.key )
41
57
  app_js_gz.content_type.should == "text/javascript"
42
58
  app_js_gz.content_encoding.should == "gzip"
43
59
  end
@@ -49,9 +65,10 @@ describe "AssetSync" do
49
65
 
50
66
  it "sync with gzip_compression=true" do
51
67
  execute "rake ASSET_SYNC_PREFIX=#{@prefix} ASSET_SYNC_GZIP_COMPRESSION=true assets:precompile"
52
- bucket(@prefix).files.size.should == 3
68
+ # bucket(@prefix).files.size.should == 3
53
69
 
54
- app_js = bucket(@prefix).files.get("#{@prefix}/application.js")
70
+ app_js_path = files.select{ |f| f.key =~ app_js_regex }.first
71
+ app_js = files.get( app_js_path.key )
55
72
  app_js.content_type.should == "text/javascript"
56
73
  end
57
74
 
@@ -1,11 +1,13 @@
1
1
  require 'rubygems'
2
2
  require 'bundler'
3
3
 
4
- if RUBY_VERSION != '1.8.7'
4
+ begin
5
5
  require 'simplecov'
6
6
  SimpleCov.start do
7
7
  add_filter 'spec'
8
8
  end
9
+ rescue LoadError
10
+ # SimpleCov ain't available - continue
9
11
  end
10
12
 
11
13
  begin
@@ -30,7 +32,7 @@ shared_context "mock without Rails" do
30
32
  if defined? Rails
31
33
  Object.send(:remove_const, :Rails)
32
34
  end
33
- AssetSync.stub!(:log)
35
+ AssetSync.stub(:log)
34
36
  end
35
37
  end
36
38
 
@@ -38,14 +40,14 @@ end
38
40
  shared_context "mock Rails" do
39
41
  before(:each) do
40
42
  unless defined? Rails
41
- Rails = mock 'Rails'
43
+ Rails = double 'Rails'
42
44
  end
43
45
  Rails.stub(:env).and_return('test')
44
- Rails.stub :application => mock('application')
45
- Rails.application.stub :config => mock('config')
46
+ Rails.stub :application => double('application')
47
+ Rails.application.stub :config => double('config')
46
48
  Rails.application.config.stub :assets => ActiveSupport::OrderedOptions.new
47
49
  Rails.application.config.assets.prefix = '/assets'
48
- AssetSync.stub!(:log)
50
+ AssetSync.stub(:log)
49
51
  end
50
52
  end
51
53