s3_website_monadic 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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.travis.yml +3 -0
- data/Gemfile +3 -0
- data/LICENSE +42 -0
- data/README.md +451 -0
- data/Rakefile +24 -0
- data/additional-docs/example-configurations.md +62 -0
- data/additional-docs/setting-up-aws-credentials.md +51 -0
- data/assembly.sbt +3 -0
- data/bin/s3_website +80 -0
- data/build.sbt +33 -0
- data/changelog.md +215 -0
- data/features/as-library.feature +29 -0
- data/features/cassettes/cucumber_tags/create-redirect.yml +384 -0
- data/features/cassettes/cucumber_tags/empty-bucket.yml +89 -0
- data/features/cassettes/cucumber_tags/new-and-changed-files.yml +303 -0
- data/features/cassettes/cucumber_tags/new-files-for-sydney.yml +211 -0
- data/features/cassettes/cucumber_tags/new-files.yml +355 -0
- data/features/cassettes/cucumber_tags/no-new-or-changed-files.yml +359 -0
- data/features/cassettes/cucumber_tags/one-file-to-delete.yml +390 -0
- data/features/cassettes/cucumber_tags/only-changed-files.yml +411 -0
- data/features/cassettes/cucumber_tags/s3-and-cloudfront-after-deleting-a-file.yml +434 -0
- data/features/cassettes/cucumber_tags/s3-and-cloudfront-when-updating-a-file.yml +435 -0
- data/features/cassettes/cucumber_tags/s3-and-cloudfront.yml +290 -0
- data/features/cloudfront.feature +54 -0
- data/features/command-line-help.feature +54 -0
- data/features/delete.feature +19 -0
- data/features/error_reporting.feature +24 -0
- data/features/instructions-for-new-user.feature +154 -0
- data/features/jekyll-support.feature +20 -0
- data/features/nanoc-support.feature +20 -0
- data/features/push.feature +115 -0
- data/features/redirects.feature +14 -0
- data/features/security.feature +15 -0
- data/features/step_definitions/steps.rb +86 -0
- data/features/support/env.rb +26 -0
- data/features/support/test_site_dirs/cdn-powered.blog.fi/_site/css/styles.css +3 -0
- data/features/support/test_site_dirs/cdn-powered.blog.fi/_site/index.html +5 -0
- data/features/support/test_site_dirs/cdn-powered.blog.fi/s3_website.yml +4 -0
- data/features/support/test_site_dirs/cdn-powered.when-deleted-a-file.blog.fi/_site/index.html +10 -0
- data/features/support/test_site_dirs/cdn-powered.when-deleted-a-file.blog.fi/s3_website.yml +5 -0
- data/features/support/test_site_dirs/cdn-powered.with-one-change.blog.fi/_site/css/styles.css +3 -0
- data/features/support/test_site_dirs/cdn-powered.with-one-change.blog.fi/_site/index.html +10 -0
- data/features/support/test_site_dirs/cdn-powered.with-one-change.blog.fi/s3_website.yml +4 -0
- data/features/support/test_site_dirs/create-redirects/_site/.gitkeep +0 -0
- data/features/support/test_site_dirs/create-redirects/s3_website.yml +6 -0
- data/features/support/test_site_dirs/ignored-files.com/_site/css/styles.css +4 -0
- data/features/support/test_site_dirs/ignored-files.com/_site/index.html +8 -0
- data/features/support/test_site_dirs/ignored-files.com/s3_website.yml +5 -0
- data/features/support/test_site_dirs/index-and-assets.blog.fi/_site/assets/picture.gif +0 -0
- data/features/support/test_site_dirs/index-and-assets.blog.fi/_site/css/styles.css +3 -0
- data/features/support/test_site_dirs/index-and-assets.blog.fi/_site/index.html +5 -0
- data/features/support/test_site_dirs/index-and-assets.blog.fi/s3_website.yml +3 -0
- data/features/support/test_site_dirs/jekyllrb.com/_site/css/styles.css +3 -0
- data/features/support/test_site_dirs/jekyllrb.com/_site/index.html +5 -0
- data/features/support/test_site_dirs/jekyllrb.com/s3_website.yml +3 -0
- data/features/support/test_site_dirs/my.blog-with-clean-urls.com/_site/css/styles.css +3 -0
- data/features/support/test_site_dirs/my.blog-with-clean-urls.com/_site/index +5 -0
- data/features/support/test_site_dirs/my.blog-with-clean-urls.com/s3_website.yml +3 -0
- data/features/support/test_site_dirs/my.blog.com/_site/css/styles.css +3 -0
- data/features/support/test_site_dirs/my.blog.com/_site/index.html +5 -0
- data/features/support/test_site_dirs/my.blog.com/s3_website.yml +3 -0
- data/features/support/test_site_dirs/my.sydney.blog.au/_site/css/styles.css +3 -0
- data/features/support/test_site_dirs/my.sydney.blog.au/_site/index.html +5 -0
- data/features/support/test_site_dirs/my.sydney.blog.au/s3_website.yml +4 -0
- data/features/support/test_site_dirs/nanoc.ws/public/output/css/styles.css +3 -0
- data/features/support/test_site_dirs/nanoc.ws/public/output/index.html +5 -0
- data/features/support/test_site_dirs/nanoc.ws/s3_website.yml +3 -0
- data/features/support/test_site_dirs/new-and-changed-files.com/_site/css/styles.css +4 -0
- data/features/support/test_site_dirs/new-and-changed-files.com/_site/index.html +8 -0
- data/features/support/test_site_dirs/new-and-changed-files.com/s3_website.yml +3 -0
- data/features/support/test_site_dirs/no-new-or-changed-files.com/_site/css/styles.css +3 -0
- data/features/support/test_site_dirs/no-new-or-changed-files.com/_site/index.html +5 -0
- data/features/support/test_site_dirs/no-new-or-changed-files.com/s3_website.yml +3 -0
- data/features/support/test_site_dirs/only-changed-files.com/_site/css/styles.css +3 -0
- data/features/support/test_site_dirs/only-changed-files.com/_site/index.html +9 -0
- data/features/support/test_site_dirs/only-changed-files.com/s3_website.yml +3 -0
- data/features/support/test_site_dirs/site-that-contains-s3-website-file.com/_site/s3_website.yml +3 -0
- data/features/support/test_site_dirs/site-that-contains-s3-website-file.com/s3_website.yml +3 -0
- data/features/support/test_site_dirs/site-with-text-doc.com/_site/file.txt +1 -0
- data/features/support/test_site_dirs/site.with.css-maxage.com/_site/css/styles.css +3 -0
- data/features/support/test_site_dirs/site.with.css-maxage.com/_site/index.html +5 -0
- data/features/support/test_site_dirs/site.with.css-maxage.com/s3_website.yml +5 -0
- data/features/support/test_site_dirs/site.with.gzipped-and-max-aged-content.com/_site/css/styles.css +3 -0
- data/features/support/test_site_dirs/site.with.gzipped-and-max-aged-content.com/_site/index.html +5 -0
- data/features/support/test_site_dirs/site.with.gzipped-and-max-aged-content.com/s3_website.yml +5 -0
- data/features/support/test_site_dirs/site.with.gzipped-html.com/_site/css/styles.css +3 -0
- data/features/support/test_site_dirs/site.with.gzipped-html.com/_site/index.html +5 -0
- data/features/support/test_site_dirs/site.with.gzipped-html.com/s3_website.yml +5 -0
- data/features/support/test_site_dirs/site.with.maxage.com/_site/css/styles.css +3 -0
- data/features/support/test_site_dirs/site.with.maxage.com/_site/index.html +5 -0
- data/features/support/test_site_dirs/site.with.maxage.com/s3_website.yml +4 -0
- data/features/support/test_site_dirs/unpublish-a-post.com/_site/css/styles.css +3 -0
- data/features/support/test_site_dirs/unpublish-a-post.com/s3_website.yml +3 -0
- data/features/support/vcr.rb +20 -0
- data/features/website-performance.feature +57 -0
- data/lib/cloudfront/invalidator.rb +37 -0
- data/lib/s3_website/config_loader.rb +55 -0
- data/lib/s3_website/diff_helper.rb +113 -0
- data/lib/s3_website/endpoint.rb +37 -0
- data/lib/s3_website/errors.rb +42 -0
- data/lib/s3_website/jekyll.rb +5 -0
- data/lib/s3_website/keyboard.rb +27 -0
- data/lib/s3_website/nanoc.rb +5 -0
- data/lib/s3_website/parallelism.rb +25 -0
- data/lib/s3_website/paths.rb +39 -0
- data/lib/s3_website/retry.rb +19 -0
- data/lib/s3_website/tasks.rb +36 -0
- data/lib/s3_website/upload.rb +137 -0
- data/lib/s3_website/uploader.rb +177 -0
- data/lib/s3_website.rb +34 -0
- data/project/assembly.sbt +1 -0
- data/project/build.properties +0 -0
- data/project/plugins.sbt +1 -0
- data/project/sbt-launch-0.13.2.jar +0 -0
- data/resources/configuration_file_template.yml +56 -0
- data/s3_website.gemspec +41 -0
- data/sbt +4 -0
- data/spec/lib/cloudfront/invalidator_spec.rb +60 -0
- data/spec/lib/config_loader_spec.rb +20 -0
- data/spec/lib/endpoint_spec.rb +31 -0
- data/spec/lib/error_spec.rb +21 -0
- data/spec/lib/keyboard_spec.rb +62 -0
- data/spec/lib/parallelism_spec.rb +81 -0
- data/spec/lib/paths_spec.rb +7 -0
- data/spec/lib/retry_spec.rb +34 -0
- data/spec/lib/upload_spec.rb +303 -0
- data/spec/lib/uploader_spec.rb +37 -0
- data/spec/sample_files/hyde_site/_site/.vimrc +5 -0
- data/spec/sample_files/hyde_site/_site/css/styles.css +3 -0
- data/spec/sample_files/hyde_site/_site/index.html +1 -0
- data/spec/sample_files/hyde_site/s3_website.yml +3 -0
- data/spec/sample_files/tokyo_site/_site/.vimrc +5 -0
- data/spec/sample_files/tokyo_site/_site/css/styles.css +3 -0
- data/spec/sample_files/tokyo_site/_site/index.html +1 -0
- data/spec/sample_files/tokyo_site/s3_website.yml +4 -0
- data/spec/spec_helper.rb +1 -0
- data/src/main/scala/s3/website/CloudFront.scala +96 -0
- data/src/main/scala/s3/website/Diff.scala +42 -0
- data/src/main/scala/s3/website/Implicits.scala +7 -0
- data/src/main/scala/s3/website/Push.scala +191 -0
- data/src/main/scala/s3/website/Ruby.scala +12 -0
- data/src/main/scala/s3/website/S3.scala +139 -0
- data/src/main/scala/s3/website/model/Config.scala +152 -0
- data/src/main/scala/s3/website/model/S3Endpoint.scala +22 -0
- data/src/main/scala/s3/website/model/Site.scala +68 -0
- data/src/main/scala/s3/website/model/errors.scala +11 -0
- data/src/main/scala/s3/website/model/push.scala +192 -0
- data/src/test/scala/s3/website/S3WebsiteSpec.scala +445 -0
- 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 @@
|
|
|
1
|
+
<div>Hello world</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div>Hello world</div>
|
data/spec/spec_helper.rb
ADDED
|
@@ -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
|
+
}
|