asset_sync 0.5.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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