s3_website_monadic 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|