s3_website_monadic 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (151) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.travis.yml +3 -0
  4. data/Gemfile +3 -0
  5. data/LICENSE +42 -0
  6. data/README.md +451 -0
  7. data/Rakefile +24 -0
  8. data/additional-docs/example-configurations.md +62 -0
  9. data/additional-docs/setting-up-aws-credentials.md +51 -0
  10. data/assembly.sbt +3 -0
  11. data/bin/s3_website +80 -0
  12. data/build.sbt +33 -0
  13. data/changelog.md +215 -0
  14. data/features/as-library.feature +29 -0
  15. data/features/cassettes/cucumber_tags/create-redirect.yml +384 -0
  16. data/features/cassettes/cucumber_tags/empty-bucket.yml +89 -0
  17. data/features/cassettes/cucumber_tags/new-and-changed-files.yml +303 -0
  18. data/features/cassettes/cucumber_tags/new-files-for-sydney.yml +211 -0
  19. data/features/cassettes/cucumber_tags/new-files.yml +355 -0
  20. data/features/cassettes/cucumber_tags/no-new-or-changed-files.yml +359 -0
  21. data/features/cassettes/cucumber_tags/one-file-to-delete.yml +390 -0
  22. data/features/cassettes/cucumber_tags/only-changed-files.yml +411 -0
  23. data/features/cassettes/cucumber_tags/s3-and-cloudfront-after-deleting-a-file.yml +434 -0
  24. data/features/cassettes/cucumber_tags/s3-and-cloudfront-when-updating-a-file.yml +435 -0
  25. data/features/cassettes/cucumber_tags/s3-and-cloudfront.yml +290 -0
  26. data/features/cloudfront.feature +54 -0
  27. data/features/command-line-help.feature +54 -0
  28. data/features/delete.feature +19 -0
  29. data/features/error_reporting.feature +24 -0
  30. data/features/instructions-for-new-user.feature +154 -0
  31. data/features/jekyll-support.feature +20 -0
  32. data/features/nanoc-support.feature +20 -0
  33. data/features/push.feature +115 -0
  34. data/features/redirects.feature +14 -0
  35. data/features/security.feature +15 -0
  36. data/features/step_definitions/steps.rb +86 -0
  37. data/features/support/env.rb +26 -0
  38. data/features/support/test_site_dirs/cdn-powered.blog.fi/_site/css/styles.css +3 -0
  39. data/features/support/test_site_dirs/cdn-powered.blog.fi/_site/index.html +5 -0
  40. data/features/support/test_site_dirs/cdn-powered.blog.fi/s3_website.yml +4 -0
  41. data/features/support/test_site_dirs/cdn-powered.when-deleted-a-file.blog.fi/_site/index.html +10 -0
  42. data/features/support/test_site_dirs/cdn-powered.when-deleted-a-file.blog.fi/s3_website.yml +5 -0
  43. data/features/support/test_site_dirs/cdn-powered.with-one-change.blog.fi/_site/css/styles.css +3 -0
  44. data/features/support/test_site_dirs/cdn-powered.with-one-change.blog.fi/_site/index.html +10 -0
  45. data/features/support/test_site_dirs/cdn-powered.with-one-change.blog.fi/s3_website.yml +4 -0
  46. data/features/support/test_site_dirs/create-redirects/_site/.gitkeep +0 -0
  47. data/features/support/test_site_dirs/create-redirects/s3_website.yml +6 -0
  48. data/features/support/test_site_dirs/ignored-files.com/_site/css/styles.css +4 -0
  49. data/features/support/test_site_dirs/ignored-files.com/_site/index.html +8 -0
  50. data/features/support/test_site_dirs/ignored-files.com/s3_website.yml +5 -0
  51. data/features/support/test_site_dirs/index-and-assets.blog.fi/_site/assets/picture.gif +0 -0
  52. data/features/support/test_site_dirs/index-and-assets.blog.fi/_site/css/styles.css +3 -0
  53. data/features/support/test_site_dirs/index-and-assets.blog.fi/_site/index.html +5 -0
  54. data/features/support/test_site_dirs/index-and-assets.blog.fi/s3_website.yml +3 -0
  55. data/features/support/test_site_dirs/jekyllrb.com/_site/css/styles.css +3 -0
  56. data/features/support/test_site_dirs/jekyllrb.com/_site/index.html +5 -0
  57. data/features/support/test_site_dirs/jekyllrb.com/s3_website.yml +3 -0
  58. data/features/support/test_site_dirs/my.blog-with-clean-urls.com/_site/css/styles.css +3 -0
  59. data/features/support/test_site_dirs/my.blog-with-clean-urls.com/_site/index +5 -0
  60. data/features/support/test_site_dirs/my.blog-with-clean-urls.com/s3_website.yml +3 -0
  61. data/features/support/test_site_dirs/my.blog.com/_site/css/styles.css +3 -0
  62. data/features/support/test_site_dirs/my.blog.com/_site/index.html +5 -0
  63. data/features/support/test_site_dirs/my.blog.com/s3_website.yml +3 -0
  64. data/features/support/test_site_dirs/my.sydney.blog.au/_site/css/styles.css +3 -0
  65. data/features/support/test_site_dirs/my.sydney.blog.au/_site/index.html +5 -0
  66. data/features/support/test_site_dirs/my.sydney.blog.au/s3_website.yml +4 -0
  67. data/features/support/test_site_dirs/nanoc.ws/public/output/css/styles.css +3 -0
  68. data/features/support/test_site_dirs/nanoc.ws/public/output/index.html +5 -0
  69. data/features/support/test_site_dirs/nanoc.ws/s3_website.yml +3 -0
  70. data/features/support/test_site_dirs/new-and-changed-files.com/_site/css/styles.css +4 -0
  71. data/features/support/test_site_dirs/new-and-changed-files.com/_site/index.html +8 -0
  72. data/features/support/test_site_dirs/new-and-changed-files.com/s3_website.yml +3 -0
  73. data/features/support/test_site_dirs/no-new-or-changed-files.com/_site/css/styles.css +3 -0
  74. data/features/support/test_site_dirs/no-new-or-changed-files.com/_site/index.html +5 -0
  75. data/features/support/test_site_dirs/no-new-or-changed-files.com/s3_website.yml +3 -0
  76. data/features/support/test_site_dirs/only-changed-files.com/_site/css/styles.css +3 -0
  77. data/features/support/test_site_dirs/only-changed-files.com/_site/index.html +9 -0
  78. data/features/support/test_site_dirs/only-changed-files.com/s3_website.yml +3 -0
  79. data/features/support/test_site_dirs/site-that-contains-s3-website-file.com/_site/s3_website.yml +3 -0
  80. data/features/support/test_site_dirs/site-that-contains-s3-website-file.com/s3_website.yml +3 -0
  81. data/features/support/test_site_dirs/site-with-text-doc.com/_site/file.txt +1 -0
  82. data/features/support/test_site_dirs/site.with.css-maxage.com/_site/css/styles.css +3 -0
  83. data/features/support/test_site_dirs/site.with.css-maxage.com/_site/index.html +5 -0
  84. data/features/support/test_site_dirs/site.with.css-maxage.com/s3_website.yml +5 -0
  85. data/features/support/test_site_dirs/site.with.gzipped-and-max-aged-content.com/_site/css/styles.css +3 -0
  86. data/features/support/test_site_dirs/site.with.gzipped-and-max-aged-content.com/_site/index.html +5 -0
  87. data/features/support/test_site_dirs/site.with.gzipped-and-max-aged-content.com/s3_website.yml +5 -0
  88. data/features/support/test_site_dirs/site.with.gzipped-html.com/_site/css/styles.css +3 -0
  89. data/features/support/test_site_dirs/site.with.gzipped-html.com/_site/index.html +5 -0
  90. data/features/support/test_site_dirs/site.with.gzipped-html.com/s3_website.yml +5 -0
  91. data/features/support/test_site_dirs/site.with.maxage.com/_site/css/styles.css +3 -0
  92. data/features/support/test_site_dirs/site.with.maxage.com/_site/index.html +5 -0
  93. data/features/support/test_site_dirs/site.with.maxage.com/s3_website.yml +4 -0
  94. data/features/support/test_site_dirs/unpublish-a-post.com/_site/css/styles.css +3 -0
  95. data/features/support/test_site_dirs/unpublish-a-post.com/s3_website.yml +3 -0
  96. data/features/support/vcr.rb +20 -0
  97. data/features/website-performance.feature +57 -0
  98. data/lib/cloudfront/invalidator.rb +37 -0
  99. data/lib/s3_website/config_loader.rb +55 -0
  100. data/lib/s3_website/diff_helper.rb +113 -0
  101. data/lib/s3_website/endpoint.rb +37 -0
  102. data/lib/s3_website/errors.rb +42 -0
  103. data/lib/s3_website/jekyll.rb +5 -0
  104. data/lib/s3_website/keyboard.rb +27 -0
  105. data/lib/s3_website/nanoc.rb +5 -0
  106. data/lib/s3_website/parallelism.rb +25 -0
  107. data/lib/s3_website/paths.rb +39 -0
  108. data/lib/s3_website/retry.rb +19 -0
  109. data/lib/s3_website/tasks.rb +36 -0
  110. data/lib/s3_website/upload.rb +137 -0
  111. data/lib/s3_website/uploader.rb +177 -0
  112. data/lib/s3_website.rb +34 -0
  113. data/project/assembly.sbt +1 -0
  114. data/project/build.properties +0 -0
  115. data/project/plugins.sbt +1 -0
  116. data/project/sbt-launch-0.13.2.jar +0 -0
  117. data/resources/configuration_file_template.yml +56 -0
  118. data/s3_website.gemspec +41 -0
  119. data/sbt +4 -0
  120. data/spec/lib/cloudfront/invalidator_spec.rb +60 -0
  121. data/spec/lib/config_loader_spec.rb +20 -0
  122. data/spec/lib/endpoint_spec.rb +31 -0
  123. data/spec/lib/error_spec.rb +21 -0
  124. data/spec/lib/keyboard_spec.rb +62 -0
  125. data/spec/lib/parallelism_spec.rb +81 -0
  126. data/spec/lib/paths_spec.rb +7 -0
  127. data/spec/lib/retry_spec.rb +34 -0
  128. data/spec/lib/upload_spec.rb +303 -0
  129. data/spec/lib/uploader_spec.rb +37 -0
  130. data/spec/sample_files/hyde_site/_site/.vimrc +5 -0
  131. data/spec/sample_files/hyde_site/_site/css/styles.css +3 -0
  132. data/spec/sample_files/hyde_site/_site/index.html +1 -0
  133. data/spec/sample_files/hyde_site/s3_website.yml +3 -0
  134. data/spec/sample_files/tokyo_site/_site/.vimrc +5 -0
  135. data/spec/sample_files/tokyo_site/_site/css/styles.css +3 -0
  136. data/spec/sample_files/tokyo_site/_site/index.html +1 -0
  137. data/spec/sample_files/tokyo_site/s3_website.yml +4 -0
  138. data/spec/spec_helper.rb +1 -0
  139. data/src/main/scala/s3/website/CloudFront.scala +96 -0
  140. data/src/main/scala/s3/website/Diff.scala +42 -0
  141. data/src/main/scala/s3/website/Implicits.scala +7 -0
  142. data/src/main/scala/s3/website/Push.scala +191 -0
  143. data/src/main/scala/s3/website/Ruby.scala +12 -0
  144. data/src/main/scala/s3/website/S3.scala +139 -0
  145. data/src/main/scala/s3/website/model/Config.scala +152 -0
  146. data/src/main/scala/s3/website/model/S3Endpoint.scala +22 -0
  147. data/src/main/scala/s3/website/model/Site.scala +68 -0
  148. data/src/main/scala/s3/website/model/errors.scala +11 -0
  149. data/src/main/scala/s3/website/model/push.scala +192 -0
  150. data/src/test/scala/s3/website/S3WebsiteSpec.scala +445 -0
  151. metadata +508 -0
@@ -0,0 +1,303 @@
1
+ require 'spec_helper'
2
+
3
+ describe S3Website::Upload do
4
+ describe 'uploading blacklisted files' do
5
+ let(:blacklisted_files) {
6
+ [ 's3_website.yml' ]
7
+ }
8
+ it 'should fail if the upload file is s3_website.yml' do
9
+ blacklisted_files.each do |blacklisted_file|
10
+ expect {
11
+ S3Website::Upload.new blacklisted_file, mock(), {}, mock()
12
+ }.to raise_error "May not upload #{blacklisted_file}, because it's blacklisted"
13
+ end
14
+ end
15
+
16
+ it 'should fail to upload configured blacklisted files' do
17
+ config = { 'exclude_from_upload' => 'vendor' }
18
+
19
+ expect {
20
+ S3Website::Upload.new "vendor/jquery/development.js", mock(), config, mock()
21
+ }.to raise_error "May not upload vendor/jquery/development.js, because it's blacklisted"
22
+ end
23
+
24
+ context 'the uploaded file matches a value in the exclude_from_upload setting' do
25
+ it 'should fail to upload any configured blacklisted files' do
26
+ config = { 'exclude_from_upload' => ['vendor', 'tests'] }
27
+
28
+ expect {
29
+ S3Website::Upload.new "vendor/jquery/development.js", mock(), config, mock()
30
+ }.to raise_error "May not upload vendor/jquery/development.js, because it's blacklisted"
31
+
32
+ expect {
33
+ S3Website::Upload.new "tests/spec_helper.js", mock(), config, mock()
34
+ }.to raise_error "May not upload tests/spec_helper.js, because it's blacklisted"
35
+ end
36
+
37
+ it 'supports regexes in the exclude_from_upload setting' do
38
+ config = { 'exclude_from_upload' => 'test.*' }
39
+
40
+ expect {
41
+ S3Website::Upload.new "tests/spec_helper.js", mock(), config, mock()
42
+ }.to raise_error "May not upload tests/spec_helper.js, because it's blacklisted"
43
+ end
44
+ end
45
+ end
46
+
47
+ describe 'reduced redundancy setting' do
48
+ let(:config) {
49
+ { 's3_reduced_redundancy' => true }
50
+ }
51
+
52
+ it 'allows storing a file under the Reduced Redundancy Storage' do
53
+ should_upload(
54
+ file = 'index.html',
55
+ site = 'features/support/test_site_dirs/my.blog.com/_site', config) { |s3_object|
56
+ s3_object.should_receive(:write).with(
57
+ anything(),
58
+ include(:reduced_redundancy => true)
59
+ )
60
+ }
61
+ end
62
+ end
63
+
64
+ describe 'content type resolving' do
65
+ it 'adds the content type of the uploaded CSS file into the S3 object' do
66
+ should_upload(
67
+ file = 'css/styles.css',
68
+ site = 'features/support/test_site_dirs/my.blog.com/_site') { |s3_object|
69
+ s3_object.should_receive(:write).with(
70
+ anything(),
71
+ include(:content_type => 'text/css; charset=utf-8')
72
+ )
73
+ }
74
+ end
75
+
76
+ it 'adds the content type of the uploaded HTML file into the S3 object' do
77
+ should_upload(
78
+ file = 'index.html',
79
+ site = 'features/support/test_site_dirs/my.blog.com/_site') { |s3_object|
80
+ s3_object.should_receive(:write).with(
81
+ anything(),
82
+ include(:content_type => 'text/html; charset=utf-8')
83
+ )
84
+ }
85
+ end
86
+
87
+ describe 'encoding of text documents' do
88
+ it 'should mark all text documents as utf-8' do
89
+ should_upload(
90
+ file = 'file.txt',
91
+ site = 'features/support/test_site_dirs/site-with-text-doc.com/_site') { |s3_object|
92
+ s3_object.should_receive(:write).with(
93
+ anything(),
94
+ include(:content_type => 'text/plain; charset=utf-8')
95
+ )
96
+ }
97
+ end
98
+ end
99
+
100
+ context 'the user specifies a mime-type for extensionless files' do
101
+ let(:config) {{
102
+ 'extensionless_mime_type' => "text/html"
103
+ }}
104
+
105
+ it 'adds the content type of the uploaded extensionless file into the S3 object' do
106
+ should_upload(
107
+ file = 'index',
108
+ site = 'features/support/test_site_dirs/my.blog-with-clean-urls.com/_site',
109
+ config) { |s3_object|
110
+ s3_object.should_receive(:write).with(
111
+ anything(),
112
+ include(:content_type => 'text/html; charset=utf-8')
113
+ )
114
+ }
115
+ end
116
+ end
117
+ end
118
+
119
+ describe 'gzip compression' do
120
+ let(:config){
121
+ {
122
+ 's3_reduced_redundancy' => false,
123
+ 'gzip' => true
124
+ }
125
+ }
126
+
127
+ subject{ S3Website::Upload.new("index.html", mock(), config, 'features/support/test_site_dirs/my.blog.com/_site') }
128
+
129
+ describe '#gzip?' do
130
+ it 'should be false if the config does not specify gzip' do
131
+ config.delete 'gzip'
132
+ subject.should_not be_gzip
133
+ end
134
+
135
+ it 'should be false if gzip is true but does not match a default extension' do
136
+ subject.stub(:path).and_return("index.bork")
137
+ subject.should_not be_gzip
138
+ end
139
+
140
+ it 'should be true if gzip is true and file extension matches' do
141
+ subject.should be_gzip
142
+ end
143
+
144
+ it 'should be true if gzip is true and file extension matches custom supplied' do
145
+ config['gzip'] = %w(.bork)
146
+ subject.stub(:path).and_return('index.bork')
147
+ subject.should be_gzip
148
+ end
149
+ end
150
+
151
+ describe '#gzipped_file' do
152
+ it 'should return a gzipped version of the file' do
153
+ gz = Zlib::GzipReader.new(subject.send(:gzipped_file))
154
+ gz.read.should == File.read('features/support/test_site_dirs/my.blog.com/_site/index.html')
155
+ end
156
+ end
157
+ end
158
+
159
+ describe 'gzip zopfli' do
160
+ let(:config){
161
+ {
162
+ 's3_reduced_redundancy' => false,
163
+ 'gzip' => true,
164
+ 'gzip_zopfli' => true
165
+ }
166
+ }
167
+
168
+ subject{ S3Website::Upload.new("index.html", mock(), config, 'features/support/test_site_dirs/my.blog.com/_site') }
169
+
170
+ # Zopfli should be compatible with the gzip format
171
+ describe '#gzipped_file' do
172
+ it 'should return a gzipped version of the file' do
173
+ gz = Zlib::GzipReader.new(subject.send(:gzipped_file))
174
+ gz.read.should == File.read('features/support/test_site_dirs/my.blog.com/_site/index.html')
175
+ end
176
+ end
177
+ end
178
+
179
+ describe 'cache control' do
180
+ let(:config){
181
+ {
182
+ 's3_reduced_redundancy' => false,
183
+ 'max_age' => 300
184
+ }
185
+ }
186
+
187
+ let(:subject) {
188
+ S3Website::Upload.new(
189
+ "index.html",
190
+ mock(),
191
+ config,
192
+ 'features/support/test_site_dirs/my.blog.com/_site'
193
+ )
194
+ }
195
+
196
+ describe '#cache_control?' do
197
+ it 'should be false if max_age is missing' do
198
+ config.delete 'max_age'
199
+ subject.should_not be_cache_control
200
+ end
201
+
202
+ it 'should be true if max_age is present' do
203
+ subject.should be_cache_control
204
+ end
205
+
206
+ it 'should be true if max_age is a hash' do
207
+ config['max_age'] = {'*' => 300}
208
+ subject.should be_cache_control
209
+ end
210
+ end
211
+
212
+ context 'the user specifies max-age as zero' do
213
+ let(:config) {{
214
+ 'max_age' => 0
215
+ }}
216
+
217
+ it 'includes the no-cache declaration in the cache-control metadata' do
218
+ subject.send(:upload_options)[:cache_control].should == 'no-cache, max-age=0'
219
+ end
220
+ end
221
+
222
+ describe '#max_age' do
223
+ it 'should be the universal value if one is set' do
224
+ subject.send(:max_age).should == 300
225
+ end
226
+
227
+ it 'should be the file-specific value if one is set' do
228
+ config['max_age'] = {'*index.html' => 500}
229
+ subject.send(:max_age).should == 500
230
+ end
231
+
232
+ it 'should be zero if no file-specific value hit' do
233
+ config['max_age'] = {'*.js' => 500}
234
+ subject.send(:max_age).should == 0
235
+ end
236
+
237
+ context 'overriding the more general setting with the more specific' do
238
+ let(:config){
239
+ {
240
+ 's3_reduced_redundancy' => false,
241
+ 'max_age' => {
242
+ '**' => 150,
243
+ 'assets/**' => 86400
244
+ }
245
+ }
246
+ }
247
+
248
+ it 'respects the most specific max-age selector' do
249
+ subject = S3Website::Upload.new(
250
+ 'assets/picture.gif',
251
+ mock(),
252
+ config,
253
+ 'features/support/test_site_dirs/index-and-assets.blog.fi/_site'
254
+ )
255
+ subject.send(:max_age).should == 86400
256
+ end
257
+
258
+ it 'respects the most specific max-age selector' do
259
+ subject = S3Website::Upload.new(
260
+ 'index.html',
261
+ mock(),
262
+ config,
263
+ 'features/support/test_site_dirs/index-and-assets.blog.fi/_site'
264
+ )
265
+ subject.send(:max_age).should == 150
266
+ end
267
+ end
268
+ end
269
+ end
270
+
271
+ def should_upload(file_to_upload, site_dir, config = {})
272
+ def create_verifying_s3_client(file_to_upload, &block)
273
+ def create_objects(file_to_upload, &block)
274
+ def create_html_s3_object(file_to_upload, &block)
275
+ s3_object = stub('s3_object')
276
+ yield s3_object
277
+ s3_object
278
+ end
279
+ objects = {}
280
+ objects[file_to_upload] = create_html_s3_object(file_to_upload, &block)
281
+ objects
282
+ end
283
+ def create_bucket(file_to_upload, &block)
284
+ bucket = stub('bucket')
285
+ bucket.stub(:objects => create_objects(file_to_upload, &block))
286
+ bucket
287
+ end
288
+ buckets = stub('buckets')
289
+ buckets.stub(:[] => create_bucket(file_to_upload, &block))
290
+ s3 = stub('s3')
291
+ s3.stub(:buckets => buckets)
292
+ s3
293
+ end
294
+
295
+ s3_client = create_verifying_s3_client(file_to_upload) do |s3_object|
296
+ yield s3_object
297
+ end
298
+ S3Website::Upload.new(file_to_upload,
299
+ s3_client,
300
+ config,
301
+ site_dir).perform!
302
+ end
303
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ describe S3Website::Uploader do
4
+ context '#load_all_local_files' do
5
+ let(:files) {
6
+ S3Website::Uploader.send(:load_all_local_files,
7
+ 'spec/sample_files/hyde_site/_site')
8
+ }
9
+
10
+ it 'loads regular files' do
11
+ files.should include('css/styles.css')
12
+ files.should include('index.html')
13
+ end
14
+
15
+ it 'loads also dotfiles' do
16
+ files.should include('.vimrc')
17
+ end
18
+ end
19
+
20
+ context "honoring the ignore_on_server setting" do
21
+ it "ignores files which match a regular expression" do
22
+ files_to_delete = S3Website::Uploader.build_list_of_files_to_delete(["a", "b", "ignored"], ["a"], "ignored")
23
+ files_to_delete.should eq ["b"]
24
+ end
25
+
26
+ it "let's the user specify the regexes in a list" do
27
+ files_to_delete = S3Website::Uploader.build_list_of_files_to_delete(["a", "b", "ignored"], ["a"], ["ignored"])
28
+ files_to_delete.should eq ["b"]
29
+ end
30
+
31
+
32
+ it "does not ignore when you don't provide an ignored regex" do
33
+ files_to_delete = S3Website::Uploader.build_list_of_files_to_delete(["a", "b", "ignored"], ["a"])
34
+ files_to_delete.should eq ["b", "ignored"]
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,5 @@
1
+ " Disable the arrow keys in the normal mode – using them there only slows you down. Use hkjl instead.
2
+ map <Left> :echo "no!"<cr>
3
+ map <Right> :echo "no!"<cr>
4
+ map <Up> :echo "no!"<cr>
5
+ map <Down> :echo "no!"<cr>
@@ -0,0 +1,3 @@
1
+ body {
2
+
3
+ }
@@ -0,0 +1 @@
1
+ <div>Hello world</div>
@@ -0,0 +1,3 @@
1
+ s3_id: <%= 'hello' %>
2
+ s3_secret: <%= 'world' %>
3
+ s3_bucket: galaxy
@@ -0,0 +1,5 @@
1
+ " Disable the arrow keys in the normal mode – using them there only slows you down. Use hkjl instead.
2
+ map <Left> :echo "no!"<cr>
3
+ map <Right> :echo "no!"<cr>
4
+ map <Up> :echo "no!"<cr>
5
+ map <Down> :echo "no!"<cr>
@@ -0,0 +1,3 @@
1
+ body {
2
+
3
+ }
@@ -0,0 +1 @@
1
+ <div>Hello world</div>
@@ -0,0 +1,4 @@
1
+ s3_id: <%= 'hello' %>
2
+ s3_secret: <%= 'world' %>
3
+ s3_bucket: galaxy
4
+ s3_endpoint: ap-northeast-1
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/../lib/s3_website.rb"
@@ -0,0 +1,96 @@
1
+ package s3.website
2
+
3
+ import s3.website.model.{Redirect, Config}
4
+ import com.amazonaws.services.cloudfront.{AmazonCloudFrontClient, AmazonCloudFront}
5
+ import s3.website.CloudFront.{FailedInvalidation, SuccessfulInvalidation, CloudFrontClientProvider}
6
+ import scala.util.{Failure, Success, Try}
7
+ import com.amazonaws.services.cloudfront.model.{TooManyInvalidationsInProgressException, Paths, InvalidationBatch, CreateInvalidationRequest}
8
+ import scala.collection.JavaConversions._
9
+ import scala.concurrent.duration._
10
+ import s3.website.S3.{SuccessfulUpload, PushSuccessReport}
11
+ import com.amazonaws.auth.BasicAWSCredentials
12
+
13
+ class CloudFront(implicit cfClient: CloudFrontClientProvider, sleepUnit: TimeUnit) {
14
+
15
+ def invalidate(invalidationBatch: InvalidationBatch, distributionId: String)(implicit config: Config): InvalidationResult = {
16
+ def tryInvalidate(implicit attempt: Int = 1): Try[SuccessfulInvalidation] =
17
+ Try {
18
+ val invalidationReq = new CreateInvalidationRequest(distributionId, invalidationBatch)
19
+ cfClient(config).createInvalidation(invalidationReq)
20
+ val result = SuccessfulInvalidation(invalidationBatch.getPaths.getItems.size())
21
+ println(s"Invalidated ${result.invalidatedItemsCount} item(s) on the CloudFront distribution $distributionId.")
22
+ result
23
+ } recoverWith {
24
+ case e: TooManyInvalidationsInProgressException =>
25
+ implicit val duration: Duration = Duration(
26
+ (fibs drop attempt).head min 15, /* AWS docs way that invalidations complete in 15 minutes */
27
+ sleepUnit
28
+ )
29
+ println(maxInvalidationsExceededInfo)
30
+ Thread.sleep(duration.toMillis)
31
+ tryInvalidate(attempt + 1)
32
+ }
33
+
34
+ tryInvalidate() match {
35
+ case Success(res) =>
36
+ Right(res)
37
+ case Failure(err) =>
38
+ println(s"Failed to invalidate the CloudFront distribution $distributionId (${err.getMessage})")
39
+ Left(FailedInvalidation())
40
+ }
41
+ }
42
+
43
+ def maxInvalidationsExceededInfo(implicit sleepDuration: Duration, attempt: Int) = {
44
+ val basicInfo = s"The maximum amount of CloudFront invalidations has exceeded. Trying again in $sleepDuration, please wait."
45
+ val extendedInfo =
46
+ s"""|$basicInfo
47
+ | For more information, see http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html#InvalidationLimits"""
48
+ .stripMargin
49
+ if (attempt == 1)
50
+ extendedInfo
51
+ else
52
+ basicInfo
53
+ }
54
+
55
+ type InvalidationResult = Either[FailedInvalidation, SuccessfulInvalidation]
56
+
57
+ lazy val fibs: Stream[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map { n => n._1 + n._2 }
58
+ }
59
+
60
+ object CloudFront {
61
+
62
+ type CloudFrontClientProvider = (Config) => AmazonCloudFront
63
+
64
+ case class SuccessfulInvalidation(invalidatedItemsCount: Int)
65
+
66
+ case class FailedInvalidation()
67
+
68
+ def awsCloudFrontClient(config: Config) =
69
+ new AmazonCloudFrontClient(new BasicAWSCredentials(config.s3_id, config.s3_secret))
70
+
71
+ def toInvalidationBatches(pushSuccessReports: Seq[PushSuccessReport])(implicit config: Config): Seq[InvalidationBatch] =
72
+ pushSuccessReports
73
+ .filterNot(isRedirect) // Assume that redirect objects are never cached.
74
+ .map("/" + _.s3Key) // CloudFront keys always have the slash in front
75
+ .map { path =>
76
+ if (config.cloudfront_invalidate_root.exists(_ == true))
77
+ path.replaceFirst("/index.html$", "/")
78
+ else
79
+ path
80
+ }
81
+ .grouped(1000) // CloudFront supports max 1000 invalidations in one request (http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html#InvalidationLimits)
82
+ .map { batchKeys =>
83
+ new InvalidationBatch() withPaths
84
+ (new Paths() withItems batchKeys withQuantity batchKeys.size) withCallerReference
85
+ s"s3_website gem ${System.currentTimeMillis()}"
86
+ }
87
+ .toSeq
88
+
89
+ def isRedirect: PartialFunction[PushSuccessReport, Boolean] = {
90
+ case SuccessfulUpload(upload) => upload.uploadType match {
91
+ case Redirect => true
92
+ case _ => false
93
+ }
94
+ case _ => false
95
+ }
96
+ }
@@ -0,0 +1,42 @@
1
+ package s3.website
2
+
3
+ import s3.website.model._
4
+ import s3.website.Ruby.rubyRegexMatches
5
+
6
+ object Diff {
7
+
8
+ def resolveDeletes(localFiles: Seq[LocalFile], s3Files: Seq[S3File], redirects: Seq[Upload with UploadTypeResolved])(implicit config: Config): Seq[S3File] = {
9
+ val keysNotToBeDeleted: Set[String] = (localFiles ++ redirects).map(_.s3Key).toSet
10
+ s3Files.filterNot { s3File =>
11
+ val ignoreOnServer = config.ignore_on_server.exists(_.fold(
12
+ (ignoreRegex: String) => rubyRegexMatches(s3File.s3Key, ignoreRegex),
13
+ (ignoreRegexes: Seq[String]) => ignoreRegexes.exists(rubyRegexMatches(s3File.s3Key, _))
14
+ ))
15
+ keysNotToBeDeleted.exists(_ == s3File.s3Key) || ignoreOnServer
16
+ }
17
+ }
18
+
19
+ def resolveUploads(localFiles: Seq[LocalFile], s3Files: Seq[S3File])(implicit config: Config):
20
+ Stream[Either[Error, Upload with UploadTypeResolved]] = {
21
+ val remoteS3KeysIndex = s3Files.map(_.s3Key).toSet
22
+ val remoteMd5Index = s3Files.map(_.md5).toSet
23
+ localFiles
24
+ .toStream // Load lazily, because the MD5 computation for the local file requires us to read the whole file
25
+ .map(resolveUploadSource)
26
+ .collect {
27
+ case errorOrUpload if errorOrUpload.right.exists(isNewUpload(remoteS3KeysIndex)) =>
28
+ for (upload <- errorOrUpload.right) yield upload withUploadType NewFile
29
+ case errorOrUpload if errorOrUpload.right.exists(isUpdate(remoteS3KeysIndex, remoteMd5Index)) =>
30
+ for (upload <- errorOrUpload.right) yield upload withUploadType Update
31
+ }
32
+ }
33
+
34
+ def isNewUpload(remoteS3KeysIndex: Set[String])(u: Upload) = !remoteS3KeysIndex.exists(_ == u.s3Key)
35
+
36
+ def isUpdate(remoteS3KeysIndex: Set[String], remoteMd5Index: Set[String])(u: Upload) =
37
+ remoteS3KeysIndex.exists(_ == u.s3Key) && !remoteMd5Index.exists(remoteMd5 => u.essence.right.exists(_.md5 == remoteMd5))
38
+
39
+ def resolveUploadSource(localFile: LocalFile)(implicit config: Config): Either[Error, Upload] =
40
+ for (upload <- LocalFile.toUpload(localFile).right)
41
+ yield upload
42
+ }
@@ -0,0 +1,7 @@
1
+ package s3.website
2
+
3
+ import s3.website.model.{Site, Config}
4
+
5
+ object Implicits {
6
+ implicit def site2Config(implicit site: Site): Config = site.config
7
+ }