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,57 @@
|
|
|
1
|
+
Feature: improve response times of your S3 website website
|
|
2
|
+
|
|
3
|
+
As a blogger
|
|
4
|
+
I want to benefit from HTTP performance optimisations
|
|
5
|
+
So that my readers would not have to wait long for my website to load
|
|
6
|
+
|
|
7
|
+
@new-files
|
|
8
|
+
Scenario: Set Cache-Control: max-age for all uploaded files
|
|
9
|
+
When my S3 website is in "features/support/test_site_dirs/site.with.maxage.com"
|
|
10
|
+
And I call the push command
|
|
11
|
+
Then the output should contain
|
|
12
|
+
"""
|
|
13
|
+
Upload css/styles.css [max-age=120]: Success!
|
|
14
|
+
"""
|
|
15
|
+
And the output should contain
|
|
16
|
+
"""
|
|
17
|
+
Upload index.html [max-age=120]: Success!
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@new-files
|
|
21
|
+
Scenario: Set Cache-Control: max-age for CSS files only
|
|
22
|
+
When my S3 website is in "features/support/test_site_dirs/site.with.css-maxage.com"
|
|
23
|
+
And I call the push command
|
|
24
|
+
Then the output should contain
|
|
25
|
+
"""
|
|
26
|
+
Upload css/styles.css [max-age=100]: Success!
|
|
27
|
+
"""
|
|
28
|
+
And the output should contain
|
|
29
|
+
"""
|
|
30
|
+
Upload index.html [max-age=0]: Success!
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
@new-files
|
|
34
|
+
Scenario: Set Content-Encoding: gzip HTTP header for HTML files
|
|
35
|
+
When my S3 website is in "features/support/test_site_dirs/site.with.gzipped-html.com"
|
|
36
|
+
And I call the push command
|
|
37
|
+
Then the output should contain
|
|
38
|
+
"""
|
|
39
|
+
Upload css/styles.css: Success!
|
|
40
|
+
"""
|
|
41
|
+
And the output should contain
|
|
42
|
+
"""
|
|
43
|
+
Upload index.html [gzipped]: Success!
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@new-files
|
|
47
|
+
Scenario: Set both the Content-Encoding: gzip and Cache-Control: max-age headers
|
|
48
|
+
When my S3 website is in "features/support/test_site_dirs/site.with.gzipped-and-max-aged-content.com"
|
|
49
|
+
And I call the push command
|
|
50
|
+
Then the output should contain
|
|
51
|
+
"""
|
|
52
|
+
Upload css/styles.css [gzipped] [max-age=300]: Success!
|
|
53
|
+
"""
|
|
54
|
+
And the output should contain
|
|
55
|
+
"""
|
|
56
|
+
Upload index.html [gzipped] [max-age=300]: Success!
|
|
57
|
+
"""
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module S3Website
|
|
2
|
+
module Cloudfront
|
|
3
|
+
class Invalidator
|
|
4
|
+
def self.invalidate(config, changed_files)
|
|
5
|
+
aws_key = config['s3_id']
|
|
6
|
+
aws_secret = config['s3_secret']
|
|
7
|
+
cloudfront_distribution_id = config['cloudfront_distribution_id']
|
|
8
|
+
s3_object_keys = apply_config config, changed_files
|
|
9
|
+
s3_object_keys << ""
|
|
10
|
+
report = SimpleCloudfrontInvalidator::CloudfrontClient.new(
|
|
11
|
+
aws_key, aws_secret, cloudfront_distribution_id
|
|
12
|
+
).invalidate(url_encode_keys s3_object_keys)
|
|
13
|
+
puts report[:text_report]
|
|
14
|
+
report[:invalidated_items_count]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def self.url_encode_keys(keys)
|
|
20
|
+
require 'uri'
|
|
21
|
+
keys.map do |key|
|
|
22
|
+
URI::encode(key, Regexp.union([URI::Parser.new.regexp[:UNSAFE],'~','@', "'"]))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.apply_config(config, changed_files)
|
|
27
|
+
if config['cloudfront_invalidate_root']
|
|
28
|
+
changed_files.map { |changed_file|
|
|
29
|
+
changed_file.sub /\/index.html$/, '/'
|
|
30
|
+
}
|
|
31
|
+
else
|
|
32
|
+
changed_files
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module S3Website
|
|
2
|
+
class ConfigLoader
|
|
3
|
+
CONFIGURATION_FILE = 's3_website.yml'
|
|
4
|
+
|
|
5
|
+
def self.check_project(site_dir)
|
|
6
|
+
raise NoWebsiteDirectoryFound unless File.directory?(site_dir)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Raise NoConfigurationFileError if the configuration file does not exists
|
|
10
|
+
def self.check_s3_configuration(dir)
|
|
11
|
+
config_file = dir + '/' + CONFIGURATION_FILE
|
|
12
|
+
unless File.exists?(config_file)
|
|
13
|
+
create_template_configuration_file config_file
|
|
14
|
+
raise NoConfigurationFileError
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.read_configuration_file_template
|
|
19
|
+
path = File.dirname(__FILE__) + '/../../resources/configuration_file_template.yml'
|
|
20
|
+
cfg_template = File.open(path).read
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Load configuration from s3_website.yml
|
|
26
|
+
# Raise MalformedConfigurationFileError if the configuration file does not contain the keys we expect
|
|
27
|
+
def self.load_configuration(config_file_dir)
|
|
28
|
+
config = load_yaml_file_and_validate config_file_dir
|
|
29
|
+
return config
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.create_template_configuration_file(file)
|
|
33
|
+
File.open(file, 'w') { |f|
|
|
34
|
+
f.write(read_configuration_file_template)
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.load_yaml_file_and_validate(config_file_dir)
|
|
39
|
+
begin
|
|
40
|
+
config = YAML.load(Erubis::Eruby.new(
|
|
41
|
+
File.read(config_file_dir + '/' + CONFIGURATION_FILE)
|
|
42
|
+
).result)
|
|
43
|
+
rescue Exception
|
|
44
|
+
raise MalformedConfigurationFileError
|
|
45
|
+
end
|
|
46
|
+
raise MalformedConfigurationFileError unless config
|
|
47
|
+
raise MalformedConfigurationFileError if
|
|
48
|
+
['s3_bucket'].any? { |key|
|
|
49
|
+
mandatory_config_value = config[key]
|
|
50
|
+
mandatory_config_value.nil? || mandatory_config_value == ''
|
|
51
|
+
}
|
|
52
|
+
config
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module S3Website
|
|
2
|
+
class DiffHelper
|
|
3
|
+
def self.resolve_files_to_upload(s3_bucket, site_dir, config)
|
|
4
|
+
with_progress_indicator('Calculating diff') { |progress_indicator|
|
|
5
|
+
s3_data_source = Filey::DataSources::AwsSdkS3.new(s3_bucket, config) { |filey|
|
|
6
|
+
progress_indicator.render_next_step
|
|
7
|
+
}
|
|
8
|
+
fs_data_source = Filey::DataSources::FileSystem.new(site_dir) { |filey|
|
|
9
|
+
progress_indicator.render_next_step
|
|
10
|
+
}
|
|
11
|
+
changed_local_files = Filey::Comparison.list_changed(
|
|
12
|
+
fs_data_source,
|
|
13
|
+
s3_data_source
|
|
14
|
+
)
|
|
15
|
+
new_local_files = Filey::Comparison.list_missing(
|
|
16
|
+
fs_data_source,
|
|
17
|
+
s3_data_source
|
|
18
|
+
)
|
|
19
|
+
[
|
|
20
|
+
reject_blacklisted(changed_local_files, config),
|
|
21
|
+
reject_blacklisted(new_local_files, config)
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def self.reject_blacklisted(file_paths, config)
|
|
29
|
+
(normalise file_paths).reject { |f| Upload.is_blacklisted(f, config) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.with_progress_indicator(diff_msg)
|
|
33
|
+
progress_indicator = DiffProgressIndicator.new(diff_msg, "... done\n")
|
|
34
|
+
result = yield progress_indicator
|
|
35
|
+
progress_indicator.finish
|
|
36
|
+
result
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.normalise(fileys)
|
|
40
|
+
fileys.map { |filey|
|
|
41
|
+
filey.full_path.sub(/\.\//, '')
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class DiffProgressIndicator
|
|
46
|
+
def initialize(init_msg, end_msg)
|
|
47
|
+
@end_msg = end_msg
|
|
48
|
+
@ordinal_direction = 'n' # start from north
|
|
49
|
+
print init_msg
|
|
50
|
+
print ' '
|
|
51
|
+
render_next_step
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def render_next_step
|
|
55
|
+
@ordinal_direction = DiffProgressIndicator.next_ordinal_direction @ordinal_direction
|
|
56
|
+
print("\b\b" + DiffProgressIndicator.render_ordinal_direction(@ordinal_direction) + ' ')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def finish
|
|
60
|
+
print "\b\b"
|
|
61
|
+
print @end_msg
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def self.render_ordinal_direction(ordinal_direction)
|
|
67
|
+
case ordinal_direction
|
|
68
|
+
when 'n'
|
|
69
|
+
'|'
|
|
70
|
+
when 'ne'
|
|
71
|
+
'/'
|
|
72
|
+
when 'e'
|
|
73
|
+
'-'
|
|
74
|
+
when 'se'
|
|
75
|
+
'\\'
|
|
76
|
+
when 's'
|
|
77
|
+
'|'
|
|
78
|
+
when 'sw'
|
|
79
|
+
'/'
|
|
80
|
+
when 'w'
|
|
81
|
+
'-'
|
|
82
|
+
when 'nw'
|
|
83
|
+
'\\'
|
|
84
|
+
else
|
|
85
|
+
raise 'Unknown ordinal direction ' + ordinal_direction
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.next_ordinal_direction(current_ordinal_direction)
|
|
90
|
+
case current_ordinal_direction
|
|
91
|
+
when 'n'
|
|
92
|
+
'ne'
|
|
93
|
+
when 'ne'
|
|
94
|
+
'e'
|
|
95
|
+
when 'e'
|
|
96
|
+
'se'
|
|
97
|
+
when 'se'
|
|
98
|
+
's'
|
|
99
|
+
when 's'
|
|
100
|
+
'sw'
|
|
101
|
+
when 'sw'
|
|
102
|
+
'w'
|
|
103
|
+
when 'w'
|
|
104
|
+
'nw'
|
|
105
|
+
when 'nw'
|
|
106
|
+
'n'
|
|
107
|
+
else
|
|
108
|
+
raise 'Unknown ordinal direction ' + current_ordinal_direction
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module S3Website
|
|
2
|
+
class Endpoint
|
|
3
|
+
DEFAULT_LOCATION_CONSTRAINT = 'us-east-1'
|
|
4
|
+
attr_reader :region, :location_constraint, :hostname, :website_hostname
|
|
5
|
+
|
|
6
|
+
def initialize(location_constraint=nil)
|
|
7
|
+
location_constraint = DEFAULT_LOCATION_CONSTRAINT if location_constraint.nil?
|
|
8
|
+
raise "Invalid S3 location constraint #{location_constraint}" unless
|
|
9
|
+
location_constraints.has_key?location_constraint
|
|
10
|
+
@region = location_constraints.fetch(location_constraint)[:region]
|
|
11
|
+
@hostname = location_constraints.fetch(location_constraint)[:endpoint]
|
|
12
|
+
@website_hostname = location_constraints.fetch(location_constraint)[:website_hostname]
|
|
13
|
+
@location_constraint = location_constraint
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# http://docs.amazonwebservices.com/general/latest/gr/rande.html#s3_region
|
|
17
|
+
def location_constraints
|
|
18
|
+
eu_west_1_region = {
|
|
19
|
+
:region => 'EU (Ireland)',
|
|
20
|
+
:website_hostname => 's3-website-eu-west-1.amazonaws.com',
|
|
21
|
+
:endpoint => 's3-eu-west-1.amazonaws.com'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
{
|
|
25
|
+
'us-east-1' => { :region => 'US Standard', :website_hostname => 's3-website-us-east-1.amazonaws.com', :endpoint => 's3.amazonaws.com' },
|
|
26
|
+
'us-west-2' => { :region => 'US West (Oregon)', :website_hostname => 's3-website-us-west-2.amazonaws.com', :endpoint => 's3-us-west-2.amazonaws.com' },
|
|
27
|
+
'us-west-1' => { :region => 'US West (Northern California)', :website_hostname => 's3-website-us-west-1.amazonaws.com', :endpoint => 's3-us-west-1.amazonaws.com' },
|
|
28
|
+
'EU' => eu_west_1_region,
|
|
29
|
+
'eu-west-1' => eu_west_1_region,
|
|
30
|
+
'ap-southeast-1' => { :region => 'Asia Pacific (Singapore)', :website_hostname => 's3-website-ap-southeast-1.amazonaws.com', :endpoint => 's3-ap-southeast-1.amazonaws.com' },
|
|
31
|
+
'ap-southeast-2' => { :region => 'Asia Pacific (Sydney)', :website_hostname => 's3-website-ap-southeast-2.amazonaws.com', :endpoint => 's3-ap-southeast-2.amazonaws.com' },
|
|
32
|
+
'ap-northeast-1' => { :region => 'Asia Pacific (Tokyo)', :website_hostname => 's3-website-ap-northeast-1.amazonaws.com', :endpoint => 's3-ap-northeast-1.amazonaws.com' },
|
|
33
|
+
'sa-east-1' => { :region => 'South America (Sao Paulo)', :website_hostname => 's3-website-sa-east-1.amazonaws.com', :endpoint => 's3-sa-east-1.amazonaws.com' }
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module S3Website
|
|
2
|
+
class S3WebsiteError < StandardError
|
|
3
|
+
end
|
|
4
|
+
|
|
5
|
+
class NoWebsiteDirectoryFound < S3WebsiteError
|
|
6
|
+
def initialize(message = "I can't find any website. Are you in the right directory?")
|
|
7
|
+
super(message)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class NoPredefinedWebsiteDirectoryFound < NoWebsiteDirectoryFound
|
|
12
|
+
def initialize(message = "I can't find a website in any of the following directories: #{Paths.site_paths.join(', ')}. Please specify the location of the website with the --site option.")
|
|
13
|
+
super(message)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class NoConfigurationFileError < S3WebsiteError
|
|
18
|
+
def initialize(message = "I've just generated a file called s3_website.yml. Go put your details in it!")
|
|
19
|
+
super(message)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class MalformedConfigurationFileError < S3WebsiteError
|
|
24
|
+
def initialize(message = "I can't parse the file s3_website.yml. It should look like this:\n#{ConfigLoader.read_configuration_file_template}")
|
|
25
|
+
super(message)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class RetryAttemptsExhaustedError < S3WebsiteError
|
|
30
|
+
def initialize(message = "Operation failed even though we tried to recover from it")
|
|
31
|
+
super(message)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.error_report(error)
|
|
36
|
+
if error.is_a? S3WebsiteError
|
|
37
|
+
"#{error.message}"
|
|
38
|
+
else
|
|
39
|
+
"#{error.message} (#{error.class})"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module S3Website
|
|
2
|
+
class Keyboard
|
|
3
|
+
def self.if_user_confirms_delete(to_delete, config, standard_input=STDIN)
|
|
4
|
+
delete_all = false
|
|
5
|
+
keep_all = false
|
|
6
|
+
confirmed_deletes = to_delete.map do |f|
|
|
7
|
+
delete = false
|
|
8
|
+
keep = false
|
|
9
|
+
until delete || delete_all || keep || keep_all
|
|
10
|
+
puts "#{f} is on S3 but not in your website directory anymore. Do you want to [d]elete, [D]elete all, [k]eep, [K]eep all?"
|
|
11
|
+
case standard_input.gets.chomp
|
|
12
|
+
when 'd' then delete = true
|
|
13
|
+
when 'D' then delete_all = true
|
|
14
|
+
when 'k' then keep = true
|
|
15
|
+
when 'K' then keep_all = true
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
if (delete_all || delete) && !(keep_all || keep)
|
|
19
|
+
f
|
|
20
|
+
end
|
|
21
|
+
end.select { |f| f }
|
|
22
|
+
Parallelism.each_in_parallel_or_sequentially(confirmed_deletes, config) { |f|
|
|
23
|
+
yield f
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module S3Website
|
|
2
|
+
class Parallelism
|
|
3
|
+
def self.each_in_parallel_or_sequentially(items, config, &operation)
|
|
4
|
+
if ENV['disable_parallel_processing']
|
|
5
|
+
items.each do |item|
|
|
6
|
+
operation.call item
|
|
7
|
+
end
|
|
8
|
+
else
|
|
9
|
+
slice_size = config['concurrency_level'] || DEFAULT_CONCURRENCY_LEVEL
|
|
10
|
+
items.each_slice(slice_size) { |items|
|
|
11
|
+
threads = items.map do |item|
|
|
12
|
+
Thread.new(item) { |item|
|
|
13
|
+
operation.call item
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
threads.each(&:join)
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
DEFAULT_CONCURRENCY_LEVEL = 3
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module S3Website
|
|
2
|
+
class Paths
|
|
3
|
+
def self.site_paths
|
|
4
|
+
[Nanoc::SITE_PATH, Jekyll::SITE_PATH]
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def self.infer_site_path(candidate_path, pwd)
|
|
8
|
+
if candidate_path == 'infer automatically'
|
|
9
|
+
infer_automatically pwd
|
|
10
|
+
else
|
|
11
|
+
candidate_path_if_exists candidate_path
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def self.candidate_path_if_exists(candidate_path)
|
|
18
|
+
raise NoWebsiteDirectoryFound.new(
|
|
19
|
+
"Can't find a website in " + candidate_path
|
|
20
|
+
) unless File.exists? candidate_path
|
|
21
|
+
candidate_path
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.infer_automatically(pwd)
|
|
25
|
+
site_path = site_paths.
|
|
26
|
+
map do |site_path|
|
|
27
|
+
pwd + '/' + site_path
|
|
28
|
+
end.
|
|
29
|
+
find do |site_path|
|
|
30
|
+
File.exists? site_path
|
|
31
|
+
end
|
|
32
|
+
if site_path
|
|
33
|
+
site_path
|
|
34
|
+
else
|
|
35
|
+
raise NoPredefinedWebsiteDirectoryFound
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module S3Website
|
|
2
|
+
class Retry
|
|
3
|
+
def self.run_with_retry(sleep_milliseconds = 3.000)
|
|
4
|
+
attempt = 0
|
|
5
|
+
begin
|
|
6
|
+
yield
|
|
7
|
+
rescue Exception => e
|
|
8
|
+
$stderr.puts "Exception Occurred: #{e.message} (#{e.class}) Retrying in 3 seconds..."
|
|
9
|
+
sleep sleep_milliseconds
|
|
10
|
+
attempt += 1
|
|
11
|
+
if attempt <= 3
|
|
12
|
+
retry
|
|
13
|
+
else
|
|
14
|
+
raise RetryAttemptsExhaustedError
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module S3Website
|
|
2
|
+
class Tasks
|
|
3
|
+
def self.push(config_file_dir, site_dir, in_headless_mode = false)
|
|
4
|
+
ConfigLoader.check_project site_dir
|
|
5
|
+
ConfigLoader.check_s3_configuration config_file_dir
|
|
6
|
+
config = S3Website::ConfigLoader.load_configuration config_file_dir
|
|
7
|
+
new_files_count, changed_files_count, deleted_files, changed_files, changed_redirects =
|
|
8
|
+
Uploader.run(site_dir, config, in_headless_mode)
|
|
9
|
+
invalidated_items_count =
|
|
10
|
+
invalidate_cf_dist_if_configured(config, changed_files + deleted_files + changed_redirects)
|
|
11
|
+
{
|
|
12
|
+
:new_files_count => new_files_count,
|
|
13
|
+
:changed_files_count => changed_files_count,
|
|
14
|
+
:deleted_files_count => deleted_files.size,
|
|
15
|
+
:invalidated_items_count => invalidated_items_count,
|
|
16
|
+
:changed_redirects_count => changed_redirects.size
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.config_create(dir)
|
|
21
|
+
ConfigLoader.check_s3_configuration dir
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def self.invalidate_cf_dist_if_configured(config, changed_files)
|
|
27
|
+
cloudfront_configured = config['cloudfront_distribution_id'] &&
|
|
28
|
+
(not config['cloudfront_distribution_id'].empty?)
|
|
29
|
+
invalidated_items_count = if cloudfront_configured
|
|
30
|
+
Cloudfront::Invalidator.invalidate(config, changed_files)
|
|
31
|
+
else
|
|
32
|
+
0
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
require 'tempfile'
|
|
2
|
+
require 'zlib'
|
|
3
|
+
require 'zopfli'
|
|
4
|
+
|
|
5
|
+
module S3Website
|
|
6
|
+
class Upload
|
|
7
|
+
attr_reader :config, :file, :path, :full_path, :s3
|
|
8
|
+
BLACKLISTED_FILES = ['s3_website.yml']
|
|
9
|
+
|
|
10
|
+
def initialize(path, s3, config, site_dir)
|
|
11
|
+
raise "May not upload #{path}, because it's blacklisted" if Upload.is_blacklisted(path, config)
|
|
12
|
+
@path = path
|
|
13
|
+
@full_path = "#{site_dir}/#{path}"
|
|
14
|
+
@file = File.open("#{site_dir}/#{path}")
|
|
15
|
+
@s3 = s3
|
|
16
|
+
@config = config
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def perform!
|
|
20
|
+
success = s3.buckets[config['s3_bucket']].objects[path].write(upload_file, upload_options)
|
|
21
|
+
upload_file.close
|
|
22
|
+
success
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def details
|
|
26
|
+
"#{path}#{" [gzipped]" if gzip?}#{" [max-age=#{max_age}]" if cache_control?}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.is_blacklisted(path, config)
|
|
30
|
+
[
|
|
31
|
+
config['exclude_from_upload'],
|
|
32
|
+
BLACKLISTED_FILES
|
|
33
|
+
].flatten.compact.any? do |blacklisted_file|
|
|
34
|
+
Regexp.new(blacklisted_file).match path
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def upload_file
|
|
41
|
+
@upload_file ||= gzip? ? gzipped_file : file
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def gzip?
|
|
45
|
+
return false unless !!config['gzip']
|
|
46
|
+
|
|
47
|
+
extensions = config['gzip'].is_a?(Array) ? config['gzip'] : S3Website::DEFAULT_GZIP_EXTENSIONS
|
|
48
|
+
extensions.include?(File.extname(path))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def gzipped_file
|
|
52
|
+
tempfile = Tempfile.new(File.basename(path))
|
|
53
|
+
tempfile.binmode
|
|
54
|
+
|
|
55
|
+
if config['gzip_zopfli']
|
|
56
|
+
gz_data = Zopfli.deflate file.read, format: :gzip
|
|
57
|
+
tempfile.write(gz_data)
|
|
58
|
+
tempfile.flush
|
|
59
|
+
else
|
|
60
|
+
gz = Zlib::GzipWriter.new(tempfile, Zlib::BEST_COMPRESSION, Zlib::DEFAULT_STRATEGY)
|
|
61
|
+
gz.mtime = File.mtime(full_path)
|
|
62
|
+
gz.orig_name = File.basename(path)
|
|
63
|
+
gz.write(file.read)
|
|
64
|
+
|
|
65
|
+
gz.flush
|
|
66
|
+
tempfile.flush
|
|
67
|
+
gz.close
|
|
68
|
+
end
|
|
69
|
+
tempfile.open
|
|
70
|
+
|
|
71
|
+
tempfile
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def upload_options
|
|
75
|
+
opts = {}
|
|
76
|
+
opts[:reduced_redundancy] = config['s3_reduced_redundancy']
|
|
77
|
+
opts[:content_type] = resolve_content_type
|
|
78
|
+
opts[:content_encoding] = "gzip" if gzip?
|
|
79
|
+
opts[:cache_control] = cache_control_value if cache_control?
|
|
80
|
+
opts
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def resolve_content_type
|
|
84
|
+
is_text = mime_type.start_with?('text/') || mime_type == 'application/json'
|
|
85
|
+
if is_text
|
|
86
|
+
"#{mime_type}; charset=utf-8" # Let's consider UTF-8 as the de-facto encoding standard for text
|
|
87
|
+
else
|
|
88
|
+
mime_type
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def cache_control_value
|
|
93
|
+
if max_age == 0
|
|
94
|
+
"no-cache, max-age=0"
|
|
95
|
+
else
|
|
96
|
+
"max-age=#{max_age}"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def cache_control?
|
|
101
|
+
!!config['max_age']
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def max_age
|
|
105
|
+
if config['max_age'].is_a?(Hash)
|
|
106
|
+
max_age_entries_most_specific_first.each do |glob_and_age|
|
|
107
|
+
(glob, age) = glob_and_age
|
|
108
|
+
return age if File.fnmatch(glob, path)
|
|
109
|
+
end
|
|
110
|
+
else
|
|
111
|
+
return config['max_age']
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
return 0
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# The most specific max-age glob == the longest glob
|
|
118
|
+
def max_age_entries_most_specific_first
|
|
119
|
+
sorted_by_glob_length = config['max_age'].
|
|
120
|
+
each_pair.
|
|
121
|
+
to_a.
|
|
122
|
+
sort_by do |glob_and_age|
|
|
123
|
+
(glob, age) = glob_and_age
|
|
124
|
+
sort_key = glob.length
|
|
125
|
+
end.
|
|
126
|
+
reverse
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def mime_type
|
|
130
|
+
if !!config['extensionless_mime_type'] && File.extname(path) == ""
|
|
131
|
+
config['extensionless_mime_type']
|
|
132
|
+
else
|
|
133
|
+
MIME::Types.type_for(path).first.to_s
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|