s3_website_monadic 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (151) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.travis.yml +3 -0
  4. data/Gemfile +3 -0
  5. data/LICENSE +42 -0
  6. data/README.md +451 -0
  7. data/Rakefile +24 -0
  8. data/additional-docs/example-configurations.md +62 -0
  9. data/additional-docs/setting-up-aws-credentials.md +51 -0
  10. data/assembly.sbt +3 -0
  11. data/bin/s3_website +80 -0
  12. data/build.sbt +33 -0
  13. data/changelog.md +215 -0
  14. data/features/as-library.feature +29 -0
  15. data/features/cassettes/cucumber_tags/create-redirect.yml +384 -0
  16. data/features/cassettes/cucumber_tags/empty-bucket.yml +89 -0
  17. data/features/cassettes/cucumber_tags/new-and-changed-files.yml +303 -0
  18. data/features/cassettes/cucumber_tags/new-files-for-sydney.yml +211 -0
  19. data/features/cassettes/cucumber_tags/new-files.yml +355 -0
  20. data/features/cassettes/cucumber_tags/no-new-or-changed-files.yml +359 -0
  21. data/features/cassettes/cucumber_tags/one-file-to-delete.yml +390 -0
  22. data/features/cassettes/cucumber_tags/only-changed-files.yml +411 -0
  23. data/features/cassettes/cucumber_tags/s3-and-cloudfront-after-deleting-a-file.yml +434 -0
  24. data/features/cassettes/cucumber_tags/s3-and-cloudfront-when-updating-a-file.yml +435 -0
  25. data/features/cassettes/cucumber_tags/s3-and-cloudfront.yml +290 -0
  26. data/features/cloudfront.feature +54 -0
  27. data/features/command-line-help.feature +54 -0
  28. data/features/delete.feature +19 -0
  29. data/features/error_reporting.feature +24 -0
  30. data/features/instructions-for-new-user.feature +154 -0
  31. data/features/jekyll-support.feature +20 -0
  32. data/features/nanoc-support.feature +20 -0
  33. data/features/push.feature +115 -0
  34. data/features/redirects.feature +14 -0
  35. data/features/security.feature +15 -0
  36. data/features/step_definitions/steps.rb +86 -0
  37. data/features/support/env.rb +26 -0
  38. data/features/support/test_site_dirs/cdn-powered.blog.fi/_site/css/styles.css +3 -0
  39. data/features/support/test_site_dirs/cdn-powered.blog.fi/_site/index.html +5 -0
  40. data/features/support/test_site_dirs/cdn-powered.blog.fi/s3_website.yml +4 -0
  41. data/features/support/test_site_dirs/cdn-powered.when-deleted-a-file.blog.fi/_site/index.html +10 -0
  42. data/features/support/test_site_dirs/cdn-powered.when-deleted-a-file.blog.fi/s3_website.yml +5 -0
  43. data/features/support/test_site_dirs/cdn-powered.with-one-change.blog.fi/_site/css/styles.css +3 -0
  44. data/features/support/test_site_dirs/cdn-powered.with-one-change.blog.fi/_site/index.html +10 -0
  45. data/features/support/test_site_dirs/cdn-powered.with-one-change.blog.fi/s3_website.yml +4 -0
  46. data/features/support/test_site_dirs/create-redirects/_site/.gitkeep +0 -0
  47. data/features/support/test_site_dirs/create-redirects/s3_website.yml +6 -0
  48. data/features/support/test_site_dirs/ignored-files.com/_site/css/styles.css +4 -0
  49. data/features/support/test_site_dirs/ignored-files.com/_site/index.html +8 -0
  50. data/features/support/test_site_dirs/ignored-files.com/s3_website.yml +5 -0
  51. data/features/support/test_site_dirs/index-and-assets.blog.fi/_site/assets/picture.gif +0 -0
  52. data/features/support/test_site_dirs/index-and-assets.blog.fi/_site/css/styles.css +3 -0
  53. data/features/support/test_site_dirs/index-and-assets.blog.fi/_site/index.html +5 -0
  54. data/features/support/test_site_dirs/index-and-assets.blog.fi/s3_website.yml +3 -0
  55. data/features/support/test_site_dirs/jekyllrb.com/_site/css/styles.css +3 -0
  56. data/features/support/test_site_dirs/jekyllrb.com/_site/index.html +5 -0
  57. data/features/support/test_site_dirs/jekyllrb.com/s3_website.yml +3 -0
  58. data/features/support/test_site_dirs/my.blog-with-clean-urls.com/_site/css/styles.css +3 -0
  59. data/features/support/test_site_dirs/my.blog-with-clean-urls.com/_site/index +5 -0
  60. data/features/support/test_site_dirs/my.blog-with-clean-urls.com/s3_website.yml +3 -0
  61. data/features/support/test_site_dirs/my.blog.com/_site/css/styles.css +3 -0
  62. data/features/support/test_site_dirs/my.blog.com/_site/index.html +5 -0
  63. data/features/support/test_site_dirs/my.blog.com/s3_website.yml +3 -0
  64. data/features/support/test_site_dirs/my.sydney.blog.au/_site/css/styles.css +3 -0
  65. data/features/support/test_site_dirs/my.sydney.blog.au/_site/index.html +5 -0
  66. data/features/support/test_site_dirs/my.sydney.blog.au/s3_website.yml +4 -0
  67. data/features/support/test_site_dirs/nanoc.ws/public/output/css/styles.css +3 -0
  68. data/features/support/test_site_dirs/nanoc.ws/public/output/index.html +5 -0
  69. data/features/support/test_site_dirs/nanoc.ws/s3_website.yml +3 -0
  70. data/features/support/test_site_dirs/new-and-changed-files.com/_site/css/styles.css +4 -0
  71. data/features/support/test_site_dirs/new-and-changed-files.com/_site/index.html +8 -0
  72. data/features/support/test_site_dirs/new-and-changed-files.com/s3_website.yml +3 -0
  73. data/features/support/test_site_dirs/no-new-or-changed-files.com/_site/css/styles.css +3 -0
  74. data/features/support/test_site_dirs/no-new-or-changed-files.com/_site/index.html +5 -0
  75. data/features/support/test_site_dirs/no-new-or-changed-files.com/s3_website.yml +3 -0
  76. data/features/support/test_site_dirs/only-changed-files.com/_site/css/styles.css +3 -0
  77. data/features/support/test_site_dirs/only-changed-files.com/_site/index.html +9 -0
  78. data/features/support/test_site_dirs/only-changed-files.com/s3_website.yml +3 -0
  79. data/features/support/test_site_dirs/site-that-contains-s3-website-file.com/_site/s3_website.yml +3 -0
  80. data/features/support/test_site_dirs/site-that-contains-s3-website-file.com/s3_website.yml +3 -0
  81. data/features/support/test_site_dirs/site-with-text-doc.com/_site/file.txt +1 -0
  82. data/features/support/test_site_dirs/site.with.css-maxage.com/_site/css/styles.css +3 -0
  83. data/features/support/test_site_dirs/site.with.css-maxage.com/_site/index.html +5 -0
  84. data/features/support/test_site_dirs/site.with.css-maxage.com/s3_website.yml +5 -0
  85. data/features/support/test_site_dirs/site.with.gzipped-and-max-aged-content.com/_site/css/styles.css +3 -0
  86. data/features/support/test_site_dirs/site.with.gzipped-and-max-aged-content.com/_site/index.html +5 -0
  87. data/features/support/test_site_dirs/site.with.gzipped-and-max-aged-content.com/s3_website.yml +5 -0
  88. data/features/support/test_site_dirs/site.with.gzipped-html.com/_site/css/styles.css +3 -0
  89. data/features/support/test_site_dirs/site.with.gzipped-html.com/_site/index.html +5 -0
  90. data/features/support/test_site_dirs/site.with.gzipped-html.com/s3_website.yml +5 -0
  91. data/features/support/test_site_dirs/site.with.maxage.com/_site/css/styles.css +3 -0
  92. data/features/support/test_site_dirs/site.with.maxage.com/_site/index.html +5 -0
  93. data/features/support/test_site_dirs/site.with.maxage.com/s3_website.yml +4 -0
  94. data/features/support/test_site_dirs/unpublish-a-post.com/_site/css/styles.css +3 -0
  95. data/features/support/test_site_dirs/unpublish-a-post.com/s3_website.yml +3 -0
  96. data/features/support/vcr.rb +20 -0
  97. data/features/website-performance.feature +57 -0
  98. data/lib/cloudfront/invalidator.rb +37 -0
  99. data/lib/s3_website/config_loader.rb +55 -0
  100. data/lib/s3_website/diff_helper.rb +113 -0
  101. data/lib/s3_website/endpoint.rb +37 -0
  102. data/lib/s3_website/errors.rb +42 -0
  103. data/lib/s3_website/jekyll.rb +5 -0
  104. data/lib/s3_website/keyboard.rb +27 -0
  105. data/lib/s3_website/nanoc.rb +5 -0
  106. data/lib/s3_website/parallelism.rb +25 -0
  107. data/lib/s3_website/paths.rb +39 -0
  108. data/lib/s3_website/retry.rb +19 -0
  109. data/lib/s3_website/tasks.rb +36 -0
  110. data/lib/s3_website/upload.rb +137 -0
  111. data/lib/s3_website/uploader.rb +177 -0
  112. data/lib/s3_website.rb +34 -0
  113. data/project/assembly.sbt +1 -0
  114. data/project/build.properties +0 -0
  115. data/project/plugins.sbt +1 -0
  116. data/project/sbt-launch-0.13.2.jar +0 -0
  117. data/resources/configuration_file_template.yml +56 -0
  118. data/s3_website.gemspec +41 -0
  119. data/sbt +4 -0
  120. data/spec/lib/cloudfront/invalidator_spec.rb +60 -0
  121. data/spec/lib/config_loader_spec.rb +20 -0
  122. data/spec/lib/endpoint_spec.rb +31 -0
  123. data/spec/lib/error_spec.rb +21 -0
  124. data/spec/lib/keyboard_spec.rb +62 -0
  125. data/spec/lib/parallelism_spec.rb +81 -0
  126. data/spec/lib/paths_spec.rb +7 -0
  127. data/spec/lib/retry_spec.rb +34 -0
  128. data/spec/lib/upload_spec.rb +303 -0
  129. data/spec/lib/uploader_spec.rb +37 -0
  130. data/spec/sample_files/hyde_site/_site/.vimrc +5 -0
  131. data/spec/sample_files/hyde_site/_site/css/styles.css +3 -0
  132. data/spec/sample_files/hyde_site/_site/index.html +1 -0
  133. data/spec/sample_files/hyde_site/s3_website.yml +3 -0
  134. data/spec/sample_files/tokyo_site/_site/.vimrc +5 -0
  135. data/spec/sample_files/tokyo_site/_site/css/styles.css +3 -0
  136. data/spec/sample_files/tokyo_site/_site/index.html +1 -0
  137. data/spec/sample_files/tokyo_site/s3_website.yml +4 -0
  138. data/spec/spec_helper.rb +1 -0
  139. data/src/main/scala/s3/website/CloudFront.scala +96 -0
  140. data/src/main/scala/s3/website/Diff.scala +42 -0
  141. data/src/main/scala/s3/website/Implicits.scala +7 -0
  142. data/src/main/scala/s3/website/Push.scala +191 -0
  143. data/src/main/scala/s3/website/Ruby.scala +12 -0
  144. data/src/main/scala/s3/website/S3.scala +139 -0
  145. data/src/main/scala/s3/website/model/Config.scala +152 -0
  146. data/src/main/scala/s3/website/model/S3Endpoint.scala +22 -0
  147. data/src/main/scala/s3/website/model/Site.scala +68 -0
  148. data/src/main/scala/s3/website/model/errors.scala +11 -0
  149. data/src/main/scala/s3/website/model/push.scala +192 -0
  150. data/src/test/scala/s3/website/S3WebsiteSpec.scala +445 -0
  151. metadata +508 -0
@@ -0,0 +1,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,5 @@
1
+ module S3Website
2
+ module Jekyll
3
+ SITE_PATH = '_site'
4
+ end
5
+ 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,5 @@
1
+ module S3Website
2
+ module Nanoc
3
+ SITE_PATH = 'public/output'
4
+ end
5
+ 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