s3_website 0.1.0

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