cloud_crooner 0.0.1

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,104 @@
1
+ require 'fog'
2
+ require 'rack/mime'
3
+
4
+ module CloudCrooner
5
+ class Storage
6
+
7
+ def initialize
8
+ @bucket_name = CloudCrooner.bucket_name
9
+ @prefix = CloudCrooner.prefix
10
+ @fog_options = CloudCrooner.fog_options
11
+ @manifest = CloudCrooner.manifest
12
+ end
13
+
14
+ def connection
15
+ @connection ||= Fog::Storage.new(@fog_options)
16
+ end
17
+
18
+ def bucket
19
+ @bucket ||= connection.directories.get(@bucket_name, :prefix => @prefix)
20
+ end
21
+
22
+ def local_compiled_assets
23
+ # compiled assets prepended with prefix for comparison against remote
24
+ @manifest.files.keys.map {|f| File.join(@prefix, f)}
25
+ end
26
+
27
+ def exists_on_remote?(file)
28
+ bucket.files.head(file)
29
+ end
30
+
31
+ def upload_files
32
+ files_to_upload = local_compiled_assets.reject { |f| exists_on_remote?(f) }
33
+ files_to_upload.each do |asset|
34
+ upload_file(asset)
35
+ end
36
+ end
37
+
38
+ def log(msg)
39
+ CloudCrooner.log(msg)
40
+ end
41
+
42
+ def upload_file(f)
43
+ # grabs the compiled asset from public_path
44
+ full_file_path = File.join(File.dirname(@manifest.dir), f)
45
+ one_year = 31557600
46
+ mime = Rack::Mime.mime_type(File.extname(f))
47
+ file = {
48
+ :key => f,
49
+ :public => true,
50
+ :content_type => mime,
51
+ :cache_control => "public, max-age=#{one_year}",
52
+ :expires => CGI.rfc1123_date(Time.now + one_year)
53
+ }
54
+
55
+ gzipped = "#{full_file_path}.gz"
56
+
57
+ # if a gzipped version of the file exists and is a smaller file size than the original, upload that in place of the uncompressed file
58
+ if File.exists?(gzipped)
59
+ original_size = File.size(full_file_path)
60
+ gzipped_size = File.size(gzipped)
61
+
62
+ if gzipped_size < original_size
63
+ file.merge!({
64
+ :body => File.open(gzipped),
65
+ :content_encoding => 'gzip'
66
+ })
67
+ log "Uploading #{gzipped} in place of #{f}"
68
+ else
69
+ file.merge!({
70
+ :body => File.open(full_file_path)
71
+ })
72
+ log "Gzip exists but has larger file size, uploading #{f}"
73
+ end
74
+ else
75
+ file.merge!({
76
+ :body => File.open(full_file_path)
77
+ })
78
+ log "Uploading #{f}"
79
+ end
80
+ # put in reduced redundancy option here later if desired
81
+
82
+ file = bucket.files.create( file )
83
+ end
84
+
85
+ def remote_assets
86
+ files = []
87
+ bucket.files.each { |f| files << f.key }
88
+ return files
89
+ end
90
+
91
+ def delete_remote_asset(f)
92
+ log "Deleting #{f.key} from remote"
93
+ f.destroy
94
+ end
95
+
96
+ def clean_remote
97
+ to_delete = remote_assets - local_compiled_assets
98
+ to_delete.each do |f|
99
+ delete_remote_asset(bucket.files.get(f))
100
+ end
101
+ end
102
+
103
+ end
104
+ end
@@ -0,0 +1,3 @@
1
+ module CloudCrooner
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,320 @@
1
+ require 'spec_helper'
2
+
3
+ describe CloudCrooner do
4
+ describe 'default general configuration' do
5
+
6
+ it 'creates a sprockets environment' do
7
+ expect(CloudCrooner.sprockets).to be_an_instance_of(Sprockets::Environment)
8
+ end
9
+
10
+ it 'sets a default prefix' do
11
+ expect(CloudCrooner.prefix).to eq("assets")
12
+ end
13
+
14
+ it 'sets a default public folder in dev' do
15
+ expect(CloudCrooner.public_folder).to eq("public")
16
+ end
17
+
18
+ it 'defaults to remote enabled' do
19
+ expect(CloudCrooner.remote_enabled?).to be_true
20
+ end
21
+
22
+ it "defaults to looking for assets in '/assets'" do
23
+ expect(CloudCrooner.asset_paths).to eq(%w(assets))
24
+ end
25
+
26
+ it 'checks ENV for Amazon credentials' do
27
+ ENV.stub(:[]).with("AWS_ACCESS_KEY_ID").and_return("asdf123")
28
+ ENV.stub(:[]).with("AWS_SECRET_ACCESS_KEY").and_return("secret")
29
+ ENV.stub(:has_key?).with("AWS_ACCESS_KEY_ID").and_return(true)
30
+ ENV.stub(:has_key?).with("AWS_SECRET_ACCESS_KEY").and_return(true)
31
+
32
+ expect(CloudCrooner.aws_access_key_id).to eq("asdf123")
33
+ expect(CloudCrooner.aws_secret_access_key).to eq("secret")
34
+ end # it
35
+
36
+ it "checks ENV for bucket name" do
37
+ ENV.stub(:[]).with("AWS_BUCKET_NAME").and_return("test-bucket")
38
+ ENV.stub(:has_key?).with("AWS_BUCKET_NAME").and_return(true)
39
+
40
+ expect(CloudCrooner.bucket_name).to eq("test-bucket")
41
+ end #it
42
+
43
+ it "checks ENV for region" do
44
+ ENV.stub(:[]).with("AWS_REGION").and_return("eu-west-1")
45
+ ENV.stub(:has_key?).with("AWS_REGION").and_return(true)
46
+
47
+ expect(CloudCrooner.region).to eq("eu-west-1")
48
+ end
49
+
50
+ it "errors if the ENV region is not valid" do
51
+ ENV.stub(:[]).with("AWS_REGION").and_return("shangrila")
52
+ ENV.stub(:has_key?).with("AWS_REGION").and_return(true)
53
+
54
+ expect{CloudCrooner.region}.to raise_error(CloudCrooner::FogSettingError)
55
+ end
56
+
57
+ it 'defaults to keeping 2 backups' do
58
+ expect(CloudCrooner.backups_to_keep).to eq(2)
59
+ end
60
+
61
+ end # describe
62
+
63
+ describe 'errors if missing required settings' do
64
+ it "errors if region is not assigned" do
65
+ ENV.stub(:[]).and_return(nil)
66
+ ENV.stub(:has_key?).with("AWS_REGION").and_return(false)
67
+ expect{CloudCrooner.region}.to raise_error(CloudCrooner::FogSettingError, "AWS Region must be set in ENV or in configure block")
68
+ end
69
+
70
+ it "errors if the bucket is not set" do
71
+ ENV.stub(:[]).and_return(nil)
72
+ ENV.stub(:has_key?).with("AWS_BUCKET_NAME").and_return(false)
73
+ expect{CloudCrooner.bucket_name}.to raise_error(CloudCrooner::FogSettingError, "Bucket name must be set in ENV or configure block")
74
+ end
75
+
76
+ it "errors if aws access key id is unset" do
77
+ ENV.stub(:[]).and_return(nil)
78
+ ENV.stub(:has_key?).with("AWS_ACCESS_KEY_ID").and_return(false)
79
+ expect{CloudCrooner.aws_access_key_id}.to raise_error
80
+ end
81
+
82
+ it "errors if aws secret access key is unset" do
83
+ ENV.stub(:[]).and_return(nil)
84
+ ENV.stub(:has_key?).with("AWS_SECRET_ACCESS_KEY").and_return(false)
85
+ expect{CloudCrooner.aws_secret_access_key}.to raise_error
86
+ end
87
+
88
+ end
89
+
90
+ describe 'default configuration that touches filesystem' do
91
+ # aka: these tests require temp files and constructs
92
+
93
+ it 'creates a manifest' do
94
+ within_construct do |c|
95
+ c.directory 'public/assets'
96
+
97
+ expect(CloudCrooner.manifest).to be_an_instance_of(Sprockets::Manifest)
98
+ expect(CloudCrooner.manifest.dir).to eq(File.join(c, "public/assets"))
99
+ end #construct
100
+ end # it
101
+
102
+ it 'defaults assets to compile to files under prefix' do
103
+ within_construct do |c|
104
+ asset_folder = c.directory 'assets'
105
+ c.file('assets/a.css')
106
+ c.file('assets/b.css')
107
+
108
+ expect(CloudCrooner.assets_to_compile).to eq(%w(a.css b.css))
109
+ end # construct
110
+ end # it
111
+
112
+ it 'adds the default asset path to sprockets load path' do
113
+ within_construct do |c|
114
+ asset_folder = c.directory 'assets'
115
+ c.file('assets/a.css')
116
+
117
+ expect(CloudCrooner.sprockets['a.css']).to be_an_instance_of(Sprockets::BundledAsset)
118
+ expect(CloudCrooner.sprockets['a.css'].pathname.to_s).to eq(File.join(c, 'assets', 'a.css'))
119
+
120
+ end # construct
121
+ end # it
122
+
123
+ it 'initializes sprockets-helpers in development' do
124
+ within_construct do |c|
125
+ c.file 'assets/a.css'
126
+ CloudCrooner.configure_sprockets_helpers
127
+
128
+ expect(Sprockets::Helpers.prefix).to eq('/assets')
129
+ expect(context.stylesheet_tag('a.css')).to eq(%Q(<link rel="stylesheet" href="/assets/a.css">))
130
+
131
+ end # context
132
+ end #it
133
+
134
+ it 'initizalizes sprockets-helpers in production' do
135
+ within_construct do |c|
136
+ c.file 'assets/a.css'
137
+ stub_env_vars
138
+ ENV.stub(:[]).with('RACK_ENV').and_return("production")
139
+ CloudCrooner.configure_sprockets_helpers
140
+ CloudCrooner.manifest.compile('a.css')
141
+
142
+ expect(context.asset_path('a.css')).to eq("http://my-bucket.s3.amazonaws.com/assets/#{CloudCrooner.sprockets['a.css'].digest_path}")
143
+ end # construct
144
+ end # it
145
+
146
+ end # describe
147
+
148
+ describe 'custom configuration' do
149
+
150
+ it 'accepts a custom prefix' do
151
+ within_construct do |c|
152
+ CloudCrooner.configure do |config|
153
+ config.prefix = "meow"
154
+ end
155
+ expect(CloudCrooner.prefix).to eq("meow")
156
+ expect(Sprockets::Helpers.prefix).to eq("/meow")
157
+ expect(CloudCrooner.manifest.dir).to eq(File.join(c, "public/meow"))
158
+ end #context
159
+ end #it
160
+
161
+ it 'adds specified asset paths to load path' do
162
+ within_construct do |c|
163
+ c.file 'foo/bar.css'
164
+ CloudCrooner.configure do |config|
165
+ config.asset_paths = (%w(foo assets))
166
+ end
167
+
168
+ expect(CloudCrooner.sprockets['bar.css']).to be_an_instance_of(Sprockets::BundledAsset)
169
+ end
170
+ end
171
+
172
+ it 'can disable remote asset host' do
173
+ CloudCrooner.remote_enabled = false
174
+ expect(CloudCrooner.remote_enabled?).to be_false
175
+ end
176
+
177
+ it 'initializes sprockets-helpers when remote is disabled' do
178
+ within_construct do |c|
179
+ # compile the manifest and asset in dev
180
+ c.file 'assets/a.css'
181
+ c.directory 'public/assets'
182
+ manifest = Sprockets::Manifest.new(CloudCrooner.sprockets, 'public/assets')
183
+ CloudCrooner.manifest = manifest
184
+ CloudCrooner.manifest.compile('a.css')
185
+
186
+ # reload the app & helpers in production
187
+ reload_crooner
188
+
189
+ ENV.stub(:[]).with('RACK_ENV').and_return('production')
190
+ CloudCrooner.configure do |config|
191
+ config.manifest = manifest
192
+ config.remote_enabled = false
193
+ end
194
+
195
+ expect(context.asset_path('a.css')).to eq("/assets/#{CloudCrooner.manifest.assets['a.css']}")
196
+ end
197
+ end
198
+
199
+ it 'accepts a custom manifest' do
200
+ within_construct do |c|
201
+ manifest = Sprockets::Manifest.new(CloudCrooner.sprockets, 'foo/bar')
202
+ CloudCrooner.configure do |config|
203
+ config.manifest = manifest
204
+ end
205
+
206
+ expect(CloudCrooner.manifest.dir).to eq(File.join(c,'foo/bar'))
207
+ end # construct
208
+ end # it
209
+
210
+ it 'accepts a list of assets to compile' do
211
+ within_construct do |c|
212
+ c.file 'assets/a.css'
213
+ c.file 'assets/b.css'
214
+ c.file 'assets/c.css'
215
+
216
+ CloudCrooner.assets_to_compile = %w(a.css b.css)
217
+ expect(CloudCrooner.assets_to_compile).to eq(%w(a.css b.css))
218
+ end # construct
219
+ end # it
220
+
221
+ it "allows bucket to be set in config and overwrites ENV setting" do
222
+ ENV.stub(:[]).with("AWS_BUCKET_NAME").and_return("test-bucket")
223
+ ENV.stub(:has_key?).with("AWS_BUCKET_NAME").and_return(true)
224
+ CloudCrooner.bucket_name = "foo_bucket"
225
+
226
+ expect(CloudCrooner.bucket_name).to eq("foo_bucket")
227
+ end
228
+
229
+ it "allows bucket to be set in config if none in env" do
230
+ CloudCrooner.bucket_name= "bar_bucket"
231
+
232
+ ENV.stub(:[]).with("AWS_BUCKET_NAME").and_return(nil)
233
+ ENV.stub(:has_key?).with("AWS_BUCKET_NAME").and_return(false)
234
+ expect(CloudCrooner.bucket_name).to eq("bar_bucket")
235
+ end # it
236
+
237
+ it "allows region to be set in config if none in env" do
238
+ CloudCrooner.region = "us-west-2"
239
+
240
+ expect(CloudCrooner.region).to eq("us-west-2")
241
+ end
242
+
243
+ it "allows region to be set in config and overwrites ENV setting" do
244
+ ENV.stub(:[]).with("AWS_REGION").and_return("eu-west-1")
245
+ ENV.stub(:has_key?).with("AWS_REGION").and_return(true)
246
+ CloudCrooner.region = "us-west-2"
247
+
248
+ expect(CloudCrooner.region).to eq("us-west-2")
249
+ end
250
+
251
+ it "errors if config region is not valid" do
252
+ expect{CloudCrooner.region = "el-dorado"}.to raise_error(CloudCrooner::FogSettingError)
253
+ end
254
+
255
+ it "allows aws_access_key_id to be set in config and overwrite ENV" do
256
+ ENV.stub(:[]).with("AWS_ACCESS_KEY_ID").and_return("asdf123")
257
+ ENV.stub(:has_key?).with("AWS_ACCESS_KEY_ID").and_return(true)
258
+ CloudCrooner.aws_access_key_id = "lkjh0987"
259
+
260
+ expect(CloudCrooner.aws_access_key_id).to eq("lkjh0987")
261
+ end
262
+
263
+ it "allows aws_access_key_id to be set in config if none in env" do
264
+ ENV.stub(:[]).with("AWS_ACCESS_KEY_ID").and_return(nil)
265
+ ENV.stub(:has_key?).with("AWS_ACCESS_KEY_ID").and_return(false)
266
+ CloudCrooner.aws_access_key_id = "lkjh0987"
267
+
268
+ expect(CloudCrooner.aws_access_key_id).to eq("lkjh0987")
269
+ end
270
+
271
+ it "allows aws_secret_access_key to be set in config and overwrite ENV" do
272
+ ENV.stub(:[]).with("AWS_SECRET_ACCESS_KEY").and_return("secret")
273
+ ENV.stub(:has_key?).with("AWS_SECRET_ACCESS_KEY").and_return(true)
274
+ CloudCrooner.aws_secret_access_key = "terces"
275
+
276
+ expect(CloudCrooner.aws_secret_access_key).to eq("terces")
277
+ end
278
+
279
+ it "allows secret access key to be set in config when ENV is empty" do
280
+ ENV.stub(:[]).with("AWS_SECRET_ACCESS_KEY").and_return(nil)
281
+ ENV.stub(:has_key?).with("AWS_SECRET_ACCESS_KEY").and_return(false)
282
+ CloudCrooner.aws_secret_access_key = "terces"
283
+
284
+ expect(CloudCrooner.aws_secret_access_key).to eq("terces")
285
+ end
286
+
287
+ it "sets the number of backups to keep" do
288
+ CloudCrooner.configure{|config| config.backups_to_keep= 5}
289
+
290
+ expect(CloudCrooner.backups_to_keep).to eq(5)
291
+ end
292
+
293
+ end # describe
294
+
295
+ it 'compiles assets' do
296
+ within_construct do |c|
297
+ mock_environment(c)
298
+ CloudCrooner.assets_to_compile = ['a.css', 'b.css']
299
+ (CloudCrooner.storage.local_compiled_assets).should == []
300
+ CloudCrooner.compile_sprockets_assets
301
+
302
+ expect(CloudCrooner.storage.local_compiled_assets).to eq(['assets/' + CloudCrooner.sprockets['a.css'].digest_path, 'assets/' + CloudCrooner.sprockets['b.css'].digest_path])
303
+ end # construct
304
+ end # it
305
+
306
+ it 'syncs assets to the cloud', :moo => true do
307
+ within_construct do |c|
308
+ mock_environment(c)
309
+ CloudCrooner.assets_to_compile = ['a.css', 'b.css']
310
+ mock_fog(CloudCrooner.storage)
311
+ CloudCrooner.sync
312
+
313
+ expect(local_equals_remote?(CloudCrooner.storage)).to be_true
314
+ end # construct
315
+ end # it
316
+
317
+ after(:each) do
318
+ reload_crooner
319
+ end
320
+ end #describe
@@ -0,0 +1,122 @@
1
+ require 'sprockets'
2
+ require 'sinatra/base'
3
+ require 'cloud_crooner'
4
+ require 'construct'
5
+ require 'securerandom'
6
+ require 'sprockets-helpers'
7
+
8
+ RSpec.configure do |rconf|
9
+ rconf.include Construct::Helpers
10
+
11
+ # don't pollute stdout with output during tests
12
+ original_stdout = $stdout
13
+ rconf.before(:all) do
14
+ # Redirect stderr and stdout
15
+ $stdout = File.new(File.join(File.dirname(__FILE__), 'rspec_output.txt'), 'w')
16
+ end
17
+ rconf.after(:all) do
18
+ $stdout = original_stdout
19
+ end
20
+
21
+ def reload_crooner
22
+ # need to unset the class instance variables
23
+ Object.send(:remove_const, 'CloudCrooner')
24
+ load 'cloud_crooner/cloud_crooner.rb'
25
+ load 'cloud_crooner/storage.rb'
26
+ Sprockets::Helpers.instance_variables.each do |var|
27
+ Sprockets::Helpers.instance_variable_set var, nil
28
+ end
29
+ end
30
+
31
+ # used for testing sprockets-helpers
32
+ def context(logical_path = 'application.js', pathname = nil)
33
+ pathname ||= Pathname.new(File.join('assets', logical_path)).expand_path
34
+ CloudCrooner.sprockets.context_class.new CloudCrooner.sprockets, logical_path, pathname
35
+ end
36
+
37
+
38
+ def stub_env_vars
39
+ ENV.stub(:has_key?).and_return(false)
40
+ ENV.stub(:[]).and_return(nil)
41
+
42
+ ENV.stub(:[]).with('AWS_BUCKET_NAME').and_return('my-bucket')
43
+ ENV.stub(:has_key?).with('AWS_BUCKET_NAME').and_return(true)
44
+
45
+ ENV.stub(:[]).with('AWS_REGION').and_return('eu-west-1')
46
+ ENV.stub(:has_key?).with('AWS_REGION').and_return(true)
47
+
48
+ ENV.stub(:[]).with('AWS_ACCESS_KEY_ID').and_return('asdf123')
49
+ ENV.stub(:has_key?).with('AWS_ACCESS_KEY_ID').and_return(true)
50
+
51
+ ENV.stub(:[]).with('AWS_SECRET_ACCESS_KEY').and_return('secret')
52
+ ENV.stub(:has_key?).with('AWS_SECRET_ACCESS_KEY').and_return(true)
53
+ end
54
+
55
+ def sample_assets(construct)
56
+ lambda { |c|
57
+ c.file('assets/main.js') do |f|
58
+ f << "//= require a\n"
59
+ f << "//= require b\n"
60
+ end
61
+ c.file('assets/a.js') do |f|
62
+ f << "var pi=3.14;"
63
+ end
64
+ c.file('assets/b.js') do |f|
65
+ f << "var person='John Doe';"
66
+ end
67
+
68
+ c.file('assets/main.css') do |f|
69
+ f << "/*\n"
70
+ f << "*= require a\n"
71
+ f << "*= require b\n"
72
+ f << "*/\n"
73
+ end
74
+ c.file('assets/a.css') do |f|
75
+ f << "p { color: red; }\n"
76
+ end
77
+ c.file('assets/b.css') do |f|
78
+ f << "li { color: pink; }"
79
+ end
80
+ c.file('assets/c.css') do |f|
81
+ f << "h1{color:blue;}\n"
82
+ f << "h2{color:blue;}\n"
83
+ f << "h3{color:blue;}\n"
84
+ end
85
+ }.call(construct)
86
+ end
87
+
88
+ def local_equals_remote?(storage)
89
+ # the remote files are not guaranteed to be ordered
90
+ frequency(storage.local_compiled_assets) == frequency(storage.remote_assets)
91
+ end
92
+
93
+ def frequency(arr)
94
+ # http://stackoverflow.com/questions/9095017/comparing-two-arrays-in-ruby
95
+ p = Hash.new(0)
96
+ arr.each{ |v| p[v] += 1 }
97
+ p
98
+ end
99
+
100
+ def uncompiled_assets_dir(construct)
101
+ "#{construct}" + "/assets"
102
+ end
103
+
104
+ def mock_fog(storage)
105
+ Fog.mock!
106
+ storage.connection.directories.create(
107
+ :key => storage.instance_variable_get(:@bucket_name),
108
+ :public => true
109
+ )
110
+ end
111
+
112
+ def mock_environment(c)
113
+ # requires a construct
114
+ CloudCrooner.configure do |config|
115
+ config.bucket_name = SecureRandom.hex
116
+ end
117
+ stub_env_vars
118
+ sample_assets(c)
119
+ end
120
+
121
+ end
122
+