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.
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