sq-asset_sync 2.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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +42 -0
  5. data/Appraisals +15 -0
  6. data/CHANGELOG.md +738 -0
  7. data/Gemfile +7 -0
  8. data/README.md +466 -0
  9. data/Rakefile +44 -0
  10. data/asset_sync.gemspec +38 -0
  11. data/docs/heroku.md +36 -0
  12. data/gemfiles/rails_3.1.gemfile +10 -0
  13. data/gemfiles/rails_3.2.gemfile +10 -0
  14. data/gemfiles/rails_4.0.gemfile +10 -0
  15. data/gemfiles/rails_4.1.gemfile +10 -0
  16. data/kochiku.yml +7 -0
  17. data/lib/asset_sync.rb +15 -0
  18. data/lib/asset_sync/asset_sync.rb +73 -0
  19. data/lib/asset_sync/config.rb +226 -0
  20. data/lib/asset_sync/engine.rb +53 -0
  21. data/lib/asset_sync/multi_mime.rb +16 -0
  22. data/lib/asset_sync/railtie.rb +5 -0
  23. data/lib/asset_sync/storage.rb +291 -0
  24. data/lib/asset_sync/version.rb +3 -0
  25. data/lib/generators/asset_sync/install_generator.rb +67 -0
  26. data/lib/generators/asset_sync/templates/asset_sync.rb +41 -0
  27. data/lib/generators/asset_sync/templates/asset_sync.yml +43 -0
  28. data/lib/tasks/asset_sync.rake +30 -0
  29. data/script/ci +31 -0
  30. data/spec/dummy_app/Rakefile +30 -0
  31. data/spec/dummy_app/app/assets/javascripts/application.js +1 -0
  32. data/spec/fixtures/aws_with_yml/config/asset_sync.yml +25 -0
  33. data/spec/fixtures/google_with_yml/config/asset_sync.yml +19 -0
  34. data/spec/fixtures/rackspace_with_yml/config/asset_sync.yml +20 -0
  35. data/spec/fixtures/with_invalid_yml/config/asset_sync.yml +24 -0
  36. data/spec/integration/aws_integration_spec.rb +77 -0
  37. data/spec/spec_helper.rb +64 -0
  38. data/spec/unit/asset_sync_spec.rb +257 -0
  39. data/spec/unit/google_spec.rb +142 -0
  40. data/spec/unit/multi_mime_spec.rb +48 -0
  41. data/spec/unit/rackspace_spec.rb +90 -0
  42. data/spec/unit/railsless_spec.rb +72 -0
  43. data/spec/unit/storage_spec.rb +244 -0
  44. metadata +248 -0
@@ -0,0 +1,53 @@
1
+ module AssetSync
2
+ class Engine < Rails::Engine
3
+
4
+ engine_name "asset_sync"
5
+
6
+ initializer "asset_sync config", :group => :all do |app|
7
+ app_initializer = Rails.root.join('config', 'initializers', 'asset_sync.rb').to_s
8
+ app_yaml = Rails.root.join('config', 'asset_sync.yml').to_s
9
+
10
+ if File.exist?( app_initializer )
11
+ AssetSync.log "AssetSync: using #{app_initializer}"
12
+ load app_initializer
13
+ elsif !File.exist?( app_initializer ) && !File.exist?( app_yaml )
14
+ AssetSync.log "AssetSync: using default configuration from built-in initializer"
15
+ AssetSync.configure do |config|
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
+
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')
23
+
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')
26
+
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')
31
+
32
+ config.existing_remote_files = ENV['ASSET_SYNC_EXISTING_REMOTE_FILES'] || "keep"
33
+
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')
36
+ end
37
+
38
+ config.prefix = ENV['ASSET_SYNC_PREFIX'] if ENV.has_key?('ASSET_SYNC_PREFIX')
39
+
40
+ config.existing_remote_files = ENV['ASSET_SYNC_EXISTING_REMOTE_FILES'] || "keep"
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')
44
+
45
+ end
46
+
47
+ if File.exist?( app_yaml )
48
+ AssetSync.log "AssetSync: YAML file found #{app_yaml} settings will be merged into the configuration"
49
+ end
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,16 @@
1
+ require 'mime/types'
2
+
3
+ module AssetSync
4
+ class MultiMime
5
+
6
+ def self.lookup(ext)
7
+ if defined?(Mime::Type)
8
+ Mime::Type.lookup_by_extension(ext)
9
+ elsif defined?(Rack::Mime)
10
+ ext_with_dot = ".#{ext}"
11
+ Rack::Mime.mime_type(ext_with_dot)
12
+ end || ::MIME::Types.type_for(ext).first
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ class Rails::Railtie::Configuration
2
+ def asset_sync
3
+ AssetSync.config
4
+ end
5
+ end
@@ -0,0 +1,291 @@
1
+ module AssetSync
2
+ class Storage
3
+ REGEXP_FINGERPRINTED_FILES = /^(.*)\/([^-]+)-[^\.]+\.([^\.]+)$/
4
+
5
+ class BucketNotFound < StandardError;
6
+ end
7
+
8
+ attr_accessor :config
9
+
10
+ def initialize(cfg)
11
+ @config = cfg
12
+ end
13
+
14
+ def connection
15
+ @connection ||= Fog::Storage.new(self.config.fog_options)
16
+ end
17
+
18
+ def bucket
19
+ # fixes: https://github.com/rumblelabs/asset_sync/issues/18
20
+ @bucket ||= connection.directories.get(self.config.fog_directory, :prefix => self.config.assets_prefix)
21
+ end
22
+
23
+ def log(msg)
24
+ AssetSync.log(msg)
25
+ end
26
+
27
+ def keep_existing_remote_files?
28
+ self.config.existing_remote_files?
29
+ end
30
+
31
+ def path
32
+ self.config.public_path
33
+ end
34
+
35
+ def ignored_files
36
+ files = []
37
+ Array(self.config.ignored_files).each do |ignore|
38
+ case ignore
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."
49
+ end
50
+ end
51
+ files.uniq
52
+ end
53
+
54
+ def local_files
55
+ @local_files ||= get_local_files.uniq
56
+ end
57
+
58
+ def always_upload_files
59
+ self.config.always_upload.map { |f| File.join(self.config.assets_prefix, f) }
60
+ end
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
+
70
+ def get_local_files
71
+ if self.config.manifest
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.exist?(self.config.manifest_path)
77
+ log "Using: Manifest #{self.config.manifest_path}"
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!
88
+ else
89
+ log "Warning: Manifest could not be found"
90
+ end
91
+ end
92
+ log "Using: Directory Search of #{path}/#{self.config.assets_prefix}"
93
+ Dir.chdir(path) do
94
+ to_load = self.config.assets_prefix.present? ? "#{self.config.assets_prefix}/**/**" : '**/**'
95
+ Dir[to_load]
96
+ end
97
+ end
98
+
99
+ def get_remote_files
100
+ raise BucketNotFound.new("#{self.config.fog_provider} Bucket: #{self.config.fog_directory} not found.") unless bucket
101
+ # fixes: https://github.com/rumblelabs/asset_sync/issues/16
102
+ # (work-around for https://github.com/fog/fog/issues/596)
103
+ files = []
104
+ bucket.files.each { |f| files << f.key }
105
+ return files
106
+ end
107
+
108
+ def delete_file(f, remote_files_to_delete)
109
+ if remote_files_to_delete.include?(f.key)
110
+ log "Deleting: #{f.key}"
111
+ f.destroy
112
+ end
113
+ end
114
+
115
+ def delete_extra_remote_files
116
+ log "Fetching files to flag for delete"
117
+ remote_files = get_remote_files
118
+ # fixes: https://github.com/rumblelabs/asset_sync/issues/19
119
+ from_remote_files_to_delete = remote_files - local_files - ignored_files - always_upload_files
120
+
121
+ log "Flagging #{from_remote_files_to_delete.size} file(s) for deletion"
122
+ # Delete unneeded remote files
123
+ bucket.files.each do |f|
124
+ delete_file(f, from_remote_files_to_delete)
125
+ end
126
+ end
127
+
128
+ def upload_file(f)
129
+ # TODO output files in debug logs as asset filename only.
130
+ one_year = 31557600
131
+ ext = File.extname(f)[1..-1]
132
+ mime = MultiMime.lookup(ext)
133
+ file = {
134
+ :key => f,
135
+ :body => File.open("#{path}/#{f}"),
136
+ :public => true,
137
+ :content_type => mime
138
+ }
139
+
140
+ uncompressed_filename = f.sub(/\.gz\z/, '')
141
+ basename = File.basename(uncompressed_filename, File.extname(uncompressed_filename))
142
+ if /-[0-9a-fA-F]{32,}$/.match(basename)
143
+ file.merge!({
144
+ :cache_control => "public, max-age=#{one_year}",
145
+ :expires => CGI.rfc1123_date(Time.now + one_year)
146
+ })
147
+ end
148
+
149
+ # overwrite headers if applicable, you probably shouldn't specific key/body, but cache-control headers etc.
150
+
151
+ if files_with_custom_headers.has_key? f
152
+ file.merge! files_with_custom_headers[f]
153
+ log "Overwriting #{f} with custom headers #{files_with_custom_headers[f].to_s}"
154
+ elsif key = self.config.custom_headers.keys.detect {|k| f.match(Regexp.new(k))}
155
+ headers = {}
156
+ self.config.custom_headers[key].each do |k, value|
157
+ headers[k.to_sym] = value
158
+ end
159
+ file.merge! headers
160
+ log "Overwriting matching file #{f} with custom headers #{headers.to_s}"
161
+ end
162
+
163
+
164
+ gzipped = "#{path}/#{f}.gz"
165
+ ignore = false
166
+
167
+ if config.gzip? && File.extname(f) == ".gz"
168
+ # Don't bother uploading gzipped assets if we are in gzip_compression mode
169
+ # as we will overwrite file.css with file.css.gz if it exists.
170
+ log "Ignoring: #{f}"
171
+ ignore = true
172
+ elsif config.gzip? && File.exist?(gzipped)
173
+ original_size = File.size("#{path}/#{f}")
174
+ gzipped_size = File.size(gzipped)
175
+
176
+ if gzipped_size < original_size
177
+ percentage = ((gzipped_size.to_f/original_size.to_f)*100).round(2)
178
+ file.merge!({
179
+ :key => f,
180
+ :body => File.open(gzipped),
181
+ :content_encoding => 'gzip'
182
+ })
183
+ log "Uploading: #{gzipped} in place of #{f} saving #{percentage}%"
184
+ else
185
+ percentage = ((original_size.to_f/gzipped_size.to_f)*100).round(2)
186
+ log "Uploading: #{f} instead of #{gzipped} (compression increases this file by #{percentage}%)"
187
+ end
188
+ else
189
+ if !config.gzip? && File.extname(f) == ".gz"
190
+ # set content encoding for gzipped files this allows cloudfront to properly handle requests with Accept-Encoding
191
+ # http://docs.amazonwebservices.com/AmazonCloudFront/latest/DeveloperGuide/ServingCompressedFiles.html
192
+ uncompressed_filename = f[0..-4]
193
+ ext = File.extname(uncompressed_filename)[1..-1]
194
+ mime = MultiMime.lookup(ext)
195
+ file.merge!({
196
+ :content_type => mime,
197
+ :content_encoding => 'gzip'
198
+ })
199
+ end
200
+ log "Uploading: #{f}"
201
+ end
202
+
203
+ if config.aws? && config.aws_rrs?
204
+ file.merge!({
205
+ :storage_class => 'REDUCED_REDUNDANCY'
206
+ })
207
+ end
208
+
209
+ file = bucket.files.create( file ) unless ignore
210
+ end
211
+
212
+ def skip_uploading?(manifest_file)
213
+ unless manifest_file
214
+ log "Failed to find manifest file"
215
+ return false
216
+ end
217
+
218
+ log "Found manifest file #{manifest_file}"
219
+
220
+ begin
221
+ s3_manifest = connection.head_object(self.config.fog_directory, manifest_file)
222
+
223
+ # If the manifest is within 24 hours old and exists on the server, we're going to assume
224
+ # we can skip uploading the assets.
225
+ created_at = Time.parse(s3_manifest.headers['Last-Modified']).utc
226
+ log "S3 Manifest was created on #{created_at} (#{Time.now.utc})"
227
+ return true if (Time.now.utc - created_at) <= 86400
228
+
229
+ rescue => ex
230
+ log "Got #{ex.class}, #{ex.message} while trying to HEAD the manifest from S3"
231
+ return false
232
+ end
233
+
234
+ false
235
+ end
236
+
237
+ def upload_files
238
+ manifest_file = self.config.manifest_digest_path
239
+ if skip_uploading?(manifest_file)
240
+ log "Skipping file upload"
241
+ return
242
+ end
243
+
244
+ # get a fresh list of remote files
245
+ remote_files = ignore_existing_remote_files? ? [] : get_remote_files
246
+ # fixes: https://github.com/rumblelabs/asset_sync/issues/19
247
+ local_files_to_upload = local_files - ignored_files - remote_files + always_upload_files
248
+ local_files_to_upload = (local_files_to_upload + get_non_fingerprinted(local_files_to_upload)).uniq
249
+
250
+ local_files_to_upload.delete manifest_file if manifest_file
251
+
252
+ # Upload new files
253
+ local_files_to_upload.each do |f|
254
+ next unless File.file? "#{path}/#{f}" # Only files.
255
+ upload_file f
256
+ end
257
+
258
+ # At the very end, upload the manifest indicating we're done
259
+ upload_file manifest_file if manifest_file
260
+
261
+ if self.config.cdn_distribution_id && files_to_invalidate.any?
262
+ log "Invalidating Files"
263
+ cdn ||= Fog::CDN.new(self.config.fog_options.except(:region))
264
+ data = cdn.post_invalidation(self.config.cdn_distribution_id, files_to_invalidate)
265
+ log "Invalidation id: #{data.body["Id"]}"
266
+ end
267
+ end
268
+
269
+ def sync
270
+ # fixes: https://github.com/rumblelabs/asset_sync/issues/19
271
+ log "AssetSync: Syncing."
272
+ upload_files
273
+ delete_extra_remote_files unless keep_existing_remote_files?
274
+ log "AssetSync: Done."
275
+ end
276
+
277
+ private
278
+
279
+ def ignore_existing_remote_files?
280
+ self.config.existing_remote_files == 'ignore'
281
+ end
282
+
283
+ def get_non_fingerprinted(files)
284
+ files.map do |file|
285
+ match_data = file.match(REGEXP_FINGERPRINTED_FILES)
286
+ match_data && "#{match_data[1]}/#{match_data[2]}.#{match_data[3]}"
287
+ end.compact
288
+ end
289
+
290
+ end
291
+ end
@@ -0,0 +1,3 @@
1
+ module AssetSync
2
+ VERSION = "2.0.0"
3
+ end
@@ -0,0 +1,67 @@
1
+ require 'rails/generators'
2
+ module AssetSync
3
+ class InstallGenerator < Rails::Generators::Base
4
+ desc "Install a config/asset_sync.yml and the asset:precompile rake task enhancer"
5
+
6
+ # Commandline options can be defined here using Thor-like options:
7
+ class_option :use_yml, :type => :boolean, :default => false, :desc => "Use YML file instead of Rails Initializer"
8
+ class_option :provider, :type => :string, :default => "AWS", :desc => "Generate with support for 'AWS', 'Rackspace', or 'Google'"
9
+
10
+ def self.source_root
11
+ @source_root ||= File.join(File.dirname(__FILE__), 'templates')
12
+ end
13
+
14
+ def aws?
15
+ options[:provider] == 'AWS'
16
+ end
17
+
18
+ def google?
19
+ options[:provider] == 'Google'
20
+ end
21
+
22
+ def rackspace?
23
+ options[:provider] == 'Rackspace'
24
+ end
25
+
26
+ def aws_access_key_id
27
+ "<%= ENV['AWS_ACCESS_KEY_ID'] %>"
28
+ end
29
+
30
+ def aws_secret_access_key
31
+ "<%= ENV['AWS_SECRET_ACCESS_KEY'] %>"
32
+ end
33
+
34
+ def google_storage_access_key_id
35
+ "<%= ENV['GOOGLE_STORAGE_ACCESS_KEY_ID'] %>"
36
+ end
37
+
38
+ def google_storage_secret_access_key
39
+ "<%= ENV['GOOGLE_STORAGE_SECRET_ACCESS_KEY'] %>"
40
+ end
41
+
42
+ def rackspace_username
43
+ "<%= ENV['RACKSPACE_USERNAME'] %>"
44
+ end
45
+
46
+ def rackspace_api_key
47
+ "<%= ENV['RACKSPACE_API_KEY'] %>"
48
+ end
49
+
50
+ def app_name
51
+ @app_name ||= Rails.application.is_a?(Rails::Application) && Rails.application.class.name.sub(/::Application$/, "").downcase
52
+ end
53
+
54
+ def generate_config
55
+ if options[:use_yml]
56
+ template "asset_sync.yml", "config/asset_sync.yml"
57
+ end
58
+ end
59
+
60
+ def generate_initializer
61
+ unless options[:use_yml]
62
+ template "asset_sync.rb", "config/initializers/asset_sync.rb"
63
+ end
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,41 @@
1
+ AssetSync.configure do |config|
2
+ <%- if aws? -%>
3
+ config.fog_provider = 'AWS'
4
+ config.aws_access_key_id = ENV['AWS_ACCESS_KEY_ID']
5
+ config.aws_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
6
+ # To use AWS reduced redundancy storage.
7
+ # config.aws_reduced_redundancy = true
8
+ <%- elsif google? -%>
9
+ config.fog_provider = 'Google'
10
+ config.google_storage_access_key_id = ENV['GOOGLE_STORAGE_ACCESS_KEY_ID']
11
+ config.google_storage_secret_access_key = ENV['GOOGLE_STORAGE_SECRET_ACCESS_KEY']
12
+ <%- elsif rackspace? -%>
13
+ config.fog_provider = 'Rackspace'
14
+ config.rackspace_username = ENV['RACKSPACE_USERNAME']
15
+ config.rackspace_api_key = ENV['RACKSPACE_API_KEY']
16
+
17
+ # if you need to change rackspace_auth_url (e.g. if you need to use Rackspace London)
18
+ # config.rackspace_auth_url = "lon.auth.api.rackspacecloud.com"
19
+ <%- end -%>
20
+ config.fog_directory = ENV['FOG_DIRECTORY']
21
+
22
+ # Invalidate a file on a cdn after uploading files
23
+ # config.cdn_distribution_id = "12345"
24
+ # config.invalidate = ['file1.js']
25
+
26
+ # Increase upload performance by configuring your region
27
+ # config.fog_region = 'eu-west-1'
28
+ #
29
+ # Don't delete files from the store
30
+ # config.existing_remote_files = "keep"
31
+ #
32
+ # Automatically replace files with their equivalent gzip compressed version
33
+ # config.gzip_compression = true
34
+ #
35
+ # Use the Rails generated 'manifest.yml' file to produce the list of files to
36
+ # upload instead of searching the assets directory.
37
+ # config.manifest = true
38
+ #
39
+ # Fail silently. Useful for environments such as Heroku
40
+ # config.fail_silently = true
41
+ end