s3_website 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +81 -0
  6. data/LICENSE +42 -0
  7. data/README.md +332 -0
  8. data/Rakefile +18 -0
  9. data/bin/s3_website +38 -0
  10. data/changelog.md +7 -0
  11. data/example-configurations.md +60 -0
  12. data/features/as-library.feature +29 -0
  13. data/features/cassettes/cucumber_tags/create-redirect.yml +384 -0
  14. data/features/cassettes/cucumber_tags/new-and-changed-files.yml +303 -0
  15. data/features/cassettes/cucumber_tags/new-files-for-sydney.yml +211 -0
  16. data/features/cassettes/cucumber_tags/new-files.yml +355 -0
  17. data/features/cassettes/cucumber_tags/no-new-or-changed-files.yml +359 -0
  18. data/features/cassettes/cucumber_tags/one-file-to-delete.yml +390 -0
  19. data/features/cassettes/cucumber_tags/only-changed-files.yml +411 -0
  20. data/features/cassettes/cucumber_tags/s3-and-cloudfront-when-updating-a-file.yml +435 -0
  21. data/features/cassettes/cucumber_tags/s3-and-cloudfront.yml +290 -0
  22. data/features/cloudfront.feature +35 -0
  23. data/features/delete.feature +18 -0
  24. data/features/instructions-for-new-user.feature +94 -0
  25. data/features/redirects.feature +16 -0
  26. data/features/step_definitions/steps.rb +67 -0
  27. data/features/support/env.rb +26 -0
  28. data/features/support/test_site_dirs/cdn-powered.blog.fi/_site/css/styles.css +3 -0
  29. data/features/support/test_site_dirs/cdn-powered.blog.fi/_site/index.html +5 -0
  30. data/features/support/test_site_dirs/cdn-powered.blog.fi/s3_website.yml +4 -0
  31. data/features/support/test_site_dirs/cdn-powered.with-one-change.blog.fi/_site/css/styles.css +3 -0
  32. data/features/support/test_site_dirs/cdn-powered.with-one-change.blog.fi/_site/index.html +10 -0
  33. data/features/support/test_site_dirs/cdn-powered.with-one-change.blog.fi/s3_website.yml +4 -0
  34. data/features/support/test_site_dirs/create-redirects/_site/.gitkeep +0 -0
  35. data/features/support/test_site_dirs/create-redirects/s3_website.yml +6 -0
  36. data/features/support/test_site_dirs/index-and-assets.blog.fi/_site/assets/picture.gif +0 -0
  37. data/features/support/test_site_dirs/index-and-assets.blog.fi/_site/css/styles.css +3 -0
  38. data/features/support/test_site_dirs/index-and-assets.blog.fi/_site/index.html +5 -0
  39. data/features/support/test_site_dirs/index-and-assets.blog.fi/s3_website.yml +3 -0
  40. data/features/support/test_site_dirs/my.blog.com/_site/css/styles.css +3 -0
  41. data/features/support/test_site_dirs/my.blog.com/_site/index.html +5 -0
  42. data/features/support/test_site_dirs/my.blog.com/s3_website.yml +3 -0
  43. data/features/support/test_site_dirs/my.sydney.blog.au/_site/css/styles.css +3 -0
  44. data/features/support/test_site_dirs/my.sydney.blog.au/_site/index.html +5 -0
  45. data/features/support/test_site_dirs/my.sydney.blog.au/s3_website.yml +4 -0
  46. data/features/support/test_site_dirs/new-and-changed-files.com/_site/css/styles.css +4 -0
  47. data/features/support/test_site_dirs/new-and-changed-files.com/_site/index.html +8 -0
  48. data/features/support/test_site_dirs/new-and-changed-files.com/s3_website.yml +3 -0
  49. data/features/support/test_site_dirs/no-new-or-changed-files.com/_site/css/styles.css +3 -0
  50. data/features/support/test_site_dirs/no-new-or-changed-files.com/_site/index.html +5 -0
  51. data/features/support/test_site_dirs/no-new-or-changed-files.com/s3_website.yml +3 -0
  52. data/features/support/test_site_dirs/only-changed-files.com/_site/css/styles.css +3 -0
  53. data/features/support/test_site_dirs/only-changed-files.com/_site/index.html +9 -0
  54. data/features/support/test_site_dirs/only-changed-files.com/s3_website.yml +3 -0
  55. data/features/support/test_site_dirs/site.with.css-maxage.com/_site/css/styles.css +3 -0
  56. data/features/support/test_site_dirs/site.with.css-maxage.com/_site/index.html +5 -0
  57. data/features/support/test_site_dirs/site.with.css-maxage.com/s3_website.yml +5 -0
  58. data/features/support/test_site_dirs/site.with.gzipped-and-max-aged-content.com/_site/css/styles.css +3 -0
  59. data/features/support/test_site_dirs/site.with.gzipped-and-max-aged-content.com/_site/index.html +5 -0
  60. data/features/support/test_site_dirs/site.with.gzipped-and-max-aged-content.com/s3_website.yml +5 -0
  61. data/features/support/test_site_dirs/site.with.gzipped-html.com/_site/css/styles.css +3 -0
  62. data/features/support/test_site_dirs/site.with.gzipped-html.com/_site/index.html +5 -0
  63. data/features/support/test_site_dirs/site.with.gzipped-html.com/s3_website.yml +5 -0
  64. data/features/support/test_site_dirs/site.with.maxage.com/_site/css/styles.css +3 -0
  65. data/features/support/test_site_dirs/site.with.maxage.com/_site/index.html +5 -0
  66. data/features/support/test_site_dirs/site.with.maxage.com/s3_website.yml +4 -0
  67. data/features/support/test_site_dirs/unpublish-a-post.com/_site/css/styles.css +3 -0
  68. data/features/support/test_site_dirs/unpublish-a-post.com/s3_website.yml +3 -0
  69. data/features/support/vcr.rb +18 -0
  70. data/features/sync.feature +80 -0
  71. data/features/website-performance.feature +57 -0
  72. data/lib/cloudfront/invalidator.rb +22 -0
  73. data/lib/s3_website.rb +20 -0
  74. data/lib/s3_website/config_loader.rb +56 -0
  75. data/lib/s3_website/diff_helper.rb +21 -0
  76. data/lib/s3_website/endpoint.rb +30 -0
  77. data/lib/s3_website/errors.rb +28 -0
  78. data/lib/s3_website/keyboard.rb +27 -0
  79. data/lib/s3_website/parallelism.rb +18 -0
  80. data/lib/s3_website/retry.rb +19 -0
  81. data/lib/s3_website/tasks.rb +42 -0
  82. data/lib/s3_website/upload.rb +103 -0
  83. data/lib/s3_website/uploader.rb +160 -0
  84. data/s3-website.gemspec +39 -0
  85. data/spec/lib/config_loader_spec.rb +20 -0
  86. data/spec/lib/endpoint_spec.rb +27 -0
  87. data/spec/lib/keyboard_spec.rb +59 -0
  88. data/spec/lib/parallelism_spec.rb +43 -0
  89. data/spec/lib/retry_spec.rb +34 -0
  90. data/spec/lib/upload_spec.rb +205 -0
  91. data/spec/lib/uploader_spec.rb +30 -0
  92. data/spec/sample_files/hyde_site/_site/.vimrc +5 -0
  93. data/spec/sample_files/hyde_site/_site/css/styles.css +3 -0
  94. data/spec/sample_files/hyde_site/_site/index.html +1 -0
  95. data/spec/sample_files/hyde_site/s3_website.yml +3 -0
  96. data/spec/sample_files/tokyo_site/_site/.vimrc +5 -0
  97. data/spec/sample_files/tokyo_site/_site/css/styles.css +3 -0
  98. data/spec/sample_files/tokyo_site/_site/index.html +1 -0
  99. data/spec/sample_files/tokyo_site/s3_website.yml +4 -0
  100. data/spec/spec_helper.rb +1 -0
  101. metadata +416 -0
@@ -0,0 +1,5 @@
1
+ s3_id: key
2
+ s3_secret: pass
3
+ s3_bucket: s3-website-test.net
4
+ max_age:
5
+ "css/*": 100
@@ -0,0 +1,5 @@
1
+ <html>
2
+ <head>
3
+ <title>hello!</title>
4
+ </head>
5
+ </html>
@@ -0,0 +1,5 @@
1
+ s3_id: key
2
+ s3_secret: pass
3
+ s3_bucket: s3-website-test.net
4
+ gzip: true
5
+ max_age: 300
@@ -0,0 +1,5 @@
1
+ <html>
2
+ <head>
3
+ <title>hello!</title>
4
+ </head>
5
+ </html>
@@ -0,0 +1,5 @@
1
+ s3_id: key
2
+ s3_secret: pass
3
+ s3_bucket: s3-website-test.net
4
+ gzip:
5
+ - .html
@@ -0,0 +1,5 @@
1
+ <html>
2
+ <head>
3
+ <title>hello!</title>
4
+ </head>
5
+ </html>
@@ -0,0 +1,4 @@
1
+ s3_id: key
2
+ s3_secret: pass
3
+ s3_bucket: s3-website-test.net
4
+ max_age: 120
@@ -0,0 +1,3 @@
1
+ s3_id: foo
2
+ s3_secret: foo
3
+ s3_bucket: s3-website-test.net
@@ -0,0 +1,18 @@
1
+ require 'vcr'
2
+
3
+ VCR.configure do |c|
4
+ c.hook_into :webmock
5
+ c.cassette_library_dir = 'features/cassettes'
6
+ end
7
+
8
+ VCR.cucumber_tags do |t|
9
+ t.tag '@new-files'
10
+ t.tag '@new-files-for-sydney'
11
+ t.tag '@new-and-changed-files'
12
+ t.tag '@only-changed-files'
13
+ t.tag '@no-new-or-changed-files'
14
+ t.tag '@s3-and-cloudfront'
15
+ t.tag '@s3-and-cloudfront-when-updating-a-file'
16
+ t.tag '@one-file-to-delete'
17
+ t.tag '@create-redirect'
18
+ end
@@ -0,0 +1,80 @@
1
+ Feature: upload S3 website to S3
2
+
3
+ In order to push my website to S3
4
+ As a blogger
5
+ I want to run s3_website and say OMG it just worked!
6
+
7
+ @new-files
8
+ Scenario: Push a new S3 website to S3
9
+ When my S3 website is in "features/support/test_site_dirs/my.blog.com"
10
+ Then s3_website will push my blog to S3
11
+ And the output should contain
12
+ """
13
+ Deploying _site/* to s3-website-test.net
14
+ Uploading 2 new file(s)
15
+ """
16
+ And the output should contain
17
+ """
18
+ Upload css/styles.css: Success!
19
+ """
20
+ And the output should contain
21
+ """
22
+ Upload index.html: Success!
23
+ """
24
+
25
+ @new-files-for-sydney
26
+ Scenario: Push a new S3 website to an S3 bucket in Sydney
27
+ When my S3 website is in "features/support/test_site_dirs/my.sydney.blog.au"
28
+ Then s3_website will push my blog to S3
29
+ And the output should contain
30
+ """
31
+ Done! Go visit: http://s3-website-test.net.s3-website-ap-southeast-2.amazonaws.com/index.html
32
+ """
33
+
34
+ @new-and-changed-files
35
+ Scenario: Upload a new blog post and change an old post
36
+ When my S3 website is in "features/support/test_site_dirs/new-and-changed-files.com"
37
+ Then s3_website will push my blog to S3
38
+ And the output should contain
39
+ """
40
+ Deploying _site/* to s3-website-test.net
41
+ Uploading 1 new and 1 changed file(s)
42
+ """
43
+ And the output should contain
44
+ """
45
+ Upload css/styles.css: Success!
46
+ """
47
+ And the output should contain
48
+ """
49
+ Upload index.html: Success!
50
+ """
51
+ And the output should contain
52
+ """
53
+ Done! Go visit: http://s3-website-test.net.s3-website-us-east-1.amazonaws.com/index.html
54
+
55
+ """
56
+
57
+ @only-changed-files
58
+ Scenario: Update an existing blog post
59
+ When my S3 website is in "features/support/test_site_dirs/only-changed-files.com"
60
+ Then s3_website will push my blog to S3
61
+ And the output should equal
62
+ """
63
+ Deploying _site/* to s3-website-test.net
64
+ Uploading 1 changed file(s)
65
+ Upload index.html: Success!
66
+ Done! Go visit: http://s3-website-test.net.s3-website-us-east-1.amazonaws.com/index.html
67
+
68
+ """
69
+
70
+ @no-new-or-changed-files
71
+ Scenario: The user runs s3_website even though he doesn't have new or changed posts
72
+ When my S3 website is in "features/support/test_site_dirs/no-new-or-changed-files.com"
73
+ Then s3_website will push my blog to S3
74
+ And the output should equal
75
+ """
76
+ Deploying _site/* to s3-website-test.net
77
+ No new or changed files to upload
78
+ Done! Go visit: http://s3-website-test.net.s3-website-us-east-1.amazonaws.com/index.html
79
+
80
+ """
@@ -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
+ Then s3_website will push my blog to S3
11
+ And 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
+ Then s3_website will push my blog to S3
24
+ And 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
+ Then s3_website will push my blog to S3
37
+ And 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
+ Then s3_website will push my blog to S3
50
+ And 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,22 @@
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
+ s3_bucket_name = config['s3_bucket']
8
+ cloudfront_distribution_id = config['cloudfront_distribution_id']
9
+ s3 = AWS::S3.new(
10
+ :access_key_id => aws_key,
11
+ :secret_access_key => aws_secret)
12
+ s3_object_keys = changed_files
13
+ s3_object_keys << ""
14
+ report = SimpleCloudfrontInvalidator::CloudfrontClient.new(
15
+ aws_key, aws_secret, cloudfront_distribution_id).invalidate(
16
+ s3_object_keys)
17
+ puts report[:text_report]
18
+ report[:invalidated_items_count]
19
+ end
20
+ end
21
+ end
22
+ end
data/lib/s3_website.rb ADDED
@@ -0,0 +1,20 @@
1
+ require 'rubygems'
2
+ require 'yaml'
3
+ require 'erubis'
4
+ require 'aws-sdk'
5
+ require 'simple-cloudfront-invalidator'
6
+ require 'filey-diff'
7
+ require 'mime/types'
8
+ require 'thor'
9
+
10
+ module S3Website
11
+ DEFAULT_GZIP_EXTENSIONS = %w(.html .css .js .svg .txt)
12
+ end
13
+
14
+ %w{errors upload uploader tasks config_loader retry keyboard diff_helper endpoint parallelism}.each do |file|
15
+ require File.dirname(__FILE__) + "/s3_website/#{file}"
16
+ end
17
+
18
+ %w{invalidator}.each do |file|
19
+ require File.dirname(__FILE__) + "/cloudfront/#{file}"
20
+ end
@@ -0,0 +1,56 @@
1
+ module S3Website
2
+ class ConfigLoader
3
+ CONFIGURATION_FILE = 's3_website.yml'
4
+ CONFIGURATION_FILE_TEMPLATE = <<-EOF
5
+ s3_id: YOUR_AWS_S3_ACCESS_KEY_ID
6
+ s3_secret: YOUR_AWS_S3_SECRET_ACCESS_KEY
7
+ s3_bucket: your.blog.bucket.com
8
+ EOF
9
+
10
+ def self.check_project(site_dir)
11
+ raise NotAJekyllProjectError unless File.directory?(site_dir)
12
+ end
13
+
14
+ # Raise NoConfigurationFileError if the configuration file does not exists
15
+ def self.check_s3_configuration(site_dir)
16
+ unless File.exists?(get_configuration_file(site_dir))
17
+ create_template_configuration_file site_dir
18
+ raise NoConfigurationFileError
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ # Load configuration from s3_website.yml
25
+ # Raise MalformedConfigurationFileError if the configuration file does not contain the keys we expect
26
+ def self.load_configuration(site_dir)
27
+ config = load_yaml_file_and_validate site_dir
28
+ return config
29
+ end
30
+
31
+ def self.create_template_configuration_file(site_dir)
32
+ File.open(get_configuration_file(site_dir), 'w') { |f|
33
+ f.write(CONFIGURATION_FILE_TEMPLATE)
34
+ }
35
+ end
36
+
37
+ def self.load_yaml_file_and_validate(site_dir)
38
+ begin
39
+ config = YAML.load(Erubis::Eruby.new(File.read(get_configuration_file(site_dir))).result)
40
+ rescue Exception
41
+ raise MalformedConfigurationFileError
42
+ end
43
+ raise MalformedConfigurationFileError unless config
44
+ raise MalformedConfigurationFileError if
45
+ ['s3_bucket'].any? { |key|
46
+ mandatory_config_value = config[key]
47
+ mandatory_config_value.nil? || mandatory_config_value == ''
48
+ }
49
+ config
50
+ end
51
+
52
+ def self.get_configuration_file(site_dir)
53
+ "#{site_dir}/../#{CONFIGURATION_FILE}"
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,21 @@
1
+ module S3Website
2
+ class DiffHelper
3
+ def self.resolve_files_to_upload(s3_bucket, site_dir)
4
+ s3_data_source = Filey::DataSources::AwsSdkS3.new(s3_bucket)
5
+ fs_data_source = Filey::DataSources::FileSystem.new(site_dir)
6
+ changed_local_files =
7
+ Filey::Comparison.list_changed(fs_data_source, s3_data_source)
8
+ new_local_files =
9
+ Filey::Comparison.list_missing(fs_data_source, s3_data_source)
10
+ [ normalise(changed_local_files), normalise(new_local_files) ]
11
+ end
12
+
13
+ private
14
+
15
+ def self.normalise(fileys)
16
+ fileys.map { |filey|
17
+ filey.full_path.sub(/\.\//, '')
18
+ }
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,30 @@
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
+ {
19
+ 'us-east-1' => { :region => 'US Standard', :website_hostname => 's3-website-us-east-1.amazonaws.com', :endpoint => 's3.amazonaws.com' },
20
+ 'us-west-2' => { :region => 'US West (Oregon)', :website_hostname => 's3-website-us-west-2.amazonaws.com', :endpoint => 's3-us-west-2.amazonaws.com' },
21
+ 'us-west-1' => { :region => 'US West (Northern California)', :website_hostname => 's3-website-us-west-1.amazonaws.com', :endpoint => 's3-us-west-1.amazonaws.com' },
22
+ 'EU' => { :region => 'EU (Ireland)', :website_hostname => 's3-website-eu-west-1.amazonaws.com', :endpoint => 's3-eu-west-1.amazonaws.com' },
23
+ 'ap-southeast-1' => { :region => 'Asia Pacific (Singapore)', :website_hostname => 's3-website-ap-southeast-1.amazonaws.com', :endpoint => 's3-ap-southeast-1.amazonaws.com' },
24
+ 'ap-southeast-2' => { :region => 'Asia Pacific (Sydney)', :website_hostname => 's3-website-ap-southeast-2.amazonaws.com', :endpoint => 's3-ap-southeast-2.amazonaws.com' },
25
+ 'ap-northeast-1' => { :region => 'Asia Pacific (Tokyo)', :website_hostname => 's3-website-ap-northeast-1.amazonaws.com', :endpoint => 's3-ap-northeast-1.amazonaws.com' },
26
+ 'sa-east-1' => { :region => 'South America (Sao Paulo)', :website_hostname => 's3-website-sa-east-1.amazonaws.com', :endpoint => 's3-sa-east-1.amazonaws.com' }
27
+ }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ module S3Website
2
+ class S3WebsiteError < StandardError
3
+ end
4
+
5
+ class NotAJekyllProjectError < S3WebsiteError
6
+ def initialize(message = "I can't find any directory called _site. Are you in the right directory?")
7
+ super(message)
8
+ end
9
+ end
10
+
11
+ class NoConfigurationFileError < S3WebsiteError
12
+ def initialize(message = "I've just generated a file called s3_website.yml. Go put your details in it!")
13
+ super(message)
14
+ end
15
+ end
16
+
17
+ class MalformedConfigurationFileError < S3WebsiteError
18
+ def initialize(message = "I can't parse the file s3_website.yml. It should look like this:\n#{ConfigLoader::CONFIGURATION_FILE_TEMPLATE}")
19
+ super(message)
20
+ end
21
+ end
22
+
23
+ class RetryAttemptsExhaustedError < S3WebsiteError
24
+ def initialize(message = "Operation failed even though we tried to recover from it")
25
+ super(message)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ module S3Website
2
+ class Keyboard
3
+ def self.if_user_confirms_delete(to_delete, 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 _site 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) { |f|
23
+ yield f
24
+ }
25
+ end
26
+ end
27
+ end